C++对象模型那点事儿(布局篇)

1 前言

在C++中类的数据成员有两种:static和nonstatic,类的函数成员由三种:static,nonstatic和virtual。上篇我们尽量说一些宏观上的东西,数据成员与函数成员在类中的布局将在微观篇中详细讨论。

每当我们声明一个类,定义一个对象,调用一个函数.....的时候,不知道你有没有一些疑惑--编译器私底下都干了些什么?普通函数,成员函数都是怎么调用的?static成员又是个什么玩意。如果你对这些东西也感兴趣,那么好,我们一起将class的底层翻个底朝天。修炼好底层的内功,我想对于上层的提供,帮助可不止一点点吧?

2 class整体布局

C语言中“数据“与函数式分开声明的,也就是说C语言并不支持”数据“与函数之间的关联性。

我们来看下面的例子。

[cpp] view
plain
copy

  1. typedef struct point3d{ //数据
  2. float x;
  3. float y;
  4. float z;
  5. }Point3d;
  6. void Point3d_print(const Point3d *pd{
  7. printf("%g,%g,%g",pd->x,pd->y,pd->z);
  8. }

我们再来看看C++中的做法。

[cpp] view
plain
copy

  1. class Point3d{
  2. float _x;
  3. float _y;
  4. float _z;
  5. public:
  6. void point3d_print(){
  7. printf("%g,%g,%g",_x,_y,_z);
  8. }
  9. };

在Point3d转换到C++之后,我们可能会问加上封装之后,成本会增加多少?

答案是class Point3d并没有增加成本。三个数据成员(_x,_y,_z)直接内含在每一个对象之中,而成员函数虽在类中声明,却不出现在对象之中。如下图所示:

凡事没有绝对,virtual看起来就会增加C++在布局及存取时间上的额外负担。稍后讨论。

好吧,我承认光说面上(宏观上)的东西东西大家都懂,而且底层的东西注定不会太宏观。那么下面我们举例子来证明上述的讨论。

需要说明的是以下是在vs2010下的运行结果,若是gcc,可能某些地方会有所差异。

[cpp] view
plain
copy

  1. class A{};         // sizeof(A) = 1  有木有很奇怪?稍后说明
  2. class B{int x;};   //  sizeof(B) = 4
  3. class C{
  4. int x;
  5. public:
  6. int get(){return x;}
  7. };                 //  sizeof(C) = 4; 是不是验证了我们上述的论述?

很奇怪sizeof(A) = 1而不是0吧?

事实上A并不是空的,他有一个隐藏的1byte大小,那是被编译器安插进去的一个char。这样做使得用同一个空类定义两个对象的时候得以在内存中配置独一无二的地址。

例如:

[cpp] view
plain
copy

  1. A a,b;
  2. if(&a == &b)cout<<"error"<<endl;

我们都知道在C语言中struct优化的时候会进行内存对齐,那么我们来看看class中有没有这个优化。

[cpp] view
plain
copy

  1. class A{
  2. char x;
  3. int y;
  4. char z;
  5. };  // sizeof(A) == 12;
  6. class B{
  7. char x;
  8. char y;
  9. int z;
  10. };  // sizeof(B) = 8;
  11. class C{
  12. int x;
  13. char y;
  14. char z;
  15. };  // sizeof(C) = 8;
  16. class D{
  17. long long x;
  18. char y;
  19. char z;
  20. };  //sizeof(D) = 16; 由于longlong为8字节大小,此处以8字节对齐

显然编译器进行类内存对齐的优化。

接着上文,我们知道stroustrup老大的设计(目前仍在使用)是:nonstatic data members 被置于每一个对象之中,static data member则被置于对象之外。static和nonstatic function members 则被放在对象之外。

[cpp] view
plain
copy

  1. class A{
  2. static int x;
  3. };  //sizeof(A) = 1;
  4. class B{
  5. int x;
  6. public:
  7. int get(){
  8. return x;
  9. }
  10. };  //sizeof(B) = 4
  11. class C{
  12. int x;
  13. public:
  14. virtual int get(){
  15. return x;
  16. }
  17. };  //sizeof(C) = 8;

显然验证了上述我所说的。

[cpp] view
plain
copy

  1. class A{
  2. void (*pf)(); //函数指针
  3. };   //sizeof(A) = 4;
  4. class B{
  5. int *p;   // 指针
  6. };   //sizeof(B) = 4;

所以含有虚函数的时候,object中会包含一个虚表指针。我们知道指针一边占用4个字节,上面的sizeof(C)就好解释了。

3 虚函数

我们都知道虚函数是下面这个样子。

[cpp] view
plain
copy

  1. class X{
  2. int a;
  3. int b;
  4. public:
  5. virtual void foo1(){cout<<"X::foo1"<<endl;}
  6. virtual void foo2(){cout<<"X::foo2"<<endl;}
  7. };

内存布局如下:

下面我们来证明这种布局。

[cpp] view
plain
copy

  1. #include<iostream>
  2. using namespace std;
  3. class X{
  4. int _a;
  5. int _b;
  6. public:
  7. virtual void foo1(){cout<<"X::foo1"<<endl;}
  8. virtual void foo2(){cout<<"X::foo2"<<endl;}
  9. };
  10. typedef void (*pf)();
  11. int main(){
  12. X a;
  13. int **tmp = (int **)&a;
  14. pf ptf;
  15. for(int i=0;i<2;i++){
  16. ptf = (pf)tmp[0][i];
  17. ptf();
  18. }
  19. }

运行结果如下图所示:

那么,我们继续往下看。

4 继承

当涉及到继承的时候,情况又会怎样呢?

[cpp] view
plain
copy

  1. class A{
  2. int x;
  3. };
  4. class B:public A{
  5. int y;
  6. };   //sizeof(B) = 8;

我们来看看涉及到继承的时候内存的布局情况。

我们继续,若基类中包含有虚函数,这时候又会如何呢?

[cpp] view
plain
copy

  1. class C{
  2. public:
  3. virtual void fooC(){
  4. cout<<"C::fooC()"<<endl;
  5. }
  6. };  //sizeof(C) = 4;
  7. class D:public C{
  8. int a;
  9. public:
  10. virtual void fooD(){
  11. cout<<"D::fooD()"<<endl;
  12. }
  13. };  //sizeof(D) = 8;

内存布局应该是这个样子:

下面我们来验证这种布局:

[cpp] view
plain
copy

  1. typedef void (*pf)();
  2. int main(){
  3. C a;
  4. D b;
  5. int **tmpc = (int **)&a;
  6. int **tmpb = (int **)&b;
  7. pf ptf;
  8. ptf = (pf)tmpc[0][0];
  9. ptf();
  10. ptf = (pf)tmpb[0][0];
  11. ptf();
  12. ptf = (pf)tmpb[0][1];
  13. ptf();
  14. }

运行结果:

显然上述的布局是对的。这个时候需要注意的是:C::fooC()在前,D::fooD()在后,若出现函数覆盖,则D中的函数会覆盖掉继承过来的同名函数,而对于没有覆盖的虚函数则追加在虚表的最后。

我们再来看看下面的涉及到虚函数的多重继承。

[cpp] view
plain
copy

  1. class A{
  2. int _a;
  3. public:
  4. virtual void fooA(){
  5. cout<<"A::fooA()"<<endl;
  6. }
  7. virtual void poo(){
  8. cout<<"A::poo()"<<endl;
  9. }
  10. };  //sizeof(A) = 8;
  11. class B{
  12. int _b;
  13. public:
  14. virtual void fooB(){
  15. cout<<"B::fooB()"<<endl;
  16. }
  17. virtual void poo(){
  18. cout<<"B::poo()"<<endl;
  19. }
  20. };  ////sizeof(B) = 8;
  21. class C:public A,public B{
  22. int _c;
  23. public:
  24. void poo(){
  25. cout<<"C::poo()"<<endl;
  26. }
  27. virtual void hoo(){
  28. cout<<"C::hoo()"<<endl;
  29. }
  30. };    //sizeof(C) = 20;

有了上面的布局信息,我们可以推测类C的布局如下:

下面我们来验证这种推测。

[cpp] view
plain
copy

  1. typedef void (*pf)();
  2. int main(){
  3. C a;
  4. int **tmp = (int **)&a;
  5. pf ptf;
  6. for(int i=0;i<3;++i){
  7. ptf = (pf)tmp[0][i];
  8. ptf();
  9. }
  10. cout<<"-----------"<<endl;
  11. int s = sizeof(A)/4; //指针与int都占用4字节大小
  12. for(int i=0;i<2;i++){
  13. ptf = (pf)tmp[2][i];
  14. ptf();
  15. }
  16. }

运行结果:

显然与我们的猜测一致。

最后,我们再来看看菱形继承的情况。

[cpp] view
plain
copy

  1. class A{
  2. int _a1;
  3. int _a2;
  4. };    //sizeof(A) = 8;
  5. class B:virtual public A{
  6. int b;
  7. };    //sizeof(B) = 16;
  8. class C:virtual public A{
  9. int c;
  10. };    //sizeof(C) = 16;
  11. class D:public B,public C{
  12. int d;
  13. };    //sizeof(D) = 28;

我们来看看这时候的内存布局:

我们来验证这种布局:

[cpp] view
plain
copy

  1. int main(){
  2. D d;
  3. A *pta = &d;
  4. B *ptb = &d;
  5. C *ptc = &d;
  6. cout<<"D:  "<<&d<<endl;
  7. cout<<"B:  "<<ptb<<"   C:  "<<ptc<<endl;
  8. cout<<"A:  "<<pta<<endl;
  9. }

你在尝试的时候地址可能会有所差异,但是偏移量应该会保持一致。至于不同的编译器是否布局都一样,我也不得而知。至于那两个虚指针所指虚表提供的也就是虚基类的成员偏移量信息,大家如果感兴趣,可以自己验证。

至此,宏观布局部分大致说完,欲知后事如何请转至“成员篇”。

时间: 2024-10-12 20:11:45

C++对象模型那点事儿(布局篇)的相关文章

C++对象模型那点事儿(成员篇)

1 前言 上篇提到了类的数据成员有两种:static和nonstatic.类中的函数成员有三种:static,nonstatic和virtual.不知道大家有没有想过类到底是怎封装数据的?为什么只能通过对象或者成员函数来访问?static数据既然不单独属于某个对象,外界可否访问?类的函数成员不存在于单个对象中,为何外界又不能访问这些函数成员?这些都是怎么做到的? 让我们带着这些问题开始这一章的阅读. 2 数据成员 我们先来看一个例子: class Point3d{ public: //.....

【转载】图说C++对象模型:对象内存布局详解

原文: 图说C++对象模型:对象内存布局详解 正文 回到顶部 0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看.本文的结论都在VS2013上得到验证.不同的编译器在内存布局的细节上可能有所不同.文章如果有解释不清.解释不通或疏漏的地方,恳请指出. 回到顶部 1.何为C++对象模型? 引用<深度探索C++对象模型>这本书中的话: 有两个概念可以解释C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各种支持的底层

【黑金原创教程】【FPGA那些事儿-驱动篇I 】连载导读

前言: 无数昼夜的来回轮替以后,这本<驱动篇I>终于编辑完毕了,笔者真的感动到连鼻涕也流下来.所谓驱动就是认识硬件,还有前期建模.虽然<驱动篇I>的硬件都是我们熟悉的老友记,例如UART,VGA等,但是<驱动篇I>贵就贵在建模技巧的升华,亦即低级建模II. 话说低级建模II,读过<建模篇>的朋友多少也会面熟几分,因为它是低级建模的进化形态.许久以前,笔者就有点燃低级建模II的念头,但是懒惰的性格让笔者别扭许久.某天,老大忽然说道:"让咱们大干一场吧

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验六:数码管模块

实验六:数码管模块 有关数码管的驱动,想必读者已经学烂了 ... 不过,作为学习的新仪式,再烂的东西也要温故知新,不然学习就会不健全.黑金开发板上的数码管资源,由始至终都没有改变过,笔者因此由身怀念.为了点亮多位数码管从而显示数字,一般都会采用动态扫描,然而有关动态扫描的信息请怒笔者不再重复.在此,同样也是动态扫描,但我们却用不同的思路去理解. 图6.1 6位数码管. 如图6.1所示,哪里有一排6位数码管,其中包好8位DIG信号还有6位SEL信号.DIG为digit,即俗称的数码管码,如果数码管

CSS布局篇——固宽、变宽、固宽+变宽

学了前端挺久了,最近写一个项目测试系统,布局时发现自己对变宽+固宽的布局还没有完全掌握,所以在这里总结一下,以后需要的时候回头看看. 1.最简单的当然是一列或多列固宽 例如两列固宽: 1.设置一个外围div--container,是两列显示的总大小,可以是固定800px等值. 2.在container内分别设置两个div--side.content 根据需要设置像素,比如一个是300px,另一个是500px: 分别设置float:left; 2.两列变宽,同1,只不过将像素改为百分比,例如是一个

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验三:按键模块② — 点击与长点击

实验三:按键模块② - 点击与长点击 实验二我们学过按键功能模块的基础内容,其中我们知道按键功能模块有如下操作: l 电平变化检测: l 过滤抖动: l 产生有效按键. 实验三我们也会z执行同样的事情,不过却是产生不一样的有效按键: l 按下有效(点击): l 长按下有效(长点击). 图3.1 按下有效,时序示意图. 图3.2 长按下有效,时序示意图. 如图3.1所示,按下有效既是"点击",当按键被按下并且消抖完毕以后,isSClick信号就有被拉高一个时钟(Short Click).

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二:按键模块① - 消抖

实验二:按键模块① - 消抖 按键消抖实验可谓是经典中的经典,按键消抖实验虽曾在<建模篇>出现过,而且还惹来一堆麻烦.事实上,笔者这是在刁难各位同学,好让对方的惯性思维短路一下,但是惨遭口水攻击 ... 面对它,笔者宛如被甩的男人,对它又爱又恨.不管怎么样,如今 I'll be back,笔者再也不会重复一样的悲剧. 按键消抖说傻不傻说难不难.所谓傻,它因为原理不仅简单(就是延迟几下下而已),而且顺序语言(C语言)也有无数不尽的例子.所谓难,那是因为人们很难从单片机的思维跳出来 ... 此外,

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验四:按键模块③ — 单击与双击

实验四:按键模块③ — 单击与双击 实验三我们创建了“点击”还有“长点击”等有效按键的多功能按键模块.在此,实验四同样也是创建多功能按键模块,不过却有不同的有效按键.实验四的按键功能模块有以下两项有效按键: l 单击(按下有效): l 双击(连续按下两下有效). 图4.1 单击有效按键,时序示意图. 实验四的“单击”基本上与实验三的“点击”一模一样,既按键被按下,经过消抖以后isSClick信号被拉高一个时钟,结果如图4.1所示,过程非常单调.反之,“双击”实现起来,会比较麻烦一些,因为我们还要

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验五:按键模块④ — 点击,长点击,双击

实验五:按键模块④ — 点击,长点击,双击 实验二至实验四,我们一共完成如下有效按键: l 点击(按下有效) l 点击(释放有效) l 长击(长按下有效) l 双击(连续按下有效) 然而,不管哪个实验都是只有两项“功能”的按键模块而已,如今我们要创建三项“功能”的按键模块,亦即点击(按下有效),长击,还有双击.实验继续之前,让我们先来复习一下各种有效按键. 图5.1 点击(按下有效). 如图5.1所示,所谓点击(按下有效)就是按键按下以后,isSClick信号(Single Click) 产生一

[Android 性能优化系列]布局篇之减少你的界面层级

大家如果喜欢我的博客,请关注一下我的微博,请点击这里(http://weibo.com/kifile),谢谢 转载请标明出处(http://blog.csdn.net/kifile),再次感谢 原文地址:http://developer.android.com/training/improving-layouts/optimizing-layout.html 在接下来的一段时间里,我会每天翻译一部分关于性能提升的Android官方文档给大家 性能优化之布局篇: [Android 性能优化系列]布