0%

深度探索C++对象模型之data语意学

3章:Data语意学

The size of class

1
2
3
4
5
6
7
8
class X
{
// char c; // 此时, sizeof(X) == 1, sizeof(Y) == 8, sizeof(Z) == 8, sizeof(A) == 16
char c; // 此时, sizeof(X) == 1, sizeof(Y) == 16, sizeof(Z) == 16, sizeof(A) == 24
};
class Y : public virtual X { };
class Z : public virtual X { };
class A : public Y, public Z { };

一个类的大小主要受三个因素的影响:

  1. 语言支持的特性所造成的负担

    比如,含虚函数的类会额外多一个vptr指针,含virtual base class会再多一个额外的指针(它或者指向virtual base class subobject,或者指向一个相关表格;表格中存放的若不是virtual base class subobject地址,就是其偏移量)。

  2. 编译器对于特殊情况所提供的优化处理

    现代编译器的对于空基类的优化处理:一个empty virtual base class被视为derived class object最开头的一部分,也就是说它并没有花费任何的额外空间。因为既然有了members,就不需要原本为了empty class而安插的一个char

  3. alignment(内存对齐)

    就是将数值调整到某数的整数倍。在64位计算机上,通常alignment8 bytes,以使bus的“运输量”达到最高效率。

nonstatic data members放置的是“个别的class object”感兴趣的数据,static data members则放置的是“整个class”感兴趣的数据。

static data members被放置在程序的一个global data segment中,不会影响个别的class object的大小。在程序之中,不管该class被产生出多少个objects(经由直接产生或间接派生),static data member永远只存在一份实体(即使该class没有任何object实体,其static data members也已存在)。

3.1 data member的绑定

类成员函数的argument list中的名称会在它们第一次遭遇时被适当地决议(resolved)完成。因此在externnested type name之间的非直觉绑定操作还是会发生。例如在下面的程序片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef std::string length;

class Test
{
public:
// 参数列表中的 length 被决议成 std::string!!!
// 里面的 val_ 被正确决议成 int
void setVal(length val)
{
val_ = val;
}

private:
typedef int length;
length val_;
};

所以这需要使用防御性的程序风格:将nested type name写在class的起始处!

3.2 数据成员的布局

下面这个template function,接受两个data member,然后判断谁先出现在class object之中。如果两个member都是不同的access sections中的第一个被声明者,此函数就可以用来判断哪一个section先出现:

1
2
3
4
5
6
template<class class_type, class data_typel, class data_type2>
std::string access_order(data_type1 class_type::* mem1, data_type2 class_type::* mem2)
{
assert(mem1 != mem2);
return mem1 < mem2 ? "member 1 occurs first" : "member 2 occurs first";
}

上述函数可以这样被调用:

1
access_order(&Point3d::y, &Point3d::z);
3.3 data member的存取

static data member

对于类中的静态数据成员,通过一个指针和通过一个对象来存取,效率完全相同,不论这个静态数据成员经过了多么复杂的继承体系。

若取一个static data member的地址不会得到指向其class member的指针(不是value_type class_type::*类型)。而是指向其数据类型的指针(类型为value_type*)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point
{
public:
static int x;
void func1()
{
}
static void func2(int a)
{
}
};
int Point::x = 0;

int main()
{
auto p = &Point::func1; // p 的类型是 void (Point::*p)()
auto q = &Point::func2; // q 的类型是 void (*q)(int a)
auto p = &Point::x; // r 的类型是 int* r
}

构造函数不能是静态成员函数:如果构造函数是静态成员函数,那么将不能访问非静态变量,也没办法完成初始化的工作。

non static data member

对于类中的非静态数据成员,通过一个指针和通过一个对象来存取,当此数据成员属于继承而来的virtual base class时,使用指针效率较低。如果使用对象直接存取,就不会有这些问题,其类型无疑是确定的,而即使它继承自virtual base classmembersoffset位置也在编译时期就固定了。

欲对一个nonstatic data member进行存取操作,编译器需要把class object的起始地址加上data member的偏移量。举个例子:

1
&origin._y == &origin + (&Point3d::_y - 1);

请注意其中的-1操作。指向data member的指针,其offset值总是被加上1,这样可以使编译系统区分出是用以指出class的第一个member还是没有指出任何member的两种情况。

3.4 C++对象布局
  1. 个别struct的数据布局

    image-20210310151329468

  2. 单一继承而且没有virtual function时的数据布局

    image-20210310151502431

  3. C++语言保证——出现在derived class中的base class subobject有其完整原样性

    image-20210310151740707

    image-20210310151814511

  4. 单一继承并含虚拟函数情况下的数据布局

    image-20210310151910413

  5. 多重继承

    image-20210310152020878

    image-20210310152043648

  6. 虚拟继承,使用pointer strategyvirtual table offset strategy

    image-20210310152208905

    image-20210310152330536

    image-20210310152521106

3.5 指向data member成员的指针

取一个nonstatic data member的地址,将会得到它在class中的offset(指针类型为data_type class_type::*),取一个“绑定于真正class object身上的data member”的地址,将会得到该member在内存中的真正地址(指针类型为data_type*)。