深入剖析C++虚函数机制和继承

大家都知道虚函数是C++的精髓,虚函数和继承配合则展现出了多态这一强大的特性,为了剖析此类机制下的对象内存布局问题,下面使用多个实例代码展现其中的内存模型分布。(读《深度探索C++对象模型》有感)。说明:下面所有的代码均在win10和VS2019开发环境下跑通。

1、单继承下基类只有一个虚函数

class Base
{
public:
    virtual void funcBase() {}
public:
    int dataBase;
};
class Derived : public Base
{
public:
    int dataDerived;
};

int main(int argc, char* argv[]) 
{
    Derived d;
    std::cout << sizeof(d) << std::endl;

    return  0;
}

下面使用VS2019自带的x86 Native Tools Command Prompt工具打印出对象的内存分布图。方法如下,首先进入x86 Native Tools Command Prompt工具环境中,使用cd命令直到进入到存放cpp代码文件的目录,而后使用命令

cl main.cpp /d1 reportSingleClassLayoutDerived

(注意字母l和数字1)。一般reportSingleClassLayout后面跟随的就是你想打印出对象所属的类名。这里是Derived,因此最后是这样的命令格式reportSingleClassLayoutDerived,打印出的内存布局如下:


1.png

下面就此说明下,第一行打印出这个类的内存大小为12字节。
下面是类Derived的内存布局了,由于Derived继承自Base类,并且Base类中有一个虚函数,因此出现了一个虚函数表指针vfptr,左边的0表示的是偏移量,0表示开始的基准位置。虚函数表指针之后是Base类中的数据成员dataBase,因为虚函数表指针是一个指针,在32位机器中占据4个字节,因此dataBase的起始位置是4,最后存放的是Derived类中的数据成员dataDerived,dataBase是int型,占4个字节,所以dataDerived的起始位置偏移量是8。dataDerived也是int型,也占4个字节,因此总共类的字节大小是12字节。

2、单继承,基类和子类中均含有虚函数和数据成员

class Base
{
public:
    virtual void func() {}
public:
    int dataBase;
};

class Derived : public Base
{
public:
    virtual void func1() {}
public:
    int dataDerived;
};
int main(int argc, char* argv[]) 
{
    Derived d;
    std::cout << sizeof(d) << std::endl;

    return  0;
}

使用命令

cl main.cpp /d1 reportSingleClassLayoutDerived
2.png

咦?类的大小怎么还是12个字节。且看答案:和情景1中一样,类Derived继承自类Base,内存首先存放的是虚函数表指针,从vftable中可以看出,虚函数表指针中存放了两个虚函数地址,一个是基类Base中的虚函数,紧接着才是Derived类中的虚函数。尽管这里多一个虚函数,但是并不影响类内存大小。

3、多重继承,基类和子类中均含有虚函数和数据成员

class A
{
public:
    virtual void func1() {}
public:
    int data1;
};

class B : public A
{
public:
    virtual void func2() {}
public:
    int data2;
};

class C : public B
{
public:
    virtual void func3() {}
public:
    int data3;
};
int main(int argc, char* argv[]) 
{
    C c;
    std::cout << sizeof(c) << std::endl;

    return  0;
}
3.png

多重普通继承下,最先调用的是祖先类构造函数,而后是父类的构造函数,最后才是子类自己的构造函数。这里需要注意的是在虚函数表中,首先存放A中虚函数地址,而后是B中虚函数,最后才是C中虚函数。下面是C中虚函数出现覆盖基类的情况

class A
{
public:
    virtual void func1() {}
public:
    int data1;
};

class B : public A
{
public:
    virtual void func2() {}
public:
    int data2;
};

class C : public B
{
public:
    virtual void func1() {}//覆盖A中的func1函数
public:
    int data3;
};
int main(int argc, char* argv[]) 
{
    C c;
    std::cout << sizeof(c) << std::endl;

    return  0;
}
4.png

和上面的内存布局图相比,确实最后子类的虚函数地址覆盖了基类A中同名虚函数的地址

4、多继承,基类和子类中均含有虚函数和数据成员

