继承和多态是面向对象语言最强大的功能。有了继承和多态,我们可以完成代码重用。在C中有许多技巧可以实现多态。本文的目的就是演示一种简单和容易的技术,在C中应用继承和多态。通过创建一个VTable(virtual table)和在基类和派生类对象之间提供正确的访问,我们能在C中实现继承和多态。VTable能通过维护一张函数表指针表来实现。为了提供基类和派生类对象之间的访问,我们可以在基类中维护派生类的引用和在派生类中维护基类的引用。
继承
构造
- 当有父类时,以填虚表为界,反是有父类构造或者成员构造,都在填虚表之前
- 如果内联了父类的构造,判断填写虚表上是否也往同一个地址填虚表
- 子类构造前会先构造父类和成员构造
- 构造先父类虚表,在成员,在自身虚表
- 调用成员对象的构造函数
- 先填写子类自己的虚表指针
- 然后执行子类的构造函数体
析构
- 虚构先自身,在成员,在父类
- 先填写子类自己的虚表指针
- 然后执行子类的析构函数体
- 调用成员对象的析构函数体
- 然后调用父类的析构函数体
- 析构在父类是虚析构,子类也是虚析构
- cout是一个全局对象,endl是一个函数指针,flush也是函数指针
- 当有父类或成员构造的时候,构造以虚表为界线,前面为父类或成员,call传递this指针(里面还有call继续有父类);
- 析构先填写虚表,再虚构的原因:每个类了为了防止将来被继承
- 构造调用父类的构造函数,调用成员对象的构造函数指针,先填写子类自己的虚表,然后执行子类的构造函数体
- 析构先填写子类自己的虚表指针,然后执行子类的析构函数体,调用成员对象的析构函数指针,再调用父类的析构函数
- 纯虚父类被强制调用
- vc6.0 push 19h ,call asmg_exit(purecall)
- vs2019指针指向一个错误信息的收集
父类对象引用子类对象,没问题;子类对象引用父类对象,有风险
子类对象
- 子类对象构造函数
- 调用父类的构造函数
- 调用成员对象的构造函数
- 先填写子类自己的虚表指针
- 然后执行子类的构造函数体
- 子类对象析构函数
- 先填写子类自己的虚表指针
- 执行子类的析构函数体
- 调用成员对象的析构函数
- 调用父类的析构函数
识别类与类的关系
在C++的继承关系中,子类具有父类的所有的数据和成员函数。子类对象可直接使用父类中声明为公有和保护的数据成员与函数;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| class CBase { public: CBase(); ~CBase(); int m_nBase; void SetNumber(int n); int GetNumber(); private:
};
CBase::CBase() { }
CBase::~CBase() { }
int CBase::GetNumber() { return m_nBase; }
void CBase::SetNumber(int n) { m_nBase = n; }
class CDerive : public CBase //派生类定义 { public: void ShowNumber(int nNumber); };
void CDerive::ShowNumber(int nNumber) { int m_nCDerive; SetNumber(nNumber); m_nCDerive = nNumber + 1; printf("%d\n", GetNumber()); printf("%d\n", m_nCDerive); }
int _tmain(int argc, _TCHAR* argv[]) { CDerive Derive; Derive.ShowNumber(argc); return 0; }
|
父类中定义了成员函数、构造函数、析构函数和两个成员函数。子类中只有一个成员函数一个数据成员。根据C++语法规则,子类将继承父类中的成员数据和成员函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| CDerive Derive; 00FD16BD lea ecx,[Derive] ;获取对象首地址作为this指针 00FD16C0 call CDerive::CDerive (0FD10A5h) ;调用子类的构造函数,编译器默认提供 00FD16C5 mov dword ptr [ebp-4],0 Derive.ShowNumber(argc); 00FD16CC mov eax,dword ptr [argc] 00FD16CF push eax 00FD16D0 lea ecx,[Derive] 00FD16D3 call CDerive::ShowNumber (0FD1032h) ;调用 Derive成员函数,传入this指针 return 0; 00FD16D8 mov dword ptr [ebp-0E0h],0 00FD16E2 mov dword ptr [ebp-4],0FFFFFFFFh 00FD16E9 lea ecx,[Derive] 00FD16EC call CDerive::~CDerive (0FD10E6h) ;调用Derive的析构函数,编译器默认提供 00FD16F1 mov eax,dword ptr [ebp-0E0h] }
;子类默认构造函数分析 CDerive::CDerive: 00FD146F pop ecx ;还原this指针 00FD1470 mov dword ptr [this],ecx 00FD1473 mov ecx,dword ptr [this] ;以子类对象首地址作为父类的this指针,调用父类构造函数 00FD1476 call CBase::CBase (0FD1172h) ;父类构造函数 00FD147B mov eax,dword ptr [this] 00FD1491 ret
;子类默认析构函数分析 CDerive::~CDerive: 00FD150F pop ecx ;还原this指针 00FD1510 mov dword ptr [this],ecx 00FD1513 mov ecx,dword ptr [this] ;以子类对象作为父类的this指针,调用父类构造函数 00FD1516 call CBase::~CBase (0FD1028h) ;调用父类的析构函数 00FD152E ret
|
编译器提供了默认的构造与析构函数。当子类中没有构造函数或析构函数时,而其父类却需要构造函数与析构函数时,编译器会为该父类的子类提供默认的析构函数与构造函数;
由于子类继承了父类,因此子类中拥有父类的各成员,类似于在子类中定义了父类的对象作为数据成员使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class CBase { public: CBase(); ~CBase(); int m_nBase; void SetNumber(int n); int GetNumber(); private:
};
class CDerive { public: CBase m_Base; int m_nDerive; };
|
父类中有构造函数,但子类中没有构造函数时,编译器会默认为子类生成构造函数,以实现成员构造函数的调用
子类中有构造函数,但父类中没有构造函数,编译器不会默认为父类提供构造函数
子类对象在内存中的数据排列为:先安排父类数据、后安排子类新定义的数据;
在已有初始化列表的情况下,将会优先执行初始化列表中的操作;其次才是自身的构造函数;
顺序为:
- 先构造父类
- 按顺序构造成员对象和初始化列表
- 最后自身的构造函数
多父类
- 构造和析构时会填多次次虚表
- 多次虚表部分各覆盖了多次虚表内容
- 识别双父类:观察填写虚表的次数
菱形结构
- 菱形结构可以设计为组合关系或者聚合关系
- 多重继承时,构造会多一个参数(bool值),表示是否构造基类
- 多重继承会在构造先填baseoffset再填虚表
- 寻找基类的虚表地址:
- (baseoffset:记录基类偏移的结构体)
- this指针+4 得到baseoffset的指针
- baseoffset指针+4得到baseoffset偏移
- this指针+4+baseoffset偏移得到虚表地址
内存结构: