c++ object model 1

26 👍 / 19 💬

从C++ Object Model 1 中我们学到了些什么?

1. Class 对象的大小

Class对象中,对象的大小并不包含以下:

a. Class申明的static members
b. 所有的static or non-static member function

Class对象中,对象的大小会包括:

a. padding 对齐引起的额外大小
b. 继承自base class 的大小
b. 如果有一个空的base class, 这个class的大小将会是1byte;这个大小也会被继承
c. 为了支持多态(虚函数)引起的额外开销,例如一个指向虚表的虚表指针 (vptr)

2. Class 对象的构造和复制构造函数

编译器会在必要的时候,自动生成构造(constructor) 和 复制构造函数 (copy constructor). 这一部分内容又叫 constructor semantics (构造语意)。也就是说,当你通过申明或者new生成一个新的class instance的时候,这个instance暗含着constructor call。如果,

a. 你的class 内部没有任何需要进行constructor call的成员,
b. 或者,你的class并不继承自一个或者多个含有虚函数(支持多态)的base class
c. 或者,你的class并不继承自一个需要进行constructor call的base class
那么,编译器将不会帮你生成一个constructor(只适用于如果你没有定义任何constructor的话);反之,编译器将会生成一个constuctor来construct所有你需要constructor的成员并生成你的虚表。

对于复制构造函数而言;复制构造函数主要适用于对象的复制初始化, 例如:

class A {}
A a(b);

A a=b;

而b是另外一个class A 的instance。如果A中的每个成员都不需要constructor的话,并且a 和 b 其实是属于同一个dynamic type 的话,那么则直接使用bitwise copy (内存复制);如果反之,A中的成员需要constructor 又或者,a 和 b 使用不同dynamic type 的虚表,那么编译器将会构造一个正确的copy constructor 来帮助你把他们正确初始化(编译器要做好多)

3. class 的 初始化列表 (initialization list)

其执行是严格按照成员在内存中的顺序来的(也就是,严格按照成员在class中按照access level申明的顺序来的);如果你不这样做的话,将会获得一些意想不到的效果。例如你打算先初始化:b(0), 再利用b的值去初始化c:c(b)。实际上c 的申明顺序在b前面;那么你将会用一个未能初始化的b去初始化c(得到的值将会是一堆乱码)。初始化列表的意义何在呢?他的目的就是在于避免constuctor自动插入一段对于需要constructor成员的构造,然后再对他进行赋值操作。这是不必要的;应该一开始就进行复制构造,然后编译器将不会再插入关于这个成员的constructor。

4. 对象数据的存放

对象的static member 将直接存放在一个固定的内存中(在这个class定义的生命周期内);
对象的static 和 non-static function member 也将直接存放在一个固定的内存中(同上);对于他们地址的记录和取得将完全由编译器完成;
例如,如果你call一个static member function: ClassA::functionA(), 实际上编译器将会把它改变成 _ClassA_functionAsv() 等等(这里的名字是虚构的)。特别要注意的是,static member function 不存在隐含的this 指针参数;也就是说它和具体的class instance无关;基本可以看作是一个和其它函数一样的全局函数。
又例如,如果你call一个non-static member function: a.functionB(), 实际上编译器将会把它改变成 _ClassA_funcitonBv(ClassA* this), 并把你的调用改写成: _ClassA_funcitonBv(&a); 如果你的调用是 b→functionB() 的话,这样更好,因为编译器就直接调用:

_ClassA_funcitonBv(b)

编译器很坏的。

唯一需要编译器进行另外操作的,就是虚函数了。因为实际上,一个函数会有很多版本;考虑如下情况:class A 定义了一个虚函数 functionA, class B 继承了 class A, 同时也override 了 function A。那么,有一个指针c,他指向一个dynamic type 是class B 的对象(地址),现在,我们进行 c→functionA(),编译器怎么知道是哪一个functionA 需要被调用呢?

为了支持多态,编译器给我们这个对象建立了一个虚表,这个表中的第一个元素就是指向那个正确function 的地址,

void (*f_ptr) (classB *) = &classB::functionA()

而且这个表在对象生成的时候就通过constructor 正确构造了;因此,即便c的static type 是 class A,我们仍然可以正确的找到他的dynamic type下正确调用的函数。

如果class B同时继承了多个class,那么,每一个被override 的函数的地址将会存在分别base class subobject 的虚表中;如果自己新引入的虚函数,那么就写入左边第一个(在inheritance diagram中)的base class 的 subobject 的虚表中;如果这时候,有一个static type 为第二个甚至更晚 base class的话,那么编译器将要负责,找到正确的虚表偏移量,使得每次关于这个指针的动态函数调用都必须是正确的(也就是说,这个base class的function member 被调用的版本都是正确override 过的)。比较可怕的情形是,这个override后的版本需要用到其它base class 的member乃至 member function。那么对于每一个函数,编译器都必须找到正确的关于this 指针的偏移。更加恐怖的虚拟继承,限于篇幅,我们这里就不解释了。有兴趣的可以去问微软的高级工程师们。

另外一个值得注意的是,我们除了可以取出class的member function的地址外,我们当然也可以取出non-static member 的地址。不过,这个地址存的其实是偏移量。例如 class A含有int a, int b, float c 三个member. 那么我们可以这么取:

int (classA::*ptr) = &classA::a

我们会发现,ptr = 1(不可以等于0,因为在c中,null_ptr就是0;所以这里的偏移都是从1开始的,醉了吗?)。对于一个具体的instance a, 你可以通过 a.*ptr 来实现和a.a 完全一样的操作。
很有趣吧;科科。

接下来的内容我们下期再见


专栏:知乎书馆

立党和他的朋友们说书唱戏的地方