此篇为阅读《Inside The C++ Object Model》时对其中相对重要的 data 语义和 function 语义的一些记录


data语义

  • 编译器一般将多个access sections连锁在一起形成一个区块,这个操作不会降低效率,诸如多个public域

  • C\C++的边界调整有可能会在中间插入若干bit(类似c的结构体内存对齐)

  • 编译器会自动生成一些内容以支撑对象例如vptr,一般vptr会被插入对象的开头或结尾,依赖编译器的处理


1
2
3
4
5
6
7
8
9
// static data member 不从属于class
// 对其的引用会得到一个指向其数据类型的指针,如下
struct Test {
static const int st_con = 10;
};

int main() {
auto pt = &Test::st_con; //pt is const int*
}

所以这时,当两个class都有同名static data member则会产生冲突,此时编译器为每个static data member编码(name-mangling),用以区分彼此


nonstatic data member在存取时和C的效率没什么两样,是C++从C中借鉴过来的一部分

存取需要通过明指或暗指(this)


但当用指针且继承结构中存在虚拟继承时就无法在编译阶段确定成员属于何对象(虚拟继承不常用,可忽略)

1
2
object.x;	
pt->x; //pt指向需要在执行阶段才能判断

派生类赋值给基类(但vs似乎避免了这个情况?),会将非继承成员放到基类因内存对齐而填充的空间中,此时基类结构发生改变,这时当另一个基类给当前基类赋值时,基类结构中的子类成员会被未知数据填充


vptr在尾部就兼容C,在头部更有利于多继承,vs中vptr被插入至头部,例子见下

单继承体系中把一个派生类地址给基类指针是一个自然过程,但在vptr在头部且派生类中含有虚函数时就需要编译器介入,多继承+虚拟继承就更需要了

多重继承,在指针变化时内部会进行计算以获得目标基类的offset(编译器介入)

编译器优化之后,封装不会对执行期效率产生什么影响

function 语意

类成员函数有3种状态:static, nonstatic, vitual

类内函数成员在编译器的优化后可以获得不低于外部函数的效率,会被编译器优化成了类外部函数实体


vitual member function

1
2
3
4
// 用指针对虚函数的调用
ptr->fun();
// 会转化为
(*ptr->vptr[1])(ptr); // 通过ptr获取虚表中的函数指针,通过函数指针调用虚函数

C++多态:以一个基类指针或引用寻址出一个派生类对象

如何在执行期确定虚函数的实体?

  • 具体是哪个类(指针或引用指向的真实类型)
  • 哪个虚函数(虚函数地址)

如何存放两个需要的信息?

  • 一个由编译器提供的vptr指针,指向vitual table,其中存储了type_info for object和 虚函数的执行期地址
  • 为了找到函数地址,每个虚函数被指定一个索引(纯虚函数同样有自己的索引

在编译时虚函数由其对象调用可知,由编译器完全控制,唯一一个在执行期才知道的消息是:slot指向的函数实体

指针的类型是定义时确定的,但指向的对象的类型不确定,诸如:base *p = &child;其中p是基类指针,但可以指向派生类对象,对象信息存放在virtual table中的RTT字段,运行时确定


  • 非成员函数,成员函数、静态函数被编译器优化成完全相同的形式,函数效率相同

  • 加上vitual后,随继承结构复杂度耗时增加


普通成员函数

同数据成员一样,直接是在内存中的真正地址,需要依赖于对象访问


inline

是**#define的一种安全替代品**,但仍然需要被小心处理,过多的参数(产生临时变量)和嵌套会导致扩展码大量增加或无法扩展开来

inline函数有惊人的效率,被视作不变表达式,编译器将其提出至循环之外,只计算一次


类结构实例研究

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Test {
Test(int _a, char _b) : a(_a) , b(_b) { };
void fun() {};
virtual void vir_fun_0() {};//有一个4B的指针
virtual void vir_fun_1() {};
int a; //4B
char b; //1B
};

struct TT : public Test {
char c;
TT(int _a, char _b, char _c) : Test(_a, _b), c(_c) {};
void vir_fun_1() {}; //override
virtual void TT_fun() {};
};

基类与派生类结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//-----vs编译后的内存布局-----//

1>class Test size(12): // 基类,12字节
1> +---
1> 0 | {vfptr} // 4字节指针,指向虚表 ;且位于头部,不兼容C
1> 4 | a // int,4字节
1> 8 | b // char,1字节
1> | <alignment member> (size=3) //内存对齐
1> +---
1>Test::$vftable@: //虚表结构
1> | &Test_meta //用于支持RTTI的类型信息
1> | 0
1> 0 | &Test::vir_fun_0 //索引 0
1> 1 | &Test::vir_fun_1 //索引 1
1>Test::vir_fun_0 this adjustor: 0 //调节器:用于多重继承中,获取夹在第一基类和派生类中间的基类开始地址
1>Test::vir_fun_1 this adjustor: 0 //就是一个偏移量,如果中间还有一个基类,则第二基类开始在12字节后(Test占12字节)
1>class TT size(16): //派生类,16字节 = 基类12B + sizeof(char) + 内存对齐3字节
1> +---
1> 0 | +--- (base class Test) //基类保持其结构不变,用于内存对齐的空间仍然存在
1> 0 | | {vfptr} //派生类继续使用基类的虚表指针,从此可得vptr在头部有利于OOP的继承机构
1> 4 | | a
1> 8 | | b
1> | | <alignment member> (size=3) //基类中的内存对齐空间任然存在
1> | +---
1>12 | c //子类数据成员
1> | <alignment member> (size=3) //内存对齐
1> +---
1>TT::$vftable@:
1> | &TT_meta
1> | 0
1> 0 | &Test::vir_fun_0 //索引0,派生类没有处理,直接继承基类虚函数
1> 1 | &TT::vir_fun_1 //索引1,基类虚函数被派生类override
1> 2 | &TT::TT_fun //索引2,派生类新的虚函数,表被扩展
1>TT::vir_fun_1 this adjustor: 0
1>TT::TT_fun this adjustor: 0