480 likes | 601 Views
The C++ Programming Language. Week 4. 内容大纲. 类的基本概念与用法 类的概念与语法 构造与析构函数 算符重载. 类与对象的概念. 类 (Class)— 可作为用户自定义的数据类型 可支持丰富的函数和算符 对象 (Object)— 某个特定类型的运行实例 微观上,类的实例;宏观上,所有类型的实例 类与对象的联系与区别 类是一个静态的概念;对象是一个运行期的动态概念 类只能有一份定义;对象实体可以有多个 例子. 类的概念. 类的组成成份 从成员类型分 数据成员(内置的、用户自定义的) —— 也称为属性
E N D
The C++ Programming Language Week 4
内容大纲 类的基本概念与用法 • 类的概念与语法 • 构造与析构函数 • 算符重载 面向对象程序设计
类与对象的概念 • 类(Class)—可作为用户自定义的数据类型 • 可支持丰富的函数和算符 • 对象(Object)—某个特定类型的运行实例 • 微观上,类的实例;宏观上,所有类型的实例 • 类与对象的联系与区别 • 类是一个静态的概念;对象是一个运行期的动态概念 • 类只能有一份定义;对象实体可以有多个 • 例子 面向对象程序设计
类的概念 • 类的组成成份 • 从成员类型分 • 数据成员(内置的、用户自定义的)——也称为属性 • 成员函数(函数、算符)——也称为行为 • 从成员地位分 • 公共接口 • 私有实现 • 封装(Encapsulation) • 类的用户只需访问公共接口,而无需了解其内部实现细节 • 无论内部实现的变化如何巨大,只要接口固定不变,客户端代码就无需修改 面向对象程序设计
类的概念 • 关于struct • 尽管在C++中,struct与class几乎可以混用,绝大多数能用class关键字的地方,也能用struct • 但还是有约定俗成的惯例 • struct用于无成员操作的数据组合 • class通常都包含成员操作 面向对象程序设计
类的语法 • 从一个例子开始讲起 • 一个string类型的栈的类 • 要求实现如下操作 • Push – 往栈内压入一个元素 • Pop – 从栈中弹出一个元素 • Full – 判断栈是否已满 • Empty – 判断栈是否已空 • Size – 查询栈内元素数目 • Peek – 访问栈顶元素,但不将其弹栈 面向对象程序设计
类的语法—声明与定义 class CStack { public: bool push(const string&); bool pop(string &); bool peek(string &); bool empty(); bool full(); int size() { return m_stack.size(); } private: vector<string> m_stack; }; //类声明 //{}类定义体 //访问权限控制 //成员函数声明 //成员函数声明与定义 //访问权限控制 //数据成员 //;不可少 面向对象程序设计
类的语法—声明与定义 • Question: 加入类A有一个指针数据成员指向类B,类B也类似,该怎么定义类A和B? • Answer: Using Forward Declaration class A; class B; class A { public: B* m_pb; }; class B { public: A* m_pa; }; 面向对象程序设计
类的语法—访问权限控制 • 用来控制数据成员和成员函数被访问的方式 • 公共(Public)—能在类外被使用 • 保护(Protected)—除了在其派生类或友元中,不可在类外被使用 • 私有(Private)—除了在友元中,不可在类外被使用 • 访问权限设置准则:一般避免直接把数据成员直接暴露于公共接口部分,应当用存取函数 面向对象程序设计
类的语法—访问权限控制 class X { public: int mf1(); int mf2() { return mf3(); } //调用私有成员函数 private: int mf3(); } x; void main() { x.mf1(); //可在类外调用 x.mf3(); //错误,私有成员不可在类外使用 } class Y { public: int m1; int get_m2() { return m2;} void set_m2(int x) { m2 = x;} private: int m2; } y; void main() { y.m1++; //不建议,应避免直接操纵数据成员 y.set_m2(1); //通过接口函数受控地访问 } 面向对象程序设计
类的语法—静态成员 • 再来看一个例子:以类的形式重新实现Fibonacci数列 • 数列的生成方式是唯一不变的,我们在不同的应用场合下其实就是使用Fibonacci数列不同的区段而已 • 因此,该类的每个对象,各自维护自己的一套数列元素,会造成重复存储,没有必要 • 最理想的方式是,所有类的对象共享同一套数列元素,每个对象保存自己使用的起始位置和长度等变量而已 • 如何来实现这一需求? 面向对象程序设计
类的语法—静态成员 静态数据成员 class CFibonacci { public: //稍后定义公共接口 private: int m_iBegPos; int m_iLength; int m_iNext; static vector<int> s_elems;//静态数据成员声明 }; 面向对象程序设计
类的语法—静态成员 静态数据成员 • 不管类的对象新建了多少个,静态数据成员在所有该类对象中,只有唯一一份存储实体,不依附于任何对象 • 所有该类对象都能访问、共享这些静态数据成员 • 除了类内声明,静态数据成员还必须要在类外进行定义和初始化 vector<int> CFibonacci::s_elems; //此句要求在类外,表明为其分配了内存,构造了一个空容器。::是必需的,表明该静态成员所属的域;但关键词static可不用再指定 面向对象程序设计
类的语法—静态成员 • 又,如果还需要实现一个成员函数is_elem(),来判断某个数是否是Fibonacci 数列中的合法元素? • 此判断过程仅与静态成员s_elems容器相关 • 如果一个成员函数从不访问任何非静态的数据成员,我们可以说它与任何的对象实例无关,也就意味着我们可将其声明为静态成员函数 面向对象程序设计
类的语法—静态成员 静态成员函数 class CFibonacci { public: static bool is_elem(int); //类内声明 //… }; bool CFibonacci::is_elem(int iValue) //类外定义。::是必需的,但static可省略 { if ((! s_elems.size()) || (s_elems[s_elems.size()-1] < iValue)) gen_elems_to_value(iValue); //另外一个静态成员函数 vector<int>::iterator found_it, end_it = s_elems.end(); found_it = find(s_elems.begin(), end_it, iValue); return found_it != end_it; } 面向对象程序设计
类的语法—静态成员 使用静态成员函数的优势——可以无需创建该类的任何对象实例而直接调用成员函数,就如同一个普通函数那样 //… #include “Fibonacci.h” int main() { char ch; bool fContinue = true; int iVal; while (fContinue) { cout << “Enter Value: ”; cin >> iVal; • bool fIsElem = CFibonacci::is_elem(iVal); • cout << iVal • << (fIsElem ? “ is ” : “ is not”) • << “a element of the sequence. • << endl << “Try another? (Y/N)”; • cin >> ch; • if (‘N’==ch || ‘n’==ch) • fContinue = false; • } • } 面向对象程序设计
构造函数 • 如果我们希望以如下多种方式来实例化Fibonacci类的对象 CFibonacci fib1(10); //宽度为10 CFibonacci fib2(8, 3); //宽度为8,从第三个元素开始 CFibonacci fib3(fib2); //从fib2复制而来 • 如何实现?——构造函数(Constructor) • 构造函数是用于初始化类的对象的类的成员函数 • 函数名与类名一致 • 无返回值 • 可在类内重载,即一个类允许有多个构造函数 面向对象程序设计
构造函数 如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做 class CFibonacci { public: CFibonacci(); //default constructor CFibonacci(int iLen); CFibonacci(int iLen, int iBegPos); //… other constructors and members (重载构造函数) }; //正确 //正确 //正确,效果同f3(10) //f4会被识别为一个函数 //正确 //调用 CFibonacci f1; CFibonacci f2( 6 ); CFibonacci f3 = 10; CFibonacci f4( ); CFibonacci f5( 10, 3 ); 面向对象程序设计
构造函数 缺省构造函数(Default Constructor) • 无需指定构造参数的构造函数 • 通常两种情况: CFibonacci::CFibonacci() //空形参表 { m_iLength = 10; m_iBegPos = 1; m_iNext = 0; }; 注意:不要同时提供这两种缺省构造函数,这会在调用时导致语义模糊 class CFibonacci { public: CFibonacci(int iLen = 10, int iBegPos = 1); //每个形参指定了缺省参数值 }; 面向对象程序设计
构造函数 只需撰写这样一个缺省构造函数,就可以支持如下多种构造形式 • CFibonacci f1; //CFibonacci::CFibonacci(10, 1); • CFibonacci f2( 12 ); • CFibonacci f3( 8, 3 ); CFibonacci::CFibonacci(int iLen = 10, int iBegPos = 1) { m_iLength = iLen > 0 ? iLen : 1; m_iBegPos = iBegPos > 0 ? iBegPos : 1; m_iNext = m_iBegPos - 1; }; //CFibonacci::CFibonacci(12, 1); //CFibonacci::CFibonacci( 8, 3); 面向对象程序设计
构造函数 成员初始化表(Member initialization list) • 另一种初始化数据成员的方式 CFibonacci::CFibonacci(int iLen, int iBegPos) : m_iLength(iLen > 0 ? iLen : 1), m_iBegPos(iBegPos > 0 ? iBegPos : 1), m_iNext(m_iBegPos – 1) { }; • 在本例中,初始化动作放在成员初始化表或放在构造函数体内,没有太大的区别 • 但成员初始化表的形式也很重要,通常用于如下情况: 面向对象程序设计
构造函数 若类内有数据成员为其它类的对象,则必须以成员初始化表的方式初始化它们 class CFibonacci { private: //… string m_szName; //… }; CFibonacci::CFibonacci(int iLen, int iBegPos) : m_szName(“Fibonacci”) { m_iLength = iLen > 0 ? iLen : 1; m_iBegPos = iBegPos > 0 ? iBegPos : 1; m_iNext = m_iBegPos - 1; }; 面向对象程序设计
析构函数 • 与构造函数不同,析构函数(Destructor)是一个由用户提供的成员函数,它在对象生命周期结束的时候被自动调用,以释放对象所掌握的额外资源 • 析构函数名也是固定的—— ~ClassName() • 析构函数无返回值,也不允许有参数,这也就意味着析构函数不能重载,一个类只能有一个析构函数 • 析构函数在类的设计中不是必需的,仅当类的对象消亡前必须执行一些清理动作,才需要撰写析构函数 面向对象程序设计
析构函数 例子:一个矩阵类,从自由内存中动态分配空间 class CMatrix { public: CMatrix(int Row, int Col) : m_iRow(Row), m_iCol(Col) { m_pdMat = new double [row * col]; } ~CMatrix() { delete [] m_pdMat; } private: int m_iRow, m_iCol; double* m_pdMat; }; int main() { CMatrix Mat(4, 4); //构造函数被自动调用,动态分配16个doulbe大小的空间 //… //生命周期结束,析构函数被自动调用,动态分配的数组被释放 } 面向对象程序设计
Copy构造函数 • 有时候有形如如下的用法,从一个已有的对象复制出一个新对象 • CFibonacci f2(f1); • 编译器缺省的行为为“成员逐一化的复制”,也称按位复制(Bitcopy),大部分情况下足以实现根据一个源对象复制一个目标对象的需求 • 然而,某些情况下…… 面向对象程序设计
Copy构造函数 { CMatrix mat(4, 4); //constructor called { CMatrix mat2 = mat; //在此使用了bitcopy,因此mat2.m_pdMat == mat.m_pdMat //mat2的作用域结束,其析构函数被调用,因此动态内存被释放 } //mat的作用域结束,其析构函数被调用,但该释放什么呢? }; • 前述的CMatrix的例子,在这样的使用情况下,会出现严重问题 • 由于类内有指针或引用成员,并指向了动态内存 • bitcopy的方式,会使多个对象的指针指向相同的地址 • 这样在使用和析构时,会导致潜在的问题 面向对象程序设计
Copy构造函数 • 解决之道——Copy构造函数(Copy Constructor) • 形如X (const X& )的构造函数 • 当缺省的成员逐一化复制的方式对某些类不适用时,应当为该类提供Copy构造函数 CMatrix::CMatrix(const CMatrix& rhs ) : m_iRow(rhs.m_iRow), m_iCol(rhs.m_iCol) { int num = m_iRow * m_iCol; m_pdMat = new double [num]; //复制一个矩阵 for (int i=0; i<num;i++) m_pdMat[i] = rhs.m_pdMat[i]; } 面向对象程序设计
Quiz • 拷贝构造函数里能调用private成员变量吗? • 以下函数哪个是拷贝构造函数,为什么? • X::X(const X&); • X::X(X); • X::X(X&, int a=1); • X::X(X&, int a=1, int b=2); • 一个类中可以存在多于一个的拷贝构造函数吗? 面向对象程序设计
算符重载 重载的运算符是函数调用的语法修饰: class Fred{public:// ... }; #if 0 // 没有算符重载:Fred add(Fred, Fred);Fred mul(Fred, Fred); Fred f(Fred a, Fred b, Fred c){ return add(add(mul(a,b), mul(b,c)), mul(c,a)); // 哈哈,多可笑...} #else // 有算符重载:Fred operator+ (Fred, Fred);Fred operator* (Fred, Fred); Fred f(Fred a, Fred b, Fred c){ return a*b + b*c + c*a;} #endif 面向对象程序设计
算符重载 实现一个Iterator类 • 假设我们需要重新改造Fibonacci数列的类,以使它能以如下的方式来操纵,如同一个标准容器类一样 CFibonacci fib(10); CFibonacci::iterator it = fib.begin(), end_it = fib.end(); while (it != end_it) { cout << *it << endl; it++; } 面向对象程序设计
算符重载 • Iterator将会被作为CFibonacci类内嵌套的类 • 我们主要讨论Iterator类的实现 • Iterator类应当重载!=, ==, *, ++算符 class CFibonacci_Iterator { public: CFibonacci_Iterator(int idx) : m_iIdx(idx - 1) {}; bool operator= =(const CFibonacci_Iterator&) const; bool operator!=(const CFibonacci_Iterator&) const; int operator*() const; CFibonacci_Iterator& operator++(); //前缀版本 const CFibonacci_Iterator operator++( int ); //后缀版本 private: void check_integrity(); //内部检察函数 int m_iIdx; //用来指明CFibonacci::s_elems中的元素 } 面向对象程序设计
算符重载 inline bool CFibonacci_Iterator ::operator==(const CFibonacci_Iterator& rhs) const { return m_iIdx == rhs.m_iIdx; } inline bool CFibonacci_Iterator ::operator!=(const CFibonacci_Iterator& rhs) const { return !(*this == rhs); } //利用了前面刚刚重载的operator == inline int CFibonacci_Iterator::operator*() const //*算符的成员函数重载方式 { check_integrity(); return CFibonacci::s_elems[ m_iIdx ] ; } CFibonacci_Iterator& operator++(); //++it { ++m_iIdx; check_integrity(); return *this; } const CFibonacci_Iterator operator++(int); //it++, int是C++标准要求的,以与前缀版本相区别,调用的时候会自动传入一个0 { CFibonacci_Iterator old = *this; // ++(*this); //基于前面的前缀版本++实现,以消除将来可能由于分别修改而造成的语义不一致 return old; } 面向对象程序设计
算符重载 • 前缀版本的operator++的返回值是类的引用 • 语义上是“先自增后返回” • 后缀版本operator++的返回值是一个常量对象 • 对象——”先返回后自增”的语义 • 常量——是为了避免这样的使用it++++; • 对绝大多数函数和算符来说,如果必须返回一个对象,最好用传值返回 • 例外情况: op++(), op[]() (写版本) it++++; //对int来说不成立,则最好对iterator也不成立 (it.operator++(0)).operator++(0); const iterator operator++(int) //返回值为const对象,则会打断第二次调用 //因为op++(int)是一个non-const成员函数 面向对象程序设计
算符重载 算符重载的准则 • 不要引入新的算符 • 不要改变算符的语义,总是考虑int的情况 • 操作数的数目不能改变,二元的算符不能重载成一元的 • 算符优先级不能被改变 • 算符重载的全局函数形式下,必须至少有一个参数为类;即不允许对标准数据类型的算符重载 inline int operator*(const CFibonacci_Iterator& rhs) const //全局函数重载形式 { rhs.check_integrity(); return CFibonacci::s_elems[ m_iIdx ] ; } 面向对象程序设计
算符重载 算符重载的准则(续) • 不能被重载的算符 • . .* :: ?: new delete sizeof typeid • static_cast dynamic_cast const_cast reinterpret_cast • 不建议重载的算符 • && || , • 可以被重载的算符 • operator new operator delete operator [] new operator [] delete • + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= • &= != << >> <<= >>= == != <= >= ++ -- ->* -> • () [] 面向对象程序设计
算符重载 由此,我们修改 CFibonacci类的定义,为它提供返回头尾iterator的begin()和end()函数 #include “CFibonacci_Iterator.h” class CFibonacci { public: typedef CFibonacci_Iterator iterator; //使具体的iterator类名对用户透明 CFibonacci_Iterator begin() const { return CFibonacci_Iterator(m_iBegPos); } CFibonacci_Iterator end() const { return CFibonacci_Iterator(m_iBegPos + m_iLength); } //… }; 面向对象程序设计
算符重载 友元 • 对于iterator类内的operator*来说,它必须要访问CFibonacci 类的私有成员如s_elems等,如何才能做到? • 将operator*在CFibonacci类内声明成友元,使得operator*能不受访问权限的限制 • 有时我们也能把整个类声明成友元 class CFibonacci { friend int CFibonacci_Iterator:: operator*() const; //此函数具有与CFibonacci的成员函数同等的权限 } class CFibonacci { friend class CFibonacci_Iterator; //该类的所有成员函数都具有了权限 } 面向对象程序设计
算符重载 • 友元的声明可以出现在类的声明内的任何地方,不受访问权限控制字的影响(private, protected) • 注意: • 尽管类定义中有友元函数原型,友元函数不是成员函数 • 不能用对象加点操作符来调用 • 友元关系声明可在类定义的任何位置,习惯在开始位置 • 友元关系不满足对称性和传递性 • 定义友元通常是出于性能的考虑,是在效率和封装性上的折衷 • 直接操纵其它类的私有数据成员 • 避免其通过其公共接口操纵的开销 面向对象程序设计
算符重载 例二:实现一个函数对象 • 函数对象——提供了函数调用算符()重载的类的对象 class LessThan { public: LessThan(int val) : m_iBaseVal(val); int read_base() const { return m_iBaseVal; } void change_base(int val) { m_iBaseVal = val; } //函数调用算符 bool operator()(int value) const { return value < m_iBaseVal; } private: int m_iBaseVal; } 面向对象程序设计
算符重载 函数对象的使用 int count_less_than(const vector<int>& vec, int iBase) { LessThan lt(iBase); int count = 0; for (int i = 0; i < vec.size(); i++) if ( lt(vec[ i ]) ) //不是构造函数,lt(vec[ i ])会被编译器 count++; //自动理解为lt.operator(vec[ i ]) return count; } 面向对象程序设计
算符重载 • 例三:假设我们需要让前面的CFibonacci对象支持如下的输入输出方式 • cout << fib1 << endl; • cin >> fib1; • 由于该类的对象并不是标准数据类型,编译器并不天生支持这样的操纵方式 • 解决方法:重载<<和>>算符 面向对象程序设计
算符重载 ostream& operator<<(ostream& os, const CFibonacci& rhs ) { os << “( ” << rhs.beg_pos() << “, ” << rhs.length() << “ )”; rhs.display(rhs.length(), rhs.beg_pos(), os); return os; } Output: ( 3, 6 )2 3 5 8 13 21 istream& operator>>(istream& is, CFibonacci& rhs ) { char ch1, ch2; int bp, len; //suppose the input format is: (3,6) cin >> ch1 >> bp >> ch2 >> len; rhs.beg_pos(bp); rhs.length(len); rhs.next_reset; return is; } 面向对象程序设计
算符重载 对于重载<<和>>算符而言,都是以全局函数的方式重载的 • 如果不这样,它们的使用方式会变得很古怪 class X { ostream& operator<<(ostream&); istream& operator>>(istream&); } b << a << cout; //b.operator<<(a.operator<<(cout)); b >> a >> cin; //b.operator>>(a.operator>>(cin)); 面向对象程序设计
运算符重载 简化版
会提示没有与这些操作数匹配的 "+" 运算符的错误 C++针对预定义基本数据类型已经对"+"运算符做了适当的重载 加号 int i;int i1=10,i2=10;i=i1+i2;std::cout<<"i1+i2="<<i<<std::endl;double d;double d1=20,d2=20;d=d1+d2;std::cout<<"d1+d2="<<d<<std::endl; class Complex //复数类{ public: double real;//实数double imag;//虚数Complex(double real=0,double imag=0) { this->real=real; this->imag=imag; }}; Complex com1(10,10),com2(20,20),sum;sum=com1 + com2; 面向对象程序设计
访问私有变量? class Complex //复数类{public: double real;//实数double imag;//虚数Complex(double real=0,double imag=0) { this->real=real; this->imag=imag; }}; Complex operator+(Complex com1,Complex com2) { return Complex(com1.real+com2.real,com1.imag+com2.imag); } Complex com1(10,10),com2(20,20),sum;sum=com1+com2; //或sum=operator+(com1,com2) 面向对象程序设计
友元函数 class Complex //复数类{private://私有double real;//实数double imag;//虚数public: Complex(double real=0,double imag=0) { this->real=real; this->imag=imag; } //友元函数重载双目运算符+ friend Complex operator+(Complex com1,Complex com2); void showSum(); }; //友元运算符重载函数Complex operator+(Complex com1,Complex com2) { return Complex(com1.real+com2.real,com1.imag+com2.imag);} 面向对象程序设计
例子 见lecture notes 面向对象程序设计