文章目录
写在前面
本篇文章详细介绍了面向对象编程中的继承概念及相关特性。首先介绍了继承的概念和定义,解释了基类和派生类之间的关系。接着讨论了基类和派生类对象之间的赋值转换规则,以及继承中的作用域问题,强调了作用域解析运算符的重要性。在派生类的默认成员函数方面,阐述了派生类继承基类的构造函数、析构函数和赋值运算符重载的机制。进一步探讨了继承与友元之间的关系,说明了友元关系不会被继承的特点。此外,还涉及了继承与静态成员之间的关联,以及复杂的菱形继承及菱形虚拟继承的问题。最后,通过总结和反思,对继承的重要性和使用注意事项进行了概括和思考。
1. 继承的概念及定义
1.1 继承的概念
继承(inheritance)机制是面向对象程序设计中最重要的手段之一,它使得代码可以更加灵活地复用。通过继承,程序员可以在保持原有类的特性基础上进行扩展和增加功能,从而创建出新的类,这些新类被称为派生类。继承的核心思想是通过建立类之间的层次结构,从而体现出由简单到复杂的认知过程。
在传统的函数复用中,我们通常使用函数来达到代码的复用目的。而继承则提供了一种更高层次的复用,即在类设计层面的复用。通过继承,可以将现有类的属性和方法作为基础,进而在其基础上添加新的功能或者修改现有的功能,而无需重新编写现有代码。这样一来,可以提高代码的复用性和可维护性。
1.2 继承定义
1.2.1定义格式
从上面的图中可以看出Person是父类,也称作基类。Student是子类,也称作派生类。
1.2.2 继承关系和访问限定符
在类中,访问限定符修饰类的成员函数以及成员变量,用于控制类的成员在类外的可访问性。而继承方式用于控制基类的成员在派生类中以及在类外的可访问性。
1.2.3 继承基类成员访问方式的变化
下面表格列举出了三种继承关系下基类成员的各类型成员访问关系的变化 :
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
表格解释:
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私
有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面
都不能去访问它。
基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected private。
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强。
2. 基类和派生类对象赋值转换
派生类对象可以赋值给基类的对象、基类的指针和基类的引用,这种现象通常被形象地称为“切片”或“切割”。这意味着派生类对象可以被“切割”成基类对象的形式。
基类对象不能直接赋值给派生类对象,因为派生类对象可能包含了基类没有的额外成员,无法直接转换。
基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用,但是这种操作需要谨慎,必须确保基类的指针或引用实际指向了一个派生类对象,否则会导致未定义的行为。通常情况下,基类指针指向的对象如果是派生类对象,那么将其转换为派生类指针或引用是安全的。当基类是多态类型时,可以使用运行时类型信息(RTTI)中的 dynamic_cast 进行识别和安全的转换。
3. 继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
在子类成员函数中,可以使用 基类::基类成员 显示访问基类基类中的同名成员。
- 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
需要注意的是:Person中的test和Student中的test不构成重载,因为不在同一作用域。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
4. 派生类的默认成员函数
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
那么在派生类中,这几个成员函数是如何生成的呢?
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
派生类的operator=必须要调用基类的operator=完成基类的赋值。
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。派生类对象初始化先调用基类构造再调派生类构造。
派生类对象析构清理先调用派生类析构再调基类的析构。
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
5. 继承与友元
友元关系不能被继承。在C++中,基类的友元函数或友元类无法直接访问派生类的私有和保护成员。虽然派生类可以访问基类的成员,但是基类的友元关系不会继承到派生类。也就是说,基类友元不能访问子类私有和保护成员。
6. 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。
7. 复杂的菱形继承及菱形虚拟继承
7. 1 菱形继承问题
C++中有单继承和多继承,而菱形继承是多继承中的一种特殊情况。
C++支持多继承就会存在菱形继承,而菱形继承又会有数据冗余和二义性的问题。
而且在访问_name的时候会存在二义性的问题。
上面虽然可以指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决。
7. 2 虚拟继承
虚拟继承是为了解决菱形继承数据冗余和二义性问题才搞出来的。如上面的继承关系,在Student和Teacher继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。在虚拟继承中,通过在派生类对共同基类的继承前面加上 virtual 关键字,使得这个基类成为虚基类。这样,无论这个虚基类在继承链中被继承了多少次,派生类都只会包含一份虚基类的实例。
7. 3 虚拟继承解决数据冗余和二义性的原理
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成
员的模型。
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余:
下图是菱形虚拟继承的内存对象成员模型:
虚拟继承以后,d里面只有一份_a了,b 和 c通过其存储的指针去找偏移量,根据偏移量计算_a的位置。为什么D中B和C部分要去找属于自己的A? 我们通过下面代码段来理解一下:
D d;
B b = d;
C c = d;
这是因为:当上面的赋值发生时,d是要去找出B/C成员中的A才能赋值过去的。
当 B C 虚继承了A以后,B C的存储结构也发生了变化:
下面是上面的Person关系菱形虚拟继承的原理解释:
至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。
创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !!!