class A
{
public:
    virtual void funcA1() {}
    virtual void funcA2() {}
public:
    int data1;
};

class B
{
public:
    virtual void funcB1() {}
    virtual void funcB2() {}
public:
    int data2;
};

class C : public A, public B
{
public:
    virtual void funcC1() {}
    virtual void funcC2() {}
public:
    int data3;
};

类C的内存布局图:


5.png

在多继承下,继承的两个类中均含有虚函数,因此有两个虚函数表存在,就有两个虚函数表指针。这里需要注意的是子类C中的虚函数存放的是在A中最后一个虚函数后面依次存放。

5、单虚继承,虚基类和派生类均含有虚函数和数据成员

class A
{
public:
    virtual void funcA1() {}
public:
    int data1;
};

class C : virtual public A
{
public:
    virtual void funcC2() {}
public:
    int data2;
};

类C的内存布局图:


6.png

在虚拟继承下,会多出来一个虚基类表,因此就有虚基类表指针的存在。这里派生类中含有虚函数,在虚继承下,内存首先为派生类进行分配空间,因此先存放的是属于C中的虚函数表指针,里面存放的是C中的虚函数的地址。而后是虚基类表指针,指向的是一个虚基类表,虚基类表中主要存放的是两类内容,第一类是派生类内存首地址相对于虚基类表指针地址的偏移量,第二类内容是虚基类的内存首地址相对于虚基类表指针地址的偏移量。看vbtable中的上下两项,第一项说明的是属于C中虚表指针在先,而后才是虚基类表指针,-4表示向前4个字节;而第二项说明的是虚基类A的首地址在虚基类表指针地址之后8个字节的位置。
如果派生类中没有虚函数的情况:

class A
{
public:
    virtual void funcA1() {}
public:
    int data1;
};

class C : virtual public A
{
public:
    int data2;
};
7.png

派生类中没有虚函数,也就没有了虚函数表指针,内存起始位置存放的就是虚基类表指针指针,因此虚基类表中的第一项地址偏移量是0。第二项还是向后8个字节的位置。

6、多虚拟继承下的对象内存布局情况

class A
{
public:
    virtual void funcA1() {}
public:
    int data1;
};

class B
{
public:
    virtual void funcB1() {}
public:
    int data2;
};

class D
{
public:
    virtual void funcD1() {}
public:
    int data4;
};

class C : virtual public A, virtual public B, virtual public D
{
public:
    virtual void funcC1() {}
public:
    int data3;
};
8.png

虚基类和派生类中均含有虚函数,并且均有数据成员
派生类C的对象内存布局是:
第一个存放派生类中的虚函数表指针
第二个存放的是虚基类表指针。虚基类表指针指向的是一个虚基类表。虚基类表中第一项是派生类地址相对于虚基类表指针地址的偏移,这里是-4,
因为C的虚表指针在前,虚基类表指针在后,一个指针的字节大小是4.虚基类表中第二项开始依次存放的是虚基类A地址相对于虚基类表指针地址
的偏移量,这里是8;之后是虚基类B地址相对于虚基类表指针地址的偏移量,这里是16;再之后是虚基类D相对于虚基类表指针地址的偏移量,
这里是24.
第三个存放的是派生类C中自己的数据成员
第四个存放的是虚基类A的虚表指针
第五个存放的是虚基类A中的数据成员
第六个存放的是虚基类B的虚表指针
第七个存放的是虚基类B中的数据成员
第八个存放的是虚基类D的虚表指针
第九个存放的是虚基类D中的数据成员
总共sizeof(C)是36个字节,按照4字节对齐的方式

7、多继承中基于虚拟继承又有普通继承情况下

class A
{
public:
    virtual void funcA1() {}
public:
    int data1;
};

class B
{
public:
     virtual void funcB1() {}
public:
    int data2;
};

class D
{
public:
      virtual void funcD1() {}
public:
    int data4;
};

class C : virtual public A,  public B, public D
{
public:
    virtual void funcC1() {}
public:
    int data3;
};
9.png

