继承和多态是面向对象语言最强大的功能。有了继承和多态,我们可以完成代码重用。在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偏移得到虚表地址 
 
 

内存结构:
