结构体-联合体-字节对齐问题

一、结构体(struct)

(一)、结构体定义(definition)

结构体是一个或多个变量的集合,这些变量可以是不同的类型,为了方便处理把这些变量放在同一个结构内。

1、方式1

先定义结构体date后再定义结构体变量Date。

1struct date 2{ 3 int year; 4 int month; 5 int day; 6}; 7date Date; 8 9

或者定义结构体date的同时顺便定义结构体变量Date。

1struct date 2{ 3 int year; 4 int month; 5 int day; 6}Date; 7 8

或者不给结构体名字,直接定义变量Date。

1struct 2{ 3 int year; 4 int month; 5 int day; 6}Date; 7 8

2、方式2

typedef关键字可来定义程序员习惯的数据类型名称,这同样可以用在结构体上,值得注意的是这里的date是结构体类型而不像上面一样是结构体变量名。

1typedef struct 2{ 3 int year; 4 int month; 5 int day; 6}date; 7 8

(二)、结构体初始化(initialization)

1Date.year = 2019; 2Date.month = 6; 3Date.day = 20; 4 5

二、结构体指针(struct pointer)

(一)、结构体指针定义

1date Date; 2date *p; //第二三句合并为date *p = &Date; 3p = &Date; 4 5

该结构体的变量在内存中的分布如下图。(以四字节对齐为例)
在这里插入图片描述

(二)、结构体指针初始化

1、方式1

1p->year = 2019; 2p->month = 6; 3p->day = 20; 4 5

使用指针的方式进行对指向的变量的进行操作。

2、方式2

1(*p).year = 2019; 2(*p).month = 6; 3(*p).day = 10; 4 5

可以看到这种方式*p需要用括号进行操作,这是因为’.‘的优先级大于’*’,如果不加括号结果就会报错——“表达式必须包含类类型”,即引用p.year(然而这种表达方式也是错误的,对指针进行结构体的读写操作)。其实(*p)就是引用了Date,所以可以用结构体的方法来进行读写。

三、结构体数组

(一)、结构体数组定义

定义一个包含三个date结构体的数组。

1date Date_array[3] ; 2//注意这种方式没有对结构体数组进行初始化,后面加={}则将结构体成员初始化为0 3 4

该结构体数组的变量在内存中的分布如下图。(以四字节对齐为例),变量之间相互独立。
在这里插入图片描述

(二)、结构体数组初始化

初始化的时候数值与成员一一对应进行初始化。

1Date_array[0]= {2018,10,1}; 2cout<<Date_array[0].day<endl;//day为1 3 4

四、联合体(union)

(一)、联合体定义

1union Data 2{ 3 int i; 4 double j; 5 char arr[16]; 6}; 7 8

该联合体的变量在内存中的分布如下图。可以看出union的各个变量之间是共用同一个起始地址(因此相对于结构体来说它所占用的空间要小得多,但同时也存在弊端),union的长度为联合中类型字节数最多的变量的类型长度的整数倍(以此图为例sizeof(Data)=16,因为联合体中字节数最多的变量为double,8字节,2*8=16,而不是因为数组长度为16)。对联合中变量进行写操作会影响到其他变量。
在这里插入图片描述

(二)、联合体初始化

由于在联合体中,各成员共享同一个内存空间。应该说明的是, 这里所谓的共享不是指把多个成员同时装入一个联合变量内, 而是指该联合变量可被赋予任一成员值,但每次只能赋一种值, 赋入新值则冲去旧值。

1 var.i = 10; 2 var.x = 20; 3 cout << var.i << endl;//var.i不等于10,而是被清零了 4 cout << var.x << endl;//var.x等于20 5 6

(三)、结构体和联合体的区别

  1. 共用体和结构体都是由多个不同的数据类型成员组成, 但在任何同一时刻, 共用体只存放了一个被选中的成员, 而结构体的所有成员都存在。
  2. 对于共用体的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构体的不同成员赋值是互不影响的。

六、字节对齐(byte alignment)

(一)、字节对齐介绍

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
64位结构体默认对齐系数为8,32位结构体的默认对齐系数为4。

(二)、字节对齐作用

各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。

(三)、字节对齐概念和规则

四个基本概念:
1.数据类型自身的对齐值:对于char型数据,其自身对齐值为1,对于short型为2,对于int,float类型,其自身对齐值为4,对于double型,其自身对齐值为8,单位字节。
2.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
3.指定对齐值:#pragma pack (value)时的指定对齐值value。
4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值的那个值。

1、概念1

类型的对齐值就是它所占字节数大小。

2、概念2

1struct date 2{ 3 int year; 4 double month; 5 char day; 6}Date; 7 8

上面结构体中对齐值最大的类型double为8字节,所以sizeof(Date)=24,它的内存分配如下图。

○ ○ ○ ○ × × × × ○ ○ ○ ○ ○ ○ ○ ○ ○ × × × × × × ×

其中○为有效的数据,x为补充的数据,该结构体所占字节数为24(3*8)。

1struct date 2{ 3 int year; 4 char day; 5 double month; 6}Date; 7 8

而这样写的话则占用16字节(2*8)。

3、概念3

1#pragma pack(4) //修改指定对齐系数为4字节 2struct date 3{ 4 int year; 5 double month; 6 char day; 7}Date; 8#pragma pack() //恢复默认的对齐系数 9 10

该结构体所占用的字节为16(4*4),它的内存分配如下图。。

○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ × × ×

4、概念4

1#pragma pack(17) 2struct date 3{ 4 int year; 5 double month; 6}Date; 7#pragma pack() 8 9

该结构体所占的字节数为16(8*2),它的内存分配和概念2的图相同,这是因为结构体自身对齐值(8)和指定对齐值(17)中小的那个是结构体自身对齐值(8)。

对齐规则:
有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0"。而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整。

(四)、其他

不同的编译方式及不同的处理器对字节对齐问题有不同要求,有的会报错,有的会允许字节不对齐。
字节不对齐时,地址是单数不在4/8字节的边界上,解决办法可以是:不使用直接数据转换,使用memcpy。

参考链接:
union-百度百科
字节对齐-百度百科

代码交流 2021