本次实验环境

环境1:Win10, QT 5.12

 

一. 背景

        当普通的类型无法满足我们的需求的时候,就需要用到结构体了。结构体可衍生出结构体数组,结构体还可以嵌套结构体,这下子数据类型就丰富多彩了,我们可以根据需要定义自己的数据类型。有时需要求结构体的大小,这就涉及到内存对齐的知识。概念、理论之类,我没有深入研究,这里主要是验证一下计算结构体大小的方法,证明学习到的方法确实有效。关于内存对齐,最开始是看了《深入理解计算机系统》中关于“数据对齐”一节,上面轻描淡写的写了下求结构体的大小,我没看明白。看《零基础入门C语言》中关于计算结构体大小的规则,算是看明白了。

 

二. 前奏

        先说点我觉得有意思的地方。数组之间是不可以直接赋值的,但是用结构体包装一下,就达到了这个效果,前者无法做到的事情却通过结构体做到了。通过代码来验证一下。

定义了两个数组arr和arr2,第14行代码,将arr赋值给arr2,编译时会报错。提示:14: error: array type 'int [5]' is not assignable。

(数组名有二义性,一是表示数组名,相当于数组的定海神针,二是表示首元素的地址。第14行代码把一个数组的首元素的地址赋值给另一个数组首元素,显然这是不允许的)

 

将第14行代码注释后。定义了一个结构体,里面定义了一个整型数组。然后定义了两个结构体变量tt1和tt2,将tt2赋值给了tt1,然后打印变量tt2中数组里面的每个元素。

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 struct numArr
 6 {
 7     int m_arr[5];
 8 };
 9 
10 int main()
11 {
12     int arr[5] = {1, 2, 3, 4, 5};
13     int arr2[5] = {0};
14 //    arr2 = arr;
15 
16     struct numArr tt1 = {{1, 2, 3, 4, 5}};
17     struct numArr tt2 = {{0}};
18     tt2 = tt1;
19 
20     for(int i = 0; i < 5; ++i)
21     {
22         cout<<tt2.m_arr[i]<<endl;
23     }
24 
25 
26 
27     return 0;
28 }

运行结果如下

 从结果可以看到,打印结构与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,求结构体的大小,一个是结构体类型的大小(模子),一个是是结构体变量的大小。我们也可以把结构体成员的地址也打印出来,查看它们的偏移量,分析起来会更清晰一些。

 1 #include <stdio.h>
 2 
 3 typedef struct stu
 4 {
 5     char a;
 6     short b;
 7     int c;
 8 } Stu;
 9 
10 int main()
11 {
12     Stu s1 = {'m', 1, 20};
13     printf("sizeof(Stu) = %dn", sizeof(Stu));
14     printf("sizeof(s1)  = %dn", sizeof(s1));
15 
16     printf("-----------n");
17     printf("&s1   = %pn", &s1);
18     printf("&s1.a = %pn", &s1.a);
19     printf("&s1.b = %pn", &s1.b);
20     printf("&s1.c = %pn", &s1.c);
21     return 0;
22 }

 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的打印顺序。代码其它部分保持不变。

1 typedef struct stu
2 {
3     short b;
4     char a;
5     int c;
6 } 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个字节。

图如下图所示

  代码运行结果如下.

结构体大小为8,符合预期。

 

3)调整结构体中成员的顺序。结构体先按照int、short、char的顺序定义。打印成员地址的时候也需要调整为c、b、a的打印顺序。代码的其它部分保持不变。

1 typedef struct stu
2 {
3     int c;
4     short b;
5     char a;
6 
7 } 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,符合预期。

 

实验调整

将pack修改为1,在前面三个实验的基础上,再验证一下,看下使用这个规则是否与实际情况一致,在代码前面添加

1 #pragma pack(1)

 

4) 结构体与1)中一致,按照char、short、int的顺序定义。打印成员地址也是按照a、b、c这个次序打印。

1 #pragma pack(1)
2 
3 typedef struct stu
4 {
5     char a;
6     short b;
7     int c;
8 } Stu;

 a) 现在pack为1。先求外对齐大小Y,结构体中类型最大的为int类型,大小为4字节,1比4小,所以Y值为1。

b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小。结构体中成员分别为char 1字节,short 2字节,int 4字节,与1(外对齐大小Y)比较,得到内对齐大小分别为 1, 1, 1。

c) 假设起始地址为0x00,0x00可以被1整除,可以存放a了。a为char类型,大小为1个字节。接着地址为0x01,1可以被1整除,可以存放b了,b的类型为short类型,大小为2个字节。然后下一个地址为0x03,0x03可以被1整除,可以存放c了。c为int类型,占4个字节。

d) 接着下一个地址为0x07,(0x07-0x00)可以被1(外对齐大小Y)整除,满足外对齐的要求。

