Member Pointer

![sunset](C:\Users\20550\Pictures\Saved Pictures\sunset4.jpg)

本文讨论C++类中数据成员(static和non-static)的排列、静态函数、成员函数和虚函数在内存中的布局以及调用过程,认识指向成员的指针,以便加深对类对象布局的理解。

指向data member的指针

static data members

静态数据成员存放于类之外,被视为一个全局变量,但仅在class生命范围内可见。因此如果一个类Obj有一个静态类型的整型变量a,则

1
&Obj::a

会获得如下内存地址:

1
int *

类中的静态成员的名称一般被使用name-mangling编码为一个独一无二的名称,以便不同类中的相同名称的静态变量不会冲突。

nonstatic data members

nonstatic data members在class object中的排列顺序和其被声明的顺序一样,任何介入的static data members都不会被放入到对象布局中。我们也可以对nonstatic 类型的data member取地址,如同static类型的变量一样,假设有一个Obj类,包含一个整型的数据成员b,则我们也可以进行如下操作:

1
&Point3d::b

不同于static类型的成员,上述操作不会得到b的内存地址,这是显而易见的,因为每个不同的对象都有一份b。其实它的类型为**int Obj::*,而不是**int *,含义是b成员在对象中的偏移(offset)。

如下测试程序,Foo类包含一个成员方法,用于打印其内部数据成员的对象内部偏移:

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
36
37
38
39
40
41
42
class Foo
{
public:
int x;
static int sx;

private:
int y;
float z;
static void *sp;

public:
double d;

protected:
char c;

public:
char a;

public:
void print() const
{
printf("static data: %p %p\n", &Foo::sx, &Foo::sp);
printf("%d %d %d %d %d %d\n",
&Foo::x, &Foo::y, &Foo::z, &Foo::d, &Foo::c, &Foo::a);
cout << sizeof(*this) << endl;
}
};

int Foo::sx = 19; // 有不为0的初始值,放置在.data
void *Foo::sp = nullptr; // 放置在.bss

int global; // 没有初始值,放置在.bss
int main()
{
Foo foo;
foo.print();
cout << &global << endl;

return 0;
}

编译运行:

1
2
3
4
5
> g++ two.cc -g -o two && ./two
static data: 0x563c95253010 0x563c95253158
0 4 8 16 24 25
32
0x563c95253160

第一行打印的是静态数据的地址,第二行是对象内部按照声明次序,打印其offset,可见成员的实际排列次序和声明次序完全吻合,与其修饰符(public、protected、private)以及类型无关,也不会将同种修饰符的成员专门安放在一起。对象还需要添加padding部分以满足对齐,因此大小为32,

反汇编查看.data段和.bss段:

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
36
> objdump -d -j .data two

two: file format elf64-x86-64


Disassembly of section .data:

0000000000004000 <__data_start>:
...

0000000000004008 <__dso_handle>:
4008: 08 40 00 00 00 00 00 00 .@......

0000000000004010 <_ZN3Foo2sxE>: # Foo::sx
4010: 13 00 00 00 ....
> objdump -d -j .bss two

two: file format elf64-x86-64


Disassembly of section .bss:

0000000000004040 <_ZSt4cout@GLIBCXX_3.4>:
...

0000000000004150 <completed.0>:
...

0000000000004158 <_ZN3Foo2spE>: # Foo::sp
...

0000000000004160 <global>: # global
4160: 00 00 00 00 ....

0000000000004164 <_ZStL8__ioinit>:
4164: 00 00 00 00

符合我们的预期。

多态——单继承

当类存在基类时,基类的数据会被作为类的一部分存在,我们也可以通过打印类成员指针的值,了解到类中数据成员是如何排列的。修改上述测试程序如下,首先我们考虑单继承:

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

class Base
{
public:
int ba;
char bb;
};

class Derived : public Base
{
public:
char da;
int db;
};

int main()
{
Derived derived;
printf("%d %d %d %d\n",
&Derived::ba, &Derived::bb, &Derived::da, &Derived::db);

char * p1 = reinterpret_cast<char *>(&derived);
char * p2 = reinterpret_cast<char *>(&derived.ba);
char * p3 = reinterpret_cast<char *>(&derived.bb);
char * p4 = reinterpret_cast<char *>(&derived.da);
char * p5 = reinterpret_cast<char *>(&derived.db);
printf("%lld %lld %lld %lld\n",
p2 - p1, p3 - p1, p4 - p1, p5 - p1);
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;


return 0;
}

编译运行:

1
2
3
4
0 4 8 12
0 4 8 12
8
16

第一行打印成员指针获取的成员offset;

第二行打印通过指针算术实际求出的对象中成员的offset,可见和第一行确实一致;

三、四行打印基类和子类的大小。

可以发现,基类的数据被存放在对象的起始处,子类自定义的数据成员相继摆放在后面。值得注意的是,编译器并未把子类和基类的数据成员“混为一谈”,编译器并未把子类的第一个char类型的成员和基类最后一个char类型成员相邻放置以节省padding的部分。基类的部分是作为一个整体存在于子类之中。

多态——多继承

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Base
{
public:
int b;
};

class Base1
{
public:
int ba{};
char bb{};
};

class Base2
{
public:
int bc{};
char bd{};
};

class Base3
{
public:
int be;
};

class Derived : public Base1, public Base2, public Base3
{
public:
char da{};
int db{};
};

int main()
{
Derived derived;
printf("%d %d\n",
&Derived::ba, &Derived::bb);

printf("%d %d %d\n", &Derived::bc, &Derived::bd, &Derived::be);

// printf("%d %d %d %d\n", &Derived::bc, &Derived::bd, &Derived::b, &Derived::be);
char * p1 = reinterpret_cast<char *>(&derived);
char * p2 = reinterpret_cast<char *>(&derived.bc);
char * p3 = &derived.bd;
printf("%lld %lld\n", p2 - p1, p3 - p1);
printf("%lld %lld\n", &Derived::da, &Derived::db);
cout << sizeof(Base1) << endl;
cout << sizeof(Base2) << endl;
cout << sizeof(Derived) << endl;
int Derived::* p = nullptr;
printf("%d %p", sizeof p, p);

return 0;
}

在多继承,尤其是多继

指向member function的指针

static function

member function

virtual function

Default & Copy Constructor

如果一个类不包含任何构造函数,当需要时,编译器会自动合成一个,这句看似司空见惯的法则,其实和第一感觉不同。关键在于这个需要时,因为有时候其实编译器并不要专门为一个类合成构造函数。我们假设有一个类,其内部只有普通的内置类型成员,当有一个该类类型的局部对象时,其内部数据成员(如果有)的值保持随意就好,当有一个该类类型的全局对象时,其处于.bss段,直接默认清零即可,拷贝构造函数也是类似,将一个对象占有的空间的内容逐个字节拷贝即可,此时构造函数被称为trivial,即无用的。但是某些时候,当我们进行默认初始化,或者拷贝初始化时,如果我们没有显式定义构造函数和拷贝构造函数,此时编译器必须为我们合成,完成必需的工作,比如一个带有虚函数的类,编译器必须合成一个构造函数完成虚指针的设定,此时构造函数被称为nontrivial

只有nontrivial default constructor才会被编译器合成,本文讨论何时编译器会真正合成默认&拷贝构造函数。

我使用的编译环境:

1
2
3
4
5
6
7
> uname -a
Linux net 5.19.0-35-generic #36~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Feb 17 15:17:25 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
> g++ --version
g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

编译的优化级别一般是O0,反汇编工具objdump,由于c++

default constructor

如下测试程序,刚开始其成员均是内置类型,包括一个int类型成员i、float成员f和指针p:

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
class Foo
{
friend ostream &operator<<(ostream &os, const Foo &foo)
{
cout << foo.i << ' ' << foo.f << ' ' << foo.p;
return os;
}

private:
int i;
float f;
void *p;
};

Foo global;

int main()
{

Foo local;
cout << local << endl;
cout << global << endl;

return 0;
}

编译运行:

1
2
3
> g++ two.cc -g -o two && ./two
1631744200 4.58701e-41 0x7fde61426e88
0 0 0

局部变量的初始值是不确定的,而默认初始化的全局变量内容被清空为0即可,因为类中包含的只有数据成员(后续会讨论当类包含虚函数和虚基类时的情况)

查看.bss段,可见global确实在此处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> objdump -d -j .bss two

two: file format elf64-x86-64


Disassembly of section .bss:

0000000000004040 <_ZSt4cout@GLIBCXX_3.4>:
...

0000000000004150 <completed.0>:
...

0000000000004160 <global>:
...

0000000000004170 <_ZStL8__ioinit>:
...

为保持反汇编结果简洁,去掉cout输出,仅调用get_i():

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
class Foo
{
friend ostream &operator<<(ostream &os, const Foo &foo)
{
// cout << foo.i << ' ' << foo.f << ' ' << foo.p;
return os;
}

private:
int i;
float f;
void *p;
};

Foo global;

int main()
{

Foo local;
cout << local;
cout << global;

return 0;
}

