浅析C++对象布局

原文地址:http://www.jianshu.com/p/060efa58875d


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

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

C++对象模型

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

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值