经分析,结构体大小为1 + 2 + 4 = 7个字节。

图如下图所示

 

 运行结果如下

 结构体大小为7,符合预期。

 

5) 调整结构体中成员的顺序。结构体按照short、char、int的顺序定义。打印成员地址的时候也需要调整下a与b的打印顺序。代码的其它部分不变。

1 #pragma pack(1)
2 
3 typedef struct stu
4 {
5     short b;
6     char a;
7     int c;
8 } Stu;

a) 现在pack为1。先求外对齐大小Y,结构体中类型最大的为int类型,大小为4字节,1比4小,所以Y值为1。

b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小X。结构体中成员分别为short 2字节,char 1字节,int 4字节,分别与1(外对齐大小Y)比较,1较小,得到内对齐大小分别为  1,1,1。

c) 假设起始地址为0x00,0x00可以被1整除,可以存放b了。b为short类型,大小为2个字节。接着地址为0x02,0x02可以被1整除,可以存入a了。a为char类型,大小为1个字节。然后下一个地址为0x03,0x03可以被1整除,c的类型为int,c的大小为4个字节。

d) 在c起始地址的基础往后4个字节,现在地址到了0x07,(0x07-0x00)可以被1整除,满足外对齐要求(不需要填充字节了)。

经分析,结构体大小为2 + 1 + 4 =7个字节。

图如下图所示

  运行结果如下

结构体大小为7,符合预期。

 

6) 调整结构体中成员的顺序。结构体按照int、short、char的顺序定义。打印成员地址的时候也需要调整为c、b、a的打印顺序。代码的其它部分不变。

1 #pragma pack(1)
2 
3 typedef struct stu
4 {
5     int c;
6     short b;
7     char a;  
8 } Stu;

a) pack现在为1。先求外对齐大小Y,结构体中类型最大的为int类型,大小为4字节,1比4小,所以Y值为1。

b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小。结构体中成员分别为int 4字节,short 2字节,char 1字节,分别与1(外对齐大小Y)比较,得到内对齐大小分别为1,  1,1。

c) 假设起始地址为0x00,0x00可以被4整除,可以存放c了。c为int类型,大小为4个字节。接着地址为0x04,0x04可以被1整除,b为short类型,大小为两个字节。然后下一个地址为0x06,0x06可以被1整除,可以存放a了,a为char类型,a大小为1个字节。

d) 接着地址到了0x07,(0x07-0x00)可以被1(外对齐大小Y)整除,满足外对齐要求(不需要填充字节)。

经分析,结构体大小为:4 + 2 + 1 = 7个字节。

图示如下

 运行结果如下

 结构体大小为7,符合预期。

 

四.例题演示

有人可能会说了,你举的这几个例子太简单了,有没有复杂的例子可以看下呢?比如结构体中包含数组的情况,这个计算方法是否适用呢?好的,我们就拿《深入理解计算机系统》这本书的习题来验证一下。

书中练习题 3.44 有5道题目,均是求结构体大小与成员的偏移量,我们将其作为案例,按照上述方法验证一下。

对下面的每个结构体声明,确定每个字段的偏移量、结构体总的大小、以及在x86-64下它的对齐要求。

(注:因为书中的案例中K值为8,所以下面的代码示例中将pack均显式设置为了8)
1) 

1 struct P1{int i; char c; int j; char d;}

a) K现在为8。先求外对齐大小Y,结构体中类型最大的为int类型,大小为4字节,4比8小,所以Y值为4。

b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小,取较小者。结构体中成员分别为int 4字节,char 1字节,int 4字节,char 1字节,分别与4(外对齐大小Y)比较,得到内对齐大小分别为4,1,4, 1。

c) 假设起始地址为0x00,0x00可以被4整除,可以存放i了。i为int类型,大小为4个字节。接着地址为0x04,0x04可以被1整除,可以存储c,c为char类型,大小为1个字节。然后下一个地址为0x05,0x05不可以被4整除,最近的能被4整除的地址就是0x08了,需要填充3个字节。于是从0x08开始存放j,j为int类型,j大小为4个字节。下一个地址为0x0c。

d) 0x0c可以被1整除, d为char类型,大小为1个字节,接着地址到了0x0d,(0x0d-0x00)不可以被4(外对齐大小Y)整除,为满足外对齐要求,需要填充3个字节,直到0x10,(0x10-0x00)是Y的整数倍,满足外对齐。

经分析,结构体大小为:4 + 1 + 3 + 4 + 1 + 3 = 16个字节。

i 偏移量 0

c 偏移量 4

j 偏移量 8

d 偏移量 12