查看反汇编结果的main函数部分:

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
> objdump -d -j .text two
... # 省略其余内容
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 48 83 ec 20 sub $0x20,%rsp
1195: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119c: 00 00
119e: 48 89 45 f8 mov %rax,-0x8(%rbp)
11a2: 31 c0 xor %eax,%eax
11a4: 48 8d 45 e0 lea -0x20(%rbp),%rax
11a8: 48 89 c6 mov %rax,%rsi
11ab: 48 8d 05 8e 2e 00 00 lea 0x2e8e(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
11b2: 48 89 c7 mov %rax,%rdi
11b5: e8 a3 00 00 00 call 125d <_ZlsRSoRK3Foo>
11ba: 48 8d 05 9f 2f 00 00 lea 0x2f9f(%rip),%rax # 4160 <global>
11c1: 48 89 c6 mov %rax,%rsi
11c4: 48 8d 05 75 2e 00 00 lea 0x2e75(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
11cb: 48 89 c7 mov %rax,%rdi
11ce: e8 8a 00 00 00 call 125d <_ZlsRSoRK3Foo>
11d3: b8 00 00 00 00 mov $0x0,%eax
11d8: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11dc: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11e3: 00 00
11e5: 74 05 je 11ec <main+0x63>
11e7: e8 94 fe ff ff call 1080 <__stack_chk_fail@plt>
11ec: c9 leave
11ed: c3 ret

可见,编译器“初始化”local的操作仅仅是下移了栈指针,分配内存,其内部数据成员的数值均为未确定,且为调用一个构造函数完成构造工作,此时,编译器并未和我们预期的那样,合成一个默认构造函数并调用之,此时构造函数是trivial的。

c++语法允许我们为class的数据成员加上默认值,此时会有constructor被合成吗?

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
class Foo
{
friend ostream &operator<<(ostream &os, const Foo &foo)
{
cout << foo.i << ' ' << foo.f << ' ' << foo.p;
return os;
}

private:
int i{12};
float f = 3.14;
void *p = (void *)0xffffffffffffffff;
};

Foo global;

int main()
{

Foo local;
cout << local << endl;
cout << global << endl;

return 0;
}

编译运行,和预期一样,此时global和local都会有默认值,:

1
2
3
4
> g++ two.cc -g -o two
> ./two
12 3.14 0xffffffffffffffff
12 3.14 0xffffffffffffffff

编译器是否合成了一个默认构造函数帮我们完成初始化操作?编译,反汇编查看main函数部分:

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
36
37
38
39
40
> objdump -d -j .text two
...
0000000000001229 <main>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: 48 83 ec 20 sub $0x20,%rsp
1235: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
123c: 00 00
123e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1242: 31 c0 xor %eax,%eax
1244: 48 8d 45 e0 lea -0x20(%rbp),%rax # 获取local地址
1248: 48 89 c7 mov %rax,%rdi # 作为第一个参数
124b: e8 64 01 00 00 call 13b4 <_ZN3FooC1Ev> # 调用默认构造函数
1250: 48 8d 45 e0 lea -0x20(%rbp),%rax
1254: 48 89 c6 mov %rax,%rsi
1257: 48 8d 05 e2 2d 00 00 lea 0x2de2(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
125e: 48 89 c7 mov %rax,%rdi
1261: e8 d6 00 00 00 call 133c <_ZlsRSoRK3Foo>
1266: 48 8b 15 63 2d 00 00 mov 0x2d63(%rip),%rdx # 3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4>
126d: 48 89 d6 mov %rdx,%rsi
1270: 48 89 c7 mov %rax,%rdi
1273: e8 78 fe ff ff call 10f0 <_ZNSolsEPFRSoS_E@plt>
1278: 48 8d 05 e1 2e 00 00 lea 0x2ee1(%rip),%rax # 4160 <global>
127f: 48 89 c6 mov %rax,%rsi
1282: 48 8d 05 b7 2d 00 00 lea 0x2db7(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
1289: 48 89 c7 mov %rax,%rdi
128c: e8 ab 00 00 00 call 133c <_ZlsRSoRK3Foo>
1291: 48 8b 15 38 2d 00 00 mov 0x2d38(%rip),%rdx # 3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4>
1298: 48 89 d6 mov %rdx,%rsi
129b: 48 89 c7 mov %rax,%rdi
129e: e8 4d fe ff ff call 10f0 <_ZNSolsEPFRSoS_E@plt>
12a3: b8 00 00 00 00 mov $0x0,%eax
12a8: 48 8b 55 f8 mov -0x8(%rbp),%rdx
12ac: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
12b3: 00 00
12b5: 74 05 je 12bc <main+0x93>
12b7: e8 44 fe ff ff call 1100 <__stack_chk_fail@plt>
12bc: c9 leave
12bd: c3 ret

查看__static_initialization_and_destruction_0(int, int),其初始化全局变量

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
> objdump -d -j .text two
...
00000000000012be <_Z41__static_initialization_and_destruction_0ii>:
12be: f3 0f 1e fa endbr64
12c2: 55 push %rbp
12c3: 48 89 e5 mov %rsp,%rbp
12c6: 48 83 ec 10 sub $0x10,%rsp
12ca: 89 7d fc mov %edi,-0x4(%rbp)
12cd: 89 75 f8 mov %esi,-0x8(%rbp)
12d0: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)
12d4: 75 4a jne 1320 <_Z41__static_initialization_and_destruction_0ii+0x62>
12d6: 81 7d f8 ff ff 00 00 cmpl $0xffff,-0x8(%rbp)
12dd: 75 41 jne 1320 <_Z41__static_initialization_and_destruction_0ii+0x62>
12df: 48 8d 05 8a 2e 00 00 lea 0x2e8a(%rip),%rax # 4170 <_ZStL8__ioinit>
12e6: 48 89 c7 mov %rax,%rdi
12e9: e8 32 fe ff ff call 1120 <_ZNSt8ios_base4InitC1Ev@plt>
12ee: 48 8d 05 13 2d 00 00 lea 0x2d13(%rip),%rax # 4008 <__dso_handle>
12f5: 48 89 c2 mov %rax,%rdx
12f8: 48 8d 05 71 2e 00 00 lea 0x2e71(%rip),%rax # 4170 <_ZStL8__ioinit>
12ff: 48 89 c6 mov %rax,%rsi
1302: 48 8b 05 ef 2c 00 00 mov 0x2cef(%rip),%rax # 3ff8 <_ZNSt8ios_base4InitD1Ev@GLIBCXX_3.4>
1309: 48 89 c7 mov %rax,%rdi
130c: e8 cf fd ff ff call 10e0 <__cxa_atexit@plt>
1311: 48 8d 05 48 2e 00 00 lea 0x2e48(%rip),%rax # 4160 <global>
1318: 48 89 c7 mov %rax,%rdi
131b: e8 94 00 00 00 call 13b4 <_ZN3FooC1Ev> # 调用默认构造函数
1320: 90 nop
1321: c9 leave
1322: c3 ret

出乎意料此时编译器真的替我们合成了一个默认构造函数(反汇编中名称为 <_ZN3FooC1Ev>),查看其内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00000000000013b4 <_ZN3FooC1Ev>:
13b4: f3 0f 1e fa endbr64
13b8: 55 push %rbp
13b9: 48 89 e5 mov %rsp,%rbp
13bc: 48 89 7d f8 mov %rdi,-0x8(%rbp) # 获取第一个参数,即对象地址
13c0: 48 8b 45 f8 mov -0x8(%rbp),%rax # 保存在rax中
13c4: c7 00 0c 00 00 00 movl $0xc,(%rax) # i = 0xc
13ca: 48 8b 45 f8 mov -0x8(%rbp),%rax
13ce: f3 0f 10 05 32 0c 00 movss 0xc32(%rip),%xmm0 # 2008 <_ZN6__pstl9execution2v1L5unseqE+0x1>
13d5: 00
13d6: f3 0f 11 40 04 movss %xmm0,0x4(%rax)
13db: 48 8b 45 f8 mov -0x8(%rbp),%rax
13df: 48 c7 40 08 ff ff ff movq $0xffffffffffffffff,0x8(%rax) # p = 0xff...
13e6: ff
13e7: 90 nop
13e8: 5d pop %rbp
13e9: c3 ret

该构造函数使用我们给定的值默认初始化成员。但是一般来说指针类型是内置类型,编译器并不需要为此合成一个默认构造函数。我猜测是因为指针的赋值使用了一个类型转换,将指针成员默认值改为nullptr,再次编译并反汇编查看main函数部分:

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
36
37
38
39
40
41
0000000000001229 <main>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: 48 83 ec 20 sub $0x20,%rsp
1235: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
123c: 00 00
123e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1242: 31 c0 xor %eax,%eax
1244: c7 45 e0 0c 00 00 00 movl $0xc,-0x20(%rbp) # -10(%rbp)为local地址,此指令将0xc赋给i
124b: f3 0f 10 05 b5 0d 00 movss 0xdb5(%rip),%xmm0 # 2008 <_ZN6__pstl9execution2v1L5unseqE+0x1>
1252: 00
1253: f3 0f 11 45 e4 movss %xmm0,-0x1c(%rbp)
1258: 48 c7 45 e8 00 00 00 movq $0x0,-0x18(%rbp) # 将0x0赋给p
125f: 00
1260: 48 8d 45 e0 lea -0x20(%rbp),%rax
1264: 48 89 c6 mov %rax,%rsi
1267: 48 8d 05 d2 2d 00 00 lea 0x2dd2(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
126e: 48 89 c7 mov %rax,%rdi
1271: e8 c7 00 00 00 call 133d <_ZlsRSoRK3Foo>
1276: 48 8b 15 53 2d 00 00 mov 0x2d53(%rip),%rdx # 3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4>
127d: 48 89 d6 mov %rdx,%rsi
1280: 48 89 c7 mov %rax,%rdi
1283: e8 68 fe ff ff call 10f0 <_ZNSolsEPFRSoS_E@plt>
1288: 48 8d 05 81 2d 00 00 lea 0x2d81(%rip),%rax # 4010 <global>
128f: 48 89 c6 mov %rax,%rsi
1292: 48 8d 05 a7 2d 00 00 lea 0x2da7(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4>
1299: 48 89 c7 mov %rax,%rdi
129c: e8 9c 00 00 00 call 133d <_ZlsRSoRK3Foo>
12a1: 48 8b 15 28 2d 00 00 mov 0x2d28(%rip),%rdx # 3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4>
12a8: 48 89 d6 mov %rdx,%rsi
12ab: 48 89 c7 mov %rax,%rdi
12ae: e8 3d fe ff ff call 10f0 <_ZNSolsEPFRSoS_E@plt>
12b3: b8 00 00 00 00 mov $0x0,%eax
12b8: 48 8b 55 f8 mov -0x8(%rbp),%rdx
12bc: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
12c3: 00 00
12c5: 74 05 je 12cc <main+0xa3>
12c7: e8 34 fe ff ff call 1100 <__stack_chk_fail@plt>
12cc: c9 leave
12cd: c3 ret

查看.data段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> objdump -d -j .data two

two: file format elf64-x86-64


Disassembly of section .data:

0000000000004000 <__data_start>:
...

0000000000004008 <__dso_handle>:
4008: 08 40 00 00 00 00 00 00 .@......

0000000000004010 <global>:
4010: 0c 00 00 00 c3 f5 48 40 00 00 00 00 00 00 00 00 ......H@........

global的值被设为给定的默认值。

此时,编译器未合成一个默认构造函数,而是直接在初始化对象的地方直接通过指令设置成员的值。

若我们显式要求编译器合成一个默认构造函数:

1
2
public:
Foo()=default;

反汇编查看main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0000000000001229 <main>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: 48 83 ec 20 sub $0x20,%rsp
1235: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
123c: 00 00
123e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1242: 31 c0 xor %eax,%eax
1244: c7 45 e0 0c 00 00 00 movl $0xc,-0x20(%rbp)
124b: f3 0f 10 05 b5 0d 00 movss 0xdb5(%rip),%xmm0 # 2008 <_ZN6__pstl9execution2v1L5unseqE+0x1>
1252: 00
1253: f3 0f 11 45 e4 movss %xmm0,-0x1c(%rbp)
1258: 48 c7 45 e8 00 00 00 movq $0x0,-0x18(%rbp)
125f: 00

其实compiler未替我们合成。但是如果我们自己提供一个空构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0000000000001229 <main>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: 48 83 ec 20 sub $0x20,%rsp
1235: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
123c: 00 00
123e: 48 89 45 f8 mov %rax,-0x8(%rbp)
1242: 31 c0 xor %eax,%eax
1244: 48 8d 45 e0 lea -0x20(%rbp),%rax
1248: 48 89 c7 mov %rax,%rdi
124b: e8 64 01 00 00 call 13b4 <_ZN3FooC1Ev>
1250: 48 8d 45 e0 lea -0x20(%rbp),%rax
1254: 48 89 c6 mov %rax,%rsi

还真的有一个默认构造函数。

待解决的疑问

  • 是否编译器合成的是inline类型的构造函数,此时构造函数直接在程序中被替换了,我还不知道inline类型的函数是否在程序中会有一个专门的函数体。但是我做了一个小测试,定义一个类内的inline函数,非常简短

    1
    2
    public:
    int get_i() const {return i;}

    它在程序中是有一个函数体的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    00000000000013c0 <_ZNK3Foo5get_iEv>:
    13c0: f3 0f 1e fa endbr64
    13c4: 55 push %rbp
    13c5: 48 89 e5 mov %rsp,%rbp
    13c8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
    13cc: 48 8b 45 f8 mov -0x8(%rbp),%rax
    13d0: 8b 00 mov (%rax),%eax
    13d2: 5d pop %rbp
    13d3: c3 ret

    所以此时我暂时认定书本上所说,不会合成构造函数

  • global对象在何时何处被初始化。在1__static_initialization_and_destruction中没有调用构造函数初始化global成员。

  • 编译器的行为其实和优化级别、编译器版本、编译器类型都有关系,上述输出结果都是在我的测试环境下表现出来的,当我调整优化级别为O2时,即使在自己定义的构造函数中有简单的语句,都会被编译器优化,而不会合成构造函数……

接下来讨论nontrivial default constructor的四种情况。

带有default constructor的member class object

如果一个class没有任何constructor,但是内含一个member object,后者具备一个default constructor,那么这个class的implicit default constructor就是nontrivial。且合成操作只有在真正需要的时候才会发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo
{
public:
Foo() {}
explicit Foo(int) {}
};

class Bar
{
public:
Foo foo{10};
char * str;
};

int main()
{
Bar bar;
cout << bar.str << endl;

return 0;
}

反汇编查看bar构造函数,调用了foo(int)构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
00000000000012b4 <_ZN3BarC1Ev>:
12b4: f3 0f 1e fa endbr64
12b8: 55 push %rbp
12b9: 48 89 e5 mov %rsp,%rbp
12bc: 48 83 ec 10 sub $0x10,%rsp
12c0: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12c4: 48 8b 45 f8 mov -0x8(%rbp),%rax
12c8: be 0a 00 00 00 mov $0xa,%esi # 第二个参数为0xa,即给定的默认值
12cd: 48 89 c7 mov %rax,%rdi # 第一个参数是foo成员的地址
12d0: e8 cd ff ff ff call 12a2 <_ZN3FooC1Ei> # foo(int)
12d5: 90 nop
12d6: c9 leave
12d7: c3 ret

查看foo构造函数,只合成了foo(int),另一个没有使用到,未被合成:

1
2
3
4
5
6
7
8
9
00000000000012a2 <_ZN3FooC1Ei>:
12a2: f3 0f 1e fa endbr64
12a6: 55 push %rbp
12a7: 48 89 e5 mov %rsp,%rbp
12aa: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12ae: 89 75 f4 mov %esi,-0xc(%rbp)
12b1: 90 nop
12b2: 5d pop %rbp
12b3: c3 ret

若main函数中添加一个默认初始化的局部变量,则默认构造函数会被合成:

1
2
3
4
5
6
7
8
int main()
{
Bar bar;
Foo f;
cout << bar.str << endl;

return 0;
}

查看反汇编结果,此时两个构造函数均被合成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
00000000000012ae <_ZN3FooC1Ev>:
12ae: f3 0f 1e fa endbr64
12b2: 55 push %rbp
12b3: 48 89 e5 mov %rsp,%rbp
12b6: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12ba: 90 nop
12bb: 5d pop %rbp
12bc: c3 ret
12bd: 90 nop

00000000000012be <_ZN3FooC1Ei>:
12be: f3 0f 1e fa endbr64
12c2: 55 push %rbp
12c3: 48 89 e5 mov %rsp,%rbp
12c6: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12ca: 89 75 f4 mov %esi,-0xc(%rbp)
12cd: 90 nop
12ce: 5d pop %rbp
12cf: c3 ret

让我们再把注意力放到Bar的合成的default constructor上,其仅调用了foo的对应的构造函数,而未给str安排一个初始值,运行结果如下:

1
2
> ./two
8�y

bar.str的值仍是“垃圾值”。即使我们显式让编译器替我们合成一个constructor,它的编译结果也是一样的:

1
2
3
4
5
6
7
class Bar
{
public:
Bar()=default;
Foo foo{10};
char * str;
};

编译运行:

1
2
> g++ two.cc -O0 -g -o two && ./two
8��

在这种情况下,编译器合成的默认构造函数的行动仅是:如果class A内涵一个或多个member class objects,则class A的每一个constructor必须调用每一个member classes的default constructor。编译器会扩张已经存在的constructors,安插一些代码,使得在user code执行前,先调用必要的default constructors。如果多个class member objects都要求constructor初始化操作,则以它们的生命次序调用各个constructors。我们使用如下程序测试:

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
class Dopey
{
public:
Dopey() {}
};

class Sneezy
{
public:
Sneezy() {}
};

class Bashful
{
public:
Bashful() {}
};

class Show_White
{
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
private:
int mumble;
};

int main()
{
Show_White sw;


return 0;
}

编译,再反汇编查看:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
> g++ two.cc -O0 -g -o two && objdump -d -j .text two
...
000000000000123a <_ZN5DopeyC1Ev>:
123a: f3 0f 1e fa endbr64
123e: 55 push %rbp
123f: 48 89 e5 mov %rsp,%rbp
1242: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1246: 90 nop
1247: 5d pop %rbp
1248: c3 ret
1249: 90 nop

000000000000124a <_ZN6SneezyC1Ev>:
124a: f3 0f 1e fa endbr64
124e: 55 push %rbp
124f: 48 89 e5 mov %rsp,%rbp
1252: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1256: 90 nop
1257: 5d pop %rbp
1258: c3 ret
1259: 90 nop

000000000000125a <_ZN7BashfulC1Ev>:
125a: f3 0f 1e fa endbr64
125e: 55 push %rbp
125f: 48 89 e5 mov %rsp,%rbp
1262: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1266: 90 nop
1267: 5d pop %rbp
1268: c3 ret
1269: 90 nop

000000000000126a <_ZN10Show_WhiteC1Ev>:
126a: f3 0f 1e fa endbr64
126e: 55 push %rbp
126f: 48 89 e5 mov %rsp,%rbp
1272: 48 83 ec 10 sub $0x10,%rsp
1276: 48 89 7d f8 mov %rdi,-0x8(%rbp)
127a: 48 8b 45 f8 mov -0x8(%rbp),%rax
127e: 48 89 c7 mov %rax,%rdi
1281: e8 b4 ff ff ff call 123a <_ZN5DopeyC1Ev>
1286: 48 8b 45 f8 mov -0x8(%rbp),%rax
128a: 48 83 c0 01 add $0x1,%rax
128e: 48 89 c7 mov %rax,%rdi
1291: e8 b4 ff ff ff call 124a <_ZN6SneezyC1Ev>
1296: 48 8b 45 f8 mov -0x8(%rbp),%rax
129a: 48 83 c0 02 add $0x2,%rax
129e: 48 89 c7 mov %rax,%rdi
12a1: e8 b4 ff ff ff call 125a <_ZN7BashfulC1Ev>
12a6: 90 nop
12a7: c9 leave
12a8: c3 ret

编译器为Show_White类合成了默认构造函数,其按照声明顺序依次调用三个成员的默认构造函数。

注释掉Sneezy的默认构造函数,再次编译并反汇编:

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
36
37
38
> g++ two.cc -O0 -g -o two && objdump -d -j .text two
...
000000000000123a <_ZN5DopeyC1Ev>:
123a: f3 0f 1e fa endbr64
123e: 55 push %rbp
123f: 48 89 e5 mov %rsp,%rbp
1242: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1246: 90 nop
1247: 5d pop %rbp
1248: c3 ret
1249: 90 nop

000000000000124a <_ZN7BashfulC1Ev>:
124a: f3 0f 1e fa endbr64
124e: 55 push %rbp
124f: 48 89 e5 mov %rsp,%rbp
1252: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1256: 90 nop
1257: 5d pop %rbp
1258: c3 ret
1259: 90 nop

000000000000125a <_ZN10Show_WhiteC1Ev>:
125a: f3 0f 1e fa endbr64
125e: 55 push %rbp
125f: 48 89 e5 mov %rsp,%rbp
1262: 48 83 ec 10 sub $0x10,%rsp
1266: 48 89 7d f8 mov %rdi,-0x8(%rbp)
126a: 48 8b 45 f8 mov -0x8(%rbp),%rax
126e: 48 89 c7 mov %rax,%rdi
1271: e8 c4 ff ff ff call 123a <_ZN5DopeyC1Ev>
1276: 48 8b 45 f8 mov -0x8(%rbp),%rax
127a: 48 83 c0 02 add $0x2,%rax
127e: 48 89 c7 mov %rax,%rdi
1281: e8 c4 ff ff ff call 124a <_ZN7BashfulC1Ev>
1286: 90 nop
1287: c9 leave
1288: c3 ret

Sneezy类的默认构造函数未被合成(trivial的,编译器也不会为我们合成,即使我们显示要求),而Show_White类的默认构造函数仅调用两个成员的默认构造函数,不会负责为mumble指定一个初始值。

替Show_White添加一个成员函数,获取mumble的值,在main中打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Show_White
{
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
int get_mumble() const
{
return mumble;
}
private:
int mumble;
};

int main()
{
Show_White sw;
cout << sw.get_mumble() << endl;

return 0;
}

编译,运行两次:

1
2
3
> g++ two.cc -O0 -g -o two && ./two && ./two 
32729
32646

mumble的值是未定义的。

修改测试程序如下,查看构造函数调用成员的构造函数的顺序是否符合声明顺序:

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
...
class Sneezy
{
public:
Sneezy() {}
Sneezy(int) {}
};
...
class Show_White
{
public:
Dopey dopey;
int mumble{2048};
Sneezy sneezy;
Bashful bashful;
Show_White() : sneezy(10)
{
}
int get_mumble() const
{
return mumble;
}
};
int main()
{
Show_White sw;
cout << sw.mumble << endl;

return 0;
}

我们为mumble指定了初始值,并且特定将其位置调整到了其他类成员之间,编译并反汇编查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
00000000000012d2 <_ZN10Show_WhiteC1Ev>:
12d2: f3 0f 1e fa endbr64
12d6: 55 push %rbp
12d7: 48 89 e5 mov %rsp,%rbp
12da: 48 83 ec 10 sub $0x10,%rsp
12de: 48 89 7d f8 mov %rdi,-0x8(%rbp) # %rdi中是Show_White对象的地址
12e2: 48 8b 45 f8 mov -0x8(%rbp),%rax
12e6: 48 89 c7 mov %rax,%rdi
12e9: e8 b2 ff ff ff call 12a0 <_ZN5DopeyC1Ev> # %rdi同时也是第一个成员的地址,调用Dopey的默认构造函数
12ee: 48 8b 45 f8 mov -0x8(%rbp),%rax
12f2: c7 40 04 00 08 00 00 movl $0x800,0x4(%rax) # %rax+0x4是mumble的地址,初始值为0x800
12f9: 48 8b 45 f8 mov -0x8(%rbp),%rax
12fd: 48 83 c0 08 add $0x8,%rax # %rax+0x8为sneezy成员地址
1301: be 0a 00 00 00 mov $0xa,%esi
1306: 48 89 c7 mov %rax,%rdi
1309: e8 a2 ff ff ff call 12b0 <_ZN6SneezyC1Ei> # 调用sneezy(int)
130e: 48 8b 45 f8 mov -0x8(%rbp),%rax
1312: 48 83 c0 09 add $0x9,%rax # %rax+0x9为bashful成员地址
1316: 48 89 c7 mov %rax,%rdi
1319: e8 a4 ff ff ff call 12c2 <_ZN7BashfulC1Ev> # 调用Bashful默认构造函数
131e: 90 nop
131f: c9 leave
1320: c3 ret

显然,结果和预期完全吻合。

“带有default constructor”的Base class

如果一个不包含任何constructors的class派生自一个“带有default constructor”的base class,则derived class的default constructor会被视为nontrivial,并因此需要被合成出来,负责调用base class的default constructor(按照它们的声明次序)。若该class同时亦存在着“带有default constructors”的member class objects,则那些default constructors在所有base class constructors后被调用。

修改上一个测试程序,Show_White类新增两个带有default constructor的父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base1
{
public:
Base1() {}
};

class Base2
{
public:
Base2() {}
};
...

class Show_White : public Base1, public Base2
{
public:
Dopey dopey;
int mumble{2048};
Sneezy sneezy;
Bashful bashful;
};

编译并反汇编(省略了Show_White类之外的类的default constructor的函数体,因为它们do nothing):

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
36
37
38
39
40
41
42
43
00000000000012a0 <_ZN5Base1C1Ev>:
...

00000000000012b0 <_ZN5Base2C1Ev>:
...

00000000000012c0 <_ZN5DopeyC1Ev>:
...

00000000000012d0 <_ZN6SneezyC1Ev>:
...

00000000000012e0 <_ZN7BashfulC1Ev>:
...

00000000000012f0 <_ZN10Show_WhiteC1Ev>:
12f0: f3 0f 1e fa endbr64
12f4: 55 push %rbp
12f5: 48 89 e5 mov %rsp,%rbp
12f8: 48 83 ec 10 sub $0x10,%rsp
12fc: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1300: 48 8b 45 f8 mov -0x8(%rbp),%rax
1304: 48 89 c7 mov %rax,%rdi
1307: e8 94 ff ff ff call 12a0 <_ZN5Base1C1Ev> # call Base1()
130c: 48 8b 45 f8 mov -0x8(%rbp),%rax
1310: 48 89 c7 mov %rax,%rdi
1313: e8 98 ff ff ff call 12b0 <_ZN5Base2C1Ev> # call Base2()
1318: 48 8b 45 f8 mov -0x8(%rbp),%rax
131c: 48 89 c7 mov %rax,%rdi
131f: e8 9c ff ff ff call 12c0 <_ZN5DopeyC1Ev> # call Dopey()
1324: 48 8b 45 f8 mov -0x8(%rbp),%rax
1328: c7 40 04 00 08 00 00 movl $0x800,0x4(%rax) # mumble = 0x800
132f: 48 8b 45 f8 mov -0x8(%rbp),%rax
1333: 48 83 c0 08 add $0x8,%rax
1337: 48 89 c7 mov %rax,%rdi
133a: e8 91 ff ff ff call 12d0 <_ZN6SneezyC1Ev> # call Sneezy()
133f: 48 8b 45 f8 mov -0x8(%rbp),%rax
1343: 48 83 c0 09 add $0x9,%rax
1347: 48 89 c7 mov %rax,%rdi
134a: e8 91 ff ff ff call 12e0 <_ZN7BashfulC1Ev> # call Bashful()
134f: 90 nop
1350: c9 leave
1351: c3 ret

可见,Show_White的默认构造函数完成的工作与预期完全吻合。

“带有virtual function”的class

当类带有虚函数时,其对象内部必须保存关于多态的信息,这些信息一般存放在虚表之中。每个对象中会有一个虚指针指向虚表,因此,即使一个什么都不做的构造函数,编译器也在其中安插了设置虚指针的代码。总的来说,编译期间会发生如下扩张操作:

  • 一个virtual function table(vtbl)会被编译器产生出来,其中包含有虚函数的地址
  • 每个class object中,一个额外的pointer member(vptr)会被合成,内含class vtbl的地址

修改上述的测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
class Show_White
{
public:
// Show_White()=default;
virtual void vfunc1() const
{
cout << "vfunc1" << endl;
}
private:
virtual void vfunc2() const
{}
};

编译并反汇编,查看合成的构造函数内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> g++ two.cc -O0 -g -o two && objdump -d two
...
00000000000011c9 <main>:
11c9: f3 0f 1e fa endbr64
11cd: 55 push %rbp
11ce: 48 89 e5 mov %rsp,%rbp
11d1: 48 83 ec 10 sub $0x10,%rsp
11d5: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11dc: 00 00
11de: 48 89 45 f8 mov %rax,-0x8(%rbp)
11e2: 31 c0 xor %eax,%eax
11e4: 48 8d 05 7d 2b 00 00 lea 0x2b7d(%rip),%rax # 3d68 <_ZTV10Show_White+0x10>
11eb: 48 89 45 f0 mov %rax,-0x10(%rbp)
11ef: 48 8d 45 f0 lea -0x10(%rbp),%rax
11f3: 48 89 c7 mov %rax,%rdi
11f6: e8 8b 00 00 00 call 1286 <_ZNK10Show_White6vfunc1Ev>
11fb: b8 00 00 00 00 mov $0x0,%eax
1200: 48 8b 55 f8 mov -0x8(%rbp),%rdx
1204: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
120b: 00 00
120d: 74 05 je 1214 <main+0x4b>
120f: e8 ac fe ff ff call 10c0 <__stack_chk_fail@plt>
1214: c9 leave
1215: c3 ret

预期编译器将合成一个Show_White的default constructor,其完成虚指针的设定。但是main函数中并未调用show_white的默认构造函数,且也没有为其合成,而是直接将虚表起始地址加上0x10的偏移赋给了对象内部的虚指针。或者说此默认构造函数是inline的。准确来说,虚表的合成是编译期间完成的,而虚指针的设定是通过安插代码在执行期间完成的。

修改测试程序,使其继续包含基类和类成员对象,此时猜测编译器必然合成default constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
class Show_White : public Base1, public Base2
{
public:
Dopey dopey;
int mumble{2048};
Sneezy sneezy;
Bashful bashful;

virtual void vfunc1() const
{
cout << "vfunc1" << endl;
}
private:
virtual void vfunc2() const
{}
};

编译并反汇编查看结果:

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
000000000000135e <_ZN10Show_WhiteC1Ev>:
135e: f3 0f 1e fa endbr64
1362: 55 push %rbp
1363: 48 89 e5 mov %rsp,%rbp
1366: 48 83 ec 10 sub $0x10,%rsp
136a: 48 89 7d f8 mov %rdi,-0x8(%rbp)
136e: 48 8b 45 f8 mov -0x8(%rbp),%rax
1372: 48 89 c7 mov %rax,%rdi
1375: e8 46 ff ff ff call 12c0 <_ZN5Base1C1Ev>
137a: 48 8b 45 f8 mov -0x8(%rbp),%rax
137e: 48 89 c7 mov %rax,%rdi
1381: e8 4a ff ff ff call 12d0 <_ZN5Base2C1Ev>
1386: 48 8d 15 8b 29 00 00 lea 0x298b(%rip),%rdx # 3d18 <_ZTV10Show_White+0x10>
138d: 48 8b 45 f8 mov -0x8(%rbp),%rax
1391: 48 89 10 mov %rdx,(%rax)
1394: 48 8b 45 f8 mov -0x8(%rbp),%rax
1398: 48 83 c0 08 add $0x8,%rax
139c: 48 89 c7 mov %rax,%rdi
139f: e8 3c ff ff ff call 12e0 <_ZN5DopeyC1Ev>
13a4: 48 8b 45 f8 mov -0x8(%rbp),%rax
13a8: c7 40 0c 00 08 00 00 movl $0x800,0xc(%rax)
13af: 48 8b 45 f8 mov -0x8(%rbp),%rax
13b3: 48 83 c0 10 add $0x10,%rax
13b7: 48 89 c7 mov %rax,%rdi
13ba: e8 31 ff ff ff call 12f0 <_ZN6SneezyC1Ev>
13bf: 48 8b 45 f8 mov -0x8(%rbp),%rax
13c3: 48 83 c0 11 add $0x11,%rax
13c7: 48 89 c7 mov %rax,%rdi
13ca: e8 31 ff ff ff call 1300 <_ZN7BashfulC1Ev>
13cf: 90 nop
13d0: c9 leave
13d1: c3 ret

编译器合成了default constructor,其先后完成了Base1默认构造、Base2默认构造、设置虚指针、按照声明次序初始化成员。

“带有virtual base class”的class

测试程序:

1
2
3
4
5
6
7
8
9
10
class Base1
{
public:
Base1() {}
// virtual ~Base1() {}
};

class Show_White : virtual public Base1
{
};

编译并反编译查看main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 48 83 ec 10 sub $0x10,%rsp
1195: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119c: 00 00
119e: 48 89 45 f8 mov %rax,-0x8(%rbp)
11a2: 31 c0 xor %eax,%eax
11a4: 48 8d 45 f0 lea -0x10(%rbp),%rax
11a8: 48 89 c7 mov %rax,%rdi
11ab: e8 9a 00 00 00 call 124a <_ZN10Show_WhiteC1Ev>
11b0: b8 00 00 00 00 mov $0x0,%eax
11b5: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11b9: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11c0: 00 00
11c2: 74 05 je 11c9 <main+0x40>
11c4: e8 b7 fe ff ff call 1080 <__stack_chk_fail@plt>
11c9: c9 leave
11ca: c3 ret

可见编译器确实合成了默认构造函数,查看其内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
000000000000124a <_ZN10Show_WhiteC1Ev>:
124a: f3 0f 1e fa endbr64
124e: 55 push %rbp
124f: 48 89 e5 mov %rsp,%rbp
1252: 48 83 ec 10 sub $0x10,%rsp
1256: 48 89 7d f8 mov %rdi,-0x8(%rbp)
125a: 48 8b 45 f8 mov -0x8(%rbp),%rax
125e: 48 89 c7 mov %rax,%rdi
1261: e8 d4 ff ff ff call 123a <_ZN5Base1C1Ev>
1266: 48 8d 15 f3 2a 00 00 lea 0x2af3(%rip),%rdx # 3d60 <_ZTT10Show_White>
126d: 48 8b 45 f8 mov -0x8(%rbp),%rax
1271: 48 89 10 mov %rdx,(%rax)
1274: 90 nop
1275: c9 leave
1276: c3 ret

首先调用了基类的构造函数,并且设置了对象起始处的虚指针为 <_ZTT10Show_White>,可以通过c++filt查看其名称:

1
2
> c++filt _ZTT10Show_White
VTT for Show_White

Note:c++涉及虚继承的部分比较复杂,尤其是当虚继承、虚函数、多重继承均涉及到的时候,关于虚继承的部分后续blog会再讨论,目前只需要先直到此种情况编译器会合成默认构造函数,其至少需要完成设置虚指针的工作。

copy constructor

default memberwise initialization

如果class没有提供一个explicit copy constructor,当进行拷贝初始化时,其内部是以所谓的default member wise initialization手法完成的。即把每一个内建的或派生的data member的值,从某个object拷贝一份到另一个object身上,但并不会拷贝其中的member class object,而是以递归的方式施行member wise initialization。

copy constructor分为trivial和nontrivial,后者会被合成于程序中,决定一个copyconstructor是否为trivial的标准为,是否class展现出所谓的bitwise copy semantics

bitwise copy semantics

类的初始化和拷贝是逐个成员进行的,即member wise。

当类具备如下特征时,被认为不具备bitwise copy semantics,此时编译器必须为不包含任何构造函数定义的类合成默认构造函数,以及拷贝构造函数。

  • 某个类成员具备默认构造函数(不管是编译器合成的还是显式定义的)
  • 类的基类具备拷贝构造函数(不管是编译器合成的还是显式定义的)
  • 包含虚函数(虚函数可以继承,即使某个类未定义自己的虚函数,但是可以从其他类继承虚函数)
  • 包含虚基类

逐一验证之。

1 内含member object 具有copy constructor

编写如下测试程序:

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
class Word
{
public:
Word() = default;
Word(const Word &w) = default;
~Word() {}

private:
char *str;
int len;
};

class String
{
public:
String() {}
String(const String &) = default;

private:
Word word;
int len;
};

int main()
{
String s1;
String s2 = s1;

return 0;
}

编译并反编译查看main函数:

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
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 53 push %rbx
1192: 48 83 ec 48 sub $0x48,%rsp
1196: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119d: 00 00
119f: 48 89 45 e8 mov %rax,-0x18(%rbp)
11a3: 31 c0 xor %eax,%eax
11a5: 48 8d 45 b0 lea -0x50(%rbp),%rax
11a9: 48 89 c7 mov %rax,%rdi
11ac: e8 d1 00 00 00 call 1282 <_ZN6StringC1Ev>
11b1: 48 8b 45 b0 mov -0x50(%rbp),%rax
11b5: 48 8b 55 b8 mov -0x48(%rbp),%rdx
11b9: 48 89 45 d0 mov %rax,-0x30(%rbp)
11bd: 48 89 55 d8 mov %rdx,-0x28(%rbp)
11c1: 48 8b 45 c0 mov -0x40(%rbp),%rax
11c5: 48 89 45 e0 mov %rax,-0x20(%rbp)
11c9: bb 00 00 00 00 mov $0x0,%ebx
11ce: 48 8d 45 d0 lea -0x30(%rbp),%rax
11d2: 48 89 c7 mov %rax,%rdi
11d5: e8 b8 00 00 00 call 1292 <_ZN6StringD1Ev>
11da: 48 8d 45 b0 lea -0x50(%rbp),%rax
11de: 48 89 c7 mov %rax,%rdi
11e1: e8 ac 00 00 00 call 1292 <_ZN6StringD1Ev>
11e6: 89 d8 mov %ebx,%eax
11e8: 48 8b 55 e8 mov -0x18(%rbp),%rdx
11ec: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11f3: 00 00
11f5: 74 05 je 11fc <main+0x73>
11f7: e8 84 fe ff ff call 1080 <__stack_chk_fail@plt>
11fc: 48 8b 5d f8 mov -0x8(%rbp),%rbx
1200: c9 leave
1201: c3 ret

可见,并未合成和调用拷贝构造函数,拷贝构造的过程仅是通过mov指令搬运内存空间的内容。

如下是修改后的测试程序,为Word类显式定义了一个拷贝构造函数:

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
class Word
{
public:
Word() = default;
Word(const Word &w)
: str(w.str), len(w.len)
{
}

~Word() {}

private:
char *str;
int len;
};

class String
{
public:
String() {}
String(const String &) = default;

private:
Word word;
int len;
};

int main()
{
String s1;
String s2 = s1;

return 0;
}

编译并反汇编,查看.text段的结果:

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
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 53 push %rbx
1192: 48 83 ec 48 sub $0x48,%rsp
1196: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119d: 00 00
119f: 48 89 45 e8 mov %rax,-0x18(%rbp)
11a3: 31 c0 xor %eax,%eax
11a5: 48 8d 45 b0 lea -0x50(%rbp),%rax
11a9: 48 89 c7 mov %rax,%rdi
11ac: e8 fb 00 00 00 call 12ac <_ZN6StringC1Ev>
11b1: 48 8d 55 b0 lea -0x50(%rbp),%rdx
11b5: 48 8d 45 d0 lea -0x30(%rbp),%rax
11b9: 48 89 d6 mov %rdx,%rsi
11bc: 48 89 c7 mov %rax,%rdi
11bf: e8 18 01 00 00 call 12dc <_ZN6StringC1ERKS_>
11c4: bb 00 00 00 00 mov $0x0,%ebx
11c9: 48 8d 45 d0 lea -0x30(%rbp),%rax
11cd: 48 89 c7 mov %rax,%rdi
11d0: e8 e7 00 00 00 call 12bc <_ZN6StringD1Ev>
11d5: 48 8d 45 b0 lea -0x50(%rbp),%rax
11d9: 48 89 c7 mov %rax,%rdi
11dc: e8 db 00 00 00 call 12bc <_ZN6StringD1Ev>
11e1: 89 d8 mov %ebx,%eax
11e3: 48 8b 55 e8 mov -0x18(%rbp),%rdx
11e7: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11ee: 00 00
11f0: 74 05 je 11f7 <main+0x6e>
11f2: e8 89 fe ff ff call 1080 <__stack_chk_fail@plt>
11f7: 48 8b 5d f8 mov -0x8(%rbp),%rbx
11fb: c9 leave
11fc: c3 ret

main函数确实调用了String类的拷贝构造函数,查看String拷贝构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
00000000000012dc <_ZN6StringC1ERKS_>:
12dc: f3 0f 1e fa endbr64
12e0: 55 push %rbp
12e1: 48 89 e5 mov %rsp,%rbp
12e4: 48 83 ec 10 sub $0x10,%rsp
12e8: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12ec: 48 89 75 f0 mov %rsi,-0x10(%rbp)
12f0: 48 8b 45 f8 mov -0x8(%rbp),%rax
12f4: 48 8b 55 f0 mov -0x10(%rbp),%rdx
12f8: 48 89 d6 mov %rdx,%rsi # %rsi = &rhs->word
12fb: 48 89 c7 mov %rax,%rdi # %rdi = &this->word
12fe: e8 69 ff ff ff call 126c <_ZN4WordC1ERKS_> # call Word::Word(const Word &)
1303: 48 8b 45 f0 mov -0x10(%rbp),%rax # %rax = rhs
1307: 8b 50 10 mov 0x10(%rax),%edx # %edx = rhs->len
130a: 48 8b 45 f8 mov -0x8(%rbp),%rax # %rax = this
130e: 89 50 10 mov %edx,0x10(%rax) # this->len = %edx
1311: 90 nop
1312: c9 leave
1313: c3 ret

可见,合成的String的拷贝构造函数类似:

1
2
3
4
5
String::String(const String & rhs)
{
word.Word::Word(rhs.word);
len = rhs.len;
}

因此该说法成立。

2 基类 具有copy constructor

如下是测试程序,Base类以及其子类Dervied,此时不满足条件一,即其成员均为基本类型,不包含拷贝构造函数,因此编译器也不会为其合成一个拷贝构造函数。

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

class Base
{
public:
Base() = default;
Base(const Base &) = default;
private:
char *str;
int len;
};

class Derived : public Base
{
public:
Derived() = default;
Derived(const Derived &) = default;
private:
double d;
};

int main()
{
String s1;
String s2 = s1;

return 0;
}

编译,反汇编查看.text段情况,此时,Base和Dervied的默认构造函数、拷贝构造函数均未被合成。

为Base类添加拷贝构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
Base() = default;
Base(const Base & b) : str(b.str), len(b.len)
{
}

private:
char *str;
int len;
};

重新编译并反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0000000000001189 <main>:
1189: f3 0f 1e fa endbr64
118d: 55 push %rbp
118e: 48 89 e5 mov %rsp,%rbp
1191: 48 83 ec 40 sub $0x40,%rsp
1195: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
119c: 00 00
119e: 48 89 45 f8 mov %rax,-0x8(%rbp)
11a2: 31 c0 xor %eax,%eax
11a4: 48 8d 55 c0 lea -0x40(%rbp),%rdx
11a8: 48 8d 45 e0 lea -0x20(%rbp),%rax
11ac: 48 89 d6 mov %rdx,%rsi
11af: 48 89 c7 mov %rax,%rdi
11b2: e8 bb 00 00 00 call 1272 <_ZN7DerivedC1ERKS_>
11b7: b8 00 00 00 00 mov $0x0,%eax
11bc: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11c0: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11c7: 00 00
11c9: 74 05 je 11d0 <main+0x47>
11cb: e8 b0 fe ff ff call 1080 <__stack_chk_fail@plt>
11d0: c9 leave
11d1: c3 ret

main函数中调用了Derived类的拷贝构造函数,且可以看到.text段中包含Base和Dervied类的拷贝构造函数:

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
36
37
38
0000000000001242 <_ZN4BaseC1ERKS_>:
1242: f3 0f 1e fa endbr64
1246: 55 push %rbp
1247: 48 89 e5 mov %rsp,%rbp
124a: 48 89 7d f8 mov %rdi,-0x8(%rbp)
124e: 48 89 75 f0 mov %rsi,-0x10(%rbp)
1252: 48 8b 45 f0 mov -0x10(%rbp),%rax
1256: 48 8b 10 mov (%rax),%rdx
1259: 48 8b 45 f8 mov -0x8(%rbp),%rax
125d: 48 89 10 mov %rdx,(%rax)
1260: 48 8b 45 f0 mov -0x10(%rbp),%rax
1264: 8b 50 08 mov 0x8(%rax),%edx
1267: 48 8b 45 f8 mov -0x8(%rbp),%rax
126b: 89 50 08 mov %edx,0x8(%rax)
126e: 90 nop
126f: 5d pop %rbp
1270: c3 ret
1271: 90 nop

0000000000001272 <_ZN7DerivedC1ERKS_>:
1272: f3 0f 1e fa endbr64
1276: 55 push %rbp
1277: 48 89 e5 mov %rsp,%rbp
127a: 48 83 ec 10 sub $0x10,%rsp
127e: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1282: 48 89 75 f0 mov %rsi,-0x10(%rbp)
1286: 48 8b 45 f8 mov -0x8(%rbp),%rax
128a: 48 8b 55 f0 mov -0x10(%rbp),%rdx
128e: 48 89 d6 mov %rdx,%rsi
1291: 48 89 c7 mov %rax,%rdi
1294: e8 a9 ff ff ff call 1242 <_ZN4BaseC1ERKS_> # call Base::Base(const Base &)
1299: 48 8b 45 f0 mov -0x10(%rbp),%rax
129d: f2 0f 10 40 10 movsd 0x10(%rax),%xmm0
12a2: 48 8b 45 f8 mov -0x8(%rbp),%rax
12a6: f2 0f 11 40 10 movsd %xmm0,0x10(%rax) # 拷贝浮点类型成员
12ab: 90 nop
12ac: c9 leave
12ad: c3 ret

Base类的拷贝构造函数完成自身非static成员的拷贝初始化,但如果我们显式定义的Base的拷贝构造为空,则编译器不会默认帮我们拷贝Base的成员,而编译器合成的Derived类的拷贝构造函数,自动调用了Base的拷贝构造函数,且帮我们拷贝初始化了double类型的成员。

因此,条件二成立。

3 带有”virtual function“的class

如下测试程序,父类和子类均包括虚函数:

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
36
37
38
class ZooAnimal
{
public:
ZooAnimal() = default;
~ZooAnimal() = default;
virtual void animate() { cout << "ZooAnimal animate\n"; }
virtual void draw() { cout << "ZooAnimal draw\n"; }

private:
};

class Bear : public ZooAnimal
{
public:
Bear() = default;
void animate() { cout << "Bear animate\n"; }
void draw() { cout << "Bear draw\n"; }
virtual void dance() { cout << "Bear dance\n"; }

private:
};

void draw(ZooAnimal &zoey)
{
zoey.draw();
}

int main()
{
Bear yogi;
Bear winnie = yogi;

ZooAnimal franny = yogi;
draw(yogi); // call Bear::draw()
draw(franny); // call ZooAnimal::draw()

return 0;
}

编译运行,父类对象和子类对象调用各自的虚函数:

1
2
3
> g++ two.cc -O0 -g -o two && ./two 
Bear draw
ZooAnimal draw

反汇编查看main函数部分:

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
00000000000011d3 <main>:
11d3: f3 0f 1e fa endbr64
11d7: 55 push %rbp
11d8: 48 89 e5 mov %rsp,%rbp
11db: 48 83 ec 20 sub $0x20,%rsp
11df: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11e6: 00 00
11e8: 48 89 45 f8 mov %rax,-0x8(%rbp)
11ec: 31 c0 xor %eax,%eax
11ee: 48 8d 05 43 2b 00 00 lea 0x2b43(%rip),%rax # 3d38 <_ZTV4Bear+0x10>
11f5: 48 89 45 e0 mov %rax,-0x20(%rbp)
11f9: 48 8d 05 38 2b 00 00 lea 0x2b38(%rip),%rax # 3d38 <_ZTV4Bear+0x10>
1200: 48 89 45 e8 mov %rax,-0x18(%rbp)
1204: 48 8d 05 55 2b 00 00 lea 0x2b55(%rip),%rax # 3d60 <_ZTV9ZooAnimal+0x10>
120b: 48 89 45 f0 mov %rax,-0x10(%rbp)
120f: 48 8d 45 e0 lea -0x20(%rbp),%rax
1213: 48 89 c7 mov %rax,%rdi
1216: e8 8e ff ff ff call 11a9 <_Z4drawR9ZooAnimal>
121b: 48 8d 45 f0 lea -0x10(%rbp),%rax
121f: 48 89 c7 mov %rax,%rdi
1222: e8 82 ff ff ff call 11a9 <_Z4drawR9ZooAnimal>
1227: b8 00 00 00 00 mov $0x0,%eax
122c: 48 8b 55 f8 mov -0x8(%rbp),%rdx
1230: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
1237: 00 00
1239: 74 05 je 1240 <main+0x6d>
123b: e8 60 fe ff ff call 10a0 <__stack_chk_fail@plt>
1240: c9 leave
1241: c3 ret

此时编译器并未合成默认构造函数和拷贝构造函数,而是直接完成了虚指针的设置。与条件三稍有出入。值得注意的是,两个Bear对象的虚指针是相同的,这意味着两个对象共享虚表。

4 virtual base class

如下测试程序,Y继承自X。此时X和Y的默认构造函数和拷贝构造函数均为trivial,因此编译器不会合成它们的默认构造函数和拷贝构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class X
{
};

class Y : public X
{
};

int main()
{
X x1;
X x2 = x1;
Y y1;
Y y2 = y1;

return 0;
}

编译,反汇编查看.text段,与我们的预期吻合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00000000000011b3 <main>:
11b3: f3 0f 1e fa endbr64
11b7: 55 push %rbp
11b8: 48 89 e5 mov %rsp,%rbp
11bb: 48 83 ec 10 sub $0x10,%rsp
11bf: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11c6: 00 00
11c8: 48 89 45 f8 mov %rax,-0x8(%rbp)
11cc: 31 c0 xor %eax,%eax
11ce: b8 00 00 00 00 mov $0x0,%eax
11d3: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11d7: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11de: 00 00
11e0: 74 05 je 11e7 <main+0x34>
11e2: e8 99 fe ff ff call 1080 <__stack_chk_fail@plt>
11e7: c9 leave
11e8: c3 ret

补充测试程序,添加一个类Z,其虚拟继承自X,其与Y的区别仅在于继承是否为虚拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
class Z : virtual public X
{
};

int main()
{
X x1;
X x2 = x1;
Z y1;
Z y2 = y1;

return 0;
}

编译并反汇编,编译器会为Z类合成默认构造函数和拷贝构造函数,且main函数会调用它们:

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
00000000000011b3 <main>:
11b3: f3 0f 1e fa endbr64
11b7: 55 push %rbp
11b8: 48 89 e5 mov %rsp,%rbp
11bb: 48 83 ec 20 sub $0x20,%rsp
11bf: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11c6: 00 00
11c8: 48 89 45 f8 mov %rax,-0x8(%rbp)
11cc: 31 c0 xor %eax,%eax
11ce: 48 8d 45 e8 lea -0x18(%rbp),%rax
11d2: 48 89 c7 mov %rax,%rdi
11d5: e8 9e 00 00 00 call 1278 <_ZN1ZC1Ev>
11da: 48 8d 55 e8 lea -0x18(%rbp),%rdx
11de: 48 8d 45 f0 lea -0x10(%rbp),%rax
11e2: 48 89 d6 mov %rdx,%rsi
11e5: 48 89 c7 mov %rax,%rdi
11e8: e8 a9 00 00 00 call 1296 <_ZN1ZC1ERKS_>
11ed: b8 00 00 00 00 mov $0x0,%eax
11f2: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11f6: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdx
11fd: 00 00
11ff: 74 05 je 1206 <main+0x53>
1201: e8 7a fe ff ff call 1080 <__stack_chk_fail@plt>
1206: c9 leave
1207: c3 ret

查看合成的两个构造函数:

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
0000000000001278 <_ZN1ZC1Ev>:
1278: f3 0f 1e fa endbr64
127c: 55 push %rbp
127d: 48 89 e5 mov %rsp,%rbp
1280: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1284: 48 8d 15 d5 2a 00 00 lea 0x2ad5(%rip),%rdx # 3d60 <_ZTT1Z>
128b: 48 8b 45 f8 mov -0x8(%rbp),%rax
128f: 48 89 10 mov %rdx,(%rax)
1292: 90 nop
1293: 5d pop %rbp
1294: c3 ret
1295: 90 nop

0000000000001296 <_ZN1ZC1ERKS_>:
1296: f3 0f 1e fa endbr64
129a: 55 push %rbp
129b: 48 89 e5 mov %rsp,%rbp
129e: 48 89 7d f8 mov %rdi,-0x8(%rbp)
12a2: 48 89 75 f0 mov %rsi,-0x10(%rbp)
12a6: 48 8d 15 b3 2a 00 00 lea 0x2ab3(%rip),%rdx # 3d60 <_ZTT1Z>
12ad: 48 8b 45 f8 mov -0x8(%rbp),%rax
12b1: 48 89 10 mov %rdx,(%rax)
12b4: 90 nop
12b5: 5d pop %rbp
12b6: c3 ret

使用命令查看三个名词:

1
2
3
4
5
6
> c++filt _ZN1ZC1Ev
Z::Z()
> c++filt _ZN1ZC1ERKS_
Z::Z(Z const&)
> c++filt _ZTT1Z
VTT for Z

构造函数主要完成VTT的设置(关于虚基类后续会有更多讨论),目前可以理解为,这种情况类似包含虚函数的类,需要做一些信息的处理。

the next…

我们可以总结,默认构造函数和拷贝构造函数,按照memberwise的方式初始化类的非static成员,若类的所有成员都是基本的内置类型,或者是没有默认构造和拷贝构造的类对象,此时其是具有bitwise语义的,可以直接通过mov指令完成初始化和拷贝。当类中存在某些成员或者subobject(即继承自父类的部分),需要调用它们的默认构造和拷贝构造,则编译器会合成此类的默认构造和拷贝构造,它们调用这些成员和父类的相应的构造操作。

如果类中包含虚函数或者其继承自虚基类,此时类中会有编译器安插的特殊成员(虚指针),这些相关的信息需要编译器帮类设置和管理,此时编译器会合成默认构造和拷贝构造。关于这部分的内容,涉及到类对象是如何布局的,以及虚表的管理,后续进一步分析……

python conditon and RLock

本文分析python中条件变量Conditon和可重入锁RLock的底层实现。完整的源代码部分在**/usr/lib/python3.10/threading.py**(Ubuntu 22.04,其他版本的Linux系统应该也类似)。

Condition

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

  • wait:一个线程等待条件变量的条件成立而挂起自身
  • notify:一个线程使得条件变量的条件成立而唤醒等待线程

我们可以理解为:一个线程通过wait等待某个条件成立,在这之前它自动挂起,直到另一个线程通过调用notify通知其条件已成立,解除挂起。python中实现条件变量的是一个Condition类,其源码注解中有对该类的说明:

1
2
3
4
5
6
7
8
9
10
"""Class that implements a condition variable.

A condition variable allows one or more threads to wait until they are
notified by another thread.

If the lock argument is given and not None, it must be a Lock or RLock
object, and it is used as the underlying lock. Otherwise, a new RLock object
is created and used as the underlying lock.

"""

init

即初始化一个条件变量,主要功能步骤为:

  • 接收一个可选参数lock,即用户给定的锁,也可以选择让condition自己准备一个锁,默认情况下condition会选择一个RLock。该锁一般称为underlying lock
  • export该锁成员的一些方法
  • 初始化一个deque类型的成员_waiters,当线程调用该条件变量的wait方法时会使用到。
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
def __init__(self, lock=None):
# 如果没有传入lock,则默认使用一个RLock
if lock is None:
lock = RLock()
self._lock = lock
# Export the lock's acquire() and release() methods
self.acquire = lock.acquire
self.release = lock.release
# If the lock defines _release_save() and/or _acquire_restore(),
# these override the default implementations (which just call
# release() and acquire() on the lock). Ditto for _is_owned().
try:
self._release_save = lock._release_save
except AttributeError:
pass
try:
self._acquire_restore = lock._acquire_restore
except AttributeError:
pass
try:
self._is_owned = lock._is_owned
except AttributeError:
pass
# 初始化一个deque
self._waiters = _deque()

wait

线程通过调用该方法,挂起自身直到被唤醒(其他线程调用notify)或超时(通过自己传入的timeout参数决定)。wait执行的步骤:

  • 获取一个锁(名为waiter),并acquire
  • 将其加入到condition的deque中
  • 尝试重新获取该锁
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
36
37
38
39
40
41
42
43
44
45
46
47
48
def wait(self, timeout=None):
"""Wait until notified or until a timeout occurs.

If the calling thread has not acquired the lock when this method is
called, a RuntimeError is raised.

This method releases the underlying lock, and then blocks until it is
awakened by a notify() or notify_all() call for the same condition
variable in another thread, or until the optional timeout occurs. Once
awakened or timed out, it re-acquires the lock and returns.

When the timeout argument is present and not None, it should be a
floating point number specifying a timeout for the operation in seconds
(or fractions thereof).

When the underlying lock is an RLock, it is not released using its
release() method, since this may not actually unlock the lock when it
was acquired multiple times recursively. Instead, an internal interface
of the RLock class is used, which really unlocks it even when it has
been recursively acquired several times. Another internal interface is
then used to restore the recursion level when the lock is reacquired.

"""
# wait时必须尝试获取到_lock
if not self._is_owned():
raise RuntimeError("cannot wait on un-acquired lock")
waiter = _allocate_lock()
waiter.acquire() # 获取新分配的锁
self._waiters.append(waiter) # 入队_waiters
saved_state = self._release_save()
gotit = False
try: # restore state no matter what (e.g., KeyboardInterrupt)
if timeout is None:
waiter.acquire()
gotit = True
else:
if timeout > 0:
gotit = waiter.acquire(True, timeout)
else:葡萄
gotit = waiter.acquire(False)
return gotit
finally:
self._acquire_restore(saved_state)
if not gotit:
try:
self._waiters.remove(waiter)
except ValueError:
pass

notify

线程通过notify,使得条件变量的条件成立,从而换醒因等待条件变量而挂起的线程。notify方法接受一个可选的参数n,指明唤醒的线程个数,其默认值为1。其执行步骤为:

  • 当指定的唤醒线程数目n不为0,取_waiters的队首元素(一个锁)waiter,并调用其release方法,唤醒等待该锁的线程,即将waiter入队的线程。并从队列中移除该锁。循环直至n为0
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
def notify(self, n=1):
"""Wake up one or more threads waiting on this condition, if any.

If the calling thread has not acquired the lock when this method is
called, a RuntimeError is raised.

This method wakes up at most n of the threads waiting for the condition
variable; it is a no-op if no threads are waiting.

"""
if not self._is_owned():
raise RuntimeError("cannot notify on un-acquired lock")
waiters = self._waiters
while waiters and n > 0:
waiter = waiters[0] # 获取队列首元素并release
try:
waiter.release()
except RuntimeError:
# gh-92530: The previous call of notify() released the lock,
# but was interrupted before removing it from the queue.
# It can happen if a signal handler raises an exception,
# like CTRL+C which raises KeyboardInterrupt.
pass
else:
n -= 1
try:
waiters.remove(waiter) # 移除该锁
except ValueError:
pass

总结

python中的条件变量,通过一个互斥锁_lock以及一个等待队列_waiters,管理线程的wait和notify。每个线程调用wait和notify方法时,需要获取underlying lock,然后才能对_waiters进行操作。对于wait方法,其分配并获取一个锁,放入waiters队列中,并尝试再次获取该锁,从而阻塞;对于notify,其从waiters中出队指定的锁数,并release这些锁,唤醒将这些锁入队后再阻塞在锁上的线程。

RLock

即可重入锁,与一般锁不同的是:一个线程如果尝试获取其已经拥有的锁,则会阻塞造成死锁。而可重入锁可以避免此种情况,需要注意的是,可重入锁需要手动释放锁,且加锁次数和释放次数要相同。python中的可重入锁由**_RLock**实现,其源码中的注解如下:

1
2
3
4
5
6
7
8
"""This class implements reentrant lock objects.

A reentrant lock must be released by the thread that acquired it. Once a
thread has acquired a reentrant lock, the same thread may acquire it
again without blocking; the thread must release it once for each time it
has acquired it.

"""

init

初始化一个RLock,使用了三个成员:_block(锁本身)、_owner(锁的拥有者,初始为None)以及_count(获取锁的次数,recursion level)。

1
2
3
4
def __init__(self):
self._block = _allocate_lock()
self._owner = None
self._count = 0

acquire

尝试获取RLock,接受两个参数:blocking(默认为True)和timeout(默认为-1)。具体执行情况有:

  • 当线程已经获取了该锁,则递增recursion level然后返回。
  • 当其他线程拥有锁,则阻塞至获取锁或超时(timeout为正浮点数时)

源程序如下:

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
def acquire(self, blocking=True, timeout=-1):
"""Acquire a lock, blocking or non-blocking.

When invoked without arguments: if this thread already owns the lock,
increment the recursion level by one, and return immediately. Otherwise,
if another thread owns the lock, block until the lock is unlocked. Once
the lock is unlocked (not owned by any thread), then grab ownership, set
the recursion level to one, and return. If more than one thread is
blocked waiting until the lock is unlocked, only one at a time will be
able to grab ownership of the lock. There is no return value in this
case.

When invoked with the blocking argument set to true, do the same thing
as when called without arguments, and return true.

When invoked with the blocking argument set to false, do not block. If a
call without an argument would block, return false immediately;
otherwise, do the same thing as when called without arguments, and
return true.

When invoked with the floating-point timeout argument set to a positive
value, block for at most the number of seconds specified by timeout
and as long as the lock cannot be acquired. Return true if the lock has
been acquired, false if the timeout has elapsed.

"""
me = get_ident()
if self._owner == me: # 如果已经拥有该锁,递增_count
self._count += 1
return 1
rc = self._block.acquire(blocking, timeout) # 尝试获取锁
if rc:
self._owner = me
self._count = 1

release

拥有锁的线程调用release递减获取的次数,次数递减至0则释放锁。只有拥有锁的线程可以调用,否则会触发运行时异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def release(self):
"""Release a lock, decrementing the recursion level.

If after the decrement it is zero, reset the lock to unlocked (not owned
by any thread), and if any other threads are blocked waiting for the
lock to become unlocked, allow exactly one of them to proceed. If after
the decrement the recursion level is still nonzero, the lock remains
locked and owned by the calling thread.

Only call this method when the calling thread owns the lock. A
RuntimeError is raised if this method is called when the lock is
unlocked.

There is no return value.

"""
if self._owner != get_ident(): # 检查是否当前线程是锁的拥有者
raise RuntimeError("cannot release un-acquired lock")
self._count = count = self._count - 1 # 递减count
if not count: # count为0则释放锁
self._owner = None
self._block.release()

RTTI


RTTI,即运行时类型识别,由两个运算符实现:

  • typeid运算符,返回表达式的类型
  • dynamic_cast运算符,将基类指针或引用安全地转换成派生类的指针或引用

本篇主要阐述typeid运算符

typeid运算符

typeid表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字,操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或其公有派生类。

typeid运算符的一些特点:

  • 忽略顶层const
  • 如果表达式是引用类型,返回其所引用对象的类型,而不是type reference
  • 作用于函数或数组时不会返回指针类型
  • 当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算符对象的静态类型。例如,指向父类的指针和引用父类的引用,均返回其声明类型
  • 当运算对象定义了至少一个虚函数,其类型直到运行时才会求得

typeid运算符使用示例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{
// case 1: ignore top-const
const int ci = 0;
int i = 0;
cout << "type of ci: " << typeid(ci).name() << endl;
cout << "type of i: " << typeid(i).name() << endl;
assert(typeid(ci) == typeid(i));

// case 2: reference
double d = 0.9;
double &rd = d;
cout << "type of d: " << typeid(d).name() << endl;
cout << "type of rd: " << typeid(rd).name() << endl;
assert(typeid(d) == typeid(rd));

// case 3: function and array
int a[10];
cout << "type of array int[10]: " << typeid(a).name() << endl;
cout << "type of fucntion main: " << typeid(main).name() << endl;
cout << "type of pointer to int: " << typeid(int *).name() << endl;

return 0;
}

输出结果为:

1
2
3
4
5
6
7
type of ci: i
type of i: i
type of d: d
type of rd: d
type of array int[10]: A10_i
type of fucntion main: FivE
type of pointer to int: Pi

当typeid作用于类类型时,基类和派生类是否具备虚函数会对结果有影响,我们使用如下两个类:base和derived验证。

1
2
3
4
5
6
7
8
9
class base
{
public:
/*virtual*/ ~base() {}
};

class derived : public base
{
};

base中的虚析构函数被注释掉了,执行如下main函数

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
int main()
{
// case 4: non-contain virtual function class
// pointer
base *p1 = new base;
base *p2 = new derived;
derived *p3 = new derived;

cout << "type of *p1: " << typeid(*p1).name() << endl;
cout << "type of *p2: " << typeid(*p2).name() << endl;
cout << "type of *p3: " << typeid(*p3).name() << endl;

// reference
base b1;
derived d1;
base &rb1 = b1;
base &rb2 = d1;
derived &rb3 = d1;

cout << "type of rb1: " << typeid(rb1).name() << endl;
cout << "type of rb2: " << typeid(rb2).name() << endl;
cout << "type of rb3: " << typeid(rb3).name() << endl;

return 0;
}

输出结果如下(忽略了构造函数和析构函数的输出)

1
2
3
4
5
6
type of *p1: 4base
type of *p2: 4base
type of *p3: 7derived
type of rb1: 4base
type of rb2: 4base
type of rb3: 7derived

取消对base虚析构函数的注释,再次运行:

1
2
3
4
5
6
type of *p1: 4base
type of *p2: 7derived
type of *p3: 7derived
type of rb1: 4base
type of rb2: 7derived
type of rb3: 7derived

Note:typeid作用于指针上只会输出静态类型信息,不能获取其指向对象是什么类型。比如下面的测试代码,其中base和derived中包含有虚函数:

1
2
3
4
5
6
base *p1 = new base;
base *p2 = new derived;
derived *p3 = new derived;
cout << "type of p1: " << typeid(p1).name() << '\t' << "type of *p1: " << typeid(*p1).name() << endl;
cout << "type of p2: " << typeid(p2).name() << '\t' << "type of *p2: " << typeid(*p2).name() << endl;
cout << "type of p3: " << typeid(p3).name() << '\t' << "type of *p3: " << typeid(*p3).name() << endl;

输出结果为:

1
2
3
type of p1: P4base	type of *p1: 4base
type of p2: P4base type of *p2: 7derived
type of p3: P7derived type of *p3: 7derived

typeid是否需要运行时检查决定了表达式是否会被求值。只有当类型含有虚函数时,编译器才会对表达式求值,反之,如果类型不含有虚函数,typeid返回表达式的静态类型。如果表达式的动态类型可能与静态类型不同,必须在运行时对表达式求值以确定返回的类型。因此,若指针指向的类型不含有虚函数,则其不必非得是有效指针。

使用RTTI

当设计类的时候,可能类中需要定义某个针对该类类型的对象的操作,则完成该操作的方法的参数一般是该类的类型。例如,为base类实现相等运算符。

1
2
3
4
5
6
7
8
9
class base
{
public:
base()
{
cout << " \n base constructor \n";
}
bool operator==(const base &);
};

当设计一个derived类继承自base时,我们可能也需要为derived定义相等运算符。但虚函数的参数必须是相符的,因此derived类的定义看起来可能是这样的:

1
2
3
4
5
6
7
8
9
class derived : public base
{
public:
derived()
{
cout << " \n derived constructor \n";
}
bool operator==(const base &);
};

此时在derived中的相等运算符中,存在两个问题:

  • 我们无法访问base类中的数据成员a,因为他们是私有的

  • 我们无法对比derived中的b成员,编译器会提醒我们class “base” has no member “b”

即使我们可以通过在base类中添加一个方法返回该私有成员,但第二个问题却难以解决。一个方法是在derived类的相等运算符方法中强制转换类型。

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
36
37
38
39
40
41
42
43
44
45
class base
{
public:
base() = default;
base(int _a) : a(_a)
{
}
virtual bool operator==(const base &rhs) const
{
return a == rhs.a;
}

virtual bool operator!=(const base &rhs) const
{
return a != rhs.a;
}

int get_a() const { return a; } // 提供访问基类私有成员的方法

private:
int a{};
};

class derived : public base
{
public:
derived() = default;
derived(int _a, int _b) : base(_a), b(_b)
{
}
bool operator==(const base &rhs) const
{
// 强制类型转换,但我们需要确保传入的参数动态类型确实是derived
return base::operator==(rhs) &&
b == (static_cast<const derived &>(rhs)).b;
}

bool operator!=(const base &rhs) const
{
return !(*this == rhs);
}

private:
int b{};
};

倘若我们不小心比较了一个base类对象和derived类对象,可能最终结果是符合我们的预期,但是该过程是未定义的。要想实现真正有效的相等比较操作,首先我们需要认清楚一个事实:如果参与对比的两个对象类型不同,则比较结果为false。因此我们可以通过RTTI解决该问题:定义相等运算符的形参是基类的引用,使用typeid检查两个运算对象是否是同一类型,类型一致才继续对比成员,并通过dynamic_cast转换类型。在派生类中,我们也可以通过明确调用基类的方法对比基类部分的成员是否相等。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class base
{
friend bool operator==(const base &lhs, const base &rhs)
{
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

friend bool operator!=(const base &lhs, const base &rhs)
{
return !(lhs == rhs);
}

public:
base() = default;
base(int _a) : a(_a)
{
}
virtual bool equal(const base &rhs) const
{
return a == rhs.a;
}

private:
int a{};
};

class derived : public base
{
public:
derived() = default;
derived(int _a, int _b) : base(_a), b(_b)
{
}
bool equal(const base &rhs) const
{
auto r = dynamic_cast<const derived &>(rhs);
return base::equal(rhs) && b == r.b;
}

private:
int b{};
};

int main()
{
// case 1: base != derived
derived d1(1, 2);
base b1(1);
assert(d1 != b1);

// case 2: base != base
base b2(1);
base b3(2);
assert(b2 != b3);

// case 3: base == base
base b4(13);
base b5(13);
assert(b4 == b5);

// case 4: derived != derived
derived d2(11, 12);
derived d3(11, 10);
assert(d2 != d3);

// case 4: derived == derived
derived d4(11, 12);
derived d5(11, 12);
assert(d4 == d5);

cout << "Pass!\n";

return 0;
}

运行结果为:

1
Pass!

type_info类

type_info类的精确定义随着编译器的不同而略有差异,但c++标准规定必须定义在typeinfo头文件中,且至少提供如下4种操作:

操作 含义
t1 == t2 是否type_info对象t1和t2表示同一种类型
t1 != t2 是否type_info对象t1和t2表示不同类型
t.name() 返回一个c风格的字符串,表示类型名字的可打印形式
t1.before(t2) 返回一个bool值,表示t1是否位于t2之前

需要注意的是,type_info对象只能由typeid运算符创建。如下程序测试c++ primer练习19.10:

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
class A
{
public:
virtual ~A() {}
};

class B : public A
{
public:
virtual ~B() {}
};

class C : public B
{
public:
virtual ~C() {}
};

int main()
{
A *pa = new C;
cout << typeid(pa).name() << endl;

C cobj;
A &ra = cobj;
cout << typeid(&ra).name() << endl;
B *px = new B;
A &rra = *px;
cout << typeid(rra).name() << endl;

delete px;
return 0;
}

运行结果如下:

1
2
3
P1A
P1A
1B

c++对象布局

本文讨论c++中的对象内存布局,包括c++对象的数据成员以及为支持多态机制而添加的额外的数据在对象中的分布。

实验环境

体系结构:x86_64

操作系统:Ubuntu 22.04 Linux os 6.2.0-36-generic

编译器:gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0

Data Member的布局

Question 1:对象中不同访问类型的成员是怎么分布的?有可能同种访问类型的成员排列在一起,然后按照指定的顺序比如public、protected、private分布,也可能按照声明顺序分布。

编写如下测试程序,类Obj中的数据成员随意声明,检验编译器是否会把同种访问类型的成员安排在相邻的内存位置。

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
36
37
38
39
40
41
42
43
44
45
46

class Print;

class Obj
{
friend class Print;

public:
int a;
uint8_t b;

private:
double c;
float d;

protected:
short e;

public:
long f;
};

class Print
{
public:
void printObj()
{
Obj obj; // offset
cout << "&obj: " << &obj << endl; // 0
cout << "&obj.a: " << &obj.a << endl; // 0
cout << "&obj.b: " << (int *)&obj.b << endl; // 4
cout << "&obj.c: " << &obj.c << endl; // 8
cout << "&obj.d: " << &obj.d << endl; // 16
cout << "&obj.e: " << &obj.e << endl; // 20
cout << "&obj.f: " << &obj.f << endl; // 24
cout << "sizeof obj: " << sizeof obj << endl;
}
};

int main()
{
Print p;
p.printObj();

return 0;
}

执行结果如下:

1
2
3
4
5
6
7
8
&obj:   0x7fffd69afeb0
&obj.a: 0x7fffd69afeb0
&obj.b: 0x7fffd69afeb4
&obj.c: 0x7fffd69afeb8
&obj.d: 0x7fffd69afec0
&obj.e: 0x7fffd69afec4
&obj.f: 0x7fffd69afec8
sizeof obj: 32

可见,数据成员是按照声明顺序排列的,这一点与c语言中的结构体一致,编译器也不会特意安排不同访问类型的数据成员的前后地址。

继承无多态

为上述的类Obj添加一个以及两个父类,子类会继承父类的数据成员,查看父类的成员在子类中的分布。

Question 1: 父类的数据成员应该被安排在子类的开头处,如果有多个父类,则按照父类的继承顺序,先安置父类的数据成员,再安置子类的数据成员。

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
36
37
38
39
40
41
42
43
44
45
46
47
48

class Base1
{
public:
int x1;

protected:
int x2;

private:
int x3;
};

class Base2
{

public:
int y1;

protected:
int y2;

private:
int y3;
};

class Obj : public Base1, public Base2
.....
void printObj()
{
Obj obj;
cout << "&obj: " << &obj << endl; // 0

cout << "&obj.x1: " << &obj.x1 << endl; // 0
cout << "&obj.x2: " << &obj.x2 << endl; // 4
// 接下来是x3,但是x3对子类来说无法直接访问
cout << "&obj.y1: " << &obj.y1 << endl; // 12
cout << "&obj.y2: " << &obj.y2 << endl; // 16
// 接下来是y3,但是y3对子类来说无法直接访问
//然后才是子类自己声明的数据成员
cout << "&obj.a: " << &obj.a << endl; // 24
cout << "&obj.b: " << (int *)&obj.b << endl;
cout << "&obj.c: " << &obj.c << endl;
cout << "&obj.d: " << &obj.d << endl;
cout << "&obj.e: " << &obj.e << endl;
cout << "&obj.f: " << &obj.f << endl;
cout << "sizeof obj: " << sizeof obj << endl;
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
&obj:   0x7fffe0548610
&obj.x1: 0x7fffe0548610
&obj.x2: 0x7fffe0548614
&obj.y1: 0x7fffe054861c
&obj.y2: 0x7fffe0548620
&obj.a: 0x7fffe0548628
&obj.b: 0x7fffe054862c
&obj.c: 0x7fffe0548630
&obj.d: 0x7fffe0548638
&obj.e: 0x7fffe054863c
&obj.f: 0x7fffe0548640
sizeof obj: 56

可见,编译器确实会按照继承的顺序安排父类的数据成员。

加上多态——单继承

修改程序,给Base1加上虚函数,Obj仅继承自Base1:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

class Base1
{
public:
int x1;
virtual void virtfunc1()
{

}
protected:
int x2;

private:
int x3;
};
....

class Obj : public Base1
{
friend class Print;

public:
int a;
uint8_t b;

private:
double c;
float d;

protected:
short e;

public:
long f;
};

class Print
{
public:
void printObj()
{
Obj obj;
cout << "&obj: " << &obj << endl; // 0

cout << "&obj.x1: " << &obj.x1 << endl; // 8
cout << "&obj.x2: " << &obj.x2 << endl; // 12
// 接下来是16 x3
// cout << "&obj.y1: " << &obj.y1 << endl;
// cout << "&obj.y2: " << &obj.y2 << endl;
cout << "&obj.a: " << &obj.a << endl; // 20
cout << "&obj.b: " << (int *)&obj.b << endl; // 24

cout << "&obj.c: " << &obj.c << endl;
cout << "&obj.d: " << &obj.d << endl;
cout << "&obj.e: " << &obj.e << endl;
cout << "&obj.f: " << &obj.f << endl;
cout << "sizeof obj: " << sizeof obj << endl;
}
};

执行结果如下:

1
2
3
4
5
6
7
8
9
10
&obj:   0x7ffc31b16580
&obj.x1: 0x7ffc31b16588
&obj.x2: 0x7ffc31b1658c
&obj.a: 0x7ffc31b16594
&obj.b: 0x7ffc31b16598
&obj.c: 0x7ffc31b165a0
&obj.d: 0x7ffc31b165a8
&obj.e: 0x7ffc31b165ac
&obj.f: 0x7ffc31b165b0
sizeof obj: 56

编译器在对象头部插入了虚指针,因此占用了8个字节。gdb查看obj对象头八个字节的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) set print object on  # 打印对象
(gdb) set print pretty on # 每行只会显示结构体的一名成员,而且还会根据成员的定义层次进行缩进
(gdb) x /x &obj
0x7fffffffde20: 0x55557d30 # 虚指针的值
(gdb) x /10x &obj
0x7fffffffde20: 0x55557d30 0x00005555 0xf7cc23a7 0x00007fff
0x7fffffffde30: 0xf7e28e88 0x00007fff 0xf7e28e88 0x00007fff
0x7fffffffde40: 0xf7e28e88 0x00007fff
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557d30 (subobject @ 0x7fffffffde20):
[0]: 0x5555555552ba <Base1::virtfunc1()>
(gdb) p obj
$4 = (Obj) {<Base1> = {
_vptr.Base1 = 0x555555557d30 <vtable for Obj+16>,
x1 = -137616473, x2 = 32767, x3 = -136147320}, a = 32767,
b = 136 '\210', c = 6.9533490812636458e-310,
d = -8.64173507e+33, e = 32767, f = 140737352208216}

通过gdb查看对象内存分布的功能,可见编译器确实将虚指针存放在了对象开头八字节,虚指针指向虚表(并非表头,虚指针所指的位置前面还有信息),虚表中存放着虚函数的地址,而虚指针指向第一个虚函数的地址条目。

接下来,我们探究对象的虚表内容,编写如下代码,似的类的继承关系为Obj -> Base2 -> Base1,Base1和Base2均有各自的虚函数:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class Base1
{
public:
int x1;
virtual void virtfunc1()
{
cout << "Base 1 virtfunc1\n";
}

protected:
int x2;

private:
int x3;
};

class Base2 : public Base1
{

public:
int y1;
virtual void virtfunc1()
{
cout << "Base 2 virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Base 2 virtfunc2\n";
}

protected:
int y2;

private:
int y3;
};

class Obj : public Base2
{
friend class Print;

public:
int a;
uint8_t b;
virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Obj virtfunc2\n";
}

private:
double c;
float d;

protected:
short e;

public:
long f;
};


int main()
{
Obj obj;
obj.virtfunc1();
obj.virtfunc2();
Base2 b2 = obj;
b2.virtfunc2();
Base1 b1 = b2;
b1.virtfunc1();
cout << hex << *(uintptr_t *)&b1 << endl; // base1 虚指针
cout << hex << *(uintptr_t *)&b2 << endl; // base2 虚指针
cout << hex << *(uintptr_t *)&obj << endl; // obj虚指针

return 0;
}

通过gdb查看三个类实例的虚表内容:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl b1
vtable for 'Base1' @ 0x555555557d30 (subobject @ 0x7fffffffddf0):
[0]: 0x55555555550c <Base1::virtfunc1()>
(gdb) info vtbl b2
vtable for 'Base2' @ 0x555555557d10 (subobject @ 0x7fffffffde10):
[0]: 0x555555555538 <Base2::virtfunc1()>
[1]: 0x555555555564 <Base2::virtfunc2()>
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557cf0 (subobject @ 0x7fffffffde30):
[0]: 0x555555555590 <Obj::virtfunc1()>
[1]: 0x5555555555bc <Obj::virtfunc2()>

每个类都有自己的虚表,虚表中的条目包含虚函数的地址,对象调用虚函数时通过查找虚表中的条目而获得虚函数的地址。可以发现,obj中继承自父类的虚函数的地址与父类的虚函数地址均不相同,因为在这里,子类都重写了父类的虚函数。如obj重写了继承自Base2和Base1的虚函数,而Base2重写了继承自Base1的虚函数。

这也可以解释为什么指针和引用可以实现多态,即使指针是父类类型,但是如果其指向一个子类,则虚指针指向的是子类的虚表。并且,定义了虚函数的类,其默认构造函数和拷贝构造函数不是bit Semantics的,因为构造对象(默认或者拷贝)时,构造函数会设置对象的虚指针指向该对象类型的虚表。因此main函数中,通过赋值构造的b1和b2的虚指针指向的是Base1和Base2的虚表。

注释掉Base2对继承自Base1的虚函数的重写,以及Object对继承自Base2的虚函数的重写,我们猜测,虚表中的相应的虚函数条目应该和父类保持一致。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

class Base2 : public Base1
{

public:
int y1;
// virtual void virtfunc1()
// {
// cout << "Base 2 virtfunc1\n";
// }

virtual void virtfunc2()
{
cout << "Base 2 virtfunc2\n";
}

protected:
int y2;

private:
int y3;
};

class Obj : public Base2
{
friend class Print;

public:
int a;
uint8_t b;
virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

// virtual void virtfunc2()
// {
// cout << "Obj virtfunc2\n";
// }

private:
double c;
float d;

protected:
short e;

public:
long f;
};

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl b1
vtable for 'Base1' @ 0x555555557d30 (subobject @ 0x7fffffffddf0):
[0]: 0x55555555550e <Base1::virtfunc1()>
(gdb) info vtbl b2
vtable for 'Base2' @ 0x555555557d10 (subobject @ 0x7fffffffde10):
[0]: 0x55555555550e <Base1::virtfunc1()>
[1]: 0x55555555553a <Base2::virtfunc2()>
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557cf0 (subobject @ 0x7fffffffde30):
[0]: 0x555555555566 <Obj::virtfunc1()>
[1]: 0x55555555553a <Base2::virtfunc2()>

取消刚刚的注释,为Base1和Base2再添加一个虚函数,以及Obj类自己的虚函数,重新编译。同时,我们验证同类型的对象的虚指针是相同的,即程序只需要维护一份虚表(没必要为不同对象维护单独的一份虚表):

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

class Base1
{
public:
int x1;
virtual void base1_virt1()
{
cout << "Base 1 virtfunc1\n";
}
virtual void base1_virt2()
{
cout << "Base 1 virtfunc2\n";
}

protected:
int x2;

private:
int x3;
};

class Base2 : public Base1
{

public:
int y1;
virtual void base1_virt1()
{
cout << "Base 2 virtfunc1 from Base 1\n";
}

virtual void base2_virt2()
{
cout << "Base 2 virtfunc2 \n";
}

protected:
int y2;

private:
int y3;
};

class Obj : public Base2
{
public:
int a;
uint8_t b;
virtual void base1_virt1()
{
cout << "Obj 1 virtfunc1 from base 1\n";
}

virtual void base2_virt1()
{
cout << "Obj 1 virtfunc1 from base 2\n";
}

virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Obj virtfunc2\n";
}

private:
double c;
float d;

protected:
short e;

public:
long f;
};


int main()
{
Obj obj;
Base2 b2 = obj;
Base1 b1 = obj;
Base1 b11; // 默认构造
Base2 b22 = b2; // 拷贝构造
cout << hex << *(uintptr_t *)&b1 << endl; // base1 虚指针
cout << hex << *(uintptr_t *)&b11 << endl; // base1 虚指针
cout << hex << *(uintptr_t *)&b2 << endl; // base2 虚指针
cout << hex << *(uintptr_t *)&b22 << endl; // base2 虚指针
cout << hex << *(uintptr_t *)&obj << endl; // obj虚指针

return 0;
}

gdb查看对象内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) p b1
$1 = {_vptr.Base1 = 0x555555557d28 <vtable for Base1+16>, x1 = -136147320, x2 = 32767, x3 = -136147320}
(gdb) p b11
$2 = {_vptr.Base1 = 0x555555557d28 <vtable for Base1+16>, x1 = 0, x2 = 0, x3 = 0}
(gdb) p b2
$3 = {<Base1> = {_vptr.Base1 = 0x555555557d00 <vtable for Base2+16>, x1 = -136147320, x2 = 32767,
x3 = -136147320}, y1 = 32767, y2 = -137033468, y3 = 32767}
(gdb) p b22
$4 = {<Base1> = {_vptr.Base1 = 0x555555557d00 <vtable for Base2+16>, x1 = -136147320, x2 = 32767,
x3 = -136147320}, y1 = 32767, y2 = -137033468, y3 = 32767}
(gdb) p obj
$5 = {<Base2> = {<Base1> = {_vptr.Base1 = 0x555555557cc0 <vtable for Obj+16>, x1 = -136147320,
x2 = 32767, x3 = -136147320}, y1 = 32767, y2 = -137033468, y3 = 32767}, a = -136147112,
b = 255 '\377', c = 6.9533490273497203e-310, d = -9.19059065e+33, e = 32767, f = 140737352208008}

b11和b1都是Base1对象,虚指针相同,而通过拷贝构造b2得到的b22,两个对象的虚指针也是一样的。但是我们可以发现,虚指针的值,并不是指向虚表开头,而是vtable+16。可见虚指针所指的位置前面还有16字节的内容。

接下来探究虚表中的内容。

查看Obj类对象的虚表前十六字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557cc0 (subobject @ 0x7fffffffe430):
[0]: 0x55555555563a <Obj::base1_virt1()>
[1]: 0x5555555555b6 <Base1::base1_virt2()>
[2]: 0x55555555560e <Base2::base2_virt2()>
[3]: 0x555555555666 <Obj::base2_virt1()>
[4]: 0x555555555692 <Obj::virtfunc1()>
[5]: 0x5555555556be <Obj::virtfunc2()>
(gdb) x /12x 0x555555557cc0
0x555555557cc0 <_ZTV3Obj+16>: 0x5555563a 0x00005555 0x555555b6 0x00005555
0x555555557cd0 <_ZTV3Obj+32>: 0x5555560e 0x00005555 0x55555666 0x00005555
0x555555557ce0 <_ZTV3Obj+48>: 0x55555692 0x00005555 0x555556be 0x00005555
(gdb) x /4x 0x555555557cc0-16 # 虚表开头16字节内容
0x555555557cb0 <_ZTV3Obj>: 0x00000000 0x00000000 0x55557d38 0x00005555
(gdb) x /4x 0x555555557d38 # 第二个64-bit字的内容
0x555555557d38 <_ZTI3Obj>: 0xf7e1dc30 0x00007fff 0x555560b1 0x00005555

虚指针所指处开始,每个条目为一个64-bit字,存储虚函数的地址。

通过c++filt命令查看de_mangle后的名字:

1
2
> c++filt _ZTI3Obj
> typeinfo for Obj

可见,第一个字是0,第二个字存了一个地址,保存着对象类型的信息。使用指针和引用时,我们可以通过其所指向的或所引用的对象的虚指针,找到对象的虚表,进而找到对象的类型信息,从而得知这个对象是父类对象还是子类对象,c++的多态机制应该也是通过这种方式实现的。

查看Base1对象的虚表:

1
2
3
4
5
6
7
8
9
10
(gdb) info vtbl b1
vtable for 'Base1' @ 0x555555557d28 (subobject @ 0x7fffffffe3b0):
[0]: 0x55555555558a <Base1::base1_virt1()>
[1]: 0x5555555555b6 <Base1::base1_virt2()>
(gdb) x /4x 0x555555557d28
0x555555557d28 <_ZTV5Base1+16>: 0x5555558a 0x00005555 0x555555b6 0x00005555
(gdb) x /4x 0x555555557d28-16 # 虚表开头16字节内容
0x555555557d18 <_ZTV5Base1>: 0x00000000 0x00000000 0x55557d68 0x00005555
(gdb) x /4x 0x555555557d68 # 第二个64-bit字的内容
0x555555557d68 <_ZTI5Base1>: 0xf7e1cfa0 0x00007fff 0x555560bd 0x00005555

c++filt查看_ZIT5Base1:

1
2
> c++filt _ZTI5Base1
typeinfo for Base1

查看Base2对象的虚表:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl b2
vtable for 'Base2' @ 0x555555557d00 (subobject @ 0x7fffffffe3f0):
[0]: 0x5555555555e2 <Base2::base1_virt1()>
[1]: 0x5555555555b6 <Base1::base1_virt2()>
[2]: 0x55555555560e <Base2::base2_virt2()>
(gdb) x /4x 0x555555557d00
0x555555557d00 <_ZTV5Base2+16>: 0x555555e2 0x00005555 0x555555b6 0x00005555
(gdb) x /4x 0x555555557d00-16 # 虚表开头16字节内容
0x555555557cf0 <_ZTV5Base2>: 0x00000000 0x00000000 0x55557d50 0x00005555
(gdb) x /4x 0x555555557d50 # 第二个64-bit字的内容
0x555555557d50 <_ZTI5Base2>: 0xf7e1dc30 0x00007fff 0x555560b6 0x00005555

c++filt查看_ZIT5Base2:

1
2
> c++filt _ZTI5Base2
typeinfo for Base2

我们可以将上述结论抽象为如下的图例:

Base1对象布局:

image-20231108201212879

Base2对象布局:

image-20231108201248944

Obj对象布局:

image-20231108201300556

多重继承

将上述程序修改为,Obj继承自Base1和Base2:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

class Base1
{
public:
int x1{};
virtual void base1_virt1()
{
cout << "Base 1 virtfunc1\n";
}
virtual void base1_virt2()
{
cout << "Base 1 virtfunc2\n";
}

int x2{};

int x3{};
};

class Base2
{

public:
int y1;
virtual void base2_virt1()
{
cout << "Base 2 virtfunc1 \n";
}

virtual void base2_virt2()
{
cout << "Base 2 virtfunc2 \n";
}

int y2;

int y3;
};

class Obj : public Base1, public Base2
{
public:
int a;
uint8_t b;
virtual void base1_virt1()
{
cout << hex << this << " Obj 1 virtfunc1 from base 1\n";
}

virtual void base2_virt1()
{
cout << hex << this <<" Obj 1 virtfunc1 from base 2\n";
}

virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Obj virtfunc2\n";
}

public:
double c;
float d;
short e;

long f;
};

int main()
{
Obj obj;
Base1 b1;
Base2 b2;
obj.base1_virt1();
obj.base2_virt1();

return 0;
}

查看三个类对象的布局:

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
36
(gdb) p obj
$1 = {
<Base1> = { # Base1父类的数据成员以及一个虚指针
_vptr.Base1 = 0x555555557ca8 <vtable for Obj+16>,
x1 = 0,
x2 = 0,
x3 = 0
},
<Base2> = { # Base2父类的数据成员以及一个虚指针
_vptr.Base2 = 0x555555557ce0 <vtable for Obj+72>,
y1 = -136147320,
y2 = 32767,
y3 = -137033468
}, # 子类自己的数据成员
members of Obj:
a = 32767,
b = 88 'X',
c = 6.9533490273497203e-310,
d = -9.19059065e+33,
e = 32767,
f = 140737352208008
}
(gdb) p b1
$2 = {
_vptr.Base1 = 0x555555557d20 <vtable for Base1+16>,
x1 = 0,
x2 = 0,
x3 = 0
}
(gdb) p b2
$3 = {
_vptr.Base2 = 0x555555557d00 <vtable for Base2+16>,
y1 = 0,
y2 = 0,
y3 = 0
}

不难发现,在子类中,父类的数据成员按照声明的顺序排列,并且每个父类子对象都包含一个虚指针。我们可以认为,子类包含每个父类完整的子对象。三个类各自的虚表都不相同,因为虚表中存放的虚函数地址都是与类相关的。

查看Base1对象的虚表内容:

1
2
3
4
5
6
7
8
(gdb) info vtbl b1
vtable for 'Base1' @ 0x555555557d20 (subobject @ 0x7fffffffdd90):
[0]: 0x555555555420 <Base1::base1_virt1()>
[1]: 0x55555555544c <Base1::base1_virt2()>
(gdb) x /4x 0x555555557d20-16 # Offset pointer to typeinfo
0x555555557d10 <_ZTV5Base1>: 0x00000000 0x00000000 0x55557d78 0x00005555
(gdb) x /4x 0x555555557d78
0x555555557d78 <_ZTI5Base1>: 0xf7e1cfa0 0x00007fff 0x555560b2 0x00005555

与上一节讨论的情况相同,b1的虚表中包含Base1的虚函数地址,以及一个偏移值(0)和一个指向类类型信息的指针。

同理,Base2对象的虚表内容也是一样的布局:

1
2
3
4
5
6
7
8
9
10
(gdb) info vtbl b2
vtable for 'Base2' @ 0x555555557d00 (subobject @ 0x7fffffffddb0):
[0]: 0x555555555478 <Base2::base2_virt1()>
[1]: 0x5555555554a4 <Base2::base2_virt2()>
(gdb) x /4x 0x555555557d00
0x555555557d00 <_ZTV5Base2+16>: 0x55555478 0x00005555 0x555554a4 0x00005555
(gdb) x /4x 0x555555557d00-16
0x555555557cf0 <_ZTV5Base2>: 0x00000000 0x00000000 0x55557d68 0x00005555
(gdb) x /4x 0x555555557d68
0x555555557d68 <_ZTI5Base2>: 0xf7e1cfa0 0x00007fff 0x555560ab 0x00005555

我们重点查看Obj类在多重继承的情况下,虚表的布局情况:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557ca8 (subobject @ 0x7fffffffddd0):
[0]: 0x5555555554d0 <Obj::base1_virt1()>
[1]: 0x55555555544c <Base1::base1_virt2()>
[2]: 0x555555555524 <Obj::base2_virt1()>
[3]: 0x555555555582 <Obj::virtfunc1()>
[4]: 0x5555555555ae <Obj::virtfunc2()>

vtable for 'Base2' @ 0x555555557ce0 (subobject @ 0x7fffffffdde8):
[0]: 0x555555555577 <non-virtual thunk to Obj::base2_virt1()>
[1]: 0x5555555554a4 <Base2::base2_virt2()>

obj中包含两个虚指针。这实际上也是为多态机制服务的。在c++中,父类指针和子类指针都可以指向一个子类对象。严格来说,父类指针指向的是子类对象中的父类子对象。

修改main函数,验证上述说法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

int main()
{
Obj obj;
Base1 b1;
Base2 b2;
Obj * p1 = &obj;
Base1 * pb1 = &obj;
Base2 * pb2 = &obj;
cout << "Obj * -> obj: " << hex << p1 << endl;
cout << "Base1 * -> obj: " << hex << pb1 << endl;
cout << "Base2 * -> obj: " << hex << pb2 << endl;
pb1 = p1;
pb2 = p1;
cout << "Obj * -> obj: " << hex << p1 << endl;
cout << "Base1 * -> obj: " << hex << pb1 << endl;
cout << "Base2 * -> obj: " << hex << pb2 << endl;

return 0;
}

输出如下:

1
2
3
4
5
6
7
> ./test
Obj * -> obj: 0x7ffdc3d28750
Base1 * -> obj: 0x7ffdc3d28750
Base2 * -> obj: 0x7ffdc3d28768 # ?? 指向同一对象但是值不同
Obj * -> obj: 0x7ffdc3d28750
Base1 * -> obj: 0x7ffdc3d28750
Base2 * -> obj: 0x7ffdc3d28768 # ?? 直接将p1赋值到pb2也是相同结果

这是因为,c++编译器为我们做了调整,因为子类对象,我们也可以将它当成一个父类对象,子类对象包含父类对象的内容,即子类对象中的父类子对象。Obj类型的对象布局如下所示,首先是Base1的子对象,其次是Base2的子对象:

image-20231110195844016

当指向Base2对象的指针实际上指向的是Obj类型对象时,编译器将其调整为指向obj对象的Base2子对象。这样当我们通过Base2指针操作Obj对象中的Base2类的数据成员时,各成员的偏移是不需要更改的。

接下来探究obj对象中的两个虚指针所指向的虚表的内容:

1
2
3
4
5
6
7
8
9
10
11
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557ca8 (subobject @ 0x7fffffffddd0):
[0]: 0x5555555554d0 <Obj::base1_virt1()>
[1]: 0x55555555544c <Base1::base1_virt2()>
[2]: 0x555555555524 <Obj::base2_virt1()>
[3]: 0x555555555582 <Obj::virtfunc1()>
[4]: 0x5555555555ae <Obj::virtfunc2()>

vtable for 'Base2' @ 0x555555557ce0 (subobject @ 0x7fffffffdde8):
[0]: 0x555555555577 <non-virtual thunk to Obj::base2_virt1()>
[1]: 0x5555555554a4 <Base2::base2_virt2()>

打印obj的对象布局时,可以看到关于虚指针的描述信息:**_vptr.Base1 = 0x555555557ca8 <vtable for Obj+16>_vptr.Base2 = 0x555555557ce0 <vtable for Obj+72>**,这两个指针相差也确实是56字节,因此两个指针应该指向的是Obj类的虚表中的不同位置。回顾一下,单继承的多态下,对象头部八字节存放虚指针,指向虚表的第16字节处,虚表的前16字节包括一个8字节的偏移量(一般为0)以及一个指向类型信息的指针,然后是虚函数地址条目。

此处_vptr.Base1所指向的仍然是虚表的第16字节,而_vptr.Base2所指为虚表的第72字节。从第16字节到第72字节,包含5个虚函数条目,分别有Obj类继承自Base1的2个虚函数,重写Base2的一个虚函数,以及自身定义的两个虚函数。5个条目共占40字节。还剩下16字节,应该包含的是一个偏移量以及一个指向类型信息的指针。

通过gdb查看:

1
2
3
4
5
6
(gdb) x /4x 0x555555557ca8-16
0x555555557c98 <_ZTV3Obj>: 0x00000000 0x00000000 0x55557d30 0x00005555
(gdb) x /4x 0x555555557ce0-16
0x555555557cd0 <_ZTV3Obj+56>: 0xffffffe8 0xffffffff 0x55557d30 0x00005555
(gdb) x /4x 0x555555557d30
0x555555557d30 <_ZTI3Obj>: 0xf7e1dcf0 0x00007fff 0x555560a6 0x00005555

可见,第一个虚指针前16字节为偏移量0和指向obj类型信息的指针,而第二个虚指针前16字节为偏移量-24和指向obj类型信息的指针。因此Obj类的虚表其实相当于组合了两个父类的虚表,子类和第一个声明的父类共用第一个虚表(因为第一个声明的父类子对象就在子类对象的最前端),后续声明的父类的虚表相继排列在后面。第一个也称之为“主虚表”(primary virtual table),后续的虚表又称之为“次虚表”(secondary virtual table)。一个含有虚函数(无论是其本身的,还是继承而来的)的类,可以有一个主虚表和多个次虚表,主虚表和次虚表构成一个虚表组(virtual table group)。

现在我们可以完善Obj类型对象的布局图:

image-20231110202652872

为了支持多态,编译器使用虚指针前面的16字节存储两个信息:

  • 偏移量:当前父类子对象到该对象起始处的偏移,称其为top_offset
  • 类型信息指针:指向当前对象的类型信息,每个父类子对象的虚指针前8字节都指向同一个类型信息。

对于虚表中虚函数条目的排列顺序,在主虚函数表中,将父类的虚函数条目按序排列,如果子类重写了该虚函数,则条目设置为子类的虚函数地址,子类自定义的新的虚函数条目按序排在后面。

对于从虚表,按照父类的虚函数条目顺序排列,子类重写的虚函数则对应条目需要重新设置。不同于主表中直接设置为子类虚函数的地址,而是设置为指向一个子类虚函数的non-virtual thunk

gdb查看non-virtual thunk的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) info vtbl obj
vtable for 'Obj' @ 0x555555557ca8 (subobject @ 0x7fffffffde20):
[0]: 0x5555555554d0 <Obj::base1_virt1()>
[1]: 0x55555555544c <Base1::base1_virt2()>
[2]: 0x555555555524 <Obj::base2_virt1()>
[3]: 0x555555555582 <Obj::virtfunc1()>
[4]: 0x5555555555ae <Obj::virtfunc2()>

