讨论主题:分析C++继承关系的实现原理。
继承关系下的构造和析构
构造由内而外。先调用基类Base的构造函数,但后执行自己的构造函数。
析构由外而内。先析构自己,再调用Base的析构函数。
值得注意的,base class的析构函数需要申明为virtual,否则在析构时,派生类仅析构基类,而析构自身,可能导致内存泄漏。
class Base{
public:
Base(){
cout << “Base constructor” << endl;
}
virtual ~Base(){
cout << “Base destructor” << endl;
}
};
class Derived : public Base{
public:
Derived() : Base() {
cout << “Derived constructor” << endl;
}
~Derived(){
cout << “Derived destructor” << endl;
}
};
{
Base* p = new Derived();
delete p;
}
根据上诉分析,打印结果为:
Base constructor
Derived constructor
Derived destructor
Base destructor
若基类的析构函数未申明为virtual,则delete p时不会执行Derived析构函数。执行结果为:
Base constructor
Derived constructor
Base destructor
如果在派生类中动态分配了内存,最后执行析构函数中释放内存操作,则造成内存泄漏。
为什么声明为virtual就可以执行派生类的析构函数???
继承+复合关系下的构造和析构
派生类Derived不仅继承Base,还包含Component对象。此时,构造Derived对象时,先构造Base,后构造Component,最后构造Derived。析构时,顺序和构造方向相反。
class Base{
public:
Base() {
cout << “Base constructor” << endl;
}
virtual ~Base() {
cout << “Base destructor” << endl;
}
};
class Component {
public:
Component() {
cout << “Component constructor” << endl;
}
~ Component() {
cout << “Component destructor” << endl;
}
};
class Derived : public Base {
private:
Component c;
public:
Derived() : Base() {
cout << “Derived constructor” << endl;
}
~Derived() {
cout << “Derived destructor” << endl;
}
};
{
Base* p = new Derived();
delete p;
}
上诉的分析得出打印的结果为:
Base constructor
component constructor
Derived constructor
Derived destructor
component destructor
Base destructor
虚指针和虚表
含有virtual修饰的类成员函数时,会多分配4bytes作为虚表指针,位于类对象的首部。
虚表中全是函数指针,按照类中虚函数的声明顺序依次存放,并以‘\0’结束。
#include <iostream>
using namespace std;
class A{
private:
int m_data1, m_data2;
public:
virtual void vfunc1(){};
virtual void vfunc2(){};
void func1(){};
void func2(){};
};
class B : public A{
private:
int m_data3;
public:
virtual void vfunc1(){};
void func2(){};
};
class C : public B{
private:
int m_data1, m_data4;
public:
virtual void vfunc1(){};
void func2(){};
};
int main()
{
A* ptrA = new A();
B* ptrB = new B();
C* ptrC = new C();
cout << "A size: " << sizeof(*ptrA) << endl;
cout << "B size: " << sizeof(*ptrB) << endl;
cout << "C size: " << sizeof(*ptrC) << endl;
cout << "A vptr: " << ptrA << endl;
cout << "B vptr: " << ptrB << endl;
cout << "C vptr: " << ptrC << endl;
unsigned int *vtblA = (unsigned int *)(*(unsigned int *)ptrA);
unsigned int *vtblB = (unsigned int *)(*(unsigned int *)ptrB);
unsigned int *vtblC = (unsigned int *)(*(unsigned int *)ptrC);
cout << "A::vfunc1 addr: " << hex << vtblA[0] << endl;
cout << "B::vfunc1 addr: " << hex << vtblB[0] << endl;
cout << "C::vfunc1 addr: " << hex << vtblC[0] << endl;
cout << "A::vfunc2 addr: " << hex << vtblA[1] << endl;
cout << "B::vfunc2 addr: " << hex << vtblB[1] << endl;
cout << "C::vfunc2 addr: " << hex << vtblC[1] << endl;
cout << "A::vbtl_end: " << hex << vtblA[2] << endl;
cout << "B::vbtl_end: " << hex << vtblB[2] << endl;
cout << "C::vbtl_end: " << hex << vtblC[2] << endl;
delete ptrA;
delete ptrB;
delete ptrC;
return 0;
}
在上例中,类间关系为:类A为基类,类B继承A,类C继承B。
类大小:类A中有2个int成员变量并存在虚函数,大小为4*2+4=12bytes。
类B继承A,且B私有1个int成员变量,大小为12+4=16bytes。
类C继承B,私有2个int成员变量,大小为16+4*2=24bytes。
类间虚函数关系,类A两个虚函数vfunc1(),vfunc2();类B覆盖了vfunc1(),继承vfunc2();
类C也覆盖了vfunc1(),继承了vfunc2()。故,在虚表中,vbtl[1]应该指向同一个vfunc2(),也就是基类的vfunc2(),但vbtl[0]各自指向自己的vfunc1()。
测试结果为:
this指针
调用非静态成员函数,传参时总会带有this指针。这是实现动态绑定,实现多态的基础。
Applicaion framework
CDocument :: OnFileOpen(){
…
Serialize(); //this->Serialize() == (*(this->vptr)[n])(this)
…
}
virtual Serialize();
Application
Class CMyDoc : Public CDocument
{
virtual Serialize() { … }
}
main(){
CMyDoc myDoc;
myDoc.OnFileOpen();
}
主程序main中,定义对象myDoc,随后通过该对象调用OnFileOpen。实际上可以翻译成
CDocument::OnFileOpen(&myDoc); //&myDoc相当于this
在OnFileOpen()中,进一步调用虚函数Serialize(),此时的this指向myDoc。所以通过this得到虚表指针,然后调用虚表中Serialize函数。这也实现了指针指向谁,就调用谁的虚函数。
Dynamic Binding,动态绑定
在C中函数调用都是静态绑定,在编译器时,就确定了函数入口地址,改变不了。这也就没办法实现多态。要实现动态绑定,在运行时,在确定函数入口地址,则需要额外的参数,它就是this指针。
B b;
A a = (A)b;
a.vfunc1();
A* pa = new B;
pa->vfunc1();
pa = &b;
pa->vfunc1();
借用上面A、B、C得继承关系。a.vfunc1()这属于静态绑定,调用call xxx_addr。因为在编译是已经确定了a.vfunc1()中this就是&a。而要实现多态,需要通过引用或指针的方式。下面两个pa->vfunc1()都属于动态绑定。调用call dword ptr [edx],不是call绝对地址。
为什么需要指针或引用调用虚函数才能实现多态???