浅析C++对象布局

C++这门语言,几乎每个学校在大一的时候,都会去学习。但是其内在的对象布局,以及virtual机制,我们又了解多少呢。为了仔细了解了解,前几天决定找点书来啃啃。然后就听闻网上的很多人推荐<深度探究C++对象模型>,就开始啃......但是可能是书比较老的缘故,很多说的很稳的道理,一实验就翻车了。所以,为了下次不翻车,就根据这本书中对象布局的讲述结构,以及查阅各种资料,在电脑实验,写下这篇博客。如果你想很清楚的弄明白C++多态,继承(单,多,虚),以及virtual机制。那么就值得一看。

实验环境:win10__Dev C++5.7.1__gcc 4.8.1__32-bit

C++对象模型

在谈对象布局之前,我们需要知道。在C++中,有俩种类成员数据:staticnonstatic。以及三种类成员函数:staticnonstaticvirtual。那么在一个类,是如何错综复杂的摆放,上述的俩种数据成员,三种函数成员呢。其实说到底,就是分析下面五种模型。

  • 无继承有虚函数
  • 单继承无虚函数
  • 单继承有虚函数(存在多态)
  • 多重继承
  • 虚拟继承(菱形继承)

0x01.无继承有虚函数

考虑如下代码。

class Point {
public:
    Point(float val);
    virtual ~Point();
    
    float x() const;
    static int PointCount();

private:
    virtual ostream& print(ostream& os) const;
    
    float _x;
    static int _point_count;
};
无继承有虚函数.png

成员摆放规则

  • 非静态数据成员:保存在每一个对象里
  • 静态数据成员:不在对象内
  • 成员函数、静态函数:不在对象内
  • 虚函数:每个类产生一个虚函数表(virtual table, vtbl),里面存放着一堆指向虚函数的指针。并且每个对象里,安插一个指针(vptr)指向该虚表。也就是说,多个对象共享一张虚表。
class Point {
public:
    Point(){ }
    virtual ~Point() { }
    
private:
};

/************************
 * 在C++中,虚表并不一定是对象内排列的第一个,所以我们通过构造一个
 * 没有数据成员,但是却存在虚函数的类,使虚表排列在第一个。(现今,
 * 绝大多数编译器实现虚表时,都会把它放在最前面)至于成员分布,C++
 * Standard要求:较晚出现的members,在对象中具有较高的地址。
 ************************/
 
int main(){
    Point a, b;
    
    cout  << "a对象地址:" << &a << "\t" << "虚表地址:" << *(int*)&a << "\t" << endl;
    cout  << "b对象地址:" << &b << "\t" << "虚表地址:" << *(int*)&b << "\t" << endl;
    
    return 0;
}
// 输出:
// a对象地址:0x29fe8c     虚表地址:4738776
// b对象地址:0x29fe88     虚表地址:4738776

0x02.单继承无虚函数

考虑如下代码。

class Point2d {
public:
    Point2d():_x(0.0), _y(0.0) { }
    // 其他函数
protected:
    float _x;
    float _y;
};

class Point3d:public Point2d {
public:
    Point3d():Point2d(), _z(0.0){ }
    // 其他函数
protected:
    float _z;
};
单继承无虚函数.png

这种模型相比较简单一些,就是把数据成员排列。至于排列顺序,C++Standard已经有了相对的要求。见0x01 代码注释。

0x03.单继承有虚函数(存在多态)

考虑如下代码。

class Point2d {
public:
    Point2d():_x(0.0), _y(0.0) { }
    virtual ~Point2d(){ }
    virtual void func1() { cout << "Point2d::func1" << endl;}
    virtual void reload() { cout << "Point2d::reload" << endl;}
    // 其他函数
protected:
    float _x;
    float _y;
};

class Point3d:public Point2d {
public:
    Point3d():Point2d(), _z(0.0){ }
    virtual ~Point3d(){ }
    virtual void func2() { cout << "Point3d::func2" << endl;}
    void reload() { cout << "Point3d::reload" << endl;}
    // 其他函数
protected:
    float _z;
};

单继承有虚函数.png

