经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
c++对象内存布局示例详解
来源:jb51  时间:2021/10/25 9:06:18  对本文有异议

前言

了解你所使用的编程语言究竟是如何实现的,对于C++程序员可能特别有意义。首先,它可以去除我们对于所使用语言的神秘感,使我们不至于对于编译器干的活感到完全不可思议;尤其重要的是,它使我们在Debug和使用语言高级特性的时候,有更多的把握。当需要提高代码效率的时候,这些知识也能够很好地帮助我们。

简单非多态的内存布局

  1. class X {
  2. int x;
  3. float xx;
  4.  
  5. public:
  6. X() {}
  7. ~X() {}
  8.  
  9. void printInt() {}
  10. void printFloat() {}
  11. };
  12.  
  1. | |
  2. |------------------------| <------ X class object memory layout
  3. | int X::x |
  4. |------------------------| stack segment
  5. | float X::xx | |
  6. |------------------------| |
  7. | | \|/
  8. | |
  9. | |
  10. ------|------------------------|----------------
  11. | X::X() |
  12. |------------------------| |
  13. | X::~X() | |
  14. |------------------------| \|/
  15. | X::printInt() | text segment
  16. |------------------------|
  17. | X::printFloat() |
  18. |------------------------|
  19. | |

在本示例中

  • 只有数据成员存储在堆栈中,且其声明顺序或者存储顺序的行为与编译器强相关
  • 所有其他方法(构造函数,析构函数和编译器扩展代码)都进存储在文本段。然后,这些方法将被调用并隐式地在调用对象的第一个参数中传递该指针。

this指针是一个隐含于每一个成员函数中的特殊指针。它是一个指向正在被该成员函数操作的对象,也就是要操作该成员函数的对象。this作用域是在类内部,当对一个对象调用成员函数时,编译程序先将对象的地址赋给this指针,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参。被调用的成员函数函数体内所有对类成员的访问,都会被转化为“this->类成员”的方式。

针对第二点,我们类似于:

  1. A x;
  2. x.printInt();

其中,X::printInt()这个行为,在编译器中,将处理为

  1. printInt(const X* this)

那么,x.printInt()调用处理将最终成为

  1. printInt(&x);

同时具有虚函数和静态数据成员的内存布局

  1. class X {
  2. int x;
  3. float xx;
  4. static int count;
  5.  
  6. public:
  7. X() {}
  8. virtual ~X() {}
  9.  
  10. virtual void printAll() {}
  11. void printInt() {}
  12. void printFloat() {}
  13. static void printCount() {}
  14. };
  15.  

其内存布局如下

  1. | |
  2. |------------------------| <------ X class object memory layout
  3. | int X::x |
  4. stack |------------------------|
  5. | | float X::xx |
  6. | |------------------------| |-------|--------------------------|
  7. | | X::_vptr |------| | type_info X |
  8. \|/ |------------------------| |--------------------------|
  9. | o | | address of X::~X() |
  10. | o | |--------------------------|
  11. | o | | address of X::printAll() |
  12. | | |--------------------------|
  13. | |
  14. ------|------------------------|------------
  15. | static int X::count | /| |------------------------| |
  16. | o | data segment
  17. | o | |
  18. | | \|/
  19. ------|------------------------|------------
  20. | X::X() |
  21. |------------------------| |
  22. | X::~X() | |
  23. |------------------------| |
  24. | X::printAll() | \|/
  25. |------------------------| text segment
  26. | X::printInt() |
  27. |------------------------|
  28. | X::printFloat() |
  29. |------------------------|
  30. | static X::printCount() |
  31. |------------------------|
  32. | |
  • 所有非静态数据成员都按照声明的顺序将空间放入堆栈中,与前面的示例顺序相同。
  • 静态数据成员将空间放入内存的数据段中。使用范围解析运算符(即::)进行的访问。但是在编译之后,就没有像作用域和名称空间那样的东西了。因为,它的名称只是由编译器执行,所以所有内容都由其绝对或相对地址引用。
  • 静态数据成员将空间放入内存的数据段中。使用范围解析运算符(即::)进行的访问。
  • 静态方法进入文本段,并通过作用域解析运算符进行调用。
  • 对于virtual关键字,编译器会自动将指向虚拟表的指针(vptr)插入对象内存表示中。通常,虚拟表是在数据段中为每个类静态创建的,但它也取决于编译器的实现。
  • 在虚拟表中,第一个条目指向type_info对象,该对象包含与当前基类和其他基类的DAG(有向无环图)相关的信息(如果从这些基类派生的信息)。

继承对象的内存布局

  1. class X {
  2. int x;
  3. string str;
  4.  
  5. public:
  6. X() {}
  7. virtual ~X() {}
  8.  
  9. virtual void printAll() {}
  10. };
  11.  
  12. class Y : public X {
  13. int y;
  14.  
  15. public:
  16. Y() {}
  17. ~Y() {}
  18.  
  19. void printAll() {}
  20. };
  21.  

