了解你所使用的编程语言究竟是如何实现的, 对于 C++ 程序员可能特别有意义. 首先, 它可以去除我们对于所使用语言的神秘感, 使我们不至于对于编译器干的活感到完全不可思议; 尤其重要的是, 它使我们在 Debug 和使用语言高级特性的时候, 有更多的把握. 当需要提高代码效率的时候, 这些知识也能够很好地帮助我们.
简单非多态的内存布局
- class X {
- int x;
- float xx;
- public:
- X() {}
- ~X() {}
- void printInt() {}
- void printFloat() {}
- };
- | |
- |------------------------| <------ X class object memory layout
- | int X::x |
- |------------------------| stack segment
- | float X::xx | |
- |------------------------| |
- | | \|/
- | |
- | |
- ------|------------------------|----------------
- | X::X() |
- |------------------------| |
- | X::~X() | |
- |------------------------| \|/
- | X::printInt() | text segment
- |------------------------|
- | X::printFloat() |
- |------------------------|
- | |
在本示例中
只有数据成员存储在堆栈中, 且其声明顺序或者存储顺序的行为与编译器强相关
所有其他方法 (构造函数, 析构函数和编译器扩展代码) 都存储在文本段. 然后, 这些方法将被调用并隐式地在调用对象的第一个参数中传递该指针.
this 指针是一个隐含于每一个成员函数中的特殊指针. 它是一个指向正在被该成员函数操作的对象, 也就是要操作该成员函数的对象. this 作用域是在类内部, 当对一个对象调用成员函数时, 编译程序先将对象的地址赋给 this 指针, 编译器会自动将对象本身的地址作为一个隐含参数传递给函数. 也就是说, 即使你没有写 this 指针, 编译器在编译的时候也是加上 this 的, 它作为非静态成员函数的隐含形参. 被调用的成员函数函数体内所有对类成员的访问, 都会被转化为 "this->类成员" 的方式.
针对第二点, 我们类似于:
- A x;
- x.printInt();
其中, X::printInt()这个行为, 在编译器中, 将处理为
printInt(const X* this)
那么, x.printInt()调用处理将最终成为
printInt(&x);
同时具有虚函数和静态数据成员的内存布局
- class X {
- int x;
- float xx;
- static int count;
- public:
- X() {}
- virtual ~X() {}
- virtual void printAll() {}
- void printInt() {}
- void printFloat() {}
- static void printCount() {}
- };
其内存布局如下
- | |
- |------------------------| <------ X class object memory layout
- | int X::x |
- stack |------------------------|
- | | float X::xx |
- | |------------------------| |-------|--------------------------|
- | | X::_vptr |------| | type_info X |
- \|/ |------------------------| |--------------------------|
- | o | | address of X::~X() |
- | o | |--------------------------|
- | o | | address of X::printAll() |
- | | |--------------------------|
- | |
- ------|------------------------|------------
- | static int X::count | /|\
- |------------------------| |
- | o | data segment
- | o | |
- | | \|/
- ------|------------------------|------------
- | X::X() |
- |------------------------| |
- | X::~X() | |
- |------------------------| |
- | X::printAll() | \|/
- |------------------------| text segment
- | X::printInt() |
- |------------------------|
- | X::printFloat() |
- |------------------------|
- | static X::printCount() |
- |------------------------|
- | |
所有非静态数据成员都按照声明的顺序将空间放入堆栈中, 与前面的示例顺序相同.
静态数据成员将空间放入内存的数据段中. 使用范围解析运算符 (即::) 进行的访问. 但是在编译之后, 就没有像作用域和名称空间那样的东西了. 因为, 它的名称只是由编译器执行, 所以所有内容都由其绝对或相对地址引用.
静态数据成员将空间放入内存的数据段中. 使用范围解析运算符 (即::) 进行的访问.
静态方法进入文本段, 并通过作用域解析运算符进行调用.
对于 virtual 关键字, 编译器会自动将指向虚拟表的指针 (vptr) 插入对象内存表示中. 通常, 虚拟表是在数据段中为每个类静态创建的, 但它也取决于编译器的实现.
在虚拟表中, 第一个条目指向 type_info 对象, 该对象包含与当前基类和其他基类的 DAG(有向无环图)相关的信息(如果从这些基类派生的信息).
继承对象的内存布局
- class X {
- int x;
- string str;
- public:
- X() {}
- virtual ~X() {}
- virtual void printAll() {}
- };
- class Y : public X {
- int y;
- public:
- Y() {}
- ~Y() {}
- void printAll() {}
- };
其内存布局信息如下
- | |
- |------------------------------| <------ Y class object memory layout
- | int X::x |
- stack |------------------------------|
- | | int string::len |
- | |string X::str ----------------|
- | | char* string::str |
- \|/ |------------------------------| |-------|--------------------------|
- | X::_vptr |------| | type_info Y |
- |------------------------------| |--------------------------|
- | int Y::y | | address of Y::~Y() |
- |------------------------------| |--------------------------|
- | o | | address of Y::printAll() |
- | o | |--------------------------|
- | o |
- ------|------------------------------|--------
- | X::X() |
- |------------------------------| |
- | X::~X() | |
- |------------------------------| |
- | X::printAll() | \|/
- |------------------------------| text segment
- | Y::Y() |
- |------------------------------|
- | Y::~Y() |
- |------------------------------|
- | Y::printAll() |
- |------------------------------|
- | string::string() |
- |------------------------------|
- | string::~string() |
- |------------------------------|
- | string::length() |
- |------------------------------|
- | o |
- | o |
- | o |
- | |
在继承模型中, 基类和数据成员类是派生类的子对象.
编译器会在类的构造函数中生成具有所有重写的虚拟功能和为_vptr 分配虚拟表的代码的虚拟表.
具有多重继承和虚拟功能的对象的内存布局
- class X {
- public:
- int x;
- virtual ~X() {}
- virtual void printX() {}
- };
- class Y {
- public:
- int y;
- virtual ~Y() {}
- virtual void printY() {}
- };
- class Z : public X, public Y {
- public:
- int z;
- ~Z() {}
- void printX() {}
- void printY() {}
- void printZ() {}
- };
内存布局如下
- | |
- |------------------------------| <------ Z class object memory layout
- stack | int X::x |
- | |------------------------------| |--------------------------|
- | | X:: _vptr |----------------->| type_info Z |
- | |------------------------------| |--------------------------|
- \|/ | int Y::y | | address of Z::~Z() |
- |------------------------------| |--------------------------|
- | Y:: _vptr |------| | address of Z::printX() |
- |------------------------------| | |--------------------------|
- | int Z::z | | |--------GUARD_AREA--------|
- |------------------------------| | |--------------------------|
- | o | |---------->| type_info Z |
- | o | |--------------------------|
- | o | | address of Z::~Z() |
- | | |--------------------------|
- ------|------------------------------|--------- | address of Z::printY() |
- | X::~X() | | |--------------------------|
- |------------------------------| |
- | X::printX() | |
- |------------------------------| |
- | Y::~Y() | \|/
- |------------------------------| text segment
- | Y::printY() |
- |------------------------------|
- | Z::~Z() |
- |------------------------------|
- | Z::printX() |
- |------------------------------|
- | Z::printY() |
- |------------------------------|
- | Z::printZ() |
- |------------------------------|
- | o |
- | o |
- | |
在多继承层次结构中, 创建的虚拟表指针 (vptr) 的确切数目将为 N-1, 其中 N 代表类的数目.
如果尝试使用任何基类指针调用 Z 类的方法, 则它将使用相应的虚拟表进行调用. 如下例子所示:
- Y *y_ptr = new Z;
- y_ptr->printY(); // OK
- y_ptr->printZ(); // Not OK, as virtual table of class Y doesn't have address of printZ() method
在上面的代码中, y_ptr 将指向完整 Z 对象内类 Y 的子对象.
结果, 调用任何方法, 例如使用 y_ptr-> printY(). 使用 y_ptr 的解析方式如下:
( *y_ptr->_vtbl[ 2 ] )( y_ptr )
虚继承内存布局
- class X {
- int x;
- };
- class Y : public virtual X {
- int y;
- };
- class Z : public virtual X {
- int z;
- };
- class A : public Y, public Z {
- int a;
- };
其布局如下:
- | |
- Y class ------> |----------------| <------ A class object memory layout
- sub-object | Y::y |
- |----------------| |------------------|
- | Y::_vptr_Y |------| | offset of X | // offset(20) starts from Y
- Z class ------> |----------------| |----> |------------------|
- sub-object | Z::z | | ..... |
- |----------------| |------------------|
- | Z::_vptr_Z |------|
- |----------------| |
- A sub-object --> | A::a | | |------------------|
- |----------------| | | offset of X | // offset(12) starts from Z
- X class -------> | X::x | |----> |------------------|
- shared |----------------| | ..... |
- sub-object | | |------------------|
具有一个或多个虚拟基类的派生类的内存表示形式分为两个区域: 不变区域和共享区域.
不变区域内的数据与对象的起始位置保持固定的偏移量, 而与后续派生无关.
共享区域包含虚拟基类, 并且随后续派生和派生顺序而波动.
总结
了解内存布局, 对我们的项目开发会提供很大的便利, 比如对 coredump 的调试.
来源: https://www.qcloud.com/developer/article/1810582