[温故而知新] 《深度探索c++对象模型》——对象数据成员的内存布局

前言

侯捷把书中第三章翻译为 “Data语意学”,另外书中有些语句得读几遍才明白他什么意思,也许是不习惯台湾的一些语言习惯。本文做一些简单的梳理。

了解对象数据成员的布局前,先了解一个语法相关的点:

member functions 的函数体的分析,会直到整个class的声明都出现了才开始,而对于 member functions 的参数列表 却是在第一次遭遇时适当地决议(resolved)。
1.

extern int x;
class Point{
public:
    float getX(){return x;}
private:
    float x;
}

这个例子的getX()返回的是Point的x成员,而不是extern的x。
2.

typedef int length;
class Point{
public:
    length getX(){return x;}
private:
    typedef float length;
    length x;
}

这个例子,我在VS2013的编译结果,会报warning:'“return”: 从“Point::length”转换到“length”,可能丢失数据'。(实际就是float转换到int)
也就是说对函数参数列表,再第一次遇到length就已经适当决议为int了。

对象数据成员的内存布局

了解对象的内存布局,先捋捋有哪些会影响对象的内存布局,如果是C的结构体的话,比如说这样一个结构:

struct Point{
int x;
int y;
};

可以很简单的理解为一个表格:

这里写图片描述

C++的类与C的结构体相比有哪些不同呢?
1.无继承,简单的class(与C的结构体一模一样)
2.单继承,没有virtual function 也没有 virtual base class
3.单继承,有virtual function ,没有 virtual base class
4.多继承,没有virtual base class
5.虚拟继承

下面就分别看看这5种情况,内存布局如何。

1.无继承,简单的class。
class Point4d{
private:
    type1 x;//这里的type1指代某种数据类型
    type2 y;//这里的type2指代某种数据类型
    type3 z;//这里的type3指代某种数据类型
    type4 t;//这里的type4指代某种数据类型
}

布局原则:较晚出现的member在对象中有较高的地址。注意,这里并没要求member中间的地址是连续的,原因很简单:内存对齐。
所以这种单个access section的情况,布局可能为这样:

这里写图片描述

对于有多个access section 的情况,例如:

class Point4d{
private:
    type1 x;//这里的type1指代某种数据类型
private:
    type2 y;//这里的type2指代某种数据类型
private:
    type3 z;//这里的type3指代某种数据类型
private:
    type4 t;//这里的type4指代某种数据类型
}

c++ standard允许编译器对这几个section自由排列,而不必在乎声明的顺序,不过目前没有谁家的编译器会无聊地随意摆放顺序,因为这并不能带来什么好处。

2.单继承,没有virtual function 也没有 virtual base class
class Point2d{
protected:
    float x;
    float y;
}
class Point3d:public Point2d{
protected:
    float z;
}

这种情况挺简单的,跟情况1类似,布局如下:
这里写图片描述

书中提到一个需要注意的点,把一个class分解为两层或者多层的继承体系,可能会造成“空间膨胀”。例如:

class Con{
public:
    int val;
    char c1;
    char c2;
    char c3;
}
//分解为三个:
class Con1{
public:
    int val;
    char c1;
}
class Con2 : public Con1{
public:
    char c2;
}
class Con3 : public Con2{
public:
    char c3;
}

由于内存对齐,内存布局变为下图所示:
这里写图片描述

3.单继承,有virtual function ,没有 virtual base class
class Animal{
public:
    char *name;
    virtual void say();
};

class Dog : public Animal{
public:
    void say(){ cout<< "wang wang~" << endl;}
};

class Cat : public Animal{
public:
    void say(){ cout<< "miao miao~" << endl;}
};

如果我们自己来实现virtual function,该怎么实现呢?
第一冒出来的想法,就是class每声明一个virtual function,就在对象的结构中增加一个指针,而class的子类实现n个virtual function后,当实例化class的子类时,就需要正确设置n个virtual function的指针指向正确的地址。

把上面的想法进一步优化一下,只使用一个指针,即vptr,指向一个virtual function的表格。
这里写图片描述

上图中,把vptr放在对象结构的末尾,好处是,可以保留base class C struct的对象布局,因而在C程序代码中也能使用。
不过也有的编译器把vptr放在了对象布局的开头,显然这么做损失了C的兼容性,但是在实现 “成员函数的指针”的时候,能带来一些好处(少一个字段),具体原因为何,下篇讲对象的成员函数再讨论。

