目录
1. 什么是继承
继承是什么:
继承是面向对象程序设计、使得代码可以复用的重要手段,它允许程序员在保持原有特性的基础上进行扩展,增加功能,这样产生的新类,称之为派生类。
继承的目的:
让子类继承和复用父类定义的成员和方法、继承下来的变量是独立的、各自拥有各自的内存空间。
2. 继承方式和访问限定符
-
可见在派生类中,子类的访问方式是,MIN(继承方式,基类访问方式)
-
实际基类的private成员在派生类不可见,只是语法上无法访问,假如你调用基类的方法还是可以用的。
但是调用继承的方法可以用
-
使用关键字class时默认的继承方式为private,使用struct时默认的继承方式是public,建议显示的写出继承方式
-
基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
-
实际运用中,一般都是使用public继承,很少并且不提倡用protected和private继承,因为这两种继承下来的成员都只能在子类的类里面使用,实际中扩展延伸性不强
2.1 基类与派生类赋值
- 派生类对象可以赋值给基类的的对象/指针/引用,这种方式叫做切片或者切割。即将子类中父类拿部分切下来,赋值过去
而且当发生切片的时候,_name是string类型,还会深拷贝。
应用场景:
形参并没有发生隐式类型转换,假如是隐式类型转换的话,形参是肯定要加const的,因为引用的是临时对象
- 基类对象不能赋值给派生类对象
类似于:参数是单向迭代器时,双向的可以传入,是双向迭代器的时候,单向的不可传入 - 基类的指针可以通过强转可以赋值给派生类的指针,前提是这个基类的指针必须指向一个派生类对象
- 所有的前提都是在public继承下才会存在
2.2 继承中的作用域
1.在继承体系中基类和派生类都有独立的作用域(即可以定义同名变量)
2.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的 直接访问,这种叫做隐藏,也叫做重定义
类似于全局变量和局部变量同名,就近使用局部变量。
在子类成员函数中,可以使用基类::基类成员 显示访问
3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
4.注意在实际中在继承体系里面最好不要定义同名的成员
2.3 派生类的成员函数
2.3.1 构造函数
这种方法是错误的。
这里将它看做一个对象。调用他的构造函数,不写的话会自己调用默认构造函数(Tom)
假如没有默认构造函数的话必须在参数列表写(jerry)
2.3.2 拷贝构造
2.3.3 赋值重载
上述四个,都是先调用父类的成员函数,然后再自己处理自己的成员变量
2.3.4 析构函数
析构函数十分特别。
第一个问题,竟然报错了,其实这里是构成了隐藏,但是函数名明明不一样为什么就构成隐藏了呢?
由于多态的需要,所有的析构函数都被处理成了 destructor,所以他们的名字在编译器看来都是一样的。所以需要显示调用。
第二个问题,这竟然调用了两次person的析构。
梳理一下过程,定义子类对象,由于参数列表中,初始化的顺序是声明的顺序,所以无论怎么写,都是父类构造函数先构造,然后在构造子类的。但是先析构的是子类对象。假如我们显示的调用就会使父类先析构。
所以在汇编层面他会自己在子类成员析构后,自动调用父类成员。
3. 继承与友元
友元关系不能继承,也就是说基类的友元不能访问子类私有和保护成员。要使用的话必须重新声明
4. 继承与静态
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
5. 菱形继承
菱形继承是C++继承设计的一个缺陷,由于需要不断向前兼容,结果越陷越深,解决问题又要引入新的问题。
这是一个单继承,不存在问题
多继承用“,”分割,但是它引发出一个问题。
那就是菱形继承,一定会引起两个问题,数据冗余和二义性
5.1 菱形虚拟继承
Person中有_name,Student,Teacher也一定会继承下来。由此助手类继承了Student和Tcacher,这样继承了两个_name。所以
所以我们使用的时候就必须加上访问限定符
所以二义性就可以这样解决。但是数据冗余怎么解决呢?
使用virtual,虚继承来解决。
注意,是在Student和Teacher类,继承时加virtual。
Assisant不变。
即
所以显示调用解决了二义性问题,但没有解决数据冗余,而virtual既解决了数据冗余而且也解决了二义性。
5.2 虚基表
D d;
sizeof(d);
显然是20
是因为d中继承了B和C里的成员。显然没有虚继承之前有着数据冗余和二义性。
将B,C两个类加上虚拟继承,那么加入虚拟继承后的类是多大呢?
前面提到,虚拟继承解决了数据冗余和二义性,只有4个成员变量。
此时是否为16呢?
研究一下底层。
原本不加虚继承之前,继承了B中的a和b,继承了C中的a和c。对d对象取地址,对应的地址存放着有效值。可以看到加了虚继承后,原本那块地址对应的是一个冗余有效数据,现在变成了地址,也就是说那块地址现在存了一个指针。我们把它叫做虚基指针。它又指向一个虚基表。原本的两个a现在变成一个a,这个a,现在是一个虚基类对象,存储在最下面。
那么这个虚基指针指向的虚基表有什么用呢?
虚基表存着当前位置距离虚基类对象的偏移量,而存储着虚基指针的那个地址地址通过虚基指针拿到偏移量,相加得到之后,就指向了最下面的公共虚基类对象。这样就完成了处理数据冗余和二义性的问题。
来看一个例子:
B& rb=d;
rb.a=1;
d对象继承了B对象的a,rb先找到继承B对象的a的地址(引用的是C类型的话,就找c对象对应的a),不过此时由于虚继承那块地址不在存储有效值,而是存储这一个虚基指针,指向一个虚基表,存储着偏移量,然后在指向公共的虚基类对象,从而修改。
有了多继承就会有菱形继承,有了菱形继承就会引进数据冗余和二义性,而C++用虚拟继承解决了这一问题,虚拟继承底层实现了,让那块地址对应的区域不在存储冗余数据的值,而是存储一个虚基指针,指向一个虚基表,虚基表存储着偏移量,对应类型来找自己类型的a时(B类型找b的a,C类型找c的a),通过+偏移量,找到公共的虚基a对象。
6. 继承的总结与反思
-
有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
-
多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
-
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象
例如动物和狗,更符合is-a,那么就用继承。头和眼睛,更符合has-a,用组合。假如两个都比较符合就用组合 -
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适
合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,但能用组合,就尽量用组合。符合高内聚低耦合。