Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
💥💥个人主页:奋斗的小羊
💥💥所属专栏:C语言
🚀本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。
目录
1、结构体类型的声明
1.1结构体变量的创建和初始化
其实之前在C语言(操作符)2中,我们已经比较详细地介绍过结构体变量的创建和初始化,这里再补充一个特殊的初始化方法——按照指定的顺序初始化。
前面我们学到的初始化方法是按结构体成员的顺序初始化,就像下面这样:
除了按顺序初始化,我们也可以按指定的顺序初始化:
这两种初始化方法得到的效果是一样的。
1.2结构体的特殊声明
我们之前学过的结构声明常规形式是这样的:
但在声明结构的时候,还可以不完全声明,就是省略掉自定义名。
但是这种不完全的结构体声明必须在声明的同时直接创建变量,并且这个类型只能使用一次,也就是创建一次变量,但是一次可以创建多个。
下面这个代码的问题在哪儿呢?用这个不完全的结构类型创建一个指针p,将p1的地址赋给p。
当我们运行起来就会发现编译器报警告,说两个指针类型不兼容。
这是因为我们创建的结构体类型是没有名字的,虽然两个成员一样,但编译器认为它们两个的地址类型是不一样的。
1.3结构的自引用
什么是结构的自引用呢?说白了就是结构自己引用自己,有点递归的意思。举个例子,当我们想将一个数据存到内存中时,可以按顺序存,也可以随机地存,只要能找到就行。那当我们随机存的时候,找到第一个数怎么找到第二个数就是一个问题。
这时候我们可以定义一个结构体类型,有两个成员名,一个存数据,另一个存下一个数据的地址,这样的话当我们找到第一个数就能找到第二个数,以此类推。
在结构体的自引用过程中,夹杂了 typedef 对不完全结构体类型声明的重命名,也容易引出问题。
上面的代码是否正确呢?我们重命名了结构体类型名,并且在结构体成员中也用了重命名后的类型名。这样做是不对的,因为我们是要重定义这个结构体类型的类型名,上面的代码是在没有重定义之前就使用了,打破了顺序的问题。
2、结构体内存对齐
2.1对齐现象
在介绍之前我们可以先猜一下结构体类型是怎么计算大小的呢?
上面两个结构体类型成员变量相同,都是两个char类型和一个int类型,那两个结构体类型的大小会是6个字节吗?
可以看到结果并不是我们猜测的,而且上面两个结构体类型只是改变了成员变量的顺序,它们的大小就发生了变化。那我们可以得到的结论是,结构体类型的大小并不是单纯的成员变量类型大小之和,而且结构体类型的大小还跟成员顺序有关系。这是为什么呢?
其实,结构体的成员在内存中是存在对齐现象的。
接着我们就来探讨一下上面两个结构体类型的大小为什么是8个字节和12个字节。假设上面是一块内存,一小格表示一个字节,第一个字节相较于起始位置偏移量为1,第二个字节相较于起始位置偏移量为2,以此类推,这就是偏移量的概念。
用结构体类型 struct S1 创建一个结构体变量s,假设s从第0个字节开始,我们知道s的大小是8个字节,那其成员n、c1、c2分别在哪个位置呢?这里再介绍一个宏 offsetof ,它的作用是计算结构体成员相较于结构体变量起始位置的偏移量。
可以看到n的偏移量为0,c1的偏移量为4,c2的偏移量为5。也就是说这三个结构体成员在内存是像下面这样存的。
但是,这也不够8个字节啊,难道说结构体变量s即使用不到第6、7个字节但还是将这两个字节霸占了吗?是的,这两个字节就相当于浪费掉了。
那用结构体类型 struct S2 创建的结构体变量所占的12个字节里n、c1、c2三个成员变量是存在哪些位置呢?
可以看到c2的偏移量为0,n的偏移量为4,c1的偏移量为8。也就是说这三个结构体成员在内存中是像下面这样存的。
可以看到上面也没有12个字节,并且把第1、2、3、9、10、11个字节都浪费了。那这又是为什么呢?
通过上面的内容我们可以得到的结论是,结构体成员在内存中存的时候并不是一个挨着一个存的,而是按一定的规则存储的,这个规则就是结构内存对齐。
2.2对齐规则
如果我们想要计算结构体类型的大小,就必须要先了解结构体的内存对齐,才能知道结构体类型在内存中究竟是如何开辟空间的。具体的对齐规则如下:
(1)结构体的第一个成员(不管是什么类型)对齐到和结构体变量起始位置偏移量为0的地址处;
(2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值,VS中默认的值为8,Linux中gcc没有默认对齐数,对齐数就是成员自身的大小.
(3)结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍;
(4)如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
了解了对齐规则,我们就来解释一下上面两个结构体类型的大小为什么是8个字节和12个字节。
先看结构体类型 struct S1,根据规则(1),我们知道n存在第0、1、2、3这四个字节中。根据规则(2),VS默认对齐数是8,c1的大小为1小于默认对齐数,c1要对齐到1的整数倍的地址处,所以c1存到了第4个字节中;再看c2,c2的大小也是1小于默认对齐数,c2要对齐到1的整数倍的地址处,所以c2存到了第5个字节中。根据规则(3),结构体成员中最大对齐数为4,此时结构体成员所占6个字节,不是4的倍数,所以还要再额外占用两个字节的空间,那这两个字节的空间就被浪费掉了。所以最终这个结构体类型的大小就是8个字节。
再看结构体类型 struct S2,根据规则(1),我们知道c2存在第0个字节中。根据规则(2),VS默认对齐数是8,n的大小为4小于默认对齐数,n要对齐到4的整数倍的地址处,所以n存到了第4、5、6、7个字节中,那第1、2、3个字节就被浪费掉了;再看c1,c1的大小是1小于默认对齐数,c1要对齐到1的整数倍的地址处,所以c1存到了第8个字节中。根据规则(3),结构体成员中最大对齐数为4,此时结构体成员所占9个字节,不是4的倍数,所以还要再额外占用三个字节的空间,那这三个字节的空间也被浪费掉了。所以最终这个结构体类型的大小就是12个字节。
再来通过下面这个练习理解规则(4):
可以看到结构体类型 struct S2 的大小是24个字节,三个结构体成员的偏移量分别为0、4和16。那么其成员变量在内存中的存储就应该是这样:
其中结构体变量s的大小是8个字节,其结构体类型中成员变量最大对齐数为4,而对于结构体类型 struct S2 其成员变量中最大对齐数为8,所以最终结构体类型的大小就是24。
2.3为什么存在内存对齐
通过上面的学习我们知道内存对齐的时候很容易就浪费了内存空间,那为什么还要存在内存对齐呢? 大部分的参考资料都是这样说的:
(1)平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,比如只能取int类型,那就只能访问4的倍数的内存空间,否则抛出硬件异常。
(2)性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或写了。否则,我们可能需要执行两次内存访问,因为对象可能被分在两个8字节内存块中。
什么意思呢?假设创建一个结构体类型,其中成员变量为char类型的c和int类型的n。
下面是不考虑对齐的情况,直接在c后面存n:
下面是考虑对齐的情况:
因为n为int类型,考虑对齐的话c后面三个字节的空间就浪费掉了。
假设我们现在要用一个32位的机器去访问这个结构体的成员变量n,32位的机器一次能访问4个字节的内存,那在开始位置访问不考虑对齐的情况时需要访问两次才能读取完整的n,但是在访问考虑对齐的情况时只需要访问一次就行了,因为n前面刚好是4个字节可以跳过就没有必要访问前面的内存了,这样的话效率就会提高。
总的来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到呢?
有一个小技巧:让占用空间小的成员尽量集中在一起。就像前面我们创建的 struct S1 和 struct S2 一样,虽然两个成员一样,但是成员顺序不一样最终两个结构体类型的大小也就不一样了。
2.4修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
上面结构体类型 struct S 的大小在默认对齐数下是12个字节。
当我们将默认对齐数改为1时,结构体类型 struct S 的大小就变成了6个字节。因为结构体成员c1、n、c2的大小分别为1、4、1,而默认对齐数是1的时候,其每个成员的对齐数都为1,就相当于没有对齐,是紧挨着存储的。
默认对齐数一般修改的都是2的次方数。