c++对象内存布局
了解你所使用的编程语言究竟是如何实现的,对于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的调试。