图示如下

 代码如下,为了避免编译器告警,与前面相比,作了调整,进行了强制类型转换。

 1 #pragma pack(8)
 2 
 3 #include <stdio.h>
 4 
 5 typedef struct P1{int i; char c; int j; char d;} PP1;
 6 
 7 int main()
 8 {
 9     PP1 p;
10     printf("sizeof(P1) = %dn", (int)sizeof(PP1));
11     printf("sizeof(p)  = %dn", (int)sizeof(p));
12 
13     printf("&p   = %pn", (void *)&p);
14     printf("&p.i = %pn", (void *)&p.i);
15     printf("&p.c = %pn", (void *)&p.c);
16     printf("&p.j = %pn", (void *)&p.j);
17     printf("&p.d = %pn", (void *)&p.d);
18 
19     return 0;

运行结果如下

 i,c, j,d的偏移量分别为0, 4, 8, 12,符合预期。

 

2)

1 struct P2 {int i; char c; char d; long j;};

a) K现在为8。先求外对齐大小Y,结构体中类型最大的为long类型,我这台电脑上long大小为4字节,4比8小,所以Y值为4。

b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小。结构体中成员分别为int 4字节,char 1字节,char 1字节,long 4字节,分别与4(外对齐大小Y)比较,取较小者,得到内对齐大小分别为4,1,1, 4。

c) 假设起始地址为0x00,0x00可以被4整除,可以存放i了。i为int类型,大小为4个字节。接着地址为0x04,0x04可以被1整除,可以存储c。c为char类型,大小为1个字节。然后下一个地址为0x05,0x05可以被1整除,可以存储d。d的类型为char,大小为1个字节。接下来地址为0x06,0x06不能被4整除,最近能被4整除的地址为0x08,需要填充2个字节,才能到0x08。j为long类型,j大小为4个字节。

d) 接着地址到了0x0c,(0x0c-0x00)可以被4(外对齐大小Y)整除,满足外对齐。

经分析,结构体大小为:4 + 1 + 1 + 2 + 4 = 12个字节。

i 偏移量 0

c 偏移量 4

d 偏移量 5

j 偏移量 8

图示如下:

 代码如下

 1 #include <stdio.h>
 2 
 3 #pragma pack(8)
 4 
 5 typedef struct P2
 6 {
 7     int i;
 8     char c;
 9     char d;
10     long j;
11 } PP2;
12 
13 int main()
14 
15 {
16     printf("sizeof(long) = %dn", (int)sizeof(long));
17 
18     PP2 p;
19     printf("sizeof(P1) = %dn", (int)sizeof(PP2));
20     printf("sizeof(p)  = %dn", (int)sizeof(p));
21 
22     printf("&p   = %pn", (void *)&p);
23     printf("&p.w = %pn", (void *)&p.i);
24     printf("&p.c = %pn", (void *)&p.c);
25     printf("&p.c = %pn", (void *)&p.d);
26     printf("&p.c = %pn", (void *)&p.j);
27 
28     return 0;
29 }

运行结果如下

  i,c,d,j的偏移量分别为0,4,5,8,符合预期。

 

3)若结构体中有数组,如何整?

1 struct P3{short w[3]; char c[3]};

a) K现在为8。先求外对齐大小Y。若有数组,取数组中元素的类型。结构体中类型最大的是short类型,大小为2字节,2比8小,所以Y值为2。

b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小。若结构体成员为数组,取成员整体的大小。结构体中成员分别为w 6字节,c 3字节,分别与2(外对齐大小Y)比较,得到内对齐大小分别为2,2。

c) 假设起始地址为0x00,0x00可以被2整除,可以存放w了。w为short数组类型,大小为6个字节。接着地址为0x06,可以被3整除,可以存储c,c为char数组类型,大小为3个字节。

d) 接着地址到了0x09,(0x09-0x00)不可以被2(外对齐大小Y)整除,为满足外对齐要求,需要填充1个字节,直到0x0a,(0x0a-0x00)是Y的整数倍,满足外对齐。

经分析,结构体大小为:6 + 3 + 1 = 10个字节。

w偏移量 0

c偏移量 6

 通过代码验证一下,代码如下

 1 #include <stdio.h>
 2 
 3 #pragma pack(8)
 4 
 5 typedef struct P3{short w[3]; char c[3];} PP3;
 6 
 7 int main()
 8 
 9 {
10     PP3 p;
11     printf("sizeof(P1) = %dn", (int)sizeof(PP3));
12     printf("sizeof(p)  = %dn", (int)sizeof(p));
13 
14     printf("&p   = %pn", (void *)&p);
15     printf("&p.i = %pn", (void *)&p.w);
16     printf("&p.c = %pn", (void *)&p.c);
17 
18     return 0;
19 }

运行结果如下

 w,c的偏移量分别为0,6,符合预期。

 

4) 如果来个指针数组呢?

1 struct P4{short w[5]; char *c[3];};

