一、单类继承
- 在父类中声明为私有的成员,子类对象无法直接访问,但是在子类对象的内存结构中,父类私有的成员数据依然存在。C++语法规定的访问限制仅限于编译层面,在编译过程中进行语法检查,因此访问控制不会影响对象的内存结构。
- 子类未提供构造函数或析构函数,而父类却需要构造函数与析构函数时,编译器会为子类提供默认的构造函数与析构函数。但是子类有构造函数,而父类不存在构造函数,且没有虚函数,则编译器不会为父类提供默认的构造函数。
1. 内存结构:
①先安排父类的数据
②后安排子类新定义的数据
说明:基于上述的内存排列方法,即父类数据成员被安排前面。不管是父类的对象,还是子类的对象,父类的数据成员在内存中相对于对象的首地址的偏移值都是一样的。而且成员数据访问都是基于this指针间接寻址的。所以,对于子类对象而言,使用父类指针或者子类指针都可以正确访问其父类数据。
2. 虚表:
虚表的排列顺序是按虚函数在类继承层次中首次声明的顺序依次排列的。只要继承了父类,其派生类的虚函数表中的父类部分的排列就与父类一样。子类新定义的虚函数会按照声明顺序紧跟其后。
3. 构造函数:
①先调用父类构造函数
②然后按照声明顺序调用成员数据变量的构造函数和初始化列表中指定的成员
③最后再执行子类构造函数的函数体。
说明:
①父类构造函数,虚表指针修改为指向父类的虚表,所以在父类构造函数内调用虚函数,调用的是父类的虚函数。
②子类构造函数,虚表指针修改为指向子类的虚表
4. 析构函数:
①先调用子类析造函数
②然后成员对象的析构函数,按照声明的顺序以倒序方式依次调用成员对象的析构函数。
③再执行父类构造函数
说明:
- 析构函数执行会首先设置虚表指针为自身虚表,再调用自身的析构函数。防止父类析构函数内调用子类对象的虚函数。
- 类有派生与继承关系,需要声明析构函数为虚函数。若析构函数不是虚函数时,当使用父类指针指向堆中的子类对象,并使用delete释放对象空间时,编译器会按照指针类型调用父类的析构函数,从而引发错误。
识别类之间的关系:
先定位构造函数,根据构造先后顺序得到与之相关的其他类。
再根据虚表,利用IDA中使用引用参考功能可得到所有的构造和析构函数。
二、多重继承
1. 内存排列:
- 数据成员的排列顺序由继承父类的先后顺序所决定,从左向右依次排列。
- 子类虚表指针的个数取决于所继承的父类的个数,有几个父类便对应几个虚表指针(虚基类除外)。
- 将一个子类对象赋值给某个父类指针时,该父类指针便指向该父类所对应的虚表指针。
三、单类继承与多重继承比较:
- 单继承类
- 在类对象占用的内存空间中,只保存一份虚表指针
- 只有一个虚表指针,对应的也只有一个虚表
- 虚表中各项保存了类中各虚函数的首地址
- 构造时先构造父类,再构造自身,并且只调用一次父类构造函数
- 析构时限析构自身,再析构父类,并且只调用一次父类析构函数
- 多重继承类
- 在类中所占用的内存空间中,根据继承父类的个数保存对应的虚表指针
- 根据所保存的虚表指针的个数,对应产生相应个数的虚表。
- 转换父类指针时,需要跳转到对象的首地址。
- 构造时需要按照继承顺序调用多个父类构造函数。
- 析构时先析构自身,然后以与构造函数相反的顺序调用所有父类的析构函数
- 当对象作为成员时,整个类对象的内存结构和多重继承相似。当类中无虚函数时,整个类对象内存结构和多重继承完全一样,可酌情还原;当父类或成员对象存在虚函数时,通过过观察虚表指针的位置和构造函数、析构函数中填写虚表指针的数目及目标地址,来还原继承或成员关系。