浅谈C++结构体和类,对象的大小该如何计算?

转载自http://www.youranshare.com/blog/sid/92.html

C++中,结构体和类它们都是有构造函数、析构函数和成员函数的,他们两者的根本区别就是:结构体中访问控制默认是public的,而类中默认的访问控制是private的。对于C++中的结构体而言,publicprotectedprivate的访问都是在编译期进行检查的,当越权访问的时候,编译过程中会给出此类的错误并给与提示,在编译成功后,程序在执行的过程中不会有任何的检查和限制,这一点你可以通过类的指针偏移做一下测试。因此在反汇编中,C++中的结构体与类没有分别,两者的原理是相同的,只是类型名称不同。

说一下对象内存的布局

对于类和对象的关系想必你已经很了解了,类是一个抽象的概念,对象是一个实例化的具体存在的。这里我们以一个简单的类,谈一下类与对象的关系:

#include <iostream>
using namespace std;
  
class CNumber{
public:
      CNumber(){
           m_One = 1;
           m_Two = 2;
      }
      int GetNumberOne(){
           return m_One;
      }
      int GetNumberTwo(){
           return m_Two;
      }
private:
      int m_One;
      int m_Two;
};

int main()
{
      CNumber num;
      cout << num.GetNumberOne() << endl;
  
      //尝试着用指针访问
      cout << *(int*)(void*) &num << endl;
      cout << *((int*)(void*)&num + 1) << endl;
      return 0;
}

我们知道类的私有成员是不能被直接访问到的,但是我们在程序运行后通过指针的偏移还是依然可以读取内存的数据,就像结构体一样,如图所示我们的运行结果:

运行结果

可以发现,将对象的指针强制转换成我们需要的数据类型,然后通过指针在内存中的偏移可以访问到对象的私有成员!这也就说明了C++中类保护属性是编译的时候进行检测的!

C++中类的成员变量和结构体一样,也是按照顺序依次放到内存中的,先定义的数据成员放到地地址,后定义的数据成员放到高地址处,但是对象的大小只包含数据成员,类的成员函数属于执行代码,不属于类对象的数据。

你可以通过sizeof获取到CNumber对象的大小,它们的大小都是8Byte,这8Byte字节是由类中的两个数据成员主城,它们都是int类型,格子的长度都为4Byte。从内存的布局上来看,类与一个数组非常相似,都是由多个数据成员组成,但是类的能力要远远大过数组。类的成员变量类型定义非常广泛,除了本身对象之外,任何已知的数据类型都可以在内种作为成员变量进行定义。

为什么在类中不能定义自身的对象呢?因为类需要在申请内存的过程中计算出自身的实际大小,用于实例化对象。如果你在类中定义了自身的对象变量,那么在计算类中各个数据成员长度时,又会因为回到自身导致无限递归下去,而这个递归没有出口,所以不能在类中定义自身的对象成员变量。但是指针是可以的,因为指针的长度是确定的,也就是相当于一个常量值,因此定义自身的指针是不会影响到类本身大小的计算的。根据上面所说的东西,我们是否可以定义一个公式用于描述类的对象长度呢?也许你会认为下面的公式可以用于计算对象的长度:

对象长度= sizeof(成员1) + …+szieof(成员2)+…+sizeof(成员n)

这里我明确的告诉你,这个公式是错误的,对象大小的计算远远没有那么简单,即是我们抛开虚函数和继承的原因,仍然有三种情况能够推翻此公式:

  1. 空类

  2. 内存对齐问题

  3. 静态数据成员

当出现上面的情况时,类的对象长度的计算就需要小心了:

空类: 就是说类内部没有任何数据成员,但是实际上类对象的长度可是为1字节

内存对齐: 在VC++6.0中,类和结构体中的数据成员是根据它们在类或者结构体中出现的顺序来依次申请内存的,但是由于内存对齐的原因,它们并不会一定像数组那样内存是连续排列的,由于数据类型不同,因此占用的内存空间大小也会不同,在申请内存的时候会按照一定的规则.

我们以一个结构体为例子来看看这个结构体中成员变量地址的排列:

struct TagTest{
          char a;
          int b;
     };
结构大小

如图所示,可以看到a ,b的地址相差为4字节,这也就是说a,b这两个变量是没有连续的分配在内存中的,这中现象就是内存地址对齐导致的.

在为结构体和类中的数据成员分配内存的时候,结构体中的当前数据成员类型长度为M,指定对齐值为N(编译器指定的,例如为8Byte),那么实际上的对齐值q = Min(M,N),也就是说这个数据成员所在的地址必须为q的倍数,所以在上面的结构体中,数据成员b,它的对齐值为q=Min(4,8) 也就是4,所以b的地址必须为4的倍数,虽然前面的数据成员的地址a18086720b只是占用了1个字节,但是b的地址需要为4的倍数,那么就需要在数据成员a后面偏移3个字节的位置放置b,如图所示的ab的内存分布:

地址分布

下面我们来分析一下a,b的地址分配过程:

首先, 开始分配a的地址,a为一个char类型,它的对齐值M=1,在VC++6.0中编译器的默认对齐值为8,那么q = Min(1,8),也就是q=1,地址需要是1的倍数,所以直接分配地址就行了。例如分配了地址为18086720(十进制)

其次,开始分配b的地址,b是一个int类型对齐值为4Byte,那么q = Min(4,8),也就是q = 4,它所在的地址需要是4的倍数,如果此时我们直接将b分配在a的后面,那么b的地址将会是18086721,不满足对齐的地址,所以需要往后偏移3个字节,对应的地址就是18086724,这个地址满足对齐值4,所以系统给b分配的地址为18086724,分配完毕。