其内存布局信息如下

  1. | |
  2. |------------------------------| <------ Y class object memory layout
  3. | int X::x |
  4. stack |------------------------------|
  5. | | int string::len |
  6. | |string X::str ----------------|
  7. | | char* string::str |
  8. \|/ |------------------------------| |-------|--------------------------|
  9. | X::_vptr |------| | type_info Y |
  10. |------------------------------| |--------------------------|
  11. | int Y::y | | address of Y::~Y() |
  12. |------------------------------| |--------------------------|
  13. | o | | address of Y::printAll() |
  14. | o | |--------------------------|
  15. | o |
  16. ------|------------------------------|--------
  17. | X::X() |
  18. |------------------------------| |
  19. | X::~X() | |
  20. |------------------------------| |
  21. | X::printAll() | \|/
  22. |------------------------------| text segment
  23. | Y::Y() |
  24. |------------------------------|
  25. | Y::~Y() |
  26. |------------------------------|
  27. | Y::printAll() |
  28. |------------------------------|
  29. | string::string() |
  30. |------------------------------|
  31. | string::~string() |
  32. |------------------------------|
  33. | string::length() |
  34. |------------------------------|
  35. | o |
  36. | o |
  37. | o |
  38. | |
  • 在继承模型中,基类和数据成员类是派生类的子对象。
  • 编译器会在类的构造函数中生成具有所有重写的虚拟功能和为_vptr分配虚拟表的代码的虚拟表。

具有多重继承和虚拟功能的对象的内存布局

  1. class X {
  2. public:
  3. int x;
  4. virtual ~X() {}
  5. virtual void printX() {}
  6. };
  7.  
  8. class Y {
  9. public:
  10. int y;
  11. virtual ~Y() {}
  12. virtual void printY() {}
  13. };
  14.  
  15. class Z : public X, public Y {
  16. public:
  17. int z;
  18. ~Z() {}
  19. void printX() {}
  20. void printY() {}
  21. void printZ() {}
  22. };
  23.  

内存布局如下

  1. | |
  2. |------------------------------| <------ Z class object memory layout
  3. stack | int X::x |
  4. | |------------------------------| |--------------------------|
  5. | | X:: _vptr |----------------->| type_info Z |
  6. | |------------------------------| |--------------------------|
  7. \|/ | int Y::y | | address of Z::~Z() |
  8. |------------------------------| |--------------------------|
  9. | Y:: _vptr |------| | address of Z::printX() |
  10. |------------------------------| | |--------------------------|
  11. | int Z::z | | |--------GUARD_AREA--------|
  12. |------------------------------| | |--------------------------|
  13. | o | |---------->| type_info Z |
  14. | o | |--------------------------|
  15. | o | | address of Z::~Z() |
  16. | | |--------------------------|
  17. ------|------------------------------|--------- | address of Z::printY() |
  18. | X::~X() | | |--------------------------|
  19. |------------------------------| |
  20. | X::printX() | |
  21. |------------------------------| |
  22. | Y::~Y() | \|/
  23. |------------------------------| text segment
  24. | Y::printY() |
  25. |------------------------------|
  26. | Z::~Z() |
  27. |------------------------------|
  28. | Z::printX() |
  29. |------------------------------|
  30. | Z::printY() |
  31. |------------------------------|
  32. | Z::printZ() |
  33. |------------------------------|
  34. | o |
  35. | o |
  36. | |

在多继承层次结构中,创建的虚拟表指针(vptr)的确切数目将为N-1,其中N代表类的数目。

如果尝试使用任何基类指针调用Z类的方法,则它将使用相应的虚拟表进行调用。如下例子所示:

  1. Y *y_ptr = new Z;
  2. y_ptr->printY(); // OK
  3. 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的解析方式如下:

  1. ( *y_ptr->_vtbl[ 2 ] )( y_ptr )

虚继承内存布局

  1. class X { int x; };
  2. class Y : public virtual X { int y; };
  3. class Z : public virtual X { int z; };
  4. class A : public Y, public Z { int a; };

其布局如下:

  1. | |
  2. Y class ------> |----------------| <------ A class object memory layout
  3. sub-object | Y::y |
  4. |----------------| |------------------|
  5. | Y::_vptr_Y |------| | offset of X | // offset(20) starts from Y
  6. Z class ------> |----------------| |----> |------------------|
  7. sub-object | Z::z | | ..... |
  8. |----------------| |------------------|
  9. | Z::_vptr_Z |------|
  10. |----------------| |
  11. A sub-object --> | A::a | | |------------------|
  12. |----------------| | | offset of X | // offset(12) starts from Z
  13. X class -------> | X::x | |----> |------------------|
  14. shared |----------------| | ..... |
  15. sub-object | | |------------------|
  • 具有一个或多个虚拟基类的派生类的内存表示形式分为两个区域:不变区域和共享区域。
  • 不变区域内的数据与对象的起始位置保持固定的偏移量,而与后续派生无关。
  • 共享区域包含虚拟基类,并且随后续派生和派生顺序而波动。

总结

了解内存布局,对我们的项目开发会提供很大的便利,比如对coredump的调试

到此这篇关于c++对象内存布局的文章就介绍到这了,更多相关c++对象内存布局内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号