a) K现在为8。先求外对齐大小Y。若有数组,取数组中元素的类型。结构体中类型最大的是指针类型,大小为8字节,8与8相等,所以Y值为8。

b) 然后将结构体中的每一个成员与Y进行比较,依次求内对齐的大小。若结构体成员为数组,取成员整体的大小。结构体中成员分别为w 10字节,c 24字节,分别与8(外对齐大小Y)比较,得到内对齐大小分别为8,8。

c) 假设起始地址为0x00,0x00可以被8整除,可以存放w了。w为short数组类型,大小为10个字节。接着地址为0x0a,不可以被8整除,需要填充6个字节,地址到了0x10,这才可以存储c。c为指针数组类型,大小为24个字节。

d) 接着地址到了0x28,(0x28-0x00)可以被8(外对齐大小Y)整除,满足外对齐。

w 偏移量 0

c 偏移量 16

经分析,结构体大小为:10 + 6 + 24 = 40个字节。这个图有些大,就不放图片了。

通过代码验证一下,代码如下

 1 #include <stdio.h>
 2 
 3 #pragma pack(8)
 4 
 5 typedef struct P4
 6 {
 7     short w[5];
 8     char *c[3];
 9 } PP4;
10 
11 int main()
12 
13 {
14     PP4 p;
15     printf("sizeof(P1) = %dn", (int)sizeof(PP4));
16     printf("sizeof(p)  = %dn", (int)sizeof(p));
17 
18     printf("&p   = %pn", (void *)&p);
19     printf("&p.w = %pn", (void *)&p.w);
20     printf("&p.c = %pn", (void *)&p.c);
21 
22     return 0;
23 }

运行结果如下

 w,c的偏移量分别为0, 16,符合预期。

 

5)假如结构体中嵌套结构体,这个方法还适用吗?那再来验证一波。

1 typedef struct P5
2 {
3     struct P3 a[2];
4     struct P2 t;
5 } PP5;

看起来有点复杂,不过还是用同样的方法。

a) K现在为8。先求外对齐大小Y。若有数组或结构体,取数组或结构体中成员的类型。结构体中类型最大的是long类型,大小为4字节,4比8比小,所以Y值为4。

b) 然后将结构体PP5中的每一个成员与Y进行比较,依次求内对齐的大小。若结构体成员为数组或结构体,取成员整体的大小。前面已经求出,结构体中成员分别为a 20字节,t 12字节,分别与4(外对齐大小Y)比较,得到内对齐大小分别为4,4。

c) 假设起始地址为0x00,0x00可以被4整除,可以存放a了。a为结构体数组,大小为20个字节。接着地址为0x14,0x14可以被4整除,可以存储t。t为结构体类型,大小为12个字节。

d) 接着地址到了0x20,(0x20-0x00)可以被4(外对齐大小Y)整除,满足外对齐。

a 偏移量 0

t  偏移量 20

经分析,结构体大小为:20 + 12 = 32个字节。

通过代码验证一下,代码如下

 1 #include <stdio.h>
 2 
 3 #pragma pack(8)
 4 
 5 struct P2
 6 {
 7     int i;
 8     char c;
 9     char d;
10     long j;
11 };
12 
13 struct P3
14 {
15     short w[3];
16     char c[3];
17 };
18 
19 typedef struct P5
20 {
21     struct P3 a[2];
22     struct P2 t;
23 } PP5;
24 
25 int main()
26 
27 {
28     printf("sizeof(long) = %dn", (int)sizeof(long));
29 
30     PP5 p;
31     printf("sizeof(PP5) = %dn", (int)sizeof(PP5));
32     printf("sizeof(p)   = %dn", (int)sizeof(p));
33 
34     printf("&p   = %pn", (void *)&p);
35     printf("&p.a = %pn", (void *)&p.a);
36     printf("&p.t = %pn", (void *)&p.t);
37 
38     return 0;
39 }

运行结果如下

 a,t的偏移量分别为0, 20,符合预期。

 

五.结语

  目前,已经验证了结构体中包含基本数据类型,结构体中包含数组,结构体中包含结构体(结构体嵌套)的情形,可能出现的情况都验证完了,也证实了书中的方法的确有效。感谢前辈们总结的经验。

 注:如果你在做《深入理解计算机系统》书上练习题时,会发现书中提供的3.44 中B和E两题练习题答案与我上面的结果不一致。不要慌,我对比了下,原因很可能是作者的机器中long类型是8字节,而我的机器中long类型是4字节。依据之一请参见书中P27页"字数据大小"一节,依据二,根据两种不同大小的long类型,分别进行计算与验证,这里就不赘述了。

 

参考材料

1.《深入理解计算机系统》布莱恩特,奥哈拉伦

2.《零基础入门C语言》  王桂林

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/zhe666/p/15317447.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!