文章目录
前言一、结构体类型的声明结构体回顾结构体的特殊声明结构体的自引用 二、结构体的内存对齐对齐规则为什么存在内存对齐?修改默认对齐数 三、结构体传参四、结构体实现位段什么是位段位段的内存分配位段的跨平台问题位段的应用位段使用的注意事项 总结
前言
事实上,我们早就有过关于结构体的学习
现在,我们再来深入理解它一下
一、结构体类型的声明
结构体回顾
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量
结构的声明
struct tag
{
member-list;
}variable-list;
例如,我们想要描述一位学生:
struct Stu
{
char name[20]; // 名字
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
}; // 分号不能丢
结构体变量的创建和初始化
运用大括号 { } 即可,在其里面进行初始化
可以按照默认顺序初始化,也可以按照指定顺序初始化
struct Stu s1 = {.age = 30, .name = “Lisi”, .sex = “nv”, .id = “2023020405”};
结构体的特殊声明
在声明结构的时候,可以不完全的声明
// 匿名结构体类型// 只能使用一次struct{int a;char b;float c;}x;struct{int a;char b;float c;}a[20], *p;
我们可以思考一下,对于上面两个结构体, p = &x; 这样的语句对不对
答案是不对的,虽然两个结构体成员相同,但是因为是匿名的,在编译器看来是两种不同的类型
大部分情况下我们不会用到匿名结构体的,你在使用的时候也需要注意一下
结构体的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
可以,我们之后学数据结构的链表就是如此,请持续关注我的博客!
在结构体自引用使用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引入问题
typedef struct{int data;Node* next; // err}Node;
答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的,一言以蔽之就是,你提前使用了重命名类型
二、结构体的内存对齐
现在我们要来深入探讨一个问题:结构体的大小计算
其实也就是知识点 -> 内存对齐
先让你诧异一下,哈哈,就拿这两个输出作为引子吧!
对齐规则
结构体的对齐规则:
结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处对齐数 = 编译器默认的一个对齐数 与 该变量成员大小的较小值- VS中默认的值为8
- Linux中gcc没有默认对齐数,对齐数就是成员自身的大小结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数当中最大的)整数倍如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
struct S1{char c1; // 1Bytechar c2; // 1Byteint i; // 4Bytes}
我们会发现,起始c1放在偏移量为 0 的位置上,c2放在偏移量为1的位置上,而 int 占 4 个字节,取较小值得到对齐数是 4 ,对齐到 4 的整数倍的位置 4 上,并占 4 5 6 7 这四个位置,并且算下来整个结构体占据了 8 个字节,是最大对齐数 4 的整数倍,所以结构体的总大小为 8 ,浪费了 2 个字节的空间,图示如下:
我们再来看S2
struct S2{char c1; // 1Byteint i; // 4Byteschar c2; // 1Byte}
c1先放在偏移量为 0 的位置上,而 int 占 4 个字节,对齐到整数位置 4 上,并占 4 5 6 7四个位置,而c2占在偏移量为8的位置上,这时候结构体的最大对齐数是4,而结构体的大小为0 ~ 8,为9,不是4的整数倍,因此还要再来 3 个字节,也就是 0 ~ 11,最终,会发现结构体的大小为 12 ,这其中就浪费了 6 个字节的内存空间,图示如下:
接下来我再给出一个例子,你用做练习自己分析吧
struct S3{double d; // 0 ~ 7char c; // 8int i; // 12 ~ 15};
答案是16个字节
那再来个嵌套结构体的:
struct S4{char c1;struct S3 s3;double d;};
首先c1放在偏移量为0的位置上,而s3是个结构体,按照上述第五条规则,应该对齐到结构体成员中最大对齐数8的整数倍位置上,显然s3放在8上,并占 8 ~ 23位置,(s3大小为16,前面求过),这时候double放到对齐数8的整数倍上,刚好24满足,并占24~31位置,这样的话,S4大小为32,恰好也满足了所有成员中对齐数的整数倍这一必要性条件,浪费7个字节,图示如下:
如果你要验证,可以自己去写输出部分验证,或者可以打开内存监视
亦或者,有一个宏 offsetof ,可以用来计算结构体成员相较于起始位置的偏移量
需要注意的是,这个宏需要包含一个头文件 stddef.h
为什么存在内存对齐?
平台原因(移植原因):不是所有的硬件平台都能够访问任意地址上的任意数据;某些硬件平台只能在某些地址处获取某些特定类型的数据,否则则出现硬件异常性能原因:
数据结构(尤其是栈)应该尽量地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要两次内存访问,而对齐的内存访问只需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证所有的double类型的数据都在对齐的地址中,那么就可以用一个内存操作来读取者写值了。否则,我们可能需要进行两次内存访问,因为对象可能分散在两个8字节内存块中。
按图来说的话,性能原因如下:
总而言之:结构体的内存对齐是拿空间来换时间的一种做法
所以在设计结构体的时候,我们既要满足对齐,又要节省空间的话,可以考虑将占用内存小的成员尽量集中在一起
比如上述的 S1 和 S2 结构体,S1 设计的就比 S2 好
修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数
#include <stdio.h> #pragma pack(1) //设置默认对齐方式为1 struct S { char c1; int i; char c2; }; #pragma pack() //取消设置的对齐,恢复默认 int main() { //输出的结果是什么? printf("%d\n", sizeof(struct S)); return 0; }
答案是6,自行分析
当我们觉得对齐方式不合理的时候,我们可以自己更改默认对齐数
三、结构体传参
#include <stdio.h>struct S{ int data[1000]; int num;};struct S s = { {1,2,3,4}, 1000 };//结构体传参 void print1(struct S s){ printf("%d\n", s.num);}//结构体地址传参 void print2(struct S* ps){ printf("%d\n", ps->num);}int main(){ print1(s); //传结构体 print2(&s); //传地址 return 0;}
上面 printf1 和 printf2 函数哪个好些?
printf2的方式更好一些,也就是说传地址方式更好一些,本质上还是因为形参是实参的一份拷贝,太浪费内存空间了
四、结构体实现位段
你可能只听说过段位,没事,现在我来跟你讲一下位段
没听过很正常,这个知识点比较细致,但是对于了解底层以及未来网络的学习还是很有用的
什么是位段
位段的声明和结构是类似的,有两个不同:
位段的成员必须是int、unsigned int或 signed int,在C99中位段成员的类型也可以选择其他类型位段的成员名后边有一个冒号和一个数字struct A{int _a:2;int _b:5;int _c:10;int _d:30;};// A就是一个位段类型
那么位段A所占内存的大小是多少?
为什么不是按照前面讲述的内存对齐,是16反而是8?
位段的内存分配
位段的出现就是为了节省空间
位段的成员可以是 int、unsigned int、signed int 或者是 char 等类型。位段的空间上是按照需要以 4 个字节(int)或者 1 个字节(char)的方式来开辟的。位段有很多不确定因素,位段是不跨平台的,注意可移植的程序应避免使用位段。(比如说一个字节从左向右还是从右向左使用?)假设从右向左,来个例子如下:
_a给两个比特位,_b给5个比特位,_c给10个比特位,_d给30个比特位
_a、_b、_c没什么好说的,但是这个_d就有两种存储方式了,是直接弃掉第一个 int 剩下的15个比特位,新开一个 int 用30个比特位存_d ;还是继续使用完这15个比特位,再新开个 int 用15个存_d,其实,C语言也没有规定,完全取决于编译器,哈哈,这又是不确定性
VS下其实是从右向左存储,如果剩余的空间不够下一个成员使用,就新开空间并浪费
我们来通过下面这段代码来进行验证:
#include <stdio.h>struct S{// char就好在是一个字节一个字节开辟char a : 3;char b : 4;char c : 5;char d : 4;};int main(){struct S s = { 0 };s.a = 10;s.b = 12;s.c = 3;s.d = 4;printf("%zd", sizeof(struct S)); // 3return 0;}
首先,a 和 b共占用了7个比特位,剩下1个比特位不够 c ,于是新开一个字节放 c,同理再新开一个字节放 d
接下来我们给 a 赋值为 10,即1010,截断后三位,放入010,同理b放入1100
这时候,第一个字节的8个比特位就是01100010
同理c放入00011
这时候,第二个字节的8个比特位就是00000011
同理d放入0100
这时候,第三个字节的8个比特位就是00000100
那么,按照十六进制位,这三个字节就是分别就是 0x62、0x03、0x04,打开内存!
果然如此!!
位段的跨平台问题
int 位段被当成有符号数还是无符号数是不确定的位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。)位段中的成员在存储中有右分配,还是从右向左分配标准尚未定义。当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位置,是含有剩余的位置是利用,这是不确定的。位段的应用
同开头所说,位段在于网络传输部分还是用处蛮大的
这个自行了解,我也是现查的,具体原理大家自辨
位段使用的注意事项
位段的几个成员共享同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位段处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; }; int main() { struct A sa = {0}; scanf("%d", &sa._b); // error // right int b = 0; scanf("%d", &b); sa._b = b; return 0; }
总结
我也没想到结构体深入了解竟然能讲出那么多东西来,哈哈
本篇还是比较难的,请你和我好好消化一下,准备接下来的学习~