vtable for 'Base2' @ 0x555555557ce0 (subobject @ 0x7fffffffde38):
[0]: 0x555555555577 <non-virtual thunk to Obj::base2_virt1()>
[1]: 0x5555555554a4 <Base2::base2_virt2()>
(gdb) x /10i 0x555555555577
0x555555555577 <_ZThn24_N3Obj11base2_virt1Ev>: endbr64
0x55555555557b <_ZThn24_N3Obj11base2_virt1Ev+4>: sub $0x18,%rdi # %rdi存储第一个参数 this指针,此处调整this指针指向子类对象
0x55555555557f <_ZThn24_N3Obj11base2_virt1Ev+8>: jmp 0x555555555524 <_ZN3Obj11base2_virt1Ev> # 调用子类虚函数
0x555555555581: nop
...

可见,non-virtual thunk是编译器根据top_offset,编写的一小段代码,调整指向父类子对象的指针指向子类对象,以便于调用子类虚函数时,this指针是指向当前对象的。

虚拟继承

精简每个类的数据成员,添加一个Base基类,探究虚拟继承下的对象布局和虚表结构:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

class Base
{
public:
int x{};
virtual void base_virt()
{
cout << "Base virtfunc\n";

}
};

class Base1: virtual public Base
{
public:
int x1{};
virtual void base1_virt()
{
cout << "Base 1 virtfunc\n";
}
};

