最近在看《深度探索 C++ 对象模型》一书,收获颇丰。如果你对 C++
底层机制感兴趣、想知道编译器对我们的代码动了什么“手脚”,推荐阅读该书。
本文不打算整理或复述《深度探索 C++
对象模型》一书的内容,因为这本书需要花费一定的时间心力阅读,一篇文章恐难覆盖全书内容。因此,本文仅展示部分代码及运行结果,加以必要的注释、解释等,以阐明
C++ 对象模型的部分知识点。代码在 64 位 WSL(Ubuntu 22.04)系统运行,使用
g++ 编译器。系统及编译器信息展示如下:
1 2 3 4 5 6 7 $ uname -a Linux arcsin2-pc 5.15.133.1-microsoft-standard-WSL2 #1 SMP Thu Oct 5 21:02:42 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux$ g++ --version g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.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.
由于部分代码涉及到裸指针操作和强制类型转换等,且 C++
对象模型在不同编译器中的实现存在区别,本文代码并不保证跨平台运行,也不保证输出结果确定。本文的分析和解释仅针对上述系统和编译器。
0. 多态与虚函数表
在开始之前,实现要对 C++
多态实现机制:虚函数表有了解。下面代码展示了如何通过强制类型转换,通过
vptr
访问虚函数表从而调用虚函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> using namespace std;class Object {public : virtual void f () { cout << "f called" << endl; } virtual void g () { cout << "g called" << endl; } virtual void h () { cout << "h called" << endl; } };int main () { Object obj; auto vptr = *reinterpret_cast <void (***)(Object *)>(&obj); vptr[0 ](&obj); vptr[1 ](&obj); vptr[2 ](&obj); return 0 ; }
代码运行结果如下:
1 2 3 4 $ g++ virtual_function.cpp && ./a.out f called g called h called
通过 vptr
访问虚函数表的不同 slot
即可实现调用不同的虚函数。
1. new 与 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 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 #include <iostream> using namespace std;class Base {public : virtual void show () { cout << "Base" << endl; } virtual ~Base () = default ; int data_; };class Derive : public Base {public : void show () override { cout << "Derive" << endl; } int age_; };int main () { cout << sizeof (Base) << endl; cout << sizeof (Derive) << endl; Base b; b.show (); Derive d; d.show (); cout << &d << "\t" << &d.data_ << "\t" << &d.age_ << endl; cout << &b << "\t" << &b.data_ << endl; Derive *p = reinterpret_cast <Derive *>(malloc (sizeof (Derive))); auto pd = reinterpret_cast <double *>(p); *pd = *reinterpret_cast <double *>(&d); p->show (); *pd = *reinterpret_cast <double *>(&b); p->show (); p = new (pd) Derive; p->show (); auto ptr = new Derive (); ptr->show (); return 0 ; }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 $ g++ new_and_malloc.cpp && ./a.out 16 16 Base Derive 0x7ffcdc4153b0 0x7ffcdc4153b8 0x7ffcdc4153bc 0x7ffcdc4153a0 0x7ffcdc4153a8 Derive Base Derive Derive
这段代码旨在说明面试中的一个常见问题:new
与
malloc
有什么区别?当然答案其实很简单,就是前者不仅分配内存,而且调用了构造函数;而后者仅进行内存分配。我们结合代码分析。
在代码第 35
行,通过 malloc
在堆内存申请足够空间以放置 Derive
类的对象。然后通过指针操作,将分配的内存区域的 vptr
,即:虚函数表指针分布赋予 Base
类和 Derive
类的虚函数表地址,于是代码 38
行和 41
行分别输出 Base
和 Derive
,实现了手动指定
vptr
指针以调用不同的虚函数。这对理解 C++
多态的实现机制:虚函数表有一定帮助。
代码 43
行,通过一个
placement operator new
,调用了 Derive
类的构造函数,从而代码 44
行输出 Derive
。
代码 46
和 47
行则通过 new
来构造对象,编译器负责帮我们分配内存即指定 vptr
,我们能够输出 Derive
。
2. C++ class 与 C struct 的转换
本小节通过代码来直观感受:如何用 C 语言提供的机制来实现 C++
的语义和运行时特征。代码如下:
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 #include <iostream> using namespace std;class X { public : X () = default ; virtual ~X () = default ; virtual void foo () { cout << "foo" << endl; } X (const X &x) = default ; };X foobar () { X xx; X *px = new X{}; xx.foo (); px->foo (); delete px; return xx; }struct c_X { int **vptr; };void c_X_destructor (c_X *ptr) {}void c_X_foo (c_X *ptr) { cout << "c_X_foo" << endl; }int *virtual_function_table[] = { nullptr , (int *)c_X_destructor, (int *)c_X_foo };void c_X_constructor (c_X *ptr) { ptr->vptr = virtual_function_table; }void c_X_copy (c_X *ptr, c_X src) { ptr->vptr = src.vptr; }void foobar (c_X &_result) { c_X_constructor (&_result); c_X *px = (c_X *)malloc (sizeof (c_X)); if (px != nullptr ) { c_X_constructor (px); } c_X_foo (&_result); ((void (*)(c_X *))(px->vptr[2 ]))(px); if (px != nullptr ) { ((void (*)(c_X *))(px->vptr[1 ]))(px); free (px); } return ; }int main () { cout << "C++ style: " << endl; foobar (); cout << "\nC style: " << endl; c_X obj; foobar (obj); }
运行结果:
1 2 3 4 5 6 7 8 $ g++ object_model.cpp && ./a.out C++ style: foo foo C style: c_X_foo c_X_foo
上述代码展示了如何用 C
语言机制实现构造函数、析构函数、拷贝构造函数等。其中的关键是虚函数表以及结构体内指向虚函数表的
vptr
指针。同时,转换的 C
语言代码还应用了编译器常用的优化策略:NRVO(Named Return Value
Optimization),即:在函数参数内添加一个参数以传递返回值,从而消除拷贝构造函数的调用,提高程序性能。
3. 成员初始化顺序
C++ 构造函数中的初始化列表的书写顺序并不代表成员的实际初始化顺序。C++
标准规定,类成员的初始化顺序和声明顺序相同,而和构造函数的初始化列表无关。下面的代码展示了可能的错误和正确的写法:
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 #include <iostream> using namespace std;class X {public : explicit X (int val) : j(val), i(j) { } void show () const { cout << "i = " << i << " j = " << j << endl; }private : int i; int j; };class Y {public : explicit Y (int val) : i(val), j(i) { } void show () const { cout << "i = " << i << " j = " << j << endl; }private : int i; int j; };int main () { X x{10 }; x.show (); Y y{10 }; y.show (); return 0 ; }
运行结果如下(多次运行以说明类 X
的数据成员
i
并没有被正确初始化):
1 2 3 4 5 6 7 8 9 $ g++ init_order.cpp && ./a.out i = 32624 j = 10 i = 10 j = 10$ g++ init_order.cpp && ./a.out i = 32757 j = 10 i = 10 j = 10$ g++ init_order.cpp && ./a.out i = 32637 j = 10 i = 10 j = 10
正确的做法是:在初始化列表内按照成员的声明顺序依次初始化,不可随意改变顺序。
4. 类成员指针
本小节仅为说明一个很少用的用法,即:如何通过指针获取类成员函数并通过类成员函数指针进行函数调用。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> using namespace std;class Object {public : void func () { cout << "func called with value = " << value << endl; } void func2 (int add) { cout << "func called with value + add = " << value + add << endl; } int value{10 }; };int main () { Object obj; auto func_ptr = &Object::func; (obj.*func_ptr)(); obj.value = 1 ; auto func_ptr2 = reinterpret_cast <void (*)(Object *, int )>(&Object::func2); func_ptr2 (&obj, 1 ); return 0 ; }
运行结果如下(编译器会发出警告,下列运行结果保留编译器输出信息):
1 2 3 4 5 6 7 $ g++ member_pointer.cpp && ./a.out member_pointer.cpp: In function ‘int main()’: member_pointer.cpp:19:76: warning: converting from ‘void (Object::*)(int)’ to ‘void (*)(Object*, int)’ [-Wpmf-conversions] 19 | auto func_ptr2 = reinterpret_cast<void (*)(Object *, int)>(&Object::func2); // warning | ^ func called with value = 10 func called with value + add = 2
代码 15
和 16
行展示了成员函数指针的用法;19
和 20
行展示了如何通过强制类型转换和 this
指针进行类成员函数调用。可以看出,程序运行结果与我们预期相同。不过,编译器对
19
行代码发出了警告,实际项目中不可使用此方式编码。
5. 虚拟继承的对象布局
虚拟继承是避免菱形继承中子类含有多个基类对象的方法。本小节解释虚拟继承是如何实现的?其对对象的内存布局会产生什么影响?见以下代码:
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 #include <iostream> using namespace std;class Point2d { public : float _x{1.0f }; float _y{2.0f }; };class Vertex : public virtual Point2d { public : Vertex *next; };class Point3d : public virtual Point2d { public : float _z; };class Vertex3d : public Vertex, public Point3d { public : float mumble; };int main () { cout << sizeof (Point2d) << endl; cout << sizeof (Vertex) << endl; cout << sizeof (Point3d) << endl; cout << sizeof (Vertex3d) << endl; Point2d p2d; cout << &p2d << " " << &p2d._x << " " << &p2d._y << endl; Vertex ver; cout << &ver << " " << &ver._x << " " << &ver._y << " " << &ver.next << endl; Point3d p3d; cout << &p3d << " " << &p3d._x << " " << &p3d._y << " " << &p3d._z << endl; Vertex3d ver3d; cout << &ver3d << " " << &ver3d._x << " " << &ver3d._y << " " << &ver3d._z << " " << &ver3d.next << " " << &ver3d.mumble << endl; return 0 ; }
运行结果如下:
1 2 3 4 5 6 7 8 9 $ g++ virtual_inheritance.cpp && ./a.out 8 24 24 40 0x7ffce4dad0d8 0x7ffce4dad0d8 0x7ffce4dad0dc 0x7ffce4dad0e0 0x7ffce4dad0f0 0x7ffce4dad0f4 0x7ffce4dad0e8 0x7ffce4dad100 0x7ffce4dad10c 0x7ffce4dad110 0x7ffce4dad108 0x7ffce4dad120 0x7ffce4dad140 0x7ffce4dad144 0x7ffce4dad138 0x7ffce4dad128 0x7ffce4dad13c
这段代码的输出结果并不那么重要,关键是代码中每个类前面的注释,注释展示了对象的内存布局。以菱形继承的最低层子类
Vertex3d
为例,由于其直接基类 Vertex
和
Point3d
都是虚拟继承自类 Point2d
,因此,其内存布局和普通的继承有很大区别。注释阐明了一种可能实现方式的内存布局,vptr
指针在这里不仅仅指向虚函数表,也需要在某个 slot
内指明虚基类 Point2d
的地址与 this
指针的偏移量。这样,就能够在运行时通过该偏移量动态访问到虚拟基类,并调用正确的函数。
6. 构造函数析构函数与虚函数
假如我们在构造函数中调用一个虚函数,会发生什么呢?调用的究竟是基类的实现还是子类改写的实现呢?以下面代码为例:
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 96 97 98 99 100 101 102 103 104 105 106 107 #include <iostream> using namespace std;class Point2d { double x_, y_;public : virtual ~Point2d () { cout << "this = " << this << "\t" ; cout << "Point2d size = " << size () << endl; } virtual int size () { return sizeof (Point2d); } Point2d () { cout << "this = " << this << "\t" ; cout << "Point2d size = " << size () << endl; } };class Point3d : public virtual Point2d { double y_;public : ~Point3d () { cout << "this = " << this << "\t" ; cout << "Point3d size = " << size () << endl; } int size () override { return sizeof (Point3d); } Point3d () { cout << "this = " << this << "\t" ; cout << "Point3d size = " << size () << endl; } };class Vertex : public virtual Point2d {public : ~Vertex () { cout << "this = " << this << "\t" ; cout << "Vertex size = " << size () << endl; } int size () override { return sizeof (Vertex); } Vertex () { cout << "this = " << this << "\t" ; cout << "Vertex size = " << size () << endl; } };class Vertex3d : public Point3d, public Vertex {public : ~Vertex3d () { cout << "this = " << this << "\t" ; cout << "Vertex3d size = " << size () << endl; } int size () override { return sizeof (Vertex3d); } Vertex3d () { cout << "this = " << this << "\t" ; cout << "Vertex3d size = " << size () << endl; } };class PVertex3d : public Vertex3d { double d_;public : ~PVertex3d () { cout << "this = " << this << "\t" ; cout << "PVertex3d size = " << size () << endl; } int size () override { return sizeof (PVertex3d); } PVertex3d () { cout << "this = " << this << "\t" ; cout << "PVertex3d size = " << size () << endl; } };int main () { cout << sizeof (Point2d) << endl; cout << sizeof (Point3d) << endl; cout << sizeof (Vertex) << endl; cout << sizeof (Vertex3d) << endl; cout << sizeof (PVertex3d) << endl; { Vertex3d obj; cout << "Destructor:" << endl; } cout << "------------------------------------" << endl; { PVertex3d pv3d; cout << "Destructor:" << endl; } 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 $ g++ virtual_func_in_constructor.cpp && ./a.out 24 40 32 48 56 this = 0x7ffd70141fa8 Point2d size = 24 this = 0x7ffd70141f90 Point3d size = 40 this = 0x7ffd70141fa0 Vertex size = 32 this = 0x7ffd70141f90 Vertex3d size = 48 Destructor: this = 0x7ffd70141f90 Vertex3d size = 48 this = 0x7ffd70141fa0 Vertex size = 32 this = 0x7ffd70141f90 Point3d size = 40 this = 0x7ffd70141fa8 Point2d size = 24 ------------------------------------ this = 0x7ffd70141fb0 Point2d size = 24 this = 0x7ffd70141f90 Point3d size = 40 this = 0x7ffd70141fa0 Vertex size = 32 this = 0x7ffd70141f90 Vertex3d size = 48 this = 0x7ffd70141f90 PVertex3d size = 56 Destructor: this = 0x7ffd70141f90 PVertex3d size = 56 this = 0x7ffd70141f90 Vertex3d size = 48 this = 0x7ffd70141fa0 Vertex size = 32 this = 0x7ffd70141f90 Point3d size = 40 this = 0x7ffd70141fb0 Point2d size = 24
输出展示了构造函数与析构函数的调用顺序与继承关系的关联。同时,运行结果也说明了:构造函数内调用的
virtual 函数并不具备多态行为,而是调用当前类的实现。这是因为 C++
语言在进行当前类的构造前,首先把 vptr
指针指向当前类的虚函数表。在指向基类的构造函数时,vptr
指向基类的虚函数表,调用的自然就是基类的实现。只有当子类的构造函数全部完成(所有基类和成员的构造函数此时必定已执行完成),vptr
指向子类的虚函数表,子类也就展现出属于子类的多态行为。析构函数中调用虚函数有类似的效果。
7. 模板实例化与静态变量
首先看代码:
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 #include <iostream> using namespace std;template <class T > void func (T t) { static int count = 0 ; cout << "count = " << count << endl; ++count; }class Base {public : static int count; };template <class T > class Derive : public Base {public : void show () { cout << "count = " << count << endl; ++count; } };int Base::count = 0 ;int main () { func (1 ); func (1.0 ); func (2 ); func (1.0f ); cout << "-------------" << endl; Derive<int > di; di.show (); Derive<double > dd; dd.show (); Derive<char > dc; dc.show (); return 0 ; }
运行结果如下:
1 2 3 4 5 6 7 8 9 $ g++ template_function.cpp && ./a.out count = 0 count = 0 count = 1 count = 0 ------------- count = 0 count = 1 count = 2
这段代码说明了一个结论:模板的不同实例内的静态变量是不同的,如代码
26
到 29
行的输出所示。如果想让某个模板类的所有类型实例共享一个静态变量,应该按照代码
10
到21
行实现,即:在基类中声明一个静态变量,模板类继承自该基类。这样即可实现模板的所有类型实例都共享一个静态变量。
8. 总结
本文通过代码和运行结果辅以适当解释说明了 C++
对象模型的部分关键知识点。全文内容基于《深度探索 C++
对象模型》一书,推荐想进一步了解 C++ 对象模型的读者阅读本书。