通过上面的分配我们可以看到,这个结构体一共占用了8Byte,而不是我们认为的5字节。另外值得说的是a后面的那3个字节里面可不是填充的0x00,一般系统都会填充0xCC

看完上面的东西,有些童鞋肯定感觉到了一个问题:“结构体本身也是一种数据类型,既然是数据类型,就像int一样,结构体本身也是有对齐的吧“.

答案是肯定的,数据类型都是有对齐值的,数据结构本身也是一种类型,与int基本类型无差别,当然也存在对齐值的问题,下面我给出一个数据结构:

struct STest{
    double da; // 8字节大小
    int    ib; // 4字节大小
    short  sc; // 2字节大小
};

这个数据结构STest的大小是16而不是14,为什么?

这是因为结构体本身也是一种数据类型,当然它也有对应的字节对齐处理,这里我们将会讨论一下对齐值对结构体整体大小的影响,如果按照VC++6.0默认的8字节对齐,那么对于一个结构体来说它的对齐值依然满足公式q=Min(M,N)VC++6.0N=8),但是需要注意的是这里的M应该是结构体中的数据成员类型的最大值,就像结构体STest,它的对齐值按照最大的数据成员double da,对齐值也就是 8Byte,这样一来可以计算出结构体STest的对齐值为8,所以编译器在STest的最后一个成员short sc后面有增加了2个字节用于填充结构体,使得整体大小为16字节,这样就满足了对齐的要求。

通过上面的介绍可以看出,结构体的对齐值是根据结构体内部最大的成员长度动态调整的,可不是固定的8字节,也可以是4字节。

C++中,虽然存在默认的对齐值,但是这个默认值也是可以修改的,我们可以使用预编译指令 #pragma pack(N)来指定对齐大小,例如我们下面的代码,将对齐值设置为1字节:

#pragma pack(1)
struct STest{
    double da; // 8字节大小
    int    ib; // 4字节大小
    short  sc; // 2字节大小
};

运行结果如下图所示,这里的sizeof计算出的结构就是 14字节了:

img

经过预编译指令后,将对齐值调整为1字节,根据对齐规则,q=Min(4,1)得出对齐值为1字节,既然是1字节,辣么就不用想了,直接就是14字节的大小了。

但是要注意的是,你使用#pragma pack(N)设置的对齐值可不一定会生效的,这是因为q=Min(M,N),要知道你的N要是太大的话~~q最终还是等于M的,就像你设置对齐值为128,根本没啥用嘛= =,对齐值的计算流程总的来说:将设定的对齐值与结构体中最大的基本类型数据成员的长度进行比较,取两者之间的较小者。

当结构体中以数组作为成员的时候,将会根据数组 元素类型 的长度计算对齐值,而不是按照数组的整体大小去计算。

结构体含有数组类型的对齐

当结构体中以数组作为成员的时候,将会根据 数组元素 的长度计算对齐值,而不是根据数组的整体长度来计算,例如下面的代码:

Struct{
      Char cChar;         // 占用一个字节内存
      Char cArray[4];     // 占用多少字节内存呢?
      Short sShort;       // 应该占用2字节内存
}

按照对齐的规定,cCharcArry它们都是char类型的数据,内存对齐没有缝隙,不需要插入空白的数据。但是当cArraysShort对齐的时候,cCharcArray已经在内存中占用了5个字节,此时按照结构体中当前的数据类型short进行对齐的时候,就需要在cArray后面在插入一个字节就OK了,其结构如下图所示:

结构

结构体内部的数据成员已经对齐了,下面就是处理结构体本身的对齐值问题了。根据结构体中的数据成元类型得到,最大的数据成员sShort2个字节,其余成员都是1字节的大小,在默认的情况下,对齐值为8,根据公式q = Min(M,N) 计算得出该结构体的对齐值为2,而此时结构体的总大小为8字节,也就是说无需填入数据即可满足对齐要求.

当结构体中出现结构体类型的数据成员的时候,不会将嵌套的结构体类型的整体长度参与到对齐值的计算中,而是以嵌套定义的结构体所使用的对齐值进行对齐计算,如下面的代码所示:

struct tagOne
{
      char cChar;     // 占用1字节
      char cArray[4]; // 占用5字节
      short sShort;   // 占用2字节
};
struct tagTwo
{
      int nInt;     // 占用4字节
      tagOne one;   // 占用8字节
};

在上面的结构体中,虽然tagOne占用了8字节大小,但是由于其对齐值为2,所以在计算tagTwo的对齐值的时候参数one的对齐值是2,所以根据对齐计算的公式q=Min(M,N)得出数据结构tagTwo的对齐值为4,占用了12个字节!而不是8对齐占用16字节。

静态数据成员

当类中的数据成员被修饰为静态成员的时候,对象的计算方法又会发生变化。因为虽然静态数据成员是在类内部进行定义,但是它与静态局部变量是类似的,存放的位置和全局变量一致。只是编译器增加了作用域的检查,在作用域外不可见,同类对象将共享有静态数据成员的空间.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,501评论 6 13
  • 转载 结构体对齐详解 结构体数据成员对齐的意义 许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会...
    erU阅读 454评论 0 3
  • 1. C++基础知识点 1.1 有符号类型和无符号类型 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值...
    Mr希灵阅读 17,690评论 3 82
  • 这样的偶遇,实在有趣。 从那日的诗词古韵, 到今天的思维碰撞, 从那天的老康头, 到今日的王老师。 几天来的眼福,...
    芥末女字阅读 363评论 0 0
  • 他是我见过最好看的人
    Leeyutttt阅读 189评论 0 0