1.结构体类型的声明
1.1结构的说明
现在假如我们来描述一个学生的各种信息,那么我么就可以创建这个结构体:
c
int main()
{
struct student
{
char name[20];
int age;
char sex[5];
char id[20];
};
return 0;
}
1.2结构体变量的创建和初始化
c
int main()
{
struct student
{
char name[20];
int age;
char sex[5];
char id[20];
};
struct student s = { "zhangsan",18,"男","25001022" };
printf("%s\n", s.name);
printf("%d\n", s.age);
printf("%s\n", s.sex);
printf("%s\n", s.id);

当然,我们也可以按照指定的顺序初始化:
c
int main()
{
struct student
{
char name[20];
int age;
char sex[5];
char id[20];
};
struct student s = {.age = 20,.sex = "女",.name = "lili",.id = "25001100"};
printf("%s\n", s.name);
printf("%d\n", s.age);
printf("%s\n", s.sex);
printf("%s\n", s.id);
return 0;
}

1.3结构的特殊声明
c
// 匿名结构体类型
struct
{
int a;
char b;
float c;
} x;
struct
{
int a;
char b;
float c;
} a[20], *p;
// 问题:下面的代码合法吗?
p = &x;
c
// 有标签的结构体(可以重复使用)
struct Point {
int x;
int y;
};
struct Point p1, p2; // 可以创建多个变量
// 匿名结构体(只能使用一次)
struct {
int x;
int y;
} p3; // p3是这个匿名结构体类型的唯一变量
答案是不合法。
为什么 p = &x 不合法?
类型系统视角:
-
虽然两个结构体的成员完全相同(都有int a; char b; float c;)
-
但编译器将它们视为两个完全不同的类型
-
就像 int 和 float 虽然都是数字类型,但不能直接赋值一样
编译器视角:
c
// 编译器看到的实际上是:
struct AnonymousType1 {
int a;
char b;
float c;
} x;
struct AnonymousType2 {
int a;
char b;
float c;
} a[20], *p;
p = &x; // 错误:AnonymousType2* 不能指向 AnonymousType1 类型
匿名结构体的限制
主要限制:
-
只能使用一次(在声明的地方)
-
不能在其他地方创建该类型的变量
-
不能作为函数参数或返回值类型
-
不能与其他结构体(即使是相同成员)互换使用
c
struct {int a; char b;} s1;
struct {int a; char b;} s2;
s1 = s2; // 错误:类型不匹配
1.4结构的自引用
在结构中包含⼀个类型为该结构本⾝的成员是否可以呢?
1.4.1错误的自我引用方式:
c
struct Node
{
int data;
struct Node next; // 错误!
};
为什么这是错误的?
问题分析:
-
如果结构体包含自身类型的成员,会导致无限递归
-
sizeof(struct Node) 会变得无限大
-
编译器无法确定结构体的大小
c
struct Node {
int data; // 4字节
struct Node next { // 又包含:
int data; // 4字节
struct Node next { // 再次包含:
int data; // 4字节
struct Node next { // 无限循环...
...
}
}
}
}
1.4.2正确的自我引用方式:
c
struct Node
{
int data;
struct Node* next; // 正确!
};
为什么这是正确的?
优势分析:
-
指针的大小是固定的(32位系统4字节,64位系统8字节)
-
sizeof(struct Node) 是确定的值:sizeof(int) + sizeof(指针)
-
可以实现链表、树等数据结构
c
struct Node {
int data; // 4字节
struct Node* next; // 4或8字节(指针)
}
// 总大小:8-12字节,是固定的
1.4.3 typedef 与匿名结构体的陷阱
错误代码:
c
typedef struct
{
int data;
Node* next; // 错误!此时Node还未定义
} Node;
问题分析:
-
这是匿名结构体(没有结构体标签)
-
在结构体内部试图使用 Node*,但此时 Node 还未定义完成
-
编译器不知道 Node 是什么类型
编译过程:
c
// 第1步:开始定义匿名结构体
typedef struct { // 匿名结构体,没有名字
int data;
Node* next; // 错误!Node在这里还未定义
} Node; // 第2步:到这里才定义Node为这个结构体类型
正确的解决方案:
方案1:使用结构体标签
c
// 第1步:定义结构体标签Node
typedef struct Node { // 编译器知道struct Node是一个类型
int data;
struct Node* next; // 正确!编译器认识struct Node
} Node; // 第2步:创建别名Node
方案2:分开声明
c
struct Node; // 前向声明
typedef struct Node Node; // 创建别名
struct Node { // 完整定义
int data;
Node* next; // 正确!Node已经声明
};
总结关键要点:
-
结构体不能直接包含自身,会导致无限大小
-
结构体可以包含指向自身的指针,指针大小固定
-
避免在匿名结构体中进行自引用
-
使用结构体标签或前向声明来解决typedef的自引用问题
记忆口诀:
"结构体包含自己,用指针不要用实体;
typedef要自引用,先给结构体起个名。"
2. 结构体内存对齐
我们已经掌握了结构体的基本使用了。
现在我们深入讨论一个问题:计算结构体的大小。
2.1对齐规则
结构体的对齐规则
第一个成员:结构体的第一个成员放在偏移量为0的地址处。其他成员:每个成员都要对齐到对齐数的整数倍的地址处。对齐数 = min(编译器默认对齐数, 该成员的大小)
在VS中默认对齐数为8,在Linux的gcc中没有默认对齐数,对齐数就是成员自身的大小。
结构体总大小:必须是所有成员中最大对齐数的整数倍。
嵌套结构体:嵌套的结构体要对齐到其自身成员中最大对齐数的整数倍处,整个结构体的大小必须是所有最大对齐数(包括嵌套结构体的成员)的整数倍。
c
struct S1
{
char c1; // 大小1,对齐数min(8,1)=1
int i; // 大小4,对齐数min(8,4)=4
char c2; // 大小1,对齐数min(8,1)=1
};
假设从0开始:
c1放在0,占用1字节。
i的对齐数是4,所以从4开始,占用4-7字节。
c2的对齐数是1,接着放在8,占用8字节。
现在总大小是9字节,但是整个结构体的最大对齐数是4,所以总大小必须是4的倍数,因此需要填充到12字节。
c
struct S2
{
char c1; // 1,对齐数1
char c2; // 1,对齐数1
int i; // 4,对齐数4
};
从0开始:
c1在0
c2对齐数1,接着在1
i对齐数4,所以从4开始,占用4-7
总大小8字节,已经是最大对齐数4的倍数。
c
struct S3
{
double d; // 8,对齐数min(8,8)=8
char c; // 1,对齐数1
int i; // 4,对齐数4
};
从0开始:
d放在0-7
c对齐数1,接着放在8
i对齐数4,所以从12开始(因为9、10、11不是4的倍数),占用12-15
总大小16,是最大对齐数8的倍数。
c
struct S4
{
char c1; // 1,对齐数1
struct S3 s3; // S3的最大对齐数是8,所以s3要对齐到8的倍数
double d; // 8,对齐数8
};
从0开始:
c1放在0
s3的对齐数是8,所以从8开始(1-7填充),s3占16字节(8到23)
d对齐数8,接着放在24(24是8的倍数),占用24-31
总大小32,是最大对齐数8的倍数。
2.2为什么存在内存对齐
1.平台原因:不是所有硬件都能访问任意地址,某些硬件只能访问特定对齐的地址。
2.性能原因:对齐的内存只需要一次访问,未对齐的可能需要两次访问。
如何节省空间?
将占用空间小的成员集中在一起,减少填充字节。
例如,S1和S2的成员相同,但S2将两个char放在一起,减少了填充,所以S2的大小小于S1。
修改默认对齐数
使用#pragma pack(n)可以修改默认对齐数,使用#pragma pack()恢复默认。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:
让占用空间小的成员尽量集中在一起就可以啦,比如说上面的s1与s2,虽然内容相同,但就因为顺序不同而导致大小不同
2.3修改默认对其数
c
#include <stdio.h>
#pragma pack(1) // 设置默认对齐数为1
struct S
{
char c1; // 大小1,对齐数1
int i; // 大小4,对齐数1
char c2; // 大小1,对齐数1
};
#pragma pack() // 取消设置,还原为默认
int main()
{
printf("%d\n", sizeof(struct S)); // 输出:6
return 0;
}
分析:
-
设置对齐数为1后,所有成员都按1字节对齐
-
内存布局:[c1][i][i][i][i][c2]
-
没有填充字节,总大小就是各成员大小之和:1+4+1=6
- 实际编程建议
空间优化:将小成员变量放在一起
1.性能考虑:对于频繁访问的结构体,保持自然对齐
2.跨平台:注意不同编译器的默认对齐数差异
3.特定需求:使用#pragma pack调整对齐,但要注意可能影响性能
4.通过理解内存对齐规则,可以更好地优化程序的内存使用和性能表现。
3.结构体传参
c
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;
}
- 两种传参方式的区别
值传递 (print1):
c
void print1(struct S s) // 创建结构体的完整副本
{
printf("%d\n", s.num);
// 函数结束时,副本s被销毁
}
指针传递 (print2):
c
void print2(struct S* ps) // 只传递地址(指针)
{
printf("%d\n", ps->num);
// 操作的是原始结构体,不创建副本
}
- 性能对比分析
内存开销:
c
struct S s; // 假设sizeof(struct S) = 1000*4 + 4 = 4004字节
// print1调用:在栈上分配4004字节的副本
print1(s); // 栈空间消耗:4004字节
// print2调用:在栈上分配一个指针(4或8字节)
print2(&s); // 栈空间消耗:4字节(32位)或8字节(64位)
时间开销:
-
print1:需要将4004字节的数据从调用者栈帧复制到被调用函数栈帧
-
print2:只需要复制一个指针地址(4-8字节)
3.其他考虑因素
c
// print1:安全,不会修改原始数据(操作的是副本)
void print1(struct S s) {
s.num = 0; // 只修改副本,不影响原始数据
}
// print2:可能意外修改原始数据
void print2(struct S* ps) {
ps->num = 0; // 修改了原始数据!
}
// 安全的指针传递版本:
void print2_safe(const struct S* ps) { // 使用const保护
printf("%d\n", ps->num);
// ps->num = 0; // 编译错误!
}
- 总结
为什么首选 print2(指针传递):
1.性能优势:
-
避免大数据复制
-
减少栈空间使用
-
提高函数调用速度
2.内存效率:
- 传递指针(4-8字节) vs 传递整个结构体(可能数千字节)
3.实际影响:
-
对于频繁调用的函数,性能差异会累积
-
在内存受限的嵌入式系统中尤为重要
4.例外情况:
-
结构体很小(通常<16字节)时,值传递也可接受
-
需要确保不修改原数据且结构体不大时
因此,在大多数情况下,结构体传参时应传递指针而不是整个结构体。
4.结构体实现位段
4.1什么是位段
位段的定义:
位段(bit-field)是C语言中一种特殊的数据结构,它允许我们按位来指定结构体成员所占用的内存大小。
位段的特点
1.位段的成员必须是 int、unsigned int 或 signed int,C99中也可以使用其他类型
2.位段的成员名后边有一个冒号和一个数字,表示该成员占用的位数
示例:
c
struct A
{
int _a:2; // 占用2位
int _b:5; // 占用5位
int _c:10; // 占用10位
int _d:30; // 占用30位
};
c
printf("%d\n", sizeof(struct A)); // 输出是多少?
4.2 位段的内存分配
内存分配规则
1.位段的成员可以是 int、unsigned int、signed int 或 char 等类型
2.位段的空间按照需要以4个字节(int)或1个字节(char)的方式开辟
3.位段涉及很多不确定因素,不跨平台,注重可移植的程序应该避免使用位段
内存分配示例分析:
c
struct S
{
char a:3; // 占用3位
char b:4; // 占用4位
char c:5; // 占用5位
char d:4; // 占用4位
};
struct S s = {0};
s.a = 10; // 二进制:1010 -> 截断为010 (2)
s.b = 12; // 二进制:1100 -> 1100 (12)
s.c = 3; // 二进制:11 -> 00011 (3)
s.d = 4; // 二进制:100 -> 0100 (4)
c
内存地址: 低地址 ------------> 高地址
字节0: 01100010 (0x62)
字节1: 00000011 (0x03)
字节2: 00000100 (0x04)
详细分解:
字节0: [b的4位][a的3位][空闲1位] = [1100][010][0] = 01100010 = 0x62
字节1: [c的低3位][空闲5位] = [011][00000] = 00000011 = 0x03
实际上c需要5位,但第一个字节只剩下1位不够,所以c从第二个字节开始
字节2: [d的4位][空闲4位] = [0100][0000] = 00000100 = 0x04
实际上d需要4位,但第二个字节剩下的5位中,由于对齐或其他原因,可能重新开一个字节
验证:
c
#include <stdio.h>
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
int main()
{
struct S s = {0};
s.a = 10; // 10的二进制1010,但只有3位,所以是010(2)
s.b = 12; // 12的二进制1100,4位正好
s.c = 3; // 3的二进制11,5位就是00011(3)
s.d = 4; // 4的二进制100,4位就是0100(4)
// 通过调试器查看内存,可以看到三个字节:0x62, 0x03, 0x04
return 0;
}