谁有虚函数,谁就先存储。比方说某一个基类中有函数,另外的没有,那么先存储这个基类的虚表指针,之后再是该基类的数据成员。而后才是其他普通继承的基类的数据成员。普通继承的基类内存分布完毕之后,是虚基类表指针,之后是派生类自己的数据成员。最后才是虚基类中的虚表指针和虚基类A中的数据成员。

8、菱形继承下的对象内存布局

class A
{
public:
    virtual void funcA1() {}
public:
    int data1;
};

class B : virtual public A
{
public:
    virtual void funcB1() {}
public:
    int data2;
};

class C : virtual public A
{
public:
    virtual void funcC1() {}
public:
    int data3;
};

class D : public B, public C
{
public:
    virtual void funcD1() {}
public:
    int data4;
};

由于类D多继承自类B和类C,先看类B和类C中哪个有虚函数,一看均有,那么首先存放的是类B的虚函数表
第一个内存空间存放的是类B的虚函数表指针,由于派生类D中也有虚函数,因此也存放在这个虚函数表中
第二个内存空间存放的是属于类B中的虚基类表指针,因为类B虚继承自类A
第三个内存空间存放的是类B的数据成员
第四个内存空间存放的是类C的虚函数表
第五个内存空间存放的是类C的虚基类表指针
第六个内存空间存放的是类C的数据成员
第七个内存空间存放的是派生类D的数据成员
第八个内存空间存放的是虚基类A的虚函数表指针
第九个内存空间存放的是虚基类A的数据成员

9、菱形继承下带有虚拟继承和普通继承的多继承

class A
{
public:
    virtual void funcA1(){}
public:
    int data1;
};

class B : virtual public A
{
public:
    virtual void funcB1() {}
public:
    int data2;
};

class C : virtual public A
{
public:
    virtual void funcC1() {}
public:
    int data3;
};

class D : virtual public B,  public C
{
public:
    virtual void funcD1() {}
public:
    int data4;
};

第一个内存空间存放的是类C的虚函数表指针,由于派生类D中也有虚函数,因此也存放在这个虚函数表中
第二个内存空间存放的是属于类C中的虚基类表指针,因为类C虚继承自类A
第三个内存空间存放的是类C的数据成员
第四个内存空间存放派生类D的数据成员
第五个内存空间存放的是虚基类A的虚函数表指针
第六个内存空间存放的是虚基类A的数据成员
第七个内存空间存放的是类B的虚函数表
第八个内存空间存放的是类B的虚基类表指针
第九个内存空间存放的是类B的数据成员

10、菱形继承下纯带有虚拟继承的多继承

class A
{
public:
    virtual void funcA1() {}
public:
    int data1;
};

class B : virtual public A
{
public:
    virtual void funcB1() {}
public:
    int data2;
};

class C : virtual public A
{
public:
    virtual void funcC1() {}
public:
    int data3;
};

class D : virtual public B, virtual public C
{
public:
    virtual void funcD1() {}
public:
    int data4;
};

第1: D虚表指针
第2:D虚基类表指针
第3:D数据成员data4
第4:A虚表指针
第5:A数据成员data1
第6:B虚表指针
第7:B虚基类表指针
第8:B数据成员data2
第9:C虚表指针
第10:C虚基类表指针
第11:C数据成员data3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容

  • 一个博客,这个博客记录了他读这本书的笔记,总结得不错。《深度探索C++对象模型》笔记汇总 1. C++对象模型与内...
    Mr希灵阅读 5,577评论 0 13
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,514评论 1 51
  • 3. 类设计者工具 3.1 拷贝控制 五种函数拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符析构函数拷贝和移...
    王侦阅读 1,797评论 0 1
  • C++虚函数 C++虚函数是多态性实现的重要方式,当某个虚函数通过指针或者引用调用时,编译器产生的代码直到运行时才...
    小白将阅读 1,734评论 4 19
  • 岁月匆匆,似白驹过隙一半,倏地,我已从一个咿呀学语的小娃变成了一个中学生,还没等我细细品尝,童年就弃我而去了。 儿...
    倾我一生一世恋阅读 158评论 0 0