struct结构体大小的计算(内存对齐)
一. 背景
当普通的类型无法满足我们的需求的时候,就需要用到结构体了。结构体可衍生出结构体数组,结构体还可以嵌套结构体,这下子数据类型就丰富多彩了,我们可以根据需要定义自己的数据类型。有时需要求结构体的大小,这就涉及到内存对齐的知识。概念、理论之类,我没有深入研究,这里主要是验证一下计算结构体大小的方法,证明学习到的方法确实有效。关于内存对齐,最开始是看了《深入理解计算机系统》中关于“数据对齐”一节,上面轻描淡写的写了下求结构体的大小,我没看明白。看《零基础入门C语言》中关于计算结构体大小的规则,算是看明白了。
二. 前奏
更多技术文章、面试资料、工具教程,还请移步:http://www.javatiku.cn/
先说点我觉得有意思的地方。数组之间是不可以直接赋值的,但是用结构体包装一下,就达到了这个效果,前者无法做到的事情却通过结构体做到了。通过代码来验证一下。
定义了两个数组arr和arr2,第14行代码,将arr赋值给arr2,编译时会报错。提示:14: error: array type 'int [5]' is not assignable。
(数组名有二义性,一是表示数组名,相当于数组的定海神针,二是表示首元素的地址。第14行代码把一个数组的首元素的地址赋值给另一个数组首元素,显然这是不允许的)
将第14行代码注释后。定义了一个结构体,里面定义了一个整型数组。然后定义了两个结构体变量tt1和tt2,将tt2赋值给了tt1,然后打印变量tt2中数组里面的每个元素。
#include <iostream> using namespace std; struct numArr { int m_arr[5]; }; int main() { int arr[5] = {1, 2, 3, 4, 5}; int arr2[5] = {0}; // arr2 = arr; struct numArr tt1 = {{1, 2, 3, 4, 5}}; struct numArr tt2 = {{0}}; tt2 = tt1; for(int i = 0; i < 5; ++i) { cout<<tt2.m_arr[i]<<endl; } return 0; }
运行结果如下
更多技术文章、面试资料、工具教程,还请移步:http://www.javatiku.cn/
从结果可以看到,打印结构与tt1中的数组中的元素一致。也就是说,将结构体变量tt1赋值给tt2后,tt2中的数组与tt1中的数组也一样了。
通过结构体这么一包装,就产生了数组"可以"赋值的现象了,挺有意思的。
三. 结构体大小的计算
内存对齐,关于这一点,《深入理解计算机系统》这本书用的篇幅蛮少的,两页不到。书上是这么介绍的。
许多计算机系统对基本数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2、4或8)的倍数。这种对齐限制简化了形成处理器和内存系统之间的接口的硬件设计。
前半句能理解,后半句,涉及硬件的东西,懵逼了。
《零基础入门C语言》这本书,从现象的角度阐述,它先讲内存不对齐的情况。
一个成员变量需要多个机器周期去读的现象,称为内存不对齐。为什么要对齐呢?本质是牺牲空间,换取时间的方法。
一般来说,大多数系统,即使不对齐,也没什么大的问题,只是原本需要一次进行内存操作读或写,现在需要两次或多次了。不过这个与硬件有关系,比如有些处理器对于某些指令有特定的要求,否则可能就真的出现异常了。有兴趣的话,建议自行去深入学习。
对齐规则/计算方法
x86(Linux默认#pragma pack(4), Window默认#pragma pack(8))。Linux最大支持4字节对齐。 方法: 1) 取pack(n)的值 (n = 1, 2, 4,8......),取结构体中类型最大值为m。两者取小即为外对齐大小 Y = (m < n ? m: n); 2) 将每一个结构体的成员大小与Y比较取小者为X,作为内对齐的大小; 3) 所谓按X对齐,即为地址(设起始地址为0)能被X整除的地方开始存放数据; 4) 外部对齐原则是依据Y的值(Y的最小整数倍),进行补空操作;
以上就是通常计算结构体大小的方法了。接下来,我们通过一些简单实验来验证一下。
首先,定义一个结构体,里面包含了char、short、int类型的变量。
1) 结构体先按照char、short、int的顺序定义。然后定义一个结构体变量s1,求结构体的大小,一个是结构体类型的大小(模子),一个是是结构体变量的大小。我们也可以把结构体成员的地址也打印出来,查看它们的偏移量,分析起来会更清晰一些。
#include <stdio.h> typedef struct stu { char a; short b; int c; } Stu; int main() { Stu s1 = {'m', 1, 20}; printf("sizeof(Stu) = %d\n", sizeof(Stu)); printf("sizeof(s1) = %d\n", sizeof(s1)); printf("-----------\n"); printf("&s1 = %p\n", &s1); printf("&s1.a = %p\n", &s1.a); printf("&s1.b = %p\n", &s1.b); printf("&s1.c = %p\n", &s1.c); return 0; }
a) Windows平台,pack默认为8。先求外对齐大小Y,结构体中类型最大的为int类型,大小为4字节,4比8小,所以Y值为4.
b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小。结构体中成员分别为char 1字节,short 2字节,int 4字节,与4(外对齐大小Y)比较,得到内对齐大小分别为 1, 2, 4。
c) 假设起始地址为0x00,0可以被1整除,可以存放a了。a为char类型,大小为1个字节。接着地址为0x01,但是0x01不能被2整除,然后下一个地址为0x02,0x022可以被2整除,因此b的起始地址为0x02(此时,a与b之间填充了一个字节)。b为short类型,大小为2个字节。接着地址到了0x04,它可以被4整除,于是可以存放c了,c为int类型,大小为4个字节。
d) 接着地址到了0x08,(0x08-0x00)它可以被4(外对齐大小Y)整除,满足外对齐要求。
经分析,结构体大小为1 + 1 + 2 + 4 = 8个字节。
图如下图所示
代码运行结果如下
从打印结果来看,结构体大小为8,与上面的分析结果一致,符合预期。
2) 调整结构体中成员的顺序。结构体先按照short、char、int的顺序定义。打印成员地址的时候也需要调整下a与b的打印顺序。代码其它部分保持不变。
typedef struct stu { short b; char a; int c; } Stu;
a) Windows平台,pack默认为8。先求外对齐大小Y,结构体中类型最大的为int类型,大小为4字节,4比8小,所以Y值为4。
b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小。结构体中成员分别为short 2字节,char 1字节,int 4字节,与4(外对齐大小Y)比较,得到内对齐大小分别为 2,1, 4。
c) 假设起始地址为0x00,0可以被2整除,可以存放b了。b为short类型,大小为2个字节。接着地址为0x02,2可以被1整除,a为char类型,大小为一个字节。然后下一个地址为0x03,0x03不可以被4整除,接着地址为0x04(此时,b与c之间填充了一个字节)。0x04可以被4整除,于是可以存放c了,c为int类型,大小为4个字节。
d) 接着地址到了0x08,(0x08-0x00)它可以被4(外对齐大小Y)整除,满足外对齐要求。
经分析,结构体大小为2 + 1 + 1 + 4 = 8个字节。
图如下图所示
更多技术文章、面试资料、工具教程,还请移步:http://www.javatiku.cn/
代码运行结果如下.
结构体大小为8,符合预期。
3)调整结构体中成员的顺序。结构体先按照int、short、char的顺序定义。打印成员地址的时候也需要调整为c、b、a的打印顺序。代码的其它部分保持不变。
typedef struct stu { int c; short b; char a; } Stu;
a) Windows平台,pack默认为8。先求外对齐大小Y,结构体中类型最大的为int类型,大小为4字节,4比8小,所以Y值为4。
b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小。结构体中成员分别为int 4字节,short 2字节,char 1字节,与4(外对齐大小Y)比较,得到内对齐大小分别为4, 2,1。
c) 假设起始地址为0x00,0x00可以被4整除,可以存放c了。c为int类型,大小为4个字节。接着地址为0x04,0x04可以被2整除,b为short类型,大小为两个字节。然后下一个地址为0x06,0x06可以被1整除,可以存放a,a大小为1个字节。
d) 接着地址到了0x07,(0x07-0x00)不能被4(外对齐大小Y)整除,为满足外对齐要求,后面需要填充1个字节。
经分析,结构体大小为:4 + 2 + 1 + 1 = 8个字节。
图示如下
运行结果如下
结构体大小为8,符合预期。
更多技术文章、面试资料、工具教程,还请移步:http://www.javatiku.cn/