class Base2: virtual public Base
{

public:
int x2;
virtual void base2_virt()
{
cout << "Base 2 virtfunc \n";
}
};

class Obj : public Base1, public Base2
{
public:
int y{};

virtual void virtfunc1()
{
cout << "Obj virtfunc1\n";
}

virtual void virtfunc2()
{
cout << "Obj virtfunc2\n";
}
};

int main()
{
Obj obj;
Base1 b1;
Base2 b2;

return 0;
}

copy control

定义一个类时,可以显式或隐式地指定此类型对象拷贝、移动、赋值和销毁时的行为。我们可以通过定义五种特殊的方法来控制这些操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。

拷贝/赋值/销毁

copy-constructor

定义:一个构造函数,第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是copy-constructor。其第一个参数必须是引用类型,否则调用时,其参数也需要被拷贝,如此无限循环。
如下,定义Obj类与其拷贝构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

class Obj
{
public:
int count{};
Obj() { }
Obj(int c) : count(c) {}
Obj(const Obj & obj, int c = 0)
{
cout << "call copy-constructor" << endl;
this->count = c;
}
};
int main()
{
Obj obj1(12);
Obj obj2(obj1, 12);
cout << obj2.count << endl;
return 0;
}

其中拷贝构造函数第二个参数具有默认值0。编译运行:

1
2
3
> g++ main.cc -O3 -g -o main && ./main
call copy-constructor
12

通过以下例子,查看拷贝构造函数调用的时刻,并为Obj类添加输出提示信息

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Obj
{
public:
int count{};
Obj()
{
cout << "call constructor" << endl;
}
Obj(int c) : count(c) { cout << "call Obj(int) constructor" << endl; }
Obj(const Obj & obj)
{
cout << "call copy-constructor" << endl;
this->count = obj.count;
}
};

class Temp
{
public:
Obj obj;
~Temp()
{
cout << "call Temp destructor" << endl;
}
};

void foo(Obj obj)
{
// do nothing
return;
}

Obj bar()
{
return Obj(1);
}

int main()
{
// 编译器掠过拷贝初始化
cout << "编译器掠过拷贝初始化:\n";
Obj o = 1;
cout << o.count << endl;
// 1. 列表初始化
cout << "列表初始化: \n";
vector<Obj> objs{1,2,3};
for (auto & obj : objs)
cout << obj.count << ' ';
cout << endl;
// 2. 传递非引用参数
cout << "传递非引用参数: \n";
foo(o);
// 3. 返回非引用参数
cout << "返回非引用参数: \n";
Obj x = bar();

return 0;
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> g++ main.cc -g -o main && ./main
编译器掠过拷贝初始化:
call Obj(int) constructor
1
列表初始化:
call Obj(int) constructor
call Obj(int) constructor
call Obj(int) constructor
call copy-constructor
call copy-constructor
call copy-constructor
1 2 3
传递非引用参数:
call copy-constructor
返回非引用参数:
call Obj(int) constructor
rda@pa ~/T/proj (slave2)>

再看一个稍微复杂的例子,判断有几次拷贝构造函数的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Obj global;
// 1. 传参时copy-constructor
Obj bar(Obj arg)
{
Obj local = arg, *heap = new Obj(global); // 2. copy-constructor 3. global传参时copy-constructor
*heap = local; // note!!这是赋值操作
Obj pa[4] = { local, *heap }; // 4,5 copy-constructor

return *heap; // 6. 返回一个非引用参数copy-constructor
}

int main()
{
bar(global);

return 0;
}

输出结果为

1
2
3
4
5
6
7
> g++ main.cc -g -o main && ./main
call copy-constructor
call copy-constructor
call copy-constructor
call copy-constructor
call copy-constructor
call copy-constructor

总而言之,当存在使用一个已有的类对象去初始化另一个类对象时会调用copy-constructor。

another example

再看一个例子,验证合成拷贝构造函数的行为。
我们定义一个新的类Num,自定义其默认构造函数,为每个对象生成一个唯一序号,保存在数据成员id中。编译器为其生成合成拷贝构造函数和拷贝赋值运算符。

1
2
3
4
5
6
7
8
9
10
11
12
class Num
{
public:
static int next_id;
int id{};
Num()
{
id = next_id++;
}
};

int Num::next_id = 1;

如下foo和bar函数打印传入参数的id,其中foo接受非引用参数,而bar接受引用类型参数

1
2
3
4
5
6
7
8
9
10
11
void foo(const Num num)
{
cout << num.id << endl;
return;
}

void bar(const Num & num)
{
cout << num.id << endl;
return;
}

main函数如下,第一次调用foo

1
2
3
4
5
6
7
8
9
int main()
{
Num a, b = a, c = b; // 默认构造a,拷贝构造b和c
foo(a); // 通过a拷贝构造参数num
foo(b); // 通过a拷贝构造参数num
foo(c); // 通过a拷贝构造参数num

return 0;
}

输出为

1
2
3
4
> g++ main.cc -g -o main && ./main
1
1
1

因为默认的拷贝构造函数会直接复制非static成员。
man函数中调用bar。输出不变。
自定义拷贝构造函数,main函数调用bar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Num
{
public:
static int next_id;
int id{};
Num()
{
id = next_id++;
}
Num(const Num & num)
{
id = next_id++;
}
};

打印出预期的信息

1
2
3
4
> g++ main.cc -g -o main && ./main
1
2
3

调用foo则不同,因为传参时拷贝构造了参数对象,其id和传入的参数不同

1
2
3
4
> g++ main.cc -g -o main && ./main
4
5
6

拷贝赋值运算符

类可以控制其对象如何赋值,即通过重载赋值运算符

1
2
Obj obj1, obj2;
obj1 = obj2; // 拷贝赋值

重载赋值运算符,即重载operate=函数,通常返回左侧对象的引用。如下,为Obj类重载赋值运算符

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

class Obj
{
public:
int count{};
Obj()
{
cout << "call constructor" << endl;
}

Obj(int c) : count(c)
{
cout << "call Obj(int) constructor" << endl;
}

Obj & operator=(const Obj & o)
{
count = o.count;
cout << "call operator = " << endl;
return *this;
}

Obj(const Obj & obj)
{
cout << "call copy-constructor" << endl;
this->count = obj.count;
}

~Obj()
{
cout << "call deconstructor" << endl;
}
};

合成赋值运算符

如果一个类未定义其拷贝赋值运算符,编译器会生成一个合成拷贝赋值运算符,默认将右侧对象的每个非static成员赋予左侧对象,并返回一个指向左侧对象的引用。

析构函数

析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。析构函数没有返回值,不接受参数。

何时调用

当一个对象被销毁时,自动调用:

  • 变量离开作用域,如函数的局部变量和非引用类型的参数
  • 一个对象被销毁时其成员对象被销毁
  • 对于动态分配的对象,对指向它的指针应用delete
    等等。

仍然是Obj类的例子,在其析构函数中添加输出信息,main函数中调用foo函数如下:

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
36
37
38
39
40
41
42
43

class Obj
{
public:
int count{};
Obj()
{
}
Obj(int c) : count(c)
{
}
Obj(const Obj & obj)
{
// cout << "call copy-constructor" << endl;
this->count = obj.count;
}
~Obj()
{
cout << "call deconstructor" << endl;
}
};

// 拷贝构造obj2
void fcn(const Obj * obj1, Obj obj2)
{
// 拷贝构造obj3和obj4
Obj obj3(*obj1), obj4(obj2);
// 销毁obj2、obj3、obj4
return;
}


Obj global;
int main()
{
Obj *p = new Obj();
fcn(p, global);

delete p; // 销毁p指向的对象

return 0; // 程序结束之前销毁全局对象global
}

输出结果:

1
2
3
4
5
6
7
8
9
> g++ main.cc -g -o main && ./main
call copy-constructor
call copy-constructor
call copy-constructor
call deconstructor
call deconstructor
call deconstructor
call deconstructor
call deconstructor

值行为类

定义类VPtr,

指针行为类

定义类PPtr,

swap

动态内存管理类

smart pointer

标准库提供了智能指针类型来更好地管理动态对象,其行为类似常规指针,但是会负责释放所指向的对象。其中,shared_ptr允许多个指针指向同一对象;unique_ptr则“独占”所指向的对象;weak_ptr是一种弱引用,指向shared_ptr所管理的对象。

shared_ptr

如果不初始化一个智能指针,它默认是一个空指针

1
2
shared_ptr<int> p;
cout << (p == nullptr) << endl; // output 1

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。为了说明,定义一个简单的类,使用shared_ptr控制其分配和销毁。当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象——通过析构函数

1
2
3
4
5
6
7
8
9
10
class Obj
{
public:
int count{};
Obj(int c) : count(c) {}
~Obj()
{
cout << "call Obj destructor" << endl;
}
};

通过赋值,两个shared_ptr共同操作一个对象

1
2
3
4
5
6
// a simple example
shared_ptr<Obj> p(new Obj(12));
cout << p.use_count() << ' ' << p->count << endl;
shared_ptr<Obj> q = p;
++q->count;
cout << q.use_count() << ' ' << q->count << endl;

输出显示为

1
2
3
4
> g++ main.cc -g -o main && ./main
1 12
2 13
call Obj destructor

包括调用函数时的参数和函数内部的局部变量,也会影响shared_ptr的计数

1
2
3
shared_ptr<Obj> p(new Obj(12));
foo(p);
cout << p.use_count() << ' ' << p->count << endl;

其中,foo的定义为

1
2
3
4
5
6
7
void foo(shared_ptr<Obj> ptr)
{
cout << ptr.use_count() << endl;
auto x = ptr;
cout << x.use_count() << endl;
x->count++;
}

输出如下:

1
2
3
4
5
> g++ main.cc -g -o main && ./main
2
3
1 13
call Obj destructor

可以使用new返回的指针初始化智能指针。接受指针参数的智能指针构造函数是explicit的,不支持隐式转换。默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存。

1
2
3
4
5
6
7
8
9
shared_ptr<int> p = new int(1024); // error
shared_ptr<int> q(new int(1024)); // true

/*
munmap_chunk(): invalid pointer
fish: Job 1, './main' terminated by signal SIGABRT (Abort)
*/
int x;
shared_ptr<int> r(&x);

get()可以返回指向智能指针管理的对象的内置指针。注意,不要使用get()返回的内置指针初始化另一个智能指针,或尝试delete它

1
2
3
4
5
6
7
shared_ptr<int> p(new int(42));
int *q = p.get();

{ // new block
shared_ptr<int> (q);
} // free the object pointed by q
int foo = *p; // undefined behaviro

use_count方法返回shared_ptr的用户个数,unique方法返回是否当前指针是唯一用户

1
2
3
4
shared_ptr<int> p(new int(42));
cout << p.use_count() << ' ' << p.unique() << endl; // 1 1
shared_ptr<int> q = p;
cout << p.use_count() << ' ' << p.unique() << endl; // 2 0

还可以调用自定义的可调用对象代替delete,通过shared_ptrp(q, d),p接管q所指向的对象的所有权,调用d替代默认的delete。

1
2
3
4
5
6
7
8
9
10
11
12
13
void my_delete(Obj * po)
{
cout << "this is my delete function" << endl;
delete po;
}

int main()
{
shared_ptr<Obj> p(new Obj(12), my_delete);
cout << p.use_count() << ' ' << p->count << endl;

return 0;
}

输出为

1
2
3
4
> g++ main.cc -g -o main && ./main
1 12
this is my delete function
call Obj destructor

unique_ptr

一个unique_ptr拥有它指向的对象,当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。且其不支持拷贝和赋值

1
2
3
4
5
6
7
unique_ptr<double> p1;            // default nullptr
cout << (p1 == nullptr) << endl; // 1

unique_ptr<int> p2(new int(12));
unique_ptr<int> p3(p2); // error
unique_ptr<int> p4;
p4 = p2; // error

可以通过release或reset将指针所有权从一个unique_ptr转移给另一个unique_ptr。

1
2
3
4
unique_ptr<Obj> p1(new Obj(12));       
unique_ptr<Obj> p2(p1.release());
unique_ptr<Obj> p3(new Obj(13));
p2.reset(p3.release());

输出为

1
2
3
> g++ main.cc -g -o main && ./main
call Obj destructor 12
call Obj destructor 13

有一种特殊情况,即当要返回的对象将要销毁时,可以执行一种特殊的拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
unique_ptr<Obj> clone(int count)
{
return unique_ptr<Obj>(new Obj(count));
}

int main()
{
auto p = clone(1);
cout << p->count <<endl;

return 0;
}
1
2
3
> g++ main.cc -g -o main && ./main
1
call Obj destructor 1

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向它。
创建weak_ptr时需要一个shared_ptr初始化。

1
2
3
4
5
6
7
8
9
10
auto p = make_shared<Obj>(11);
weak_ptr<Obj> wp(p);
cout << p.use_count() << endl; // weak_ptr do not change the use count

if (auto np = wp.lock())
{
cout << np.use_count() << endl;
++np->count;
}
cout << p->count << endl;

由于weak_ptr指向的对象可能不存在,需要调用lock函数检查weak_ptr指向的对象是否存在,如果存在则返回一个指向共享对象的shared_ptr,因此可以通过该函数使用weak_ptr访问指向的对象。

implement shared_ptr and weak_ptr

malloc

本文主要分为两部分:探究malloc执行背后涉及的系统调用,以及分配大内存和小内存的执行差别。在此之前先说明一些关于malloc的“传闻”:通过brk系统调用控制heap的增长,自身负责管理一些内存块,对于可以满足的分配需求,可以在用户态下直接分配已有的空闲内存块完成;对于大内存需求,调用mmap系统调用;第二部分是关于现在使用的较多的malloc库的设计和实现思想,包括ptmalloc和tcmalloc。

malloc

我们通过观察一个例子中malloc的行为,探究malloc的行为。

a simple example

使用一个简单的示例程序,程序分别调用malloc分配小块内存(1KB)和大块内存(1GB)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define MEM_SIZE (1<<10)

int main(int argc, char **argv)
{
printf("I am %d\n", getpid());
getchar();
char *big = malloc(MEM_SIZE);
char *small = malloc(1<<10);
printf("address: %p %p\n", big, small);
getchar();
free(big);
free(small);
getchar();

return 0;
}

同时,我们通过查看进程的内存映射情况,确定heap的范围,以及小内存和大内存分别处于哪块内存区域。在程序中插入了getchar(),方便在分配内存前、分配内存后、释放内存后分别查看内存映射的情况。我们将三个阶段的映射数据分别保存在before、alloc和free的文件中,并通过对比三个文件的差异,可得知malloc和free对内存映射的影响。
第一步,设置MEM_SIZE为1<<10,与小内存大小相同,即均为1KB时,编译并运行main,由于getchar()的存在,程序会有一些停顿(以下程序的输出做了些许调整,与正常输出不同)

1
2
3
4
> gcc main.c -g -o main
> ./main
I am 153825
address: 0x555555559ac0 0x555555559ed0

在程序停顿时,另开一窗口,每次停顿后分别执行

1
2
3
> cat /proc/153825/maps > Templates/before 
> cat /proc/153825/maps > Templates/alloc
> cat /proc/153825/maps > Templates/free

使用diff命令检查三个文件的不同

1
2
> diff before alloc 
> diff alloc free

使用strace跟踪mmap的调用情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fbb000
mmap(NULL, 71183, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7fa9000
mmap(NULL, 2260560, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7d81000
mmap(0x7ffff7da9000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7ffff7da9000
mmap(0x7ffff7f3e000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7ffff7f3e000
mmap(0x7ffff7f96000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x214000) = 0x7ffff7f96000
mmap(0x7ffff7f9c000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff7f9c000
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7d7e000
I am 154037

address: 0x555555559ac0 0x555555559ed0


+++ exited with 0 +++

在输出”I am …”信息后,并未调用mmap系统调用。
三个文件内容相同,且未调用mmap。malloc分配小内存时,进程的内存映射没有变化,查看三个文件中任一一个的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> cat before 
555555554000-555555555000 r--p 00000000 08:03 271805 /home/rda/Templates/main
555555555000-555555556000 r-xp 00001000 08:03 271805 /home/rda/Templates/main
555555556000-555555557000 r--p 00002000 08:03 271805 /home/rda/Templates/main
555555557000-555555558000 r--p 00002000 08:03 271805 /home/rda/Templates/main
555555558000-555555559000 rw-p 00003000 08:03 271805 /home/rda/Templates/main
555555559000-55555557a000 rw-p 00000000 00:00 0 [heap]
7ffff7d7e000-7ffff7d81000 rw-p 00000000 00:00 0
7ffff7d81000-7ffff7da9000 r--p 00000000 08:03 526570 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7da9000-7ffff7f3e000 r-xp 00028000 08:03 526570 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7f3e000-7ffff7f96000 r--p 001bd000 08:03 526570 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7f96000-7ffff7f9a000 r--p 00214000 08:03 526570 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7f9a000-7ffff7f9c000 rw-p 00218000 08:03 526570 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7f9c000-7ffff7fa9000 rw-p 00000000 00:00 0
7ffff7fbb000-7ffff7fbd000 rw-p 00000000 00:00 0
7ffff7fbd000-7ffff7fc1000 r--p 00000000 00:00 0 [vvar]
7ffff7fc1000-7ffff7fc3000 r-xp 00000000 00:00 0 [vdso]
7ffff7fc3000-7ffff7fc5000 r--p 00000000 08:03 526564 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffff7fc5000-7ffff7fef000 r-xp 00002000 08:03 526564 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffff7fef000-7ffff7ffa000 r--p 0002c000 08:03 526564 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffff7ffb000-7ffff7ffd000 r--p 00037000 08:03 526564 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffff7ffd000-7ffff7fff000 rw-p 00039000 08:03 526564 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

heap的范围为555555559000-55555557a000,而p和q的地址分别为0x555555559ac0和0x555555559ed0,均位于其中。因此,malloc会管理一个预先分配好的heap,如果请求分配的内存可以满足,则直接分配即可。
设置MEM_SIZE为1<<30,即1GB,显然,上述heap的大小为0x7a000 - 0x59000 = 0x21000,远小于MEM_SIZE。
重新编译并执行

1
2
3
4
> gcc main.c -g -o main
rda@pa ~/Templates> ./main
I am 154154
address: 0x7fffb7d7d010 0x555555559ac0

使用diff查看三个文件的差异

1
2
3
4
5
6
7
8
9
10
11
> diff before alloc
7c7
< 7ffff7d7e000-7ffff7d81000 rw-p 00000000 00:00 0
---
> 7fffb7d7d000-7ffff7d81000 rw-p 00000000 00:00 0
> diff alloc free
7c7
< 7fffb7d7d000-7ffff7d81000 rw-p 00000000 00:00 0
---
> 7ffff7d7e000-7ffff7d81000 rw-p 00000000 00:00 0
> diff before free

可见,在执行malloc之前和执行free后的文件内容是相同的。而执行malloc后,内存空间的差异仅有一处:有一块内存区域的从7ffff7d7e000-7ffff7d81000变为了7fffb7d7d000-7ffff7d81000。即起始处从0x7ffff7d7e000变味了0x7fffb7d7d000,p(大内存)的地址为0x7fffb7d7d010,恰好在该内存区域的起始处偏移0x10的位置,而q(小内存)的地址为0x555555559ac0,仍位于heap中。
执行strace跟踪mmap和munmap的调用情况

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
> strace -e mmap ./main
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fbb000
mmap(NULL, 71183, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7fa9000
mmap(NULL, 2260560, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7d81000
mmap(0x7ffff7da9000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7ffff7da9000
mmap(0x7ffff7f3e000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7ffff7f3e000
mmap(0x7ffff7f96000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x214000) = 0x7ffff7f96000
mmap(0x7ffff7f9c000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff7f9c000
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7d7e000
I am 154333

mmap(NULL, 1073745920, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fffb7d7d000
address: 0x7fffb7d7d010 0x555555559ac0


+++ exited with 0 +++
> strace -e munmap ./main
munmap(0x7ffff7fa9000, 71183) = 0
I am 154371

address: 0x7fffb7d7d010 0x555555559ac0

munmap(0x7fffb7d7d000, 1073745920) = 0

+++ exited with 0 +++

可见,在malloc申请大内存后执行了mmap,返回的地址为 0x7fffb7d7d000,而free后,调用了munmap(0x7fffb7d7d000, 1073745920)

summary

malloc自身维护了一块地址较低的内存,对于小块的内存请求,malloc从自身管理的空闲内存块中分配;对于大块内存,通过调用mmap和munmap分配和释放。

malloc实例

总结几个常用的malloc库的实现思想和机制:

  • glibc中的ptmalloc
  • Google的tcmalloc
  • Facebook的jemalloc

ptmalloc

tcmalloc

reset与checkout

基础

首先,我们需要对git的存储逻辑分区有一定的概念:

  • working directory:工作目录,是本地直接编辑的可见部分
  • staging area:暂存区,暂存working directory中我们准备下一次提交的内容
  • repository:仓库,保存的是上一次提交的快照

执行git add 命令,可以将指定的文件(一般包含最新的修改)暂存到暂存区,而git commit 命令将当前暂存区中的内容执行一次新的提交。一般通过git add命令暂存新的修改内容,而通过git commit提交已经暂存的准备提交到仓库的内容。
git diff命令可以用来检查这三个逻辑分区的差异。例如

  • 执行git diff,查看工作目录和暂存区的差异
  • 执行git diff –staged,查看暂存区和仓库最新提交的差异
  • 执行git diff HEAD,查看工作目录与仓库最新的提交的差异

平时,开发项目的时候,我们可能想舍弃当前工作目录中的修改内容,或者将某一源文件还原到前几次提交的某一次提交时的内容,这时候我们想起了git中的checkout和reset好像有某些类似的功能,可以协助我们完成。但这两个命令有一定的差别,使用不当可能造成意想不到的结果,很可能最新修改好的程序就被覆盖了。本文通过一个小实验探究reset和checkout的功能以及差异。

准备工作

初始化一个目录为git仓库,目录中包含两个文件

1
2
3
> mkdir test
> cd test/
> git init

创建两个文件foo和bar,并写入第一条字符串”this is history1”,执行git status查看当前状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> touch foo bar
> echo 'this is history1' > bar
> echo 'this is history1' > foo
> git status
On branch master

No commits yet

Untracked files:
(use "git add <file>..." to include in what will be committed)
bar
foo

nothing added to commit but untracked files present (use "git add" to track)

执行git add命令,track两个文件,并写入提交信息”history1”

1
2
3
4
5
6
> git add bar foo
> git commit -m 'history1'
[master (root-commit) f010d6d] history1
2 files changed, 2 insertions(+)
create mode 100644 bar
create mode 100644 foo

在两个文件末尾追加”history2””history3”,再进行两次新的提交,每次的提交信息分别为”history2””history3”

1
2
3
4
5
6
7
8
9
10
11
12
> echo 'this is history2' >> foo
> echo 'this is history2' >> bar
> git add bar foo
> git commit -m 'history2'
[master 0c52905] history2
2 files changed, 2 insertions(+)
> echo 'this is history3' >> foo
> echo 'this is history3' >> bar
> git add bar foo
> git commit -m 'history3'
[master cabad03] history3
2 files changed, 2 insertions(+)

查看提交日志,此时仓库有了三次提交的记录

1
2
3
4
> git log --pretty=oneline
cabad03e9605163b939d137aacd28d3441f5ebea (HEAD -> master) history3
0c5290517531ada5847cc418e9db2941e33fb62e history2
f010d6d2831e827a16d4687076cf2de40f89076d history1

查看foo和bar的内容:

1
2
3
4
5
6
7
8
> cat bar
this is history1
this is history2
this is history3
rda@pa ~/T/test (master)> cat foo
this is history1
this is history2
this is history3

我们将基于这两个文件原有的内容做简单的添加,以及执行reset和checkout,探究两者的功能和差异。本文不涉及介绍git中指针,头指针的操作和其他概念。
在foo文件尾添加”this is stage”的文本,并执行git add foo,暂存该修改,然后再在文件尾添加”this is work dir”的文本

1
2
3
> echo 'this is stage' >> foo
> git add foo
> echo 'this is work dir' >> foo

执行git diff,查看工作目录和暂存区的差别

1
2
3
4
5
6
7
8
9
10
> git diff
diff --git a/foo b/foo
index fc8c574..64512a0 100644
--- a/foo
+++ b/foo
@@ -2,3 +2,4 @@ this is history1
this is history2
this is history3
this is stage
+this is work dir

执行git diff –staged,查看暂存区和仓库的差别

1
2
3
4
5
6
7
8
9
10
> git diff --staged
diff --git a/foo b/foo
index 55a27cf..fc8c574 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,4 @@
this is history1
this is history2
this is history3
+this is stage

可见,相对于仓库,暂存区中添加了文本”this is stage”,而相对于暂存区,工作目录添加了”this is work dir”,同理,执行git diff HEAD,对比工作目录和仓库,文件foo追加了两条字符串

1
2
3
4
5
6
7
8
9
10
11
> git diff HEAD
diff --git a/foo b/foo
index 55a27cf..64512a0 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,5 @@
this is history1
this is history2
this is history3
+this is stage
+this is work dir

我们也可以执行git status -s,左边显示的M表示foo已经修改并暂存,右边的M表示foo再暂存后又有新的修改

1
2
> git status -s
MM foo

reset

git reset使用respository中特定commit来重置head下的repository、stage、workspace,可进一步的细分成三种模式hard、sort、mixed(默认)

hard

git reset –hard会同时重置repository、stage、workspace。
执行git reset –hard HEAD,将当前头指针指向的commit复制到暂存区和工作目录,相当于丢弃了暂存区和工作目录的修改

1
2
3
4
5
6
7
8
9
10
11
12
 git reset --hard HEAD
HEAD is now at cabad03 history3
> git diff
> git diff --staged
> git diff HEAD
> cat foo
this is history1
this is history2
this is history3
> git status
On branch master
nothing to commit, working tree clean

可见,工作目录、暂存区和仓库的内容是一致的。
可以使用该命令“回退”到某一历史commit

1
2
3
4
5
6
7
8
> git reset --hard HEAD~
HEAD is now at 0c52905 history2
> git status
On branch master
nothing to commit, working tree clean
> git log --pretty=oneline
0c5290517531ada5847cc418e9db2941e33fb62e (HEAD -> master) history2
f010d6d2831e827a16d4687076cf2de40f89076d history1

可见,当前分支master和HEAD指针均移动到了上一次commit处,并且工作目录和暂存区与仓库保持一致
查看bar

1
2
3
> cat bar
this is history1
this is history2

内容也回退到了第二次提交的内容,因为我们操作的对象是所以被追踪的文件。

mixed

git reset –mixed会重置repository、stage,只保留workspace中的改动。
重复类似刚刚测试–hard的操作,我们同时在foo文件和bar文件末尾追加”this is stage”文本,并暂存一次,再追加”this is work dir”文本

1
2
3
> echo 'this is stage' >> foo
> git add foo
> echo 'this is work dir' >> foo

执行git diff、git diff –staged、git diff HEAD,查看三个逻辑区的差异

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
> git diff
diff --git a/foo b/foo
index 4c1d2ab..bd45079 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,4 @@
this is history1
this is history2
this is stage
+this is work dir
> git diff --staged
diff --git a/foo b/foo
index 8e02c46..4c1d2ab 100644
--- a/foo
+++ b/foo
@@ -1,2 +1,3 @@
this is history1
this is history2
+this is stage
> git diff HEAD
diff --git a/foo b/foo
index 8e02c46..bd45079 100644
--- a/foo
+++ b/foo
@@ -1,2 +1,4 @@
this is history1
this is history2
+this is stage
+this is work dir

执行git reset –mixed HEAD,预期该命令执行结果是,暂存区的内容被丢弃,与最新的提交保持一致,而工作目录不变。

1
2
3
> git reset --mixed 
Unstaged changes after reset:
M foo

执行git diff、git diff –staged、git diff HEAD,查看三个逻辑区的差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> git diff
diff --git a/foo b/foo
index 8e02c46..bd45079 100644
--- a/foo
+++ b/foo
@@ -1,2 +1,4 @@
this is history1
this is history2
+this is stage
+this is work dir
> git diff --staged
> git diff HEAD
diff --git a/foo b/foo
index 8e02c46..bd45079 100644
--- a/foo
+++ b/foo
@@ -1,2 +1,4 @@
this is history1
this is history2
+this is stage
+this is work dir

此时,原本已经暂存的内容也被丢弃,算作了工作目录和上一次提交的差异。–mixed是默认项,因此可以省略。
执行git reset HEAD~,预期该指令执行结果是:
for:暂存区内容和第一次提交内容一致,均为”this is history1”,而工作目录不变,追加了三条字符串,分别是”this is history2””this is stage””this is work dir”
bar:暂存区和第一次提交一致,工作目录中的”this is history2”为新加的内容

1
2
3
4
> git reset HEAD~
Unstaged changes after reset:
M bar
M foo

执行git diff、git diff –staged、git diff HEAD,查看三个逻辑区的差异

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
> git diff 
diff --git a/bar b/bar
index 361b0e4..8e02c46 100644
--- a/bar
+++ b/bar
@@ -1 +1,2 @@
this is history1
+this is history2
diff --git a/foo b/foo
index 361b0e4..bd45079 100644
--- a/foo
+++ b/foo
@@ -1 +1,4 @@
this is history1
+this is history2
+this is stage
+this is work dir
> git diff --staged
> git diff HEAD
diff --git a/bar b/bar
index 361b0e4..8e02c46 100644
--- a/bar
+++ b/bar
@@ -1 +1,2 @@
this is history1
+this is history2
diff --git a/foo b/foo
index 361b0e4..bd45079 100644
--- a/foo
+++ b/foo
@@ -1 +1,4 @@
this is history1
+this is history2
+this is stage
+this is work dir

soft

git reset –soft会重置repository,但保留stage、workspace中的改动。
首先将仓库恢复到初始状态,即有三次commit,并且暂存区中有”this is stage”,工作目录中还额外添加了”this is work dir”。恢复的方法是,根据第三次commit,checkout到commit3(checkout可以移动HEAD指针,然后在commit3处新建一个分支,并将master合并到该分支即可)
执行git reset –soft HEAD~2,即reset到第一次commit,预期执行结果是:
foo文件:仓库仅有”this is history1”,暂存区中有”this is history2””this is history3””this is stage”,而工作目录额外有”this is work dir”
bar文件:仓库仅有”this is history1”,暂存区中有”this is history2””this is history3”,工作目录与暂存区内容一致

1
> git reset --soft HEAD~2

查看两个文件内容

1
2
3
4
5
6
7
8
9
10
> cat bar
this is history1
this is history2
this is history3
> cat foo
this is history1
this is history2
this is history3
this is stage
this is work dir

执行git diff、git diff –staged、git diff HEAD

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
36
37
38
39
40
41
42
43
44
45
46
47
> git diff
diff --git a/foo b/foo
index fc8c574..64512a0 100644
--- a/foo
+++ b/foo
@@ -2,3 +2,4 @@ this is history1
this is history2
this is history3
this is stage
+this is work dir
> git diff --staged
diff --git a/bar b/bar
index 361b0e4..55a27cf 100644
--- a/bar
+++ b/bar
@@ -1 +1,3 @@
this is history1
+this is history2
+this is history3
diff --git a/foo b/foo
index 361b0e4..fc8c574 100644
--- a/foo
+++ b/foo
@@ -1 +1,4 @@
this is history1
+this is history2
+this is history3
+this is stage
> git diff HEAD
diff --git a/bar b/bar
index 361b0e4..55a27cf 100644
--- a/bar
+++ b/bar
@@ -1 +1,3 @@
this is history1
+this is history2
+this is history3
diff --git a/foo b/foo
index 361b0e4..64512a0 100644
--- a/foo
+++ b/foo
@@ -1 +1,5 @@
this is history1
+this is history2
+this is history3
+this is stage
+this is work dir

查看日志

1
2
> git log --pretty=oneline
f010d6d2831e827a16d4687076cf2de40f89076d (HEAD -> master) history1

不难发现,git reset实质是通过移动当前分支进行操作的,而hard、mixed、soft其实是用于指示git是否将仓库的内容覆盖到暂存区和工作目录

checkout

不同与reset,checkout可以对某一个文件进行“重置”操作。
再次将仓库还原到commit3,并且对foo和bar进行相同的操作,即:
追加一个文本并暂存,然后再次追加一个新的文本。我们称该状态为初始状态。

1
2
3
4
5
6
7
8
> echo 'this is stage' >> foo
> echo 'this is stage' >> bar
> git add foo bar
> echo 'this is work dir' >> foo
> echo 'this is work dir' >> bar
> git status -s
MM bar
MM foo

执行git checkout HEAD foo,git diff和git diff –staged

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> git checkout HEAD foo
Updated 1 path from 3ea0ad5
> git diff
diff --git a/bar b/bar
index fc8c574..64512a0 100644
--- a/bar
+++ b/bar
@@ -2,3 +2,4 @@ this is history1
this is history2
this is history3
this is stage
+this is work dir
> git diff --staged
diff --git a/bar b/bar
index 55a27cf..fc8c574 100644
--- a/bar
+++ b/bar
@@ -1,3 +1,4 @@
this is history1
this is history2
this is history3
+this is stage

可见,foo的暂存区和工作目录被HEAD处的提交覆盖了,相当于丢弃了暂存区和工作目录中的修改,而bar未受影响
同样,我们对bar进行操作,首先将仓库的状态恢复到初始状态。
执行git checkout HEAD~2 bar

1
2
> git checkout HEAD~2 bar
Updated 1 path from f6fa925

查看foo文件的各个逻辑区对比

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
> git diff foo
diff --git a/foo b/foo
index fc8c574..64512a0 100644
--- a/foo
+++ b/foo
@@ -2,3 +2,4 @@ this is history1
this is history2
this is history3
this is stage
+this is work dir
> git diff --staged foo
diff --git a/foo b/foo
index 55a27cf..fc8c574 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,4 @@
this is history1
this is history2
this is history3
+this is stage
> git diff HEAD foo
diff --git a/foo b/foo
index 55a27cf..64512a0 100644
--- a/foo
+++ b/foo
@@ -1,3 +1,5 @@
this is history1
this is history2
this is history3
+this is stage
+this is work dir

未受到影响。
查看bar文件的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> git diff bar
> git diff --staged bar
diff --git a/bar b/bar
index 55a27cf..361b0e4 100644
--- a/bar
+++ b/bar
@@ -1,3 +1 @@
this is history1
-this is history2
-this is history3
> git diff HEAD bar
diff --git a/bar b/bar
index 55a27cf..361b0e4 100644
--- a/bar
+++ b/bar
@@ -1,3 +1 @@
this is history1
-this is history2
-this is history3

可见,git checkout HEAD~2 bar相当于将commit1中的bar复制到当前工作目录并暂存

总结

模式 reset reposity reset stage reset working directory
hard
mixed(default) x
soft x x
使用checkout,相当于从指定的commit中取出指定的文件,覆盖当前文件并暂存。 当checkout后未指定文件时,相当于移动HEAD指针。

mesi与伪共享

随着现代CPU设计技术的不断发展,CPU的性能和内存性能差距逐步拉大,程序执行时需要不断通过总线访问内存读写数据,与内存之间的数据传输成为了计算机性能的巨大瓶颈。

cache

局部性

计算机程序中的指令一般情况下是按顺序执行,但也存在着诸如循环等结构,循环会不断执行相同的指令,而执行函数时,也会访问存储在函数栈帧中的局部变量。因此程序执行通常具有局部性,局部性有两种不同的表现形式:

  • 时间局部性:被引用过的内存位置很可能在不久的将来再次引用,如循环结构中访问的变量以及循环结构本身的指令
  • 空间局部性:被引用过的内存位置,其附近的数据项很可能在不久的将来被引用,如遍历数组、访问结构体等

memory hierarchy

一般来说,处于更高层的存储结构价格较高,容量较小,但访问速度快,CPU访问频率也越高,而处于低层次的存储结构价格较低,容量较大,但访问速度慢CPU,访问频率越低。

可见,相对于访问cache,直接从内存中存取数据消耗的时间是前者的几十倍之大。因此,现代计算机系统一般提供使用层次化方法设计存储器系统,将程序经常访问的以及更倾向于访问的数据存放在离CPU较近的存储层中,提升数据存取的速度,进而缩短程序执行的时间。根据局部性,提供的存储系统具备近乎较高层次的快速和较低层次的大容量特点。

cache一致性问题

在一个多核处理器系统中,不同的处理器可能对同一物理地址进行操作,cache共享数据引入的一个新的问题是,由于两个不同的处理器保存的存储器视图都是通过各自的cache获得的,如果没有任何额外的保护措施,那么处理器可能看到两个不同的值。例如,在一个多线程的程序中,两个不同的线程分别在两个不同的处理器上运行,并且访问同一全局变量,这个全局变量分别存放在两个处理器的cache中,当一个线程更新全局变量时,由于cache采用的是write back、write allocate策略,因此该线程所处的处理器会在cache中更新该变量,而不会立即写回内存,另一个处理器cache中的该全局变量的值就是旧的。
简单来说,如果对任何数据项的读取都能返回该数据项的最新值,则该存储系统是一致的。一致包含了存储系统行为的两个方面:第一是coherence,即一致性,定义了读取操作会返回什么值;第二是consistency,即连续性,定义了写入的值什么时候会被读取操作返回。

监听协议

一个常用的cache一致性协议是监听。cache中既包含物理存储器数据的副本,也含有该数据块的共享状态,但并不集中保留状态。这些cache都可以通过一些广播媒体(总线或网络)访问,而且所有的cache控制器都可以监听媒介,以确定它们是否有总线访问所需的数据块的副本。

MESI

introduce

缓存一致性协议MESI(Modified Exclusive Shared Invalid),四个字母缩写代表了缓存行的四种状态:

  • M:modified,表示当前cache行中的副本数据已经被修改,和主存中的数据不一致,若其他处理器想读取主存中的数据,必须等待当前行的副本写回主存。可以理解为,当前CPU更改了副本的值,主存中的数据是旧的,若其他CPU需要读取,必须等当前cache行先写回新的值
  • E:exclusive,当前cache行中的数据的副本是独占中,别的处理器cache还未包含该数据的副本,当前cache行中的副本和主存中的数据值是一致的
  • S:shared,即多个CPU的cache都缓存了主存中数据的副本,处于共享状态,且均和主存中数据一致。可以理解为当前cache行中的副本数据在其他CPU的cache中也存在,且均是“干净”的
  • I:invalid,即无效状态,表示当前cache行中的数据副本已经失效,需要重新从内存中读取

状态迁移图

考虑多核处理器系统中,多个core对相关的(即缓存的副本是内存中的统一数据)cache行进行操作时,会影响自身或其他core中cache行的状态,大致状态转移图如下,共存在4种event:

  • local read:当前core读取cache行中的数据副本
  • local write:当前core修改cache行中的数据副本
  • remote read:其他core读取自己的cache行中的数据副本
  • remote write:其他core修改自己的cache行中的数据副本

我们通过一个简单的例子帮助理解状态以及其转移的含义。假设在多核处理器系统中,有三个线程分别运行在core A、core B和core C(下文简称A、B、C)上,对同一变量进行访问。初始状态3个core中的cache行均是Invalid。cache采用write back + write allocate策略。

  • A读取变量,触发local read事件,将其缓存在自身的cache中,状态设为Exclusive(当前仅一份副本)。B和C中cache行仍是Invalid
  • A修改变量,触发local write事件,直接修改其缓存的cache中的副本,状态设为Modified。B和C中cache行仍是Invalid
  • B读取变量,本地触发local read事件,对于A出发remote read事件。首先A将其自身的相关cache行写回,状态设为Shared(与B共享)。B从主存中读取变量并缓存到cache行中,状态也设为Shared。C中cache行仍是Invalid
  • A读取变量,触发local read事件,直接从自身cache中获取最新数据副本
  • B修改变量,触发local write事件,对于A触发remote write事件。B直接修改自身的cache行,状态设为Modified。A中cache行缓存的已是旧值,设为Invalid,下次使用需要重新读取。C中cache行仍是Invalid
  • C修改变量,触发local write事件,对于B触发remote write事件。B中cache行的状态为Modified,首先写回cache行,并且状态设为Invalid(因为C将进行修改操作,B中的数据将会是旧值)。C从内存读取最新的数据(B刚写回的),并缓存在自身cache中,然后修改该副本,状态变为Modified。

多个core在进行操作时,会通过总线进行通信,此处略过通信的具体过程,有兴趣的朋友可以上网搜索。

状态迁移表

State Event Action
Modified local read 从cache中读取,状态不变
local write 修改cache中的副本,状态不变
remote read cache中的副本写回主存,remote core再访问主存获取最新数据,remote core中的cache行和当前cache行状态变为Shared
remote write cache中的副本写回主存,其他core再访问主存获取最新数据并修改,状态变为Modified,当前cache行的状态Invalid
Exclusive local read 从cache中读取,状态不变
local write 修改cache中的副本,状态变为Modified
remote read 与其他core共享数据,相应的cache行状态均变为Shared
remote write 其他core访问主存获取数据并修改,状态变为Modified,当前cache行的状态变为Invalid
Shared local read 从cache中读取,状态不变
local write 修改cache中的副本,状态变为Modified,其他core中共享数据的cache行状态变为Invalid
remote read 其他core访问主存获取数据,状态变为Shared,当前cache行状态不变
remote write 其他core访问主存获取最新数据并修改,状态变为Modified,当前core状态变为Invalid
Invalid local read 如果其他core的cache中有这份数据的副本并且已经修改过(状态为Modified),则需要先写回;当前core从主存中读取,如果有其他core共享数据,则相关的core的cahce行的状态均设为Shared,反之当前core的cache行状态设为Exclusive
local write 如果其他core的cache中有这份数据的副本并且已经修改过(状态为Modified),则需要先写回;当前core从主存中读取,如果有其他core共享数据,则其他的core的cache行状态均设为Invalid,当前core的cache行状态设为Modified
remote read 状态不变
remote write 状态不变
注意,所提及的cache行状态是指包含相应数据副本的cache行,例如,如果cache行有64bytes,修改了其中的某些bytes后,该cache行的状态就变为Modified,状态是属于cache行的,而非整个cache。写回操作也是以cache行为进行操作。

通过上述对MESI的描述,可以得知,多个core的状态并非独立的,例如,不可能同时存在两个core的状态是Modified或Exclusive,Shared状态的core的数量不能少于2。

M E S I
M × × ×
E × × ×
S × ×
I

simulation

通过C语言实现一个简单的MESI模拟。
定义枚举类型,包含4种状态。定义cache行的结构体。每个cache行结构体包含一个变量用于缓存数据,并包含一个枚举类型的状态变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef enum
{
MODIFIED,
EXCLUSIVE,
SHARED,
INVALID
} state_t;


typedef struct
{
state_t state;
uint32_t data;
} line_t;

为确保不引入cache自身工作原理相关的复杂性,我们用line_t的数组替代整个多核系统的cache,NUM_CORE是系统的核心数,每个核心编号为0~NUM_CORE - 1,核心i的cache为cache[i],即一个line_t,程序中我们只会操作每个core对应的line_t。
state_count用于跟踪当前系统中各core的cache行的状态的数量。state_ch是用于debug的数组,根据cache行的状态简单地标识一个输出信号。全局变量mem_data用于表示内存中的一个数据,结构体line_t中的变量就是用来缓存该全局变量的。程序中模拟访问内存中的数据只会使用该变量。同时,定义两个全局静态变量,modified_idx和exclusive_idx,分别跟踪当前系统中状态为Modified或Exclusive的cache行所在的core的编号。

1
2
3
4
5
6
7
8
typedef line_t cache_t[NUM_CORE]; // cache lines
uint32_t state_count[4]; // cache line state count
char state_ch[4] =
{'M', 'E', 'S', 'I'}; // cache line state character
cache_t cache; // cache
uint32_t mem_data = 6828; // memory data

static uint32_t modified_idx, exclusive_idx;

定义一个检查当前多核系统中各core的cache行状态是否合法的函数,防止系统cache行经过多次改变后进入了一个不可能存在的情况(例如多个Modified和Exclusive)。一个打印函数,输出当前系统中各个core的cache行的状态和数据副本、state_count的值以及全局变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int check_cache_state()
{
// 需要满足:1. M或E的状态数不能超过1,且S状态数大于1,剩余状态均是I,或均是I
if (((state_count[MODIFIED] == 1 || state_count[EXCLUSIVE] == 1) && state_count[INVALID] == NUM_CORE - 1)
|| (state_count[SHARED] > 1 && state_count[INVALID] == NUM_CORE - state_count[SHARED])
|| state_count[INVALID] == NUM_CORE)
return 0;
return 1;
}


void print_cache()
{
for (int i = 0; i < NUM_CORE; ++i)
printf("[%d] %c %d\n", i, state_ch[cache[i].state], cache[i].data);
for (int i = 0; i < 4; ++i)
printf("%c %d\t", state_ch[i], state_count[i]);
printf("\nmemory: %d\n", mem_data);
}

定义读取函数,给定一个core编号,表示该core读取数据。我们每次操作均需要根据当前情况,该边当前core甚至其他core的cache行的状态,以及更新state_count数组和两个static变量。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
void read_line(uint32_t idx, uint32_t * pd)
{
// 如果当前cache行的状态为无效
if (cache[idx].state == INVALID)
{
// 1.如果其他core没有这份数据,当前core的数据从内存获得,cache行状态变为E
if (state_count[INVALID] == NUM_CORE)
{
cache[idx].state = EXCLUSIVE;
cache[idx].data = mem_data;

// update state to EXCLUSIVE
--state_count[INVALID];
++state_count[EXCLUSIVE];
exclusive_idx = idx;

#ifdef DEBUG
printf("[%d] read miss; read from memory data %d\n", idx, cache[idx].data);
#endif
}
// 2.如果其他core有这份数据,且状态为M
else if (state_count[INVALID] == NUM_CORE - 1 && state_count[MODIFIED] == 1)
{
// write back
#ifdef DEBUG
printf("[%d] read miss; [%d] write back; update memory from %d to %d; read from other line data %d\n",
idx, modified_idx, mem_data, cache[modified_idx].data, mem_data);
#endif
mem_data = cache[modified_idx].data;

// read from M cache line
cache[idx].data = cache[modified_idx].data;

// update state to SHARED
cache[modified_idx].state = cache[idx].state = SHARED;
--state_count[MODIFIED];
--state_count[INVALID];
state_count[SHARED] = 2;
}
// 3. 如果其他core有这份数据,且状态为S或者为E
else
{
#ifdef DEBUG
printf("[%d] read miss; broadcast shared; read from other line; data %d\n", idx, cache[idx].data);
#endif
// update state
cache[idx].state = SHARED;
--state_count[INVALID];
++state_count[SHARED];
if (state_count[EXCLUSIVE] == 1) // 存在一个E状态的core
{
cache[exclusive_idx].state = SHARED;
--state_count[EXCLUSIVE];
++state_count[SHARED];
// read from E line
cache[idx].data = cache[exclusive_idx].data;
}
else
{
for (int i = 0; i < NUM_CORE; ++i)
{
// read from S line
if (i != idx && cache[i].state == SHARED)
{
cache[idx].data = cache[i].data;
break;
}
}
}
}
}
else
{
#ifdef DEBUG
printf("[%d] read hit; data %d\n", idx, cache[idx].data);
#endif
}

*pd = cache[idx].data;
}

接下来是写操作,给定core的编号和一个数据,表示该core进行了一个写操作。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
void write_line(uint32_t idx, uint32_t * pd)
{
if (cache[idx].state == MODIFIED)
{
#ifdef DEBUG
printf("[%d] write hit; update data from %d to %d\n", idx, cache[idx].data, *pd);
#endif
cache[idx].data = *pd;
}
else if (cache[idx].state == EXCLUSIVE)
{
#ifdef DEBUG
printf("[%d] write hit; write data %d\n", idx, *pd);
#endif
// update state
cache[idx].state = MODIFIED;
--state_count[EXCLUSIVE];
++state_count[MODIFIED];
cache[idx].data = *pd;
modified_idx = idx;
}
else if (cache[idx].state == SHARED)
{
#ifdef DEBUG
printf("[%d] write hit; broadcast invalid; write %d\n", idx, *pd);
#endif
// update state
cache[idx].state = MODIFIED;
--state_count[SHARED];
++state_count[MODIFIED];
cache[idx].data = *pd;
modified_idx = idx;

// update other shared core to invalid
for (int i = 0; i < NUM_CORE && state_count[SHARED]; ++i)
{
if (cache[i].state == SHARED)
{
cache[i].state = INVALID;
++state_count[INVALID];
--state_count[SHARED];
}
}
}
else
{
#ifdef DEBUG
printf("[%d] write miss\n", idx);
#endif
// 如果其他core中保存有data且状态为M,则先写回其他core
if (state_count[MODIFIED] == 1)
{
#ifdef DEBUG
printf("[%d] write back %d\n", modified_idx, cache[modified_idx].data);
#endif
mem_data = cache[modified_idx].data;
cache[modified_idx].state = INVALID;
--state_count[MODIFIED];
++state_count[INVALID];
}
// read from memory and write
#ifdef DEBUG
printf("[%d] read from memory %d; update to %d\n", idx, mem_data, *pd);
#endif
cache[idx].data = mem_data;
cache[idx].data = *pd;

// update state
cache[idx].state = MODIFIED;
--state_count[INVALID];
++state_count[MODIFIED];
modified_idx = idx;
// update other core state
if (state_count[EXCLUSIVE] == 1)
{
cache[exclusive_idx].state = INVALID;
--state_count[EXCLUSIVE];
++state_count[INVALID];
}
else if (state_count[SHARED] > 0)
{
for (int i = 0; i < NUM_CORE && state_count[SHARED]; ++i)
{
if (cache[i].state == SHARED)
{
cache[i].state = INVALID;
++state_count[INVALID];
--state_count[SHARED];
}
}
}
}
}

再定义了一个evict操作,表示当前cache行被替换了,可能进行写回操作

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
void evict_line(uint32_t idx, uint32_t * pd)
{
#ifdef DEBUG
printf("[%d] evict", idx);
#endif
// if modified, write back
if (cache[idx].state == MODIFIED)
{
#ifdef DEBUG
printf("; write back %d", cache[idx].data);
#endif
mem_data = cache[idx].data;
*pd = cache[idx].data;
}
#ifdef DEBUG
printf("\n");
#endif
--state_count[cache[idx].state];
cache[idx].state = INVALID;
++state_count[INVALID];

// if S state count is 1, update it to E
if (state_count[SHARED] == 1)
{
for (int i = 0; i < NUM_CORE; ++i)
if (cache[i].state == SHARED)
{
cache[i].state = EXCLUSIVE;
--state_count[SHARED];
++state_count[EXCLUSIVE];
exclusive_idx = i;
break;
}
}
}

最后通过main函数进行模拟和测试。使用随机数选择每次需要进行的操作和相应的core编号,每次操作后,检查当前多核系统是否处于一个合法的状态,若进入了一个不合法状态,则打印一个state error信息,并退出;反之打印pass。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("usage: ./mesi <times>\n");
exit(0);
}
int times = atoi(argv[1]);
void (*op_arr[3])(uint32_t, uint32_t *) = {read_line, write_line, evict_line};
uint32_t idx, op, data;

srand(15213);

// 初始化cache
for (int i = 0; i < NUM_CORE; ++i)
{
cache[i].state = INVALID;
cache[i].data = 0;
}
state_count[INVALID] = NUM_CORE;

for (int i = 0; i < times; ++ i)
{
#ifdef DEBUG
printf("========================================\n");
#endif
idx = rand() % NUM_CORE;
op = rand() % 3;

#ifdef DEBUG
switch (op)
{
case 0:
printf("read line <%d>\n", idx);
break;
case 1:
printf("write line <%d>\n", idx);
break;
case 2:
printf("evict line <%d>\n", idx);
break;
default:
exit(1);
}
#endif
if (op == 1)
data = rand() % 10009;
op_arr[op](idx, &data);

if (check_cache_state())
{
fprintf(stderr, "state error\n");
exit(1);
}
#ifdef DEBUG
printf("\n");
print_cache();
#endif
}

printf("pass\n");

return 0;
}

编译程序,可以通过定义DEBUG宏定义变量,输出详细的操作过程信息。

1
> gcc -DDEBUG mesi.c -g -o mesi

执行时给出需要执行的次数,查看输出结果

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
> ./mesi 5
========================================
evict line <1>
[1] evict

[0] I 0
[1] I 0
[2] I 0
[3] I 0
M 0 E 0 S 0 I 4
memory: 6828
========================================
evict line <1>
[1] evict

[0] I 0
[1] I 0
[2] I 0
[3] I 0
M 0 E 0 S 0 I 4
memory: 6828
========================================
read line <3>
[3] read miss; read from memory data 6828

[0] I 0
[1] I 0
[2] I 0
[3] E 6828
M 0 E 1 S 0 I 3
memory: 6828
========================================
write line <3>
[3] write hit; write data 7815

[0] I 0
[1] I 0
[2] I 0
[3] M 7815
M 1 E 0 S 0 I 3
memory: 6828
========================================
write line <1>
[1] write miss
[3] write back 7815
[1] read from memory 7815; update to 5828

[0] I 0
[1] M 5828
[2] I 0
[3] I 7815
M 1 E 0 S 0 I 3
memory: 7815
pass

默认core的数量是4,调整到64,以及更多的操作次数,取消DEBUG的编译标识,查看运行结果

1
2
3
> gcc mesi.c -g -o mesi
> ./mesi 123456789
pass

伪共享

伪共享就是多线程操作位于同一缓存行的不同变量时,由于缓存失效而引发性能下降的问题。例如,假设有2个线程运行在不同core上,cache行大小为64bytes,线程访问两个相邻的int变量a和b,因此两个core的cache中会分别缓存这两个变量。由于每次cache会直接读入64bytes,因此这两个相邻的变量可能同时被读入了cache行。当前两个core中均缓存了a和b且在同一行上,根据MESI,当两个线程操作这两个看似无关的变量时,会不断触发remote write,反复写回一个core的cache行,而另一个core重新访问内存,造成了性能的降低。“真”共享就是两个core访问内存区的同一变量。
通过一个示例程序,查看伪共享、“真”共享和“无”共享的情况下的性能对比。程序中,通过创建两个线程,执行同一函数,对两个变量进行自增操作,这两个变量是相邻的(伪共享)、相同的(“真”共享)或相隔较远的(保证不再同一cache行)。线程执行自增任务之前,根据传入的参数,将自己绑定到指定的core上执行。
查看cache的行大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> getconf -a | grep CACHE
LEVEL1_ICACHE_SIZE 32768
LEVEL1_ICACHE_ASSOC
LEVEL1_ICACHE_LINESIZE 64
LEVEL1_DCACHE_SIZE 32768
LEVEL1_DCACHE_ASSOC 8
LEVEL1_DCACHE_LINESIZE 64
LEVEL2_CACHE_SIZE 524288
LEVEL2_CACHE_ASSOC 8
LEVEL2_CACHE_LINESIZE 64
LEVEL3_CACHE_SIZE 16777216
LEVEL3_CACHE_ASSOC 0
LEVEL3_CACHE_LINESIZE 64
LEVEL4_CACHE_SIZE
LEVEL4_CACHE_ASSOC
LEVEL4_CACHE_LINESIZE

查看页大小

1
2
> getconf PAGE_SIZE
4096

在程序中预定义常量CACHELINE_SIZE为64,设置数组占64bytes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define TIMES (1e8)
#define CACHELINE_SIZE (64)

// 线程执行需要传入的参数
typedef struct
{
uint64_t * ptr; // 访问的内存地址
uint32_t cpu_id; // 需要绑定的core
uint32_t times; // 自增操作次数
} param_t;

// 线程访问的变量
uint64_t val1[CACHELINE_SIZE / sizeof(uint64_t)];
uint64_t val2[CACHELINE_SIZE / sizeof(uint64_t)];
uint64_t val3[CACHELINE_SIZE / sizeof(uint64_t)];
uint64_t val4[CACHELINE_SIZE / sizeof(uint64_t)];

定义worker函数,即线程执行的函数,会输出tid、所在的core、访问的内存地址和操作后的数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void * worker(void * param)
{
param_t * p = (param_t *)param;
cpu_set_t mask;

// bind to core[core_id]
CPU_ZERO(&mask);
CPU_SET(p->cpu_id, &mask);
if (pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) == -1)
exit(1);
while (p->times--)
++(*(p->ptr));
printf("T[%d] run on core <%d> result %#x: %ld\n", pthread_self(), sched_getcpu(), p->ptr, *p->ptr);
return NULL;
}

模拟无共享,两个线程分别访问&val1[0]和&val2[0],理论上这两个地址相隔64bytes,存放在不同的cache行中

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
void no_sharing()
{
pthread_t t1, t2;

param_t param1 =
{
.ptr = &val1[0],
.cpu_id = 0,
.times = TIMES
};

param_t param2 =
{
.ptr = &val2[0],
.cpu_id = 1,
.times = TIMES
};

clock_t start = clock();

pthread_create(&t1, NULL, worker, &param1);
pthread_create(&t2, NULL, worker, &param2);

pthread_join(t1, NULL);
pthread_join(t2, NULL);

printf("======================[NO]\tcost %ld\n", clock() - start);
}

模拟伪共享,两个线程访问的内存地址分别为&val3[0]和&val3[1],很可能处于同一cache行中(也可能刚好处于不同行中),运行后可以通过查看虚拟地址判断。

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
void false_sharing()
{
pthread_t t1, t2;

param_t param1 =
{
.ptr = &val3[0],
.cpu_id = 0,
.times = TIMES
};

param_t param2 =
{
.ptr = &val3[1],
.cpu_id = 1,
.times = TIMES
};

clock_t start = clock();

pthread_create(&t1, NULL, worker, &param1);
pthread_create(&t2, NULL, worker, &param2);

pthread_join(t1, NULL);
pthread_join(t2, NULL);

printf("======================[FALSE]\tcost %ld\n", clock() - start);
}

模拟真共享,两个线程访问同一内存地址,即&val4[0]。

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
void true_sharing()
{
pthread_t t1, t2;

param_t param1 =
{
.ptr = &val4[0],
.cpu_id = 0,
.times = TIMES
};

param_t param2 =
{
.ptr = &val4[0],
.cpu_id = 1,
.times = TIMES
};

clock_t start = clock();

pthread_create(&t1, NULL, worker, &param1);
pthread_create(&t2, NULL, worker, &param2);

pthread_join(t1, NULL);
pthread_join(t2, NULL);

printf("======================[TRUE]\tcost %ld\n", clock() - start);
}

main函数运行三个测试函数

1
2
3
4
5
6
int main()
{
no_sharing();
false_sharing();
true_sharing();
}

运行,查看结果

1
2
3
4
5
6
7
8
9
10
> ./false_sharing
T[140737351505472] run on core <0> result 0x55558040: 100000000
T[140737343112768] run on core <1> result 0x55558080: 100000000
======================[NO] cost 1646328
T[140737343112768] run on core <0> result 0x555580c0: 100000000
T[140737351505472] run on core <1> result 0x555580c8: 100000000
======================[FALSE] cost 2323293
T[140737343112768] run on core <1> result 0x55558100: 124568282
T[140737351505472] run on core <0> result 0x55558100: 142349628
======================[TRUE] cost 3019775

可见,no sharing执行的时间最短,两个线程访问的内存地址(虚拟地址)分别为0x55558040和0x55558080,页大小为4KB,两个地址的页偏移为0x40和0x80,虽然我们不知道访问的具体物理地址是多少,但是可以确定这两个地址位于同一物理页框,因为它们的虚拟页号是相同的。0x40和0x80读入的cache行是不同的,因此符合no sharing,执行时间最短。
同理,false sharing访问的地址为0x555580c0和0x555580c8,虚拟页号相同,物理页框也相同,cache行大小为64bytes,0xc0和0xc8确定的cache索引号是相同的,块内偏移不同,因此这两个内存区域的数据会读入同一行。
true sharing的执行时间最长。