继承
面向对象三大特性:封装,继承,多态。继承这里时刻牢记一个类继承了父类时,这个类中==既有自己的成员,也有来自父类的成员!==如何统筹协调这两类成员就是继承的性质。
这些面向对象的特性是针对所有 面向对象程序语言 的!并不是特指C++,这里只是学习C++中面向对象特性的实现。
1. 再次理解封装
封装的第一层理解:封装成类后加上访问限定符,是一种==更严格的管理方式!==同时也可以很好的做到解耦。
封装的第二层理解:STL迭代器的设计。对容器的底层结构进行封装,在不暴露底层数据结构的情况下,给用户提供统一的访问方式,降低使用成本。
封装的第三层理解:STL中的适配器模式。通过对底层容器的封装适配得到需要的其他容器;通过对正向迭代器的封装得到反向迭代器。
2. 继承的基本知识
1. 基本概念
继承是面向对象程序设计中代码复用的重要手段!是 类 设计层次的复用。
// 每个人都有name和tel,就把这些共有的属性提取出来单独设计一个类,其他的类继承我! class Person // 父类/基类 { protected: string name_; string tel_; } class Student : public Person // 子类/派生类 { public: int stuId_; // ... } class Teacher : public Person // 子类/派生类 { public: int TeachId_; // ... }
2. 继承方式
继承方式也有三种:public,protected,private
基类成员/继承方式 |
public继承 |
protected继承 |
private继承 |
基类的public成员 |
派生类的public成员 |
派生类的protected成员 |
派生类的private成员 |
基类的protected成员 |
派生类的protected成员 |
派生类的protected成员 |
派生类的private成员 |
基类的private成员 |
在派生类中不可见 |
在派生类中不可见 |
在派生类中不可见 |
不可见是指基类的私有成员还是被继承到了派生类中,但是语法上限制==派生类对象不管在类里面还是类外面都无法访问基类的私有成员。==基类的private成员就是为了不让派生类访问修改。
一般而言,基类的访问限定符都是public/protected,继承方式都是public。
3. 基类和派生类对象的赋值转换
在public继承中,派生类的对象可以赋值给基类的对象/基类的指针/基类的引用,并且中间不会产生临时变量。就是把派生类中基类的那部分赋给基类的对象,这叫做切割/切片。这个过程是天然支持的,没有类型转换。
基类的对象不能赋值给派生类的对象。
但是当基类的指针是指向派生类时,这个基类的指针可以 强转后赋值给派生类的指针/引用。
4. 继承中的作用域
基类和派生类有自己独立的作用域。
当基类和派生类中有同名成员(包括成员变量和成员函数)时,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)。对于成员函数也是一样,只要基类和派生类函数名相同就构成隐藏。
隐藏是在不同的作用域中,而函数重载是在同一作用域中。
3. 子类的默认成员函数
在调用子类的构造/拷贝构造函数时,会先自动调用父类的构造/拷贝构造函数对父类的成员进行初始化,然后调用子类的构造/拷贝构造函数进行初始化!
在调用子类的析构函数时,为了保证析构顺序会先调用子类的析构函数清理子类的资源,然后自动调用父类的析构函数清理父类的资源!不需要显示调用!(栈帧后进先出)
而对于赋值重载,则在子类的赋值重载函数中需要显示的调用父类的赋值重载函数。
注意一下:子类和父类的赋值重载函数构成了隐藏 (函数名相同),所以调用的时候要指定作用域!另外,析构也构成隐藏—实际析构函数是覆盖,参考多态章节。
4. 继承与友元
友元关系不能被继承!也就是说,父类的友元函数不是子类的友元函数,无法访问子类的私有和保护成员。
5. 继承中的静态成员
静态成员在整个继承体系中只有一份,基类和所有的派生类的对象共享,这些对象共用同一个静态成员。
6. 继承与组合的区别
继承是一种 **"is-a"**的关系,可以认为派生类就是一个基类,在基类的基础上多了派生类自己的成员。
而组合是指在一个类中有另一个类对象,这是一种 **"has-a" **的关系—我里面有一个你,也可以做到类的复用。
// 组合示例 class A { // ... protected: int a_; }; class B : public A { // ... protected: int b_; // B类里有一个A类。 A classA_; };
这两种方式都是代码复用的做法。但是继承破坏了封装—public继承下 子类也可以访问修改父类的protected成员!所以认为继承是一种白盒复用;而组合就不会破坏代码的封装—B类不能访问修改A类的protected成员,所以认为组合是一种黑盒复用。
而且继承机制中基类和派生类的耦合度更强,组合机制中两个类之间的耦合度就要低一些。
在实际中能用组合就尽量用组合!但是有些情况下只能用继承那就用继承,而且继承配合多态可以使代码的复用性,灵活性更好!
组合的典型应用:STL中的容器适配器,如stack和queue中就有一个deque类,还配合了模板达到泛型编程。
7. 菱形继承与菱形虚拟继承
1. 菱形继承的产生与带来的问题
首先有多继承,比如D类继承B,C两个类!这时若B类和C类都继承自同一个类,就会导致菱形继承。见下图:
// 菱形继承示例 class A { // ... protected: int a_; }; class B : public A { // ... protected: int b_; }; class C : public A { // ... protected: int c_; }; class D : public B,public C { // ... protected: int d_; };
菱形继承带来的问题:D类中又两份A类的数据—一份是继承B类得到的,一份是继承C类得到的!这就造成了数据冗余和二义性的问题!(有两份A类的数据—数据冗余,D类访问A类的数据时必须指明访问的是哪一个A类的数据—二义性)
2. 菱形继承问题的解决—菱形虚拟继承
采用菱形虚拟继承就可以解决上述问题!
// 菱形虚拟继承示例 class A { // ... protected: int a_; }; // 虚继承后B对象的存储结构就变了!不直接存A类的数据,而是存虚基表指针 // 当然A类的数据还在B类中,但是是通过虚基表访问的,不是直接访问的!! class B : virtual public A // 在这两个位置进行虚继承 { // ... protected: int b_; }; class C : virtual public A // 在这两个位置进行虚继承 { // ... protected: int c_; }; class D : public B,public C { // ... protected: int d_; };
在虚继承中,D类里的B类和C类不再存A类的数据,而是存一个虚基表指针,指向虚基表,虚基表里存放A类数据的偏移量,通过偏移量就可以找到同一份A类的数据!!这就解决了数据冗余和二义性的问题!
简单来说,就是把冗余的基类数据放到一个公共的区域(还在D类里),第一代派生类继承时不存基类的具体数据,而是存一个指针可以找到这个公共区域。其实这个基类的数据变成了临界资源!
在VS下观察如下:
思路:多继承–>菱形继承–>数据冗余和二义性问题–>菱形虚拟继承解决–>具体解决思路(虚基表指针,虚基表存偏移量)
**虚继承后,B对象和C对象的存储结构就已经改变了!!**使用虚基表的方式找A对象。
由于虚继承的复杂,所以实际中尽量避免使用多继承(多继承是菱形继承的根本原因)!!C++标准库中的I/O流就是菱形继承!!