《C++多态的底层原理和虚函数表的那些事 一》

Posted 程序员Edison

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《C++多态的底层原理和虚函数表的那些事 一》相关的知识,希望对你有一定的参考价值。

多态是面向对象编程的一个重要特性,如果你只知道“在公有继承中,父类指针指向子类对象的地址,父类指针再去调用公共接口”这句话,那赶紧去找本《Effective C++》看看压压惊。基于自己日常的积累和学习,本文将详细地介绍下多态的底层原理和虚函数表,由于涉及的内容较多,我将分两篇进行介绍。
先剖析多态的原理。C++中多态分为两种,一种是静多态,另一种是动多态。
静多态就是函数重载,通过改变函数参数的个数、类型、类型顺序,让编译器在编译期间,通过“命名倾轧”将同一个函数名修饰成不同名称的符号,这样便实现了同样函数名的两个函数执行不同的逻辑。
动多态就是我们常见的“多态”,为什么称为“动多态”,因为多态的特性是在程序运行才被体现出来。换言之,在编译期间,编译器并不知道这段代码是要实现多态,那么如何实现动多态?这里涉及到了C++的 RTTI(运行时类型信息)技术,C++提供了typeid和dynamic_cast实现了类型信息的判断,咱们看一段简单的代码。
class AA{public: virtual void f(){ cout << "AA::virtual void f()" << endl; } virtual void g(){ cout << "AA::virtual void g()" << endl; } virtual void h(){ cout << "AA::virtual void h()" << endl; }    virtual void FUNC(){}private int a; int b;};class BB :public AA{public: virtual void f(){ cout << "BB::virtual void f()" << endl; } virtual void g(){ cout << "BB::virtual void g()" << endl; } virtual void h(){ cout << "BB::virtual void h()" << endl; } virtual void FUNC1(){}private:    int c;    int d;};class CC :public AA{public: virtual void f(){ cout << "CC::virtual void f()" << endl; } virtual void g(){ cout << "CC::virtual void g()" << endl; } virtual void h(){ cout << "CC::virtual void h()" << endl; }
};int main(){ AA *pa = new BB; AA *pa1 = new CC; AA *pa2 = new BB; if (typeid(*pa).name() != typeid(*pa1).name()) { cout << "pa和pa1不是同一类型的指针"<< endl; cout << typeid(*pa).name() << endl; cout << typeid(*pa1).name() << endl; } if (typeid(*pa).name() == typeid(*pa2).name()) { cout << "pa和pa2是同一类型的指针" << endl; cout << typeid(*pa).name() << endl; cout << typeid(*pa2).name() << endl; }    //为什么父类指针转换成子类指针需要强转,而子类转父类可以直接转换?    //因为父类指针访问的内存范围小于子类的指针,如果用一个访问范围大的    //指针指向一块内存地址范围稍小的空间,将会产生不安全的行为。    //因此需要强转。但是反过来却不一样,将子类指针赋值给父类,也就是用    //一个访问范围小的指针指向一块范围较大的内存地址空间,无论如何也不会    //出现指针访问越界的情况,因此是安全的。 BB *pb = dynamic_cast<BB *>(pa); if (pb) { cout << "pa是指向BB类对象内存地址的指针" << endl; } CC *pc = dynamic_cast<CC *>(pa1); if (pc) { cout << "pa1是指向CC类对象内存地址的指针" << endl;    }    BB *pb1 = dynamic_cast<BB *>(pa1); if (pb1 == nullptr) {        cout << "pa1不是指向BB类对象内存地址的指针" << endl;    }     return 0;}
附上这段代码运行的结果:
AA *pa = new BB;
当我们完成指针pa的初始化时,C++自带的RTTI便能为我们判断pa是什么类型的指针,应该去访问谁的内存空间;也即父类指针可以调用到子类的成员函数,咋一看离多态是不是进了一步?好!我们继续,那如何去调用“公共接口”,何为“公共接口”,也即是父类和子类中都存在相同的函数(同名同参同返回),而且都是虚函数。当一个类中有虚函数时,该类对象内存空间便会预留一段空间出来用于存储虚函数,所有的虚函数用一个虚函数表来装载。
在上述示例代码中,BB公有继承自AA,而且BB覆写了AA中三个虚函数,那么BB类对象中虚函数表是怎样的?我们在ubuntu64位系统上进行了验证,使用Clang 命令( clang -Xclang -fdump-record-layouts PolyMorphism.cpp )打印出BB类对象的内存布局。

《C++多态的底层原理和虚函数表的那些事 一》

从上图可以看出类BB对象的内存布局,该对象内存首地址存放的是从AA继承过来的虚函数表指针,那有同学就会好奇,BB类对象的虚函数表去哪里了? 带着疑问,我们再看看BB类对象中虚函数表的布局。

我们可以看到Vtable for ‘BB’这一项,该项偏移量为0的位置是offset_to_top(0),它表示虚函数表离类BB对象内存首地址的偏移量为0,紧接着是RTTI指针,该指针存储是运行时类型信息(上文有描述),从上图可以看到类AA对象和类BB对象内存中虚函数表共用一块内存地址(有点类似联合体union),由于BB只覆写了AA中g()、h()、f()这三个接口函数,因此AA类对象虚函数表的前三个虚函数被BB类的虚函数给覆盖了。
分析到这一步,我们继续回到上面的问题,父类指针pa如何调用公共接口?现在我们应该很清晰了。父类指针pa是属于BB类型的指针,当它访问公共接口f()时,将直接调用BB类对象内存首地址虚函数表中的f(),自然就可以执行BB类中f()函数的程序逻辑了。至于如何寻找指定的虚函数,父类指针也是先找到类对象在内存地址空间中虚函数表的地址,然后再逐个进行偏移查找,找到和pa指针调用接口名相同的函数符号,便执行与该函数符号对应地址空间的代码逻辑(虚函数表存储了虚函数的地址)。
现在讲解完了多态的底层原理,下一节将着重讲解虚函数表,比如,面试官经常问的问题,在菱形继承中,各类对象的内存布局是怎样的?虚函数表的布局又是怎样的?以前我遇到这类问题也是一脸懵逼。


以上是关于《C++多态的底层原理和虚函数表的那些事 一》的主要内容,如果未能解决你的问题,请参考以下文章

C++多态底层剖析

C++多态底层剖析

C++中的多态和虚函数及多态原理

C++中的多态和虚函数及多态原理

C++多态 --- 多态实现原理简析

在c++中虚函数和多态性是啥意思