4.3位段的跨平台问题
主要跨平台问题
1.符号问题:int 位段被当成有符号数还是无符号数不确定
2.最大位数:位段中最大位的数目不能确定
16位机器最大16位,32位机器最大32位,写成27位在16位机器会出问题
3.分配方向:位段成员在内存中从左向右还是从右向左分配,标准未定义
4.空间利用:当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余位还是利用,不确定
总结:
与普通结构体相比,位段可以达到同样的效果,并且可以很好地节省空间,但是存在跨平台问题。
4.4位段的应用
网络协议中的应用:
位段在网络协议中广泛应用,如IP数据报格式:
c
IP数据报格式:
0 15 16 31
┌───────────────────┬───────────────────┬───────────────────┬───────────────────┐
│ 4位版本号 │ 4位首部长度 │ 8位服务类型 │ 16位总长度 │
├───────────────────┼───────────────────┼───────────────────┼───────────────────┤
│ 16位标识符 │ 3位标志 │ 13位片偏移 │ │
├───────────────────┼───────────────────┼───────────────────┼───────────────────┤
│ 8位生存时间 │ 8位协议 │ 16位首部校验和 │ │
├───────────────────┴───────────────────┴───────────────────┴───────────────────┤
│ 32位源IP地址 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 32位目的IP地址 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 32位选项(若有) │
└─────────────────────────────────────────────────────────────────────────────┘
优势
-
很多属性只需要几个bit位就能描述
-
使用位段能够实现想要的效果,同时节省空间
-
网络传输的数据报大小较小,有助于网络畅通
4.5位段使用的注意事项
重要限制:不能取地址
位段的几个成员共有同一个字节,有些成员的起始位置并不是某个字节的起始位置,这些位置处是没有地址的。
原因:
-
内存中每个字节分配一个地址
-
一个字节内部的bit位是没有地址的
-
因此不能对位段的成员使用&操作符
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输⼊值,只能是先输入放在一个变量中,然后赋值给位段的成员。
错误示例:
c
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b); // 错误!不能对位段成员取地址
return 0;
}
正确使用方法:
c
int main()
{
struct A sa = {0};
// 正确的示范
int b = 0;
scanf("%d", &b); // 先输入到普通变量
sa._b = b; // 再赋值给位段成员
return 0;
}
位段使用总结
优点
1.节省空间:可以精确控制每个成员占用的位数
2.内存效率:对于标志位、状态位等小数据特别有用
3.网络应用:在网络协议中广泛使用,减少数据传输量
缺点
1.不可移植:不同编译器、不同平台的行为可能不同
2.不能取地址:无法直接对位段成员使用&操作符
3.调试困难:内存布局不直观,调试时难以理解
使用建议:
-
在需要极致节省内存的嵌入式系统中可以考虑使用
-
在网络编程中处理协议头时可以使用
-
在注重可移植性的应用中尽量避免使用
-
使用时要充分测试目标平台的行为