目录
前言
本篇文章会介绍C语言中结构体的详细内容,以及结构体中最重要的部分(内存对齐)
结构体
结构体的声明
结构体是一些的值的集合,这些值被称为成员变量。结构体的每个成员可以是不同类型的变量。(数组是一些相同类型的元素集合)
如下图:结构体的声明:tag是结构体类型名称
、
结构体的匿名声明
但在匿名声明后,就不能在其他地方创建结构体变量了,只能在当前结构体花括号外面分号里面创建结构体变量,创建的这个结构体变量是全局变量。如下图所示:
结构自引用
结构体的自引用,就是链式访问的一种表现形式,开辟一个空间,一半存数据,一半存下一个数据的地址,这样我们就可以通过上一个数据来找到下一个数据的地址,从而形成链式访问。如下图所示:
结构体的定义和初始化
结构体的定义与初始化如下图所示:
上述情况是顺序初始化,还可以乱序初始化,只需要在创建的时候加个 .成员=内容。
结构体内存对齐(重要)
我们通过一个例题来了解结构体内存对齐。
通过上述情况,我们可以发现,结构体内存并不是按照顺序,在内存中连续存放的,而是有一定的对齐规则。(偏移量我们能通过offsetof宏来找到,其头文件为:#include<stddef>使用方法为:offsetof (type,member),括号内是类型名,结构体成员)
对齐规则:
- 结构体第一个成员,用于放在相较于结构体变量起始位置偏移量为0的位置(即结构体首地址)
- 从第二个开始,往后每个成员都要对齐到某个对齐数的整倍数。对齐数:结构体成员自身大小和默认对齐数的较小值(vs上默认对齐数为8,gcc没有默认对齐数),对齐数就是结构体成员的自身大小。
- 结构体的总大小,必须是最大对齐数的整数倍(最大对齐数,是所以成员的对齐数的最大值,即结构体内最大的类型内存)
- 如果嵌套了结构体类型,嵌套的结构体对齐到自身最大对齐数的整数倍处,结构体的整体大小就是所以最大对齐数的整数倍。
通过上述这个方法,我们来看第一个12是怎么得到的
方法1:首先先存第一个成员char,他存储在偏移量为0的位置,接着我们要存int类型,所以我们要向后找到内被4整除的偏移量,最近的就是4,在4的偏移量处存下int的4个字节,所以是1+3+4=8,然后存char,由于char只占一个字节在偏移量为9的内存处,9又能被1整除,所以char能直接存储进去。最后由于结构体总大小要被最大对齐数整除,所以必须要加到12才能被4整除。
方法2:我们可以另类来解读,首先char是一个字节,接着int类型是4个字节,1+4=5,这时由于第二点,5不是4的整数倍,要把5加到8,这三个空间就会加在char和int之间。最后还有和char类型,8+1=9,9能被1整除,不考虑第二点,但是结构体结束了,9又不能被最大对齐数4整除,所以又要向上加到12,才能被4整除,所以最终答案是12。
具体内存情况如下图所示:
S1内存分布:
S2内存分布:
对于对齐规则的第四点,我们可以看一个案例,如下图所示。
当结构体内嵌套了结构体,依次往下计算,
先算S3:8+1=9能被1整除,继续9+4=13不能被4整除,加到16,S3的最大对齐数是8,16能被8整除,所以S3的就占16个字节。
接着看S4:1+16=17,根据第四条规则,由于S3的最大对齐数是8,所以我们要把17上升到能被8整除的数字,升成了24,再继续加上double,24+8=32,由于S4的最大对齐数是8,32也能被8整除,所以32就是S4所占的内存字节。
为什么存在内存对齐?
- 平台原因(移植原因)
不是所以硬件平台都能访问地址上的任意数据的;某些硬件平台只能在某些地址处取在某些特定类型的数据,否则会有硬件异常、
- 性能原因
数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
假如没有对齐内存,当我们要读取结构体的中间的某个变量时,由于32位机器一次只能读取4个,当int存在char后面时,读取第一次得到char的内容和int的前三个字节的内容,要读取第2次才能读取到i的全部内容,这样会消耗更多的时间。
总的来说,内存对齐就是拿空间来换取时间的做法
那在设计结构体变量时,我们如何做到满足对齐,又要尽量节约空间呢?我们只需要让空间小的成员尽量集中在一起。
修改默认对齐数
我们通过使用#pragma pack(内存数)来修改默认对齐数。如果我们把默认字节设置成1,#pragma pack(1),就是做到不对齐的效果
结构体传参
结构体传参也分为传值调用和传址调用,一般情况为了节约空间,我们都会选择传址调用,(传值调用会额外开辟空间,传址不会)具体使用方法如下,传址调用是传结构体变量的地址,形参用指针接收,想要打印成员时,用结构体形参的指针->成员变量。