注意:无论在派生类中有没有使用virtual函数,派生类中的虚表指针(vptr)都是指向另外一张表,与基类虚表(vtable)没有任何关系,不过除基类中的虚函数会被派生类的虚表所引用。但是如果出现虚函数被覆盖的情况,那么派生类虚表中的指针指向覆盖后的函数。如上图reloadfunc1的区别。通过派生类对虚函数的覆盖,然后向下转型,同一函数,不同体现,就是多态实现原理。测试代码如下:

/*******************************
 * 虚表(vtable)地址: *(int *)&a
 * 第一个虚函数地址:*(int *)*(int *)&a 
 * 第 i个虚函数地址:*(int *)*(int *)&a+i 
 * 但是在上图中,第一个却不是虚函数,当一个类的析够函数被定义为虚函
 * 数,它会在虚表中占俩个格子。调用vtable[0],vtable[1]都是析够函数
 * 。如果这个类是单个虚表的话,在vtable结束的地方,会补一个0.
 */
 
 typedef void(*fun)(void);
int main(){
    Point2d a;
    Point3d c;
    
    cout << "a对象虚表地址:" << *(int*)&a << endl;
    cout << "-------------------------------" << endl;
    for(int i = 0; *((int *)*(int *)&a+i) != 0; i++){
        cout <<"第" << i<<"个虚函数地址: "<< *((int *)*(int *)&a+i) << endl;
    } 
    cout << endl << endl;
    
    cout << "c对象虚表地址:" << *(int*)&c << endl;
    cout << "-------------------------------" << endl;
    for(int i = 0; *((int *)*(int *)&c+i) != 0; i++){
        cout <<"第" << i<<"个虚函数地址:  "<< *((int *)*(int *)&c+i) << endl;
    } 
    cout << endl;
    
    ((fun)*((int *)*(int *)&a+2))();    // 调用a.vtable[2]
    ((fun)*((int *)*(int *)&c+2))();    // 调用c.vtable[2]
    ((fun)*((int *)*(int *)&c+3))();    // 调用c.vtable[3]
    return 0;
}

// 输出结果:
// a对象虚表地址:4738904
// -------------------------------
// 第0个虚函数地址: 4318248
// 第1个虚函数地址: 4318216
// 第2个虚函数地址: 4318032
// 第3个虚函数地址: 4318080
// 
// c对象虚表地址:4738928
// -------------------------------
// 第0个虚函数地址:  4318500
// 第1个虚函数地址:  4318468
// 第2个虚函数地址:  4318032
// 第3个虚函数地址:  4318376
// 第4个虚函数地址:  4318328
// 
// Point2d::func1
// Point2d::func1
// Point3d::reload

多重继承

考虑如下代码。

class Point2d {
public:
    Point2d():_x(0.0), _y(0.0) { }
    virtual ~Point2d(){ }
    virtual void func1() { cout << "Point2d::func1" << endl;}
    //  其他函数
protected:
    float _x;
    float _y;
};

class Vertex {
public:
    Vertex(): next(NULL) { }
    virtual ~Vertex() { }
    virtual void func2() { cout << "Vertex::func2" << endl;}
    //其他函数
protected:
    Vertex* next;
};
class Vertex2d:public Point2d, public Vertex {
public:
    Vertex2d():Point2d(), Vertex(){ }
    virtual ~Vertex2d(){ }
    virtual void func3() { cout << "Vertex2d::func3" << endl;}
    //  其他函数
protected:
};
多重继承.png

在多重继承的模型下,派生类内部布局会出现不止一张虚表,具体根据继承情况而定。而派生类的虚函数会被加到其中的一个虚表里,一般而言,是第一个继承父类想类似的虚表中。并且第一个虚表的也不会以0结束。在我电脑上是以-12。测试代码如下:

