1 / 48

The C++ Programming Language

The C++ Programming Language. Week 4. 内容大纲. 类的基本概念与用法 类的概念与语法 构造与析构函数 算符重载. 类与对象的概念. 类 (Class)— 可作为用户自定义的数据类型 可支持丰富的函数和算符 对象 (Object)— 某个特定类型的运行实例 微观上,类的实例;宏观上,所有类型的实例 类与对象的联系与区别 类是一个静态的概念;对象是一个运行期的动态概念 类只能有一份定义;对象实体可以有多个 例子. 类的概念. 类的组成成份 从成员类型分 数据成员(内置的、用户自定义的) —— 也称为属性

rhys
Download Presentation

The C++ Programming Language

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. The C++ Programming Language Week 4

  2. 内容大纲 类的基本概念与用法 • 类的概念与语法 • 构造与析构函数 • 算符重载 面向对象程序设计

  3. 类与对象的概念 • 类(Class)—可作为用户自定义的数据类型 • 可支持丰富的函数和算符 • 对象(Object)—某个特定类型的运行实例 • 微观上,类的实例;宏观上,所有类型的实例 • 类与对象的联系与区别 • 类是一个静态的概念;对象是一个运行期的动态概念 • 类只能有一份定义;对象实体可以有多个 • 例子 面向对象程序设计

  4. 类的概念 • 类的组成成份 • 从成员类型分 • 数据成员(内置的、用户自定义的)——也称为属性 • 成员函数(函数、算符)——也称为行为 • 从成员地位分 • 公共接口 • 私有实现 • 封装(Encapsulation) • 类的用户只需访问公共接口,而无需了解其内部实现细节 • 无论内部实现的变化如何巨大,只要接口固定不变,客户端代码就无需修改 面向对象程序设计

  5. 类的概念 • 关于struct • 尽管在C++中,struct与class几乎可以混用,绝大多数能用class关键字的地方,也能用struct • 但还是有约定俗成的惯例 • struct用于无成员操作的数据组合 • class通常都包含成员操作 面向对象程序设计

  6. 类的语法 • 从一个例子开始讲起 • 一个string类型的栈的类 • 要求实现如下操作 • Push – 往栈内压入一个元素 • Pop – 从栈中弹出一个元素 • Full – 判断栈是否已满 • Empty – 判断栈是否已空 • Size – 查询栈内元素数目 • Peek – 访问栈顶元素,但不将其弹栈 面向对象程序设计

  7. 类的语法—声明与定义 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; }; //类声明 //{}类定义体 //访问权限控制 //成员函数声明 //成员函数声明与定义 //访问权限控制 //数据成员 //;不可少 面向对象程序设计

  8. 类的语法—声明与定义 • 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; }; 面向对象程序设计

  9. 类的语法—访问权限控制 • 用来控制数据成员和成员函数被访问的方式 • 公共(Public)—能在类外被使用 • 保护(Protected)—除了在其派生类或友元中,不可在类外被使用 • 私有(Private)—除了在友元中,不可在类外被使用 • 访问权限设置准则:一般避免直接把数据成员直接暴露于公共接口部分,应当用存取函数 面向对象程序设计

  10. 类的语法—访问权限控制 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); //通过接口函数受控地访问 } 面向对象程序设计

  11. 类的语法—静态成员 • 再来看一个例子:以类的形式重新实现Fibonacci数列 • 数列的生成方式是唯一不变的,我们在不同的应用场合下其实就是使用Fibonacci数列不同的区段而已 • 因此,该类的每个对象,各自维护自己的一套数列元素,会造成重复存储,没有必要 • 最理想的方式是,所有类的对象共享同一套数列元素,每个对象保存自己使用的起始位置和长度等变量而已 • 如何来实现这一需求? 面向对象程序设计

  12. 类的语法—静态成员 静态数据成员 class CFibonacci { public: //稍后定义公共接口 private: int m_iBegPos; int m_iLength; int m_iNext; static vector<int> s_elems;//静态数据成员声明 }; 面向对象程序设计

  13. 类的语法—静态成员 静态数据成员 • 不管类的对象新建了多少个,静态数据成员在所有该类对象中,只有唯一一份存储实体,不依附于任何对象 • 所有该类对象都能访问、共享这些静态数据成员 • 除了类内声明,静态数据成员还必须要在类外进行定义和初始化 vector<int> CFibonacci::s_elems; //此句要求在类外,表明为其分配了内存,构造了一个空容器。::是必需的,表明该静态成员所属的域;但关键词static可不用再指定 面向对象程序设计

  14. 类的语法—静态成员 • 又,如果还需要实现一个成员函数is_elem(),来判断某个数是否是Fibonacci 数列中的合法元素? • 此判断过程仅与静态成员s_elems容器相关 • 如果一个成员函数从不访问任何非静态的数据成员,我们可以说它与任何的对象实例无关,也就意味着我们可将其声明为静态成员函数 面向对象程序设计

  15. 类的语法—静态成员 静态成员函数 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; } 面向对象程序设计

  16. 类的语法—静态成员 使用静态成员函数的优势——可以无需创建该类的任何对象实例而直接调用成员函数,就如同一个普通函数那样 //… #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; • } • } 面向对象程序设计

  17. 构造函数 • 如果我们希望以如下多种方式来实例化Fibonacci类的对象 CFibonacci fib1(10); //宽度为10 CFibonacci fib2(8, 3); //宽度为8,从第三个元素开始 CFibonacci fib3(fib2); //从fib2复制而来 • 如何实现?——构造函数(Constructor) • 构造函数是用于初始化类的对象的类的成员函数 • 函数名与类名一致 • 无返回值 • 可在类内重载,即一个类允许有多个构造函数 面向对象程序设计

  18. 构造函数 如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做 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 ); 面向对象程序设计

  19. 构造函数 缺省构造函数(Default Constructor) • 无需指定构造参数的构造函数 • 通常两种情况: CFibonacci::CFibonacci() //空形参表 { m_iLength = 10; m_iBegPos = 1; m_iNext = 0; }; 注意:不要同时提供这两种缺省构造函数,这会在调用时导致语义模糊 class CFibonacci { public: CFibonacci(int iLen = 10, int iBegPos = 1); //每个形参指定了缺省参数值 }; 面向对象程序设计

  20. 构造函数 只需撰写这样一个缺省构造函数,就可以支持如下多种构造形式 • 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); 面向对象程序设计

  21. 构造函数 成员初始化表(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) { }; • 在本例中,初始化动作放在成员初始化表或放在构造函数体内,没有太大的区别 • 但成员初始化表的形式也很重要,通常用于如下情况: 面向对象程序设计

  22. 构造函数 若类内有数据成员为其它类的对象,则必须以成员初始化表的方式初始化它们 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; }; 面向对象程序设计

  23. 析构函数 • 与构造函数不同,析构函数(Destructor)是一个由用户提供的成员函数,它在对象生命周期结束的时候被自动调用,以释放对象所掌握的额外资源 • 析构函数名也是固定的—— ~ClassName() • 析构函数无返回值,也不允许有参数,这也就意味着析构函数不能重载,一个类只能有一个析构函数 • 析构函数在类的设计中不是必需的,仅当类的对象消亡前必须执行一些清理动作,才需要撰写析构函数 面向对象程序设计

  24. 析构函数 例子:一个矩阵类,从自由内存中动态分配空间 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大小的空间 //… //生命周期结束,析构函数被自动调用,动态分配的数组被释放 } 面向对象程序设计

  25. Copy构造函数 • 有时候有形如如下的用法,从一个已有的对象复制出一个新对象 • CFibonacci f2(f1); • 编译器缺省的行为为“成员逐一化的复制”,也称按位复制(Bitcopy),大部分情况下足以实现根据一个源对象复制一个目标对象的需求 • 然而,某些情况下…… 面向对象程序设计

  26. Copy构造函数 { CMatrix mat(4, 4); //constructor called { CMatrix mat2 = mat; //在此使用了bitcopy,因此mat2.m_pdMat == mat.m_pdMat //mat2的作用域结束,其析构函数被调用,因此动态内存被释放 } //mat的作用域结束,其析构函数被调用,但该释放什么呢? }; • 前述的CMatrix的例子,在这样的使用情况下,会出现严重问题 • 由于类内有指针或引用成员,并指向了动态内存 • bitcopy的方式,会使多个对象的指针指向相同的地址 • 这样在使用和析构时,会导致潜在的问题 面向对象程序设计

  27. 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]; } 面向对象程序设计

  28. Quiz • 拷贝构造函数里能调用private成员变量吗? • 以下函数哪个是拷贝构造函数,为什么? • X::X(const X&);       • X::X(X);       • X::X(X&, int a=1);       • X::X(X&, int a=1, int b=2);  • 一个类中可以存在多于一个的拷贝构造函数吗? 面向对象程序设计

  29. 算符重载 重载的运算符是函数调用的语法修饰: 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 面向对象程序设计

  30. 算符重载 实现一个Iterator类 • 假设我们需要重新改造Fibonacci数列的类,以使它能以如下的方式来操纵,如同一个标准容器类一样 CFibonacci fib(10); CFibonacci::iterator it = fib.begin(), end_it = fib.end(); while (it != end_it) { cout << *it << endl; it++; } 面向对象程序设计

  31. 算符重载 • 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中的元素 } 面向对象程序设计

  32. 算符重载 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; } 面向对象程序设计

  33. 算符重载 • 前缀版本的operator++的返回值是类的引用 • 语义上是“先自增后返回” • 后缀版本operator++的返回值是一个常量对象 • 对象——”先返回后自增”的语义 • 常量——是为了避免这样的使用it++++; • 对绝大多数函数和算符来说,如果必须返回一个对象,最好用传值返回 • 例外情况: op++(), op[]() (写版本) it++++; //对int来说不成立,则最好对iterator也不成立 (it.operator++(0)).operator++(0); const iterator operator++(int) //返回值为const对象,则会打断第二次调用 //因为op++(int)是一个non-const成员函数 面向对象程序设计

  34. 算符重载 算符重载的准则 • 不要引入新的算符 • 不要改变算符的语义,总是考虑int的情况 • 操作数的数目不能改变,二元的算符不能重载成一元的 • 算符优先级不能被改变 • 算符重载的全局函数形式下,必须至少有一个参数为类;即不允许对标准数据类型的算符重载 inline int operator*(const CFibonacci_Iterator& rhs) const //全局函数重载形式 { rhs.check_integrity(); return CFibonacci::s_elems[ m_iIdx ] ; } 面向对象程序设计

  35. 算符重载 算符重载的准则(续) • 不能被重载的算符 • . .* :: ?: new delete sizeof typeid • static_cast dynamic_cast const_cast reinterpret_cast • 不建议重载的算符 • && || , • 可以被重载的算符 • operator new operator delete operator [] new operator [] delete • + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= • &= != << >> <<= >>= == != <= >= ++ -- ->* -> • () [] 面向对象程序设计

  36. 算符重载 由此,我们修改 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); } //… }; 面向对象程序设计

  37. 算符重载 友元 • 对于iterator类内的operator*来说,它必须要访问CFibonacci 类的私有成员如s_elems等,如何才能做到? • 将operator*在CFibonacci类内声明成友元,使得operator*能不受访问权限的限制 • 有时我们也能把整个类声明成友元 class CFibonacci { friend int CFibonacci_Iterator:: operator*() const; //此函数具有与CFibonacci的成员函数同等的权限 } class CFibonacci { friend class CFibonacci_Iterator; //该类的所有成员函数都具有了权限 } 面向对象程序设计

  38. 算符重载 • 友元的声明可以出现在类的声明内的任何地方,不受访问权限控制字的影响(private, protected) • 注意: • 尽管类定义中有友元函数原型,友元函数不是成员函数 • 不能用对象加点操作符来调用 • 友元关系声明可在类定义的任何位置,习惯在开始位置 • 友元关系不满足对称性和传递性 • 定义友元通常是出于性能的考虑,是在效率和封装性上的折衷 • 直接操纵其它类的私有数据成员 • 避免其通过其公共接口操纵的开销 面向对象程序设计

  39. 算符重载 例二:实现一个函数对象 • 函数对象——提供了函数调用算符()重载的类的对象 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; } 面向对象程序设计

  40. 算符重载 函数对象的使用 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; } 面向对象程序设计

  41. 算符重载 • 例三:假设我们需要让前面的CFibonacci对象支持如下的输入输出方式 • cout << fib1 << endl; • cin >> fib1; • 由于该类的对象并不是标准数据类型,编译器并不天生支持这样的操纵方式 • 解决方法:重载<<和>>算符 面向对象程序设计

  42. 算符重载 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; } 面向对象程序设计

  43. 算符重载 对于重载<<和>>算符而言,都是以全局函数的方式重载的 • 如果不这样,它们的使用方式会变得很古怪 class X { ostream& operator<<(ostream&); istream& operator>>(istream&); } b << a << cout; //b.operator<<(a.operator<<(cout)); b >> a >> cin; //b.operator>>(a.operator>>(cin)); 面向对象程序设计

  44. 运算符重载 简化版

  45. 会提示没有与这些操作数匹配的 "+" 运算符的错误 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; 面向对象程序设计

  46. 访问私有变量? 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) 面向对象程序设计

  47. 友元函数 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);} 面向对象程序设计

  48. 例子 见lecture notes 面向对象程序设计

More Related