1 前言

在C++中类的数据成员有两种:static和nonstatic。类的函数成员由三种:static,nonstatic和virtual。

上篇我们尽量说一些宏观上的东西,数据成员与函数成员在类中的布局将在微观篇中具体讨论。

每当我们声明一个类。定义一个对象。调用一个函数.....的时候,不知道你有没有一些疑惑--编译器私底下都干了些什么?普通函数。成员函数都是怎么调用的?static成员又是个什么玩意。

假设你对这些东西也感兴趣,那么好,我们一起将class的底层翻个底朝天。修炼好底层的内功,我想对于上层的提供。帮助可不止一点点吧?

2 class总体布局

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

我们来看以下的样例。
typedef struct point3d{ //数据
float x;
float y;
float z;
}Point3d;
void Point3d_print(const Point3d *pd{
printf("%g,%g,%g",pd->x,pd->y,pd->z);
}

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

class Point3d{
float _x;
float _y;
float _z;
public:
void point3d_print(){
printf("%g,%g,%g",_x,_y,_z);
}
};

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

答案是class Point3d并没有添加成本。三个数据成员(_x,_y,_z)直接内含在每个对象之中。而成员函数虽在类中声明,却不出如今对象之中。例如以下图所看到的:
凡事没有绝对,virtual看起来就会添加C++在布局及存取时间上的额外负担。稍后讨论。

好吧。我承认光说面上(宏观上)的东西东西大家都懂。并且底层的东西注定不会太宏观。

那么以下我们举样例来证明上述的讨论。

须要说明的是以下是在vs2010下的执行结果。若是gcc,可能某些地方会有所差异。
class A{};         // sizeof(A) = 1  有木有非常奇怪?稍后说明
class B{int x;}; // sizeof(B) = 4
class C{
int x;
public:
int get(){return x;}
}; // sizeof(C) = 4; 是不是验证了我们上述的论述?
非常奇怪sizeof(A) = 1而不是0吧?
其实A并非空的,他有一个隐藏的1byte大小,那是被编译器安插进去的一个char。

这样做使得用同一个空类定义两个对象的时候得以在内存中配置独一无二的地址。

比如:
A a,b;
if(&a == &b)cout<<"error"<<endl;

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

class A{
char x;
int y;
char z;
}; // sizeof(A) == 12;
class B{
char x;
char y;
int z;
}; // sizeof(B) = 8;
class C{
int x;
char y;
char z;
}; // sizeof(C) = 8;
class D{
long long x;
char y;
char z;
}; //sizeof(D) = 16; 因为longlong为8字节大小,此处以8字节对齐

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

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

static和nonstatic function members 则被放在对象之外。

class A{
static int x;
}; //sizeof(A) = 1;
class B{
int x;
public:
int get(){
return x;
}
}; //sizeof(B) = 4
class C{
int x;
public:
virtual int get(){
return x;
}
}; //sizeof(C) = 8;

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

class A{
void (*pf)(); //函数指针
}; //sizeof(A) = 4;
class B{
int *p; // 指针
}; //sizeof(B) = 4;

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

3 虚函数

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

class X{
int a;
int b;
public:
virtual void foo1(){cout<<"X::foo1"<<endl;}
virtual void foo2(){cout<<"X::foo2"<<endl;}
};

内存布局例如以下:

以下我们来证明这样的布局。
#include<iostream>
using namespace std;
class X{
int _a;
int _b;
public:
virtual void foo1(){cout<<"X::foo1"<<endl;}
virtual void foo2(){cout<<"X::foo2"<<endl;}
};
typedef void (*pf)();
int main(){
X a;
int **tmp = (int **)&a;
pf ptf;
for(int i=0;i<2;i++){
ptf = (pf)tmp[0][i];
ptf();
}
}

执行结果例如以下图所看到的:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYmxvb2RfZmxvd2luZw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

那么,我们继续往下看。

4 继承

当涉及到继承的时候。情况又会怎样呢?
class A{
int x;
};
class B:public A{
int y;
}; //sizeof(B) = 8;

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

我们继续,若基类中包括有虚函数,这时候又会怎样呢?
class C{
public:
virtual void fooC(){
cout<<"C::fooC()"<<endl;
}
}; //sizeof(C) = 4;
class D:public C{
int a;
public:
virtual void fooD(){
cout<<"D::fooD()"<<endl;
}
}; //sizeof(D) = 8;

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYmxvb2RfZmxvd2luZw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

以下我们来验证这样的布局:
typedef void (*pf)();
int main(){
C a;
D b;
int **tmpc = (int **)&a;
int **tmpb = (int **)&b;
pf ptf;
ptf = (pf)tmpc[0][0];
ptf();
ptf = (pf)tmpb[0][0];
ptf();
ptf = (pf)tmpb[0][1];
ptf();
}

执行结果:

显然上述的布局是对的。这个时候须要注意的是:C::fooC()在前,D::fooD()在后,若出现函数覆盖,则D中的函数会覆盖掉继承过来的同名函数,而对于没有覆盖的虚函数则追加在虚表的最后。
我们再来看看以下的涉及到虚函数的多重继承。
class A{
int _a;
public:
virtual void fooA(){
cout<<"C::fooA()"<<endl;
}
virtual void poo(){
cout<<"A::poo()"<<endl;
}
}; //sizeof(A) = 8;
class B{
int _b;
public:
virtual void fooB(){
cout<<"C::fooB()"<<endl;
}
virtual void poo(){
cout<<"B::poo()"<<endl;
}
}; ////sizeof(B) = 8;
class C:public A,public B{
int _c;
public:
void poo(){
cout<<"C::poo()"<<endl;
}
virtual void hoo(){
cout<<"C::hoo()"<<endl;
}
}; //sizeof(C) = 20;

有了上面的布局信息,我们能够猜測类C的布局例如以下:
以下我们来验证这样的猜測。
typedef void (*pf)();
int main(){
C a;
int **tmp = (int **)&a;
pf ptf;
for(int i=0;i<3;++i){
ptf = (pf)tmp[0][i];
ptf();
}
cout<<"-----------"<<endl;
int s = sizeof(A)/4; //指针与int都占用4字节大小
for(int i=0;i<2;i++){
ptf = (pf)tmp[2][i];
ptf();
}
}

执行结果:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYmxvb2RfZmxvd2luZw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

显然与我们的猜測一致。
最后。我们再来看看菱形继承的情况。
class A{
int _a1;
int _a2;
}; //sizeof(A) = 8;
class B:virtual public A{
int b;
}; //sizeof(B) = 16;
class C:virtual public A{
int c;
}; //sizeof(C) = 16;
class D:public B,public C{
int d;
}; //sizeof(D) = 28;

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

我们来验证这样的布局:
int main(){
D d;
A *pta = &d;
B *ptb = &d;
C *ptc = &d;
cout<<"D: "<<&d<<endl;
cout<<"B: "<<ptb<<" C: "<<ptc<<endl;
cout<<"A: "<<pta<<endl;
}

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYmxvb2RfZmxvd2luZw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" />

你在尝试的时候地址可能会有所差异。可是偏移量应该会保持一致。至于不同的编译器是否布局都一样,我也不得而知。至于那两个虚指针所指虚表提供的也就是虚基类的成员偏移量信息。大家假设感兴趣,能够自己验证。
至此,宏观布局部分大致说完,欲知后事怎样请转至“成员篇”。

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

  1. C++对象模型的那些事儿之三:默认构造函数

    前言 继前两篇总结了C++对象模型及其内存布局后,我们继续来探索一下C++对象的默认构造函数.对于C++的初学者来说,有如下两个误解: 任何class如果没有定义default constructor ...

  2. C++对象模型的那些事儿之二:对象模型(下)

    前言 上一篇博客C++对象模型的那些事儿之一为大家讲解了C++对象模型的一些基本知识,可是C++的继承,多态这些特性如何体现在对象模型上呢?单继承.多重继承和虚继承后内存布局上又有哪些变化呢?多态真正 ...

  3. C++对象模型的那些事儿之五:NRV优化和初始化列表

    前言 在C++对象模型的那些事儿之四:拷贝构造函数中提到如果将一个对象作为函数参数或者返回值的时候,会调用拷贝构造函数,编译器是如何处理这些步骤,又会对其做哪些优化呢?本篇博客就为他家介绍一个编译器的 ...

  4. C++对象模型的那些事儿之四:拷贝构造函数

    前言 对于一个没有实例化的空类,编译器不会给它默认生成任何函数,当实例化一个空类后,编译器会根据需要生成相应的函数.这类函数包括一下几个: 构造函数 拷贝构造函数 析构函数 赋值运算符 在上一篇博文C ...

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

    原文: 图说C++对象模型:对象内存布局详解 正文 回到顶部 0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看.本文的结论都在 ...

  6. react-native 之布局篇

    一.宽度单位和像素密度 react的宽度不支持百分比,设置宽度时不需要带单位,那么默认的单位是什么呢? /** * Sample React Native App * https://github.c ...

  7. 深入css布局篇(3)完结 — margin问题与格式化上下文

    深入css布局(3) - margin问题与格式化上下文      在css知识体系中,除了css选择器,样式属性等基础知识外,css布局相关的知识才是css比较核心和重要的点.今天我们来深入学习一下 ...

  8. 深入css布局篇(2) — 定位与浮动

    深入css布局(2) - 定位与浮动      在css知识体系中,除了css选择器,样式属性等基础知识外,css布局相关的知识才是css比较核心和重要的点.今天我们来深入学习一下css布局相关的知识 ...

  9. Angular Material 教程之布局篇

    Angular Material 教程之布局篇 (一) : 布局简介https://segmentfault.com/a/1190000007215707 Angular Material 教程之布局 ...

  10. 【WPF】 布局篇

    [WPF] 布局篇 一. 几个常用且至关重要的属性 1. Width,Height : 设置窗体,控件宽高. 这里注意,WPF是自适应的, 所以把这2个属性设置 Auto, 则控件宽高会自动改变. 2 ...

随机推荐

  1. VM虚拟机不能上网的问题解决

    VM虚拟机不能上网的问题解决 说在前面的话:很多网友看了我的文章后,虚拟机还是不能上网,就联系我帮忙,结果帮他们给弄好后,都说怪自己太粗心,没有仔细看文章.我不是怕网友麻烦我,我是真诚的希望各位要首先 ...

  2. Intellij IDEA常用快捷键整理

    ps:使用Intellij IDEA编辑器有蛮长一段时间了.哈哈.整合一下常用的快捷.避免自己以后还要再找. 顺序大概就是从上到下的 Alt + ENTER : 自动修正,提示Alt + Insert ...

  3. STM32F10x_RTC日历

    Ⅰ.概述 接着上一篇文章来讲述关于RTC的计数功能,我们以实例RTC日历(读写年.月.日.星期.时.分.秒)来讲述该章节. STM32F1系列芯片的RTC功能和其他系列(F0.F2.F4等)相比来说, ...

  4. 解决linux下/etc/rc.local开机器不执行的原因

    前不久因项目需要写了开机启动其他程序的shell脚本,因工作忙,调试完给了技术支持人员,也没去注意过. 到后来,有几台服务器突然被重启了,这时候领导问,怎么开机启动的脚本没起作用,还被批了一顿,哎,做 ...

  5. head管理EC下载,配置启动

    参考文档:https://blog.csdn.net/yx1214442120/article/details/55102298

  6. 命令行执行php

    D:\software\phpStudy\php55

  7. 第二百一十四节,jQuery EasyUI,Calendar(日历)组件

    jQuery EasyUI,Calendar(日历)组件 学习要点: 1.加载方式 2.属性列表 3.事件列表 4.方法列表 本节课重点了解 EasyUI 中 Canlendar(日历)组件的使用方法 ...

  8. 请写出一段JavaScript代码,要求页面有一个按钮,点击按钮弹出确认框。程序可以判断出用

    请写出一段JavaScript代码,要求页面有一个按钮,点击按钮弹出确认框.程序可以判断出用 户点击的是“确认”还是“取消”. 解答: <HTML> <HEAD> <TI ...

  9. vs的快捷键包含部分代码的自动生成

    VS2010 快捷键 全屏:Shift+Alt+Enter注释选定内容:Ctrl+E+C/Crtr+E+U代码格式化:ctrl+E+F VS2008 使用小技巧——快捷键1. 怎样调整代码排版的格式? ...

  10. .NET开发笔记--对config文件的操作(3)

    1.添加新节点前进行判断看是否已存在相同的属性值,若存在进行更新,不存在则进行添加操作. protected bool AddPizza() { //初始化id int newId; string f ...