int main(){
    Vertex2d a;
    Point2d b;
    Vertex c;
    
    cout << "a对象虚表地址:"<< endl;
    cout << "-------------------------------" << endl;
    for(int i=0,j=1; i < 5; i++) {
        if(*((int*)&a+i)) 
            cout << "第"<< j++ <<"虚表地址:" << *((int*)&a+i) << endl;
    }
    // 查找虚表
    
    // 如果不知道第一虚表以什么结束,尝试多输出几个
    for(int i = 0; *((int *)*(int *)&a+i) != 0 && *((int *)*(int *)&a+i) != -12; i++){
        cout <<"第1_" << i<<"个虚函数地址: "<< *((int *)*(int *)&a+i) << endl;
    }
    
    for(int i = 0; *((int *)*((int *)&a+3)+i) != 0 && *((int *)*(int *)&a+i) != -12; i++){
        cout <<"第2_" << i<<"个虚函数地址: "<< *((int *)*((int *)&a+3)+i) << endl;
    } 
    cout << endl << endl;
    
    cout << "c对象虚表地址:" << *(int*)&c << endl;
    cout << "-------------------------------" << endl;
    for(int i = 0; *((int *)*(int *)&c+i) != 0; i++){
        cout <<"第" << i<<"个虚函数地址:  "<< *((int *)*(int *)&c+i) << endl;
    } 
    cout << endl << endl;
    
    cout << "b对象虚表地址:" << *(int*)&b << endl;
    cout << "-------------------------------" << endl;
    for(int i = 0; *((int *)*(int *)&b+i) != 0; i++){
        cout <<"第" << i<<"个虚函数地址:  "<< *((int *)*(int *)&b+i) << endl;
    } 
    cout << endl << endl;  
    
   // ((fun)*((int *)*((int *)&a+3)+1))();
   // ((fun)*((int *)*(int *)&a+1))();

    return 0;
}

// a对象虚表地址:
//     第1虚表地址:4739048
//     第2虚表地址:4739072
// -------------------------------
// 第1_0个虚函数地址: 4319472
// 第1_1个虚函数地址: 4319440
// 第1_2个虚函数地址: 4319088
// 第1_3个虚函数地址: 4319336
// 第2_0个虚函数地址: 4670488
// 第2_1个虚函数地址: 4670480
// 第2_2个虚函数地址: 4318864
// 
// 
// c对象虚表地址:4739000
// -------------------------------
// 第0个虚函数地址:  4319008
// 第1个虚函数地址:  4318976
// 第2个虚函数地址:  4318864
// 
// 
// b对象虚表地址:4739024
// -------------------------------
// 第0个虚函数地址:  4319256
// 第1个虚函数地址:  4319224
// 第2个虚函数地址:  4319088

虚拟继承

回顾上面的多继承,假如Point2dVertex同时继承与另外一个基类,那么根据我们之前的分配规则,很容易可以得出,在派生类Vertex2d里面会存在俩份基类的数据,这还是不是最可怕的,可怕的时候,当我们修改的时候,编译器完全不知道我们要修改那一个。从而编译错误。那么有没有办法解决呢,有,虚拟继承。考虑如下代码。

class Point2d {
public:
    Point2d():_x(0.0), _y(0.0) { }
    virtual ~Point2d(){ }
    virtual void func1() { cout << "Point2d::func1" << endl;}
    //   其他函数
protected:
    float _x;
    float _y;
};

class Vertex: virtual public Point2d {
public:
    Vertex(): Point2d(), next(NULL) { }
    virtual ~Vertex() { }
    virtual void func2() { cout << "Vertex::func2" << endl;}
    // 其他函数
protected:
    Vertex* next;
};
class Point3d: virtual public Point2d {
public:
    Point3d():Point2d(), _z(0.0){ }
    virtual ~Point3d(){ }
    virtual void func3() { cout << "Point3d::func3" << endl;}
    //   其他函数
protected:
    float _z;
};

class Vertex3d:public Point3d, public Vertex {
public:
    Vertex3d():Point3d(), Vertex(){ }
    virtual ~Vertex3d(){ }
    virtual void func4() { cout << "Vertex3d::func4" << endl;}
    //   其他函数
protected:
};
虚拟继承.png

通过观察,对象分布图,我们可以看出,在最后的派生类中只出现了一次基类,达到我们期望的效果。测试代码如下:

