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

本文深入探讨了C++中类的底层布局,包括数据成员、函数成员、静态成员和虚函数的布局方式,以及类继承、虚函数在内存中的布局特点。通过实例解析,清晰展示了类内存布局的细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 前言


在C++中类的数据成员有两种:static和nonstatic,类的函数成员由三种:static,nonstatic和virtual。上篇我们尽量说一些宏观上的东西,数据成员与函数成员在类中的布局将在微观篇中详细讨论。
每当我们声明一个类,定义一个对象,调用一个函数.....的时候,不知道你有没有一些疑惑--编译器私底下都干了些什么?普通函数,成员函数都是怎么调用的?static成员又是个什么玩意。如果你对这些东西也感兴趣,那么好,我们一起将class的底层翻个底朝天。修炼好底层的内功,我想对于上层的提供,帮助可不止一点点吧?

2 class整体布局


C语言中“数据“与函数式分开声明的,也就是说C语言并不支持”数据“与函数之间的关联性。
我们来看下面的例子。
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  1. A a,b;  
  2. if(&a == &b)cout<<"error"<<endl;  

我们都知道在C语言中struct优化的时候会进行内存对齐,那么我们来看看class中有没有这个优化。
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  1. class A{  
  2.       int x;  
  3. };  
  4. class B:public A{  
  5.       int y;  
  6. };   //sizeof(B) = 8;  
我们来看看涉及到继承的时候内存的布局情况。

我们继续,若基类中包含有虚函数,这时候又会如何呢?
[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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 在CODE上查看代码片 派生到我的代码片
  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. }  


你在尝试的时候地址可能会有所差异,但是偏移量应该会保持一致。至于不同的编译器是否布局都一样,我也不得而知。至于那两个虚指针所指虚表提供的也就是虚基类的成员偏移量信息,大家如果感兴趣,可以自己验证。
至此,宏观布局部分大致说完,欲知后事如何请转至“成员篇”。
资源下载链接为: https://pan.quark.cn/s/1bfadf00ae14 “STC单片机电压测量”是一个以STC系列单片机为基础的电压检测应用案例,它涵盖了硬件电路设计、软件编程以及数据处理等核心知识。STC单片机凭借其低功耗、高性价比和丰富的I/O接口,在电子工程领域得到了广泛应用。 STC是Specialized Technology Corporation的缩写,该公司的单片机基于8051内核,具备内部振荡器、高速运算能力、ISP(在系统编程)和IAP(在应用编程)功能,非常适合用于各种嵌入式控制系统。 在源代码方面,“浅雪”风格的代码通常简洁易懂,非常适合初学者学习。其中,“main.c”文件是程序的入口,包含了电压测量的核心逻辑;“STARTUP.A51”是启动代码,负责初始化单片机的硬件环境;“电压测量_uvopt.bak”和“电压测量_uvproj.bak”可能是Keil编译器的配置文件备份,用于设置编译选项和项目配置。 对于3S锂电池电压测量,3S锂电池由三节锂离子电池串联而成,标称电压为11.1V。测量时需要考虑电池的串联特性,通过分压电路将高电压转换为单片机可接受的范围,并实时监控,防止过充或过放,以确保电池的安全和寿命。 在电压测量电路设计中,“电压测量.lnp”文件可能包含电路布局信息,而“.hex”文件是编译后的机器码,用于烧录到单片机中。电路中通常会使用ADC(模拟数字转换器)将模拟电压信号转换为数字信号供单片机处理。 在软件编程方面,“StringData.h”文件可能包含程序中使用的字符串常量和数据结构定义。处理电压数据时,可能涉及浮数运算,需要了解STC单片机对浮数的支持情况,以及如何高效地存储和显示电压值。 用户界面方面,“电压测量.uvgui.kidd”可能是用户界面的配置文件,用于显示测量结果。在嵌入式系统中,用
资源下载链接为: https://pan.quark.cn/s/abbae039bf2a 在 Android 开发中,Fragment 是界面的一个模块化组件,可用于在 Activity 中灵活地添加、删除或替换。将 ListView 集成到 Fragment 中,能够实现数据的动态加载与列表形式展示,对于构建复杂且交互丰富的界面非常有帮助。本文将详细介绍如何在 Fragment 中使用 ListView。 首先,需要在 Fragment 的布局文件中添加 ListView 的 XML 定义。一个基本的 ListView 元素代码如下: 接着,创建适配器来填充 ListView 的数据。通常会使用 BaseAdapter 的子类,如 ArrayAdapter 或自定义适配器。例如,创建一个简单的 MyListAdapter,继承自 ArrayAdapter,并在构造函数中传入数据集: 在 Fragment 的 onCreateView 或 onActivityCreated 方法中,实例化 ListView 和适配器,并将适配器设置到 ListView 上: 为了提升用户体验,可以为 ListView 设置击事件监听器: 性能优化也是关键。设置 ListView 的 android:cacheColorHint 属性可提升滚动流畅度。在 getView 方法中复用 convertView,可减少视图创建,提升性能。对于复杂需求,如异步加载数据,可使用 LoaderManager 和 CursorLoader,这能更好地管理数据加载,避免内存泄漏,支持数据变更时自动刷新。 总结来说,Fragment 中的 ListView 使用涉及布局设计、适配器创建与定制、数据绑定及事件监听。掌握这些步骤,可构建功能强大的应用。实际开发中,还需优化 ListView 性能,确保应用流畅运
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值