C++ 一篇搞懂多态的实现原理( 三 )


我们用 sizeof 来运算有有虚函数的类和没虚函数的类的大小,会是什么结果呢?
class A {public:int i;virtual void Print() { } // 虚函数};class B{public:int n;void Print() { } };int main() {cout << sizeof(A) << ","<< sizeof(B);return 0;}在64位机子,执行的结果:
16,4从上面的结果,可以发现有虚函数的类,多出了 8 个字节,在 64 位机子上指针类型大小正好是 8 个字节,这多出 8 个字节的指针有什么作用呢?
01 虚函数表每一个有「虚函数」的类(或有虚函数的类的派生类)都有一个「虚函数表」,该类的任何对象中都放着 虚函数表的指针  。「虚函数表」中列出了该类的「虚函数」地址 。
多出来的 8 个字节就是用来放「虚函数表」的地址 。// 基类class Base {public:int i;virtual void Print() { } // 虚函数};// 派生类class Derived : public Base{public:int n;virtual void Print() { } // 虚函数};上面 Derived 类继承了 Base类,两个类都有「虚函数」,那么它「虚函数表」的形式可以理解成下图:

C++ 一篇搞懂多态的实现原理

文章插图
 
多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中 存放的虚函数表的地址 ,在虚函数表中查找虚函数地址,并调用虚函数的指令 。
02 证明虚函数表指针的作用在上面我们用 sizeof 运算符计算了有虚函数的类的大小,发现是多出了 8 字节大小(64位系统),这多出来的 8 个字节就是指向「虚函数表的指针」 。「虚函数表」中列出了该类的「虚函数」地址 。
下面用代码的例子,来证明「虚函数表指针」的作用:
// 基类class A {public:virtual void Func()// 虚函数{cout << "A::Func" << endl;}};// 派生类class B : public A {public:virtual void Func()// 虚函数{cout << "B::Func" << endl;}};int main() {A a;A * pa = new B();pa->Func(); // 多态// 64位程序指针为8字节int * p1 = (int *) & a;int * p2 = (int *) pa;* p2 = * p1;pa->Func();return 0;}输出结果:
B::FuncA::Func
  • 第 25-26 行代码中的 pa 指针指向的是 B 类对象,所以 pa->Func() 调用的是 B 类对象的虚函数 Func() ,输出内容是 B::Func ;
  • 第 29-30 行代码的目的是把 A 类的头 8 个字节的「虚函数表指针」存放到 p1 指针和把 B 类的头 8 个字节的「虚函数表指针」存放到 p2 指针;
  • 第 32 行代码目的是把 A 类的「虚函数表指针」 赋值给 B 类的「虚函数表指针」,所以相当于把 B 类的「虚函数表指针」 替换 成了 A 类的「虚函数表指针」;
  • 由于第 32 行的作用,把 B 类的「虚函数表指针」 替换 成了 A 类的「虚函数表指针」,所以第 33 行调用的是 A 类的虚函数 Func() ,输出内容是 A::Func
通过上述的代码和讲解,可以有效的证明了「虚函数表的指针」的作用,「虚函数表的指针」指向的是「虚函数表」,「虚函数表」里存放的是类里的「虚函数」地址,那么在调用过程中,就能实现多态的特性 。
虚析构函数析构函数是在删除对象或退出程序的时候,自动调用的函数,其目的是做一些资源释放 。
那么在多态的情景下,通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数,这就会存在派生类对象的析构函数没有调用到,存在资源泄露的情况 。
看如下的例子:
// 基类class A {public:A()// 构造函数{cout << "construct A" << endl;}~A() // 析构函数{cout << "Destructor A" << endl;}};// 派生类class B : public A {public:B()// 构造函数{cout << "construct B" << endl;}~B()// 析构函数{cout << "Destructor B" << endl;}};int main() {A *pa = new B();delete pa;return 0;}输出结果:
construct Aconstruct BDestructor A从上面的输出结果可以看到,在删除 pa 指针对象时,B 类的析构函数没有被调用 。
解决办法:把基类的析构函数声明为virtual
  • 派生类的析构函数可以 virtual 不进行声明;
  • 通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数,还是遵循「先构造,后虚构」的规则 。
将上述的代码中的基类的析构函数,定义成「虚析构函数」:
// 基类class A {public:A(){cout << "construct A" << endl;}virtual ~A() // 虚析构函数{cout << "Destructor A" << endl;}};


推荐阅读