int main(){
    Vertex3d a;
    Point3d b;
    Vertex c;
    Point2d d;  
    
    cout << "a对象虚表地址:" << endl;
    for(int i=0,j=1; i < 7; i++) {
        if(*((int*)&a+i)) 
            cout << "    第"<< j++ <<"虚表地址:" << *((int*)&a+i) << endl;
    }
    cout << "-------------------------------" << endl;
    
    for(int i = 0; *((int *)*(int *)&a+i) != 0 && *((int *)*(int *)&a+i) != 8; i++){
        cout <<"第1_" << i<<"个虚函数地址: "<< *((int *)*(int *)&a+i) << endl;
    }
    
    for(int i = 0; *((int *)*((int *)&a+2)+i) != 0 && *((int *)*(int *)&a+i) != 8; i++){
        cout <<"第2_" << i<<"个虚函数地址: "<< *((int *)*((int *)&a+2)+i) << endl;
    }
     
    for(int i = 0; *((int *)*((int *)&a+2)+i) != 0 && *((int *)*(int *)&a+i) != 8; i++){
        cout <<"第3_" << i<<"个虚函数地址: "<< *((int *)*((int *)&a+4)+i) << endl;
    } 
    cout << endl << endl;
    
    cout << "b对象虚表地址:" << *(int*)&b << endl;
    cout << "-------------------------------" << endl;
    for(int i = 0; *((int *)*(int *)&b+i) != 0; i++){
        cout <<"第" << i<<"个虚函数地址:  "<< *((int *)*(int *)&b+i) << endl;
    } 
    cout << endl << endl;  
    
    cout << "c对象虚表地址:" << *(int*)&c << endl;
    cout << "-------------------------------" << endl;
    for(int i = 0; *((int *)*(int *)&c+i) != 0; i++){
        cout <<"第" << i<<"个虚函数地址:  "<< *((int *)*(int *)&c+i) << endl;
    } 
    cout << endl << endl; 
    
    cout << "d对象虚表地址:" << *(int*)&d << endl;
    cout << "-------------------------------" << endl;
    for(int i = 0; *((int *)*(int *)&d+i) != 0; i++){
        cout <<"第" << i<<"个虚函数地址:  "<< *((int *)*(int *)&d+i) << endl;
    } 
    cout << endl << endl; 
   
    return 0;
}

//输出结果
// a对象虚表地址:
//     第1虚表地址:4739404
//     第2虚表地址:4739432
//     第3虚表地址:4739460
// -------------------------------
// 第1_0个虚函数地址: 4320604
// 第1_1个虚函数地址: 4320572
// 第1_2个虚函数地址: 4320036
// 第1_3个虚函数地址: 4320416
// 第2_0个虚函数地址: 4671528
// 第2_1个虚函数地址: 4671520
// 第2_2个虚函数地址: 4319408
// 第3_0个虚函数地址: 4671724
// 第3_1个虚函数地址: 4671712
// 第3_2个虚函数地址: 4319788
// 
// 
// b对象虚表地址:4739340
// -------------------------------
// 第0个虚函数地址:  4320240
// 第1个虚函数地址:  4320208
// 第2个虚函数地址:  4320036
// 
// 
// c对象虚表地址:4739244
// -------------------------------
// 第0个虚函数地址:  4319612
// 第1个虚函数地址:  4319580
// 第2个虚函数地址:  4319408
// 
// 
// d对象虚表地址:4739304
// -------------------------------
// 第0个虚函数地址:  4319956
// 第1个虚函数地址:  4319924
// 第2个虚函数地址:  4319788

如有问题,欢迎提出,讨论。

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

推荐阅读更多精彩内容

  • 一个博客,这个博客记录了他读这本书的笔记,总结得不错。《深度探索C++对象模型》笔记汇总 1. C++对象模型与内...
    Mr希灵阅读 5,545评论 0 13
  • C++ 面向对象编程 博客园地址:http://www.cnblogs.com/xiongxuanwen/p/42...
    先之阅读 682评论 0 1
  • 1. 结构体和共同体的区别。 定义: 结构体struct:把不同类型的数据组合成一个整体,自定义类型。共同体uni...
    breakfy阅读 2,101评论 0 22
  • C++虚函数 C++虚函数是多态性实现的重要方式,当某个虚函数通过指针或者引用调用时,编译器产生的代码直到运行时才...
    小白将阅读 1,723评论 4 19
  • 21天挑战暗线Day 14
    M有如果阅读 249评论 0 0