继承基本概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构
,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
继承语法
继承基本语法: class 子类(派生类):继承方式 父类1(基类) ,继承方式 父类2 ……
1.继承关系和访问限定符
类成员 \ 继承方式 | public(公开继承) | protected(保护继承) | private(私有继承) |
---|---|---|---|
基类中的公有成员 | 派生类的public成员 | 派生类的protected | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
用图来演示就如下图:
注:其中的不可见,并不是子类没有此成员,而是在派生类(子类)中访问不到该成员。
总结:虽然上面的继承方式花里胡哨,但我们平时使用继承时,父类的成员都是设成公开的,继承一般也使用公开继承和保护继承。
2. 重定义(隐藏)和作用域
注意点:
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
当我们使用继承语法时,难免会出现子类的成员与父类的成员名字相同,此时就会构成隐藏(重定义)
例如:
A和B 中有同名函数Func和成员变量a,而当对象bb 调用Func 和访问a时是默认访问派生类中的同名成员
此时如果想访问被隐藏起来的基类的同名成员,我们只需在访问成员前加上固定作用域即可 即指定作用域访问
基类与子类之间的赋值
1.子类对象可以赋给父类对象,指针,引用,但父类对象不能赋给子类对象,且父类对象赋给子类引用用或者指针可能会造成越界访问。
2.子类对象赋给父类对象,指针,引用,会发生切片(切割)(通俗说就是从派生类中将属于父类那部分赋给父类)
3.父类对象赋给子类应用或者子类指针时,必须要强制转换类型
- 子类对象赋给父类对象/父类指针/父类引用
- 父类对象赋给子类(这个编译器会直接报错)
- 父类对象赋给子类引用或者子类指针
在没有通过子类指针或者引用访问子类成员时,系统是不会报错的
但一旦通过子类指针或者引用访问子类成员时,就会有非法访问内存的错误出现,如图,访问到了随机值
切片
寓意:把派生类中父类那部分切来赋值过去
如图
而上面使用被父类对象赋值的子类指针或者引用访问会出现错误的原因是:访问权限出现冲突
子类的默认成员函数
普通的类如果我们不写默认的成员函数,系统会自动生成,而派生类中也是如此吗?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
- 派生类中如果我们不写默认的成员函数,系统提供的会完成以下工作
1.调用父类(基类)的构造函数对父类成员完成初始化
2.派生类中的自定义类型成员调用对应的构造函数(同普通的类一样)
3.派生类中的内置类型成员不作处理(同普通的类一样)
如图:
且只要父类中有
默认构造函数,派生类的构造函数(自己写的和系统生成的都会去做)就不用显示调用父类的构造函数,系统会自动去调用;而当父类中没有实现默认的构造函数时,且我们没有在派生类的构造函数中显示调用其他构造函数去初始化父类成员,编译器就会报错
注:
以下情况我们都要自己写默认成员函数:
- 基类中没有默认的构造函数,此时我们需要在派生类的构造函数中,在初始化列表显示调用父类中合适的构造函数
- 派生类中有需要释放的资源,需要我们自己写析构函数
- 子类存在深浅拷贝问题时,也需要我们自己实现拷贝构造和赋值=函数
如图:
注:显示调用基类成员函数只能在初始化列表处,不然会报错说xx重定义
- 其中有一个特殊例子就是,派生类的析构函数中就不需要显示调用基类的析构函数,不然会出问题
1.析构函数名都会被统一处理成destructor(),于是父类与子类中的析构函数就会构成隐藏,我们调用时也要加上指定的作用域才能调用到父类析构函数
2.因为要确保先析构子类再析构父类内容,系统会在子类析构函数被调用后,调用父类的析构函数,所以我们不需要再子类当中再调用父类的析构函数
继承与友员
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
执行上面的代码会报错:stuNum不可访问!
继承中静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
菱形继承问题和虚菱形继承
菱形继承问题是使用多继承语法时产生的问题
菱形继承具有二义性和数据冗余的问题
- 单继承:
- 多继承:
- 菱形继承:
至此:会出现俩个问题
1.继承了俩份相同的数据(数据赘余)
2.访问数据时不知道是访问父类中那一个(二义性)
以下面代码作分析
使用内存来看对象d1的数据分布是这样的
使用对象模型框架来看是这样的
查看这个需要通过命令行 然后先cd到当前路径下,然后输入
cl /d1 reportSingleClassLayoutXX XX代表类名
从俩图中都可看出d1中包含了从父类B中继承的a和从父类C中继承的a,只是目前数据较小,赘余的影响不是很明显,而当数据量大时,这将是致命的,所以为了解决这个问题又提出了虚继承这个概念
虚菱形继承底层
- 虚继承(virtual)
语法:class 类名 :virtual public 父类
加了virtual关键字后,对象d1的数据分布变成了这样
我们会发现同之前不同的是内存部分,原来存放a数据位置的变成一个地址,而数据a被放到了最底下,俩份数据a变成了一份数据a,而通过去看这俩个地址,我们会发现里面记录了当前位置与数据a的偏移量
20正是第一个虚基类指针据数据a的偏移量
12©也正是第二个虚基类指针距数据a的偏移量
我们再通过对象模型框架来看
我们会发现使用虚继承后,对象模型中多了vbptr(virtual base pointer)即虚基类指针,指向虚基列表(vbtable),这个表中记录的是当前指针与唯一数据的偏移量,通过这俩个指针都可以找到那份唯一的数据
总结:虚继承底层是子类从父类中继承的不是数据,而是一个虚基类指针,然后通过虚基类指针去找到虚基列表拿到偏移量,然后去找到那份唯一的数据,然后对数据进行操作
感谢各位大佬的观看,如果有错误之处望指出,交流学习! 😐