引例
我们看下面的两个类,发现他们的成员变量、函数有很多相同的东西,_age、_name、_id以及函数print_name,对于我们代码有重复的可以用函数进行封装,那么我们的成员变量和成员函数相同要怎么办呢?
第一种方法是用组合,我们定义一个person类将这些成员封装起来。第二种方案就是用继承来实现 :
继承
上面的类我们可以这样写:
下面我们就看看看具体的格式是什么:
继承的格式
我们看到Person是父类,也称作基类。Student是子类,也称作派生类。(因为翻译的原因,所以
既叫父类/子类,也叫父类/子类)
这样创建出来的类就继承了基类的成员变量和成员函数。
继承父类成员访问方式的变化
1. 父类private成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它。
2. 父类private成员在子类中是不能被访问,如果父类成员不想在类外直接被访问,但需要在子类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,父类的私有成员在子类都是不可见。父类的其他成员在子类的访问方式 == Min(成员在父类的访问限定符,继承方式),public > protected > private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用
protetced/private继承,因为protetced/private继承下来的成员都只能在子类的类里面使用,实际
中扩展维护性不强。
继承类模版
继承类模版不好讲,大家看下面的例子就懂了:
父类和子类对象赋值兼容转换
一、切片、切割
public继承的子类对象 可以赋值给 父类的对象 / 父类的指针 / 父类的引用。这里有个形象的说法叫切片或者切割。寓意把子类中父类那部分切来赋值过去。
举例:还是用上面的Person类和Student类,但是方便改值我把person的成员变量改成共有的。
我们看到结果可知:用Person类来接受赋值就是拷贝属于它的几个成员变量,然后创建一个新的Person类;用Person类的引用就是取别名指向student的那几个成员变量;用Person类的指针就是产生一个Person的指针来指向 对应Student的那一块的首地址。
这里提出一个问题:这里的赋值兼容转换是否是隐式类型转换呢?
这里我们我们要先知道隐式类型转换是什么:
它就是在能进行两个变量类型转换的条件下,将被转换值转换成要转换的变量类型,通过一个中间量存储起来,这个中间量具有常性,那么我们用引用来接收这个值就必须要用const的变量类型,否则就无法转化。
了解到这个就简单了,上面的类引用前面都没有加const进行修饰,说明没有中间变量产生,所以没有隐式类型转换。上面我也说了都是直接指向子类里面关于父类的那一块。
二、父类不能赋值给子类对象
父类对象不能赋值给子类对象。
因为属于Student里面的score不知道赋值上什么,所以不接受少赋值给多的情况。
三、父类指针或引用转换成子类指针或引用
父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用RTTI(Run-Time Type
Information)的dynamic_cast 来进行识别后进行安全转换。
这里放开一个口子的原因是,我们的父类的引用可以引用子类也可以引用父类,如果父类引用的是子类,而我用子类引用来引用子类当然也是可以的,那么我父类引用转换成子类引用按理来说是安全的。同理指针也是如此。
那么这里不可以用C语言的强制类型转换,只能用C++的转换,并且父类还要是多态类型。
如下(这里的父类是多态类的)
继承中的作用域
隐藏规则
1、在继承体系中父类和子类都有自己独立的作用域。
2、子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使用 父类::父类成员 显示访问)
举例:还是以这两个类,但是子类多增了一个和父类同名的成员函数和成员变量。
我们运行下面的程序:
我们发现它直接默认访问的是子类的成员,如果我们加上空间限定符:
它就访问了父类的成员,这也说明继承后原来父类的和现在子类的变量是有自己独立的作用域的。
我进行调试查看:
发现里面person类的单独分了一块,并不是和子类的成员搞在一起的。
3、
先看下面的例子:
A和B类中的两个func构成什么关系()
A. 重载 B. 隐藏 C.没关系
下面程序的编译运行结果是什么()
A. 编译报错 B. 运行报错 C. 正常运行
第一题是隐藏,这里不是重载的原因是,重载的必要条件是要在同一个空间里面,所以一定不是重载。那么这里就涉及到第三点:
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
所以这个第一题是构成隐藏。
关于隐藏,例如父子类的操作符重载构成隐藏这是好得出的,值得提出的是,析构函数虽然看着不同名,但是它也是构成隐藏的。因为最后会把析构重命名为destruction,导致重名。
4、我们可以看出隐藏还是挺麻烦的,所以迫不得已不要父子里面创造同名的变量。
子类的默认成员函数
默认构造函数就是我们可以自己不用写,那么在子类里面的默认构造函数是怎么样的呢?
1. 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用。
2. 子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。
3. 子类的operator=必须要调用父类的operator=完成父类的复制。需要注意的是子类的operator=隐藏了父类的operator=,所以显示调用父类的operator=,需要指定父类作用域
4. 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。
5. 子类对象初始化先调用父类构造再调子类构造。
6. 子类对象析构清理先调用子类析构再调父类的析构。
7. 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
下面我就详细说明一下这几个默认函数
通过上面的这两个类就可以搞清楚这几个默认函数自己实现是怎么写的了
实现一个无法被继承的类
方法1:父类的构造函数私有,子类的构成必须调用父类的构造函数,但是父类的构成函数私有化
以后,子类看不见就不能调用了,那么缺点就是子类就无法实例化出对象。
方法2:C++11新增了一个final关键字,final修改父类,子类就不能继承了。
继承与友元
继承的子类是无法继承父类的友元的。
如果还要允许这个函数访问子类私有保护的成员,那么就要自己在子类里面声明对应的友元。
继承与静态成员
父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,
都只有一个static成员实例。
多继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,多继承对象在内存中的模型
是,先继承的父类在前面,后面继承的父类在后面,子类成员在放到最后面。
多继承格式
多继承可以继承很多个基类,往后面一直写类就行了。写的每一个类前面都要指定是什么继承,否则默认是private继承(class)或者public继承(struct)
继承后内部成员变量在内存的分布
通过上面默认函数的认识,大家估计已经大概知道内部成员在内存里面的分布,下面详细介绍一下:
首先继承的父类会集合成一个域放在类的最上面,然后是子类的成员按代码里面写的先后顺序来一次向下分布排列。如果是多继承,那么写在前面的类就在上边。
改一下继承顺序和代码顺序。
多继承指针偏移问题
多继承中指针偏移问题?下面说法正确的是( )
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
从上面内存分布可以分析:
先继承Base1,它的地址在顶部,所以Base1的地址和Derive的地址是一样的,而Base2是在Base1下面的,所以和Base1地址不同。
所以选C。
菱形继承问题
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。一份是teacher里面的,一份是student里面的。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
以下面为例:
我们发现它继承了两个类,并把两个类都放进去了。其中B里面的A的两个a构成隐藏,但是C里面A的a和B里面A的a是不构成隐藏的,因为B和C不构成继承关系,但是又在同一个域里面,所以要指定类域。
虚继承解决菱形继承问题
如下图:
我们在B和C继承的时候继承方式加个virtual(都要加),这样就会将B里面的A和C里面的A合成一个A单独提出来。图中绿色就是提出来的A,我们改变a值为100,发现A里面的a改了。这里B里的A和C里的A不是真实的,下面的A才是真的。
虚继承的默认函数(选看)
因为虚继承后,A被单独提取出来了,所以在默认函数初始化列表的时候不仅要B、C来实例化他们那块的,还需要单独用A实例化提取出来的那块A。
菱形继承就这么复杂。
大家可能觉得还好,那么我就再变一下:
这里我改了100,200,300
请问最后打印结果是什么
我们已经知道虚继承会将最初的A单独拿出来,所以B里面的和C里面的是假的A,所以关键看A的构造函数,所以答案是300。
IO库中的菱形虚拟继承
这里我们可以看ios被istream和ostream继承,然后istream和otream被iostream继承。
所以有些时候迫不得已会用菱形继承,那么就要用虚继承。
继承和组合
public继承是一种is-a的关系。也就是说每个子类对象都是一个父类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
继承允许你根据父类的实现来定义子类的实现。这种通过生成子类的复用通常被称为白箱复用
(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见
继承一定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系
很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关
系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太
那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间
的关系既适合用继承(is-a)也适合组合(has-a),就用组合。
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承
就有菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。多
继承可以认为是C++的缺陷之一,后来的一些编程语言都没有多继承,如Java。
到这继承就结束了,点个赞吧