4.多继承,没有virtual base class

这个与单继承相比,在对象的布局方面,实在没什么创新的。

class A{
public:
    int a;
    //... 其它成员
};

class B{
public:
    int b;
    //... 其它成员
};

class C:public A,public B{
public:
    int c;
    //... 其它成员
};

上面例子的内存布局,跟我们想象的一样简单:
这里写图片描述

多重继承给C++编译器带来的一些额外的工作:

C cobj;
A *pa;
B *pb;
C *pc;

pa = &cobj; //简单的拷贝地址即可
pb = &cobj; //需要内部转换 pb = (B*)(((char*)&cobj) + sizeof(A));
pb = pc;    //需要内部转换 pb = pc?(B*)(((char*)pc)+sizeof(A)):0;

注意比较上面三种指针的赋值方式:

第2种方式与第1种比较,pa能直接拷贝&cobj的原因,在于A是C的继承体系中的第一个base class,而第2种方式需要加上一些偏移量,因为B在C中不是第一个base class。

第3种方式与第2中方式比较,区别在于,一个是引用,一个是指针,对于引用,不存在引用“空”的情况,而指针则可能为“空”,所以第3种情况需要加个条件判断。

ps:
书中在描述第3中情况的时候,给出的表达式少了个括号,如果对照成我上面重新写的例子,书中对于对于 pb = pc 的解读就类似:
pc?(B*)((char*)pc)+sizeof(A):0,少了个括号,是错误的!侯捷也有粗心的时候!读者可以自行验证。

5.虚拟继承
class Share{
public:
    int share;
    //...other member
};

class A:public virtual Share{
public:
    int a;
    //...other member
};

class B:public virtual Share{
public:
    int b;
    //other member
}

class C:public A,public B{
public:
    int c;
    //other member;
}

继承体系,为典型的“菱形”结构:

这里写图片描述

怎么实现virtual继承呢?使上面的继承体系中,Share对象在C的对象中只有一份拷贝。

如果让我来实现,我可能会为对象加一个跟vptr类似的指针,指向virtual base class的对象。那如果有多个virtual base class呢?这个问题跟virtual function如果有多个怎么办类似,就直接只用一个指针,指向一个表格,这个表格存的就是一些指向virtual base class object的指针啦。

ok,我上面提到的思路,其实微软的编译器就是这么干的,画个图看看这种策略:

这里写图片描述

(ps:书中只画了只有一个virtual base class 的情况,我这里把多个的情况也给画了出来,如果有误,请指出,谢谢!)

从上图可以看到,每个通过virtual继承的class object都多了一个指针,指向一个virtual base class table。书中还提到另一种策略,直接去掉virtual base class ptr,就直接利用现有的vptr和vtable就行了!看下图:

这里写图片描述

虚拟继承这部分就差不多完了。书中还提到一个问题,“由于虚拟继承链的加长,导致间接存取层次的增加”。也就是有n层虚拟派生,就需要经过n个virtual base class 指针进行间接存取,而我们理想的情况是希望有固定的存取时间,不随虚拟派生的深度而改变。

这个问题…我就不画图了,书中提到目前的大部分编译器采用的做法是:把nesed virtual base class的指针放到 derived class object中。编译器提供选项供程序员选择是否产生双重指针。


C++的一个小特性
指向数据成员的指针。
还是接着上面的虚拟继承的例子(与虚拟继承无关,只用它的继承结构)

这里写图片描述

void test(int C::*dp, C *p){
    printf("offset:%d, value:%d\n", dp,p->*dp);
}

//...
C cobj;
cobj.a = 1;
cobj.b = 2;

C *p = &cobj;
int A::*dpa = &A::a;    //dpa的值为 a在A中的offset
int B::*dpb = &B::b;    //dpb的值为 b在B中的offset

test(dpa, p);     //打印offset 0, value 1(即a的值)
test(dpb, p);     //打印offset 4, value 2(即b的值)

上面怎么做到的呢?还是编译器干了活,在函数调用的时候,

test(dpa, p);     //打印出cobj 中 a 的值 1
test(dpb, p);     //打印出cobj 中 b 的值 2
//实际上编译器得内部转换:
//test(dpa?dpa+0:0,p)         //只是思路,这么写实际上编译通不过。
//test(dpb?dpb+sizeof(A):0,p) //只是思路,这么写实际上编译通不过。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值