目录
1、结构体的定义
之前我们学的数组是一些值的集合,而结构也是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构体的语法结构:
struct tag//结构体名称
{
member-list;//成员列表
}variable-list;//变量列表(全局变量)
比如我们要描述一本书,有作者、书名、价格、卡号等。
struct Book
{
char book_name[20]; // 书名
float price; // 价格
char id[18]; // 书号
char author[15]; // 作者
}; // 分号不能漏
2、创建与初始化结构体变量
2.0 举例
✅代码:
struct Book
{
char book_name[20]; // 书名
float price; // 价格
char id[18]; // 书号
char author[15]; // 作者
}b3,b4,b5; // 分号不能漏 //b3 b4 ,b5 也是结构体变量
int main()
{
struct Book b1 = { "结构体",33.3f,"DF202408012","等风来" };// 按顺序初始化
struct Book b2 = { .author = "随风扬",.book_name = "联合体",.price = 55.5f,.id = "SF202408013" }; //不按顺序初始化
printf("%s %f %s %s\n", b1.book_name, b1.price, b1.id, b1.author); // ->
printf("%s %f %s %s\n", b2.book_name, b2.price, b2.id, b2.author);
return 0;
}
运行结果如下:
第一个结果有点误差说明,浮点数在内存中有可能是不能精确保存的。
2.1 结构体的特殊声明
2.1.0 匿名结构体
匿名结构体,顾名思义,就是在声明结构体时没有给结构体指定一个标签(tag)名。这种结构体类型只能在它被声明的地方直接使用,因为它没有名字,所以无法在其他地方引用。
在声明结构的时候,可以不完全的声明。
//匿名结构体类型
struct // 这个类型没有名字
{
int a;
char b;
float c;
}s1; // 但是用这个类型创建了一个变量
struct
{
int a;
char b;
float c;
}* p; // 匿名结构体指针类型,ps为指针变量
在上面的基础上,加上
int main()
{
ps = &s1; // ? 这种方式不予许,类型不兼容
return 0;
}
会出现警告:
编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
2.1.1 结构体的自引用
链表在数据结构中会学,这里先用一下。
定义一个链表的节点:
struct Node
{
int data;
struct Node next;
};
上面的代码是错误的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。
正确自引用写法:
struct Node
{
int data; // 存放数据 -- 数据域
struct Node* next; // 指针域,指向下一个节点
};
// 或者使用typedef简化类型名
typedef struct Node {
int data;
struct Node* next;
} Node;
看看下面的代码,可行吗?
typedef struct Node
{
int data;
Node* next; // 省略了Node
} Node; // struct Node 重命名为Node
答案是不行的,虽然Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量是不行的。解决方案上面已经给出,就是在Node* next; 改为struct Node* next;
3、结构体内存对齐
3.0 为什么要内存对齐
原因:
1. 平台原因 (移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;硬件平台通常对数据访问有严格的地址对齐约束。这意味着,特定类型的数据(如整数、浮点数等)必须被存储在符合其类型大小边界的内存地址上。若尝试在不满足这些对齐要求的地址上访问这些数据,硬件可能会拒绝执行该操作,导致程序崩溃或执行异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
-> 对于总以8字节为单位从内存中取数据的处理器,确保double类型数据地址对齐至8的倍数,即可通过单次内存操作完成读写,避免可能的效率损失。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
✅代码:
struct S1
{
char c1;
char c2;
int n;
};
struct S2
{
char c1;
int n;
char c2;
};
int main()
{
printf("%zd\n", sizeof(struct S1)); // 8
printf("%zd\n", sizeof(struct S2)); // 12
}
上面代码的结果不是我们理所应当想象的6个字节,结果不是,为什么呢?我们先使用offsetof 看一下它们的内存偏移量。记得包含offsetof 头文件 #include<stddef.h>。offsetof是一个宏,可以计算结构体成员相较于结构体变量起始位置的偏移量 。
通过绘图分析我们解决了一些问题,同时又引入了新的问题,比如计算S2它内存时还要多出的三个内存单元(字节)才达到12,为什么呢?这就涉及到内存对齐规则的问题了。
绘图如下:
3.1 对齐规则
1. 结构体的第一个成员总是从结构体变量内存分配的起始位置(偏移量为0的地址处)开始。如果该成员的大小小于编译器的默认对齐数,它仍然从该位置开始,但后面可能会有填充来满足后续成员的对齐要求。
2. 结构体的每个后续成员都会对齐到某个数字(“对齐数”)的整数倍地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。
● VS 中默认的值为 8
● Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小会调整为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大值)的整数倍。
4. 如果结构体中包含了其他结构体作为成员(即嵌套结构体),那么这些嵌套的结构体成员也会遵循上述的对齐规则。嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
✅示例:
#include<stdio.h>
#include<stddef.h>
struct S1
{
char c1;
char c2;
int n;
};
int main()
{
struct S1 s1;
return 0;
}
根据结构体内存对齐规则
首先,内存为结构体s1开辟内存空间,而结构体第一个成员char c1 从结构体偏移量为0的地址处开始,填充了一个字节;
第二个成员char c2 和默认对齐数(这里是在VS环境下)比较,取得较小值 1 ,我们 c2 要对齐到默认对齐数也就是 1 的整数倍地址处(实际上任何一处地址都是 1 的倍数)。所以 c2 直接存在 c1 后,char 占一个字节;
n 也一样,找 4 的倍数,我们发现它会浪费到两个字节空间后开始存储 int n,占四个字节空间。
总体上用了八个字节空间,8 是它们所以结构体成员中最大对齐数 4 的整数倍,不需要调整了,结果就是8 。
我们来看看s2 ,为什么它的结果是12呢?s2 跟上面步骤差不多,在对规则第三点处不同,刚刚 s1 的刚好就是算到 8 就是 4 的倍数,而s2 不是,s2 需要调整,将 9 调整为 4 的倍数,所以结果是12。
掌握了前面3 点对齐规则,下面的练习自己尝试做一下吧,我就不解析了。结果是 16。
#include<stdio.h>
#include<stddef.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S3)); // 16
return 0;
}
✅还有第4 点对齐规则,代码如下:
#include<stdio.h>
#include<stddef.h>
// - 结构体嵌套问题
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4)); // 32
return 0;
}
其实就是嵌套了一个结构体S3而已,和前面三点对齐规则结合用,在算到s3 时,它的最大对齐数为8 ,而我们前面练习已经知道了它的大小为16 ,所以在c1 后会浪费7 个字节再填充嵌套结构体s3 的16 个字节,刚好d 对齐数8 是24 的倍数,接着填充8 个字节。最后,它们总共偏移了 32 个字节,是它们所有包括嵌套结构体中成员的对齐数的最大整数8 的倍数,所以结果就是32 。
结合下图来理解更清晰:
那我们在设计结构体时如何做到对齐又尽量节省空间呢?
将占用空间小的成员尽量集中在一起,特别是将char、short等类型放在结构体开始的位置,可以减少填充字节。前面有相关代码S1和S2。
3.2 如何修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
✅结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数,一般我们设计为2 的倍数。
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1; // 1 1 1
int i; // 4 1 1
char c2; // 1 1 1
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S)); // 6
return 0;
}
4、结构体传参
✅代码:
#include <stdio.h>
struct S
{
int arr[1000];
int n;
char ch;
};
void Print1(struct S tmp) //结构体传参-传值调用
{
int i = 0;
for (int i = 0; i < 10; i++)
{
printf("%d ", tmp.arr[i]);
}
printf("\n");
printf("n = %d\n", tmp.n);
printf("ch = %c\n", tmp.ch);
}
void Print2(struct S *ps) //结构体地址传参
{
int i = 0;
for (int i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
printf("n = %d\n", ps->n);
printf("ch = %c\n", ps->ch);
}
int main()
{
struct S s = { {1,2,3,4,5,6,7,8,9,10},10,'f' };
Print1(s);
Print2(&s);
return 0;
}
打印结果:
print1 和 print2 函数,我们首选print2 函数
原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
所以结构体传参的时候,要传结构体的地址。
5、结构体中的位段使用
5.0 什么是位段?
定义:位段是一种通过结构体实现的数据存储结构,它可以把数据以位的形式紧凑地储存,并允许程序员对此结构的位进行操作。位段中的位指的是二进制的位(bit)。
性质:位段的成员必须是整型家族的成员(如int、unsigned int、signed int等),且每个成员后面跟有一个冒号和一个数字,该数字表示该成员在内存中占用的二进制位大小。在C99中位段成员的类型也可以选择其他类型。
✅如下面这段代码,当我们设计位段时是8 字节,当我们不设计时,为16 字节。
可见,位段的出现本质上还是节省空间。
5.1 位段的内存分配
1. 位段的成员可以是 int、unsigned int、 signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
✅代码:
// 例如:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s;
printf("%zd\n", sizeof(struct S)); // 3
return 0;
}
1、给定了空间后,在空间内部是从左向右使用,还是从右向左使用,这个不确定。
假设:从右向左
2、当剩下的空间不足以存放下一个成员的时候,空间是浪费还是使用不确定。
假设:浪费
我们根据上面的假设得到的结果确实是3 ,那我们的分析是否正确呢?下面来验证一下。
✅代码:
struct S
{
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\n", sizeof(struct S)); // 3
return 0;
}
根据下图来看确实和假设一样。
5.2 位段的跨平台问题
1、数值类型的解释不确定性
有符号与无符号:位段中的int成员被当成有符号数还是无符号数是不确定的。这可能导致在不同平台上对相同位段值的解释不一致。2、最大位数的不确定性
位数限制:位段中成员的最大位数依赖于具体平台的位宽。例如,在16位机器上,位段成员的最大位数可能是16,而在32位机器上可能是32。如果编写的位段成员位数超出了当前平台的限制(如声明了一个27位的int成员在16位机器上),则可能导致编译错误或运行时问题。
3、内存分配方向的不确定性
分配方向:位段中的成员在内存中的分配方向(从左到右或从右到左)尚未有统一的标准。4、剩余位处理的不确定性
剩余位利用:当一个结构体包含两个位段,且第二个位段成员较大,无法容纳于第一个位段剩余的位时,是否舍弃剩余的位还是利用这些位是不确定的。这种不确定性可能导致数据在不同平台间的传输或存储时出现不一致。
总结: 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
5.3 位段的应用
在网络协议中,IP数据报的格式设计得非常紧凑,其中许多属性仅需要几个比特(bit)来描述。位段的应用优化了IP数据报的存储与传输,对维护网络流畅性起到了积极作用。
5.4 位段使用的注意事项
位段成员共享字节空间时,它们的起始位置可能不直接映射到内存地址的边界,而是位于字节内部的某个bit位置,这些内部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);//错误示范
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
⛳ 点赞☀收藏 ⭐ 关注!
如有不足欢迎评论区指出
Respect!!!