[C]基础17.自定义类型:结构体
- 博客主页:向不悔
- 本篇专栏:[C]
- 您的支持,是我的创作动力。
文章目录
- 0、总结
- 1、结构体类型的声明
- 1.1 结构的声明
- 1.2 结构体变量的创建和初始化
- 1.3 结构体的特殊声明
- 1.4 结构的自引用
- 1.5 typedef与结构自引用
- 2、结构体内存对齐
- 2.1 对齐规则
- 2.2 为什么存在内存对齐?
- 2.3 修改默认对齐数
- 3、结构体传参
- 4、结构体实现位段
- 4.1 什么是位段
- 4.2 位段的内存分配
- 4.3 位段的跨平台问题
- 4.4 位段的应用场景
- 4.5 位段使用的注意事项
0、总结
1、结构体类型的声明
结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1 结构的声明
struct tag {member-list;
} variable-list;
例如描述一个学生
struct Stu {char name[20]; // 名字int age; // 年龄char sex[5]; // 性别char id[20]; // 学号
}; // 分号不能丢
1.2 结构体变量的创建和初始化
#include <stdio.h>
struct Stu
{char name[20]; //名字int age; //年龄char sex[5]; //性别char id[20]; //学号
};int main()
{//按照结构体成员的顺序初始化struct Stu s = { "张三", 20, "男", "20250529001" };printf("name :%s\n", s.name);printf("age :%d\n", s.age);printf("sex :%s\n", s.sex);printf("id :%s\n", s.id);printf("-----------------\n");//按照指定的顺序初始化struct Stu s2 = { .age = 18,.name = "lisi",.id = "20250530002",.sex = "女" };printf("name :%s\n", s2.name);printf("age :%d\n", s2.age);printf("sex :%s\n", s2.sex);printf("id :%s\n", s2.id);return 0;
}
运行:
name :张三
age :20
sex :男
id :20250529001
-----------------
name :lisi
age :18
sex :女
id :20250530002
1.3 结构体的特殊声明
在声明结构的时候,可以不完全的声明(匿名结构体)。
// 匿名结构体类型
struct {int a;char b;float c;
} x;struct {int a;char b;float c;
} a[20], *p;
注意:编译器会把上面的两个声明当成完全不同的两个类型,所以下面的代码是非法的:
p = &x; // 非法操作
思考:为什么非法?
- 类型不同:即使两个匿名结构体成员完全相同(
int a; char b; float c;
),编译器仍将它们视为完全不同的类型。 - 匿名特性:没有结构体标签(
tag
),无法声明相同类型。 - 指针不兼容:
p
是指向类型B的指针,&x
是类型A的地址,类型不匹配。
如果要解决,可以这样:
-
1、添加标签
struct Standard { // 添加标签int a; char b; float c; }; struct Standard x; struct Standard a[20], *p; p = &x; // 合法
-
2、使用 typedef 统一类型
typedef struct { // 创建类型别名int a; char b; float c; } MyType; MyType x; MyType a[20], *p; p = &x; // 合法
警告:匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
1.4 结构的自引用
结构自引用指在结构体内部包含指向自身类型结构体的指针成员,这是实现链式数据结构(如链表、树等)的基础。
在结构中包含一个类型为该结构本身的成员:
struct Node {int data;struct Node next; // 错误,会导致无限大小
};
这种写法是错误的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷大。
Node = int + Node= int + (int + Node)= int + (int + (int + Node))... // 无限循环
正确的自引用方式:
struct Node {int data;struct Node* next; // 使用指针
};
优势:
-
固定大小:指针大小固定(4/8字节),不引起递归问题
-
内存高效:仅存储地址,不复制整个结构
1.5 typedef与结构自引用
错误用法分析:
typedef struct { // 匿名结构体int data;Node* next; // 错误:Node尚未定义
} Node;
问题原因:
-
编译顺序:编译器先处理结构体定义
-
未定义类型:在结构体内部使用
Node*
时,Node
尚未被定义
解决方案:定义结构体不要使用匿名结构体。
-
完整标签声明
typedef struct Node { // 明确结构体标签int data;struct Node* next; // 使用标签声明指针 } Node; // typedef别名
-
分步声明
struct Node; // 前向声明typedef struct Node {int data;struct Node* next; // 使用前向声明 } Node;
总结:
-
结构自引用必须使用指针而非完整结构体
-
typedef自引用需配合结构体标签或前向声明
-
匿名结构体不适合需要自引用的场景
-
正确使用自引用可实现高效链式数据结构
2、结构体内存对齐
2.1 对齐规则
首成员规则:结构体的第一个成员对齐到偏移量为0的地址处
成员对齐规则:其他成员变量要对齐到"对齐数"的整数倍地址
- 对齐数 = min(编译器默认对齐数, 成员自身大小)
VS编译器
默认对齐数=8,Linux gcc
无默认对齐数(对齐数=成员大小)
结构体大小规则:结构体总大小为最大对齐数(所有成员中对齐数的最大值)的整数倍
嵌套结构体规则:
- 嵌套的结构体成员对齐到其内部成员的最大对齐数的整数倍处
- 整体结构体大小是所有最大对齐数(含嵌套结构体成员)的整数倍
练习1:struct S1
struct S1 {char c1; // 大小1,对齐数min(8,1)=1int i; // 大小4,对齐数min(8,4)=4 → 需对齐到4的倍数char c2; // 大小1,对齐数1
};
// 总大小:12字节(最大对齐数4的倍数)
0 1 2 3 4 5 6 7 8 9 10 11
[c1] 空 空 空 [i] [i] [i] [i] [c2] 空 空 空
练习2:struct S2
struct S2 {char c1; // 大小1,对齐数1char c2; // 大小1,对齐数1int i; // 大小4,对齐数4
};
// 总大小:8字节(最大对齐数4的倍数)
0 1 2 3 4 5 6 7
[c1][c2] 空 空 [i] [i] [i] [i]
练习3:struct S3
struct S3 {double d; // 大小8,对齐数min(8,8)=8char c; // 大小1,对齐数1int i; // 大小4,对齐数4 → 需对齐到4的倍数
};
// 总大小:16字节(最大对齐数8的倍数)
0-7 8 9 10 11 12-15
[d][d][d][d][d][d][d][d] [c] 空 空 空 [i][i][i][i]
练习4:嵌套结构体
struct S4 {char c1; // 大小1,对齐数1struct S3 s3; // 大小16,对齐数8(S3中最大对齐数)double d; // 大小8,对齐数8
};
// 总大小:32字节(最大对齐数8的倍数)
0 1-7 8-23 24-31
[c1] 空(对齐) [s3:16字节] [d][d][d][d][d][d][d][d]
练习5:混合字符串类型
struct MixedString {char *pname; // 指针8字节,对齐数8char name[12]; // 字符数组,对齐数1int age; // 4字节,对齐数4
};
// 总大小:32字节
0-7 : [pname] // 指针(8)
8-19 : [name] // char[12] (12字节)
20-23: [age] // int(4)
24-31: 填充 // 总大小32(最大对齐数8的倍数)
练习6:数组+嵌套结构体
struct Inner {char c; // 1字节,对齐数1double d; // 8字节,对齐数8 → 最大对齐数8
};
// 总大小:16字节struct Outer {int arr[2]; // 元素大小4,对齐数4struct Inner in; // 嵌套结构体,对齐数8short s; // 2字节,对齐数2
};
// 总大小:32字节
0-7 : [arr] // int[2] (4×2=8字节)
8-15 : [in.c] // Inner.c
16-23: [in.d] // Inner.d
24-25: [s] // short(2)
26-31: 填充 // 总大小32(最大对齐数8的倍数)
2.2 为什么存在内存对齐?
两大核心原因:
平台兼容性
- 不是所有硬件都能访问任意地址上的任意数据
- 某些硬件平台只能在特定地址访问特定类型数据
- 未对齐访问可能导致硬件异常
性能优化
-
对齐的内存访问只需一次操作
-
未对齐访问可能需要两次操作
-
示例:读取8字节double类型
对齐地址(8的倍数):一次读取完成 未对齐地址:可能跨越两个内存块,需要两次读取+数据拼接
空间优化技巧:
-
让占用空间小的成员尽量集中在一起
-
对比S1和S2:成员相同但大小不同,S2更优!
struct S1 { char c1; int i; char c2; }; // 12字节 struct S2 { char c1; char c2; int i; }; // 8字节
2.3 修改默认对齐数
使用 #pragma pack
指令
#include <stdio.h>#pragma pack(1) // 设置默认对齐数为1
struct S {char c1; // 大小1,对齐数min(1,1)=1int i; // 大小4,对齐数min(1,4)=1char c2; // 大小1,对齐数1
};
#pragma pack() // 恢复默认对齐数int main() {printf("%d\n", sizeof(struct S)); // 输出:6return 0;
}
使用场景
-
空间敏感场景:网络传输、嵌入式系统
-
兼容特定硬件:需要1字节对齐的设备
-
数据打包:文件存储格式定义
注意:修改对齐数可能导致性能下降,仅在必要时使用
3、结构体传参
#include <stdio.h>struct S {int data[1000]; // 4000字节数组int num; // 4字节
};void print1(struct S s) { // 传结构体值printf("%d\n", s.num);
}void print2(struct S* ps) { // 传结构体地址printf("%d\n", ps->num);
}int main() {struct S s = { {1,2,3,4}, 1000 };print1(s); // 传结构体print2(&s); // 传地址return 0;
}
运行:
1000
1000
上面的print1
和print2
函数哪个好些?
答案是:首选print2
函数
原因:
- 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
总结:结构体传参的时候,要传结构体的地址。
4、结构体实现位段
4.1 什么是位段
位段(Bit-field)是C语言中一种特殊的数据结构,它允许我们将多个变量紧凑地存储在同一个字节或字中,从而有效地节省内存空间。
位段的声明方式与结构体类似,但有两点关键区别:
-
位段成员必须是整型(如
int
、unsigned int
或signed int
),在C99标准中也可以使用其他类型 -
每个成员名后需要跟一个冒号和数字,表示该成员占用的位数
struct A {int _a:2; // 占用2位int _b:5; // 占用5位int _c:10; // 占用10位int _d:30; // 占用30位 };
4.2 位段的内存分配
位段在内存中的分配方式有以下特点:
- 空间按需分配,通常以4字节(int)或1字节(char)为单位开辟
- 位段成员在内存中的排列顺序(从左到右或从右到左)取决于编译器和平台
- 当一个位段无法容纳在剩余空间时,可能舍弃剩余位或利用下一单元,这由编译器决定
struct S {char a:3;char b:4;char c:5;char d:4;
};
在VS2013环境下,上述结构体可能的内存布局如下(假设从右向左分配):
- 第一个字节:
a
(3位) +b
(4位) + 1位剩余 - 第二个字节:
c
(5位) + 3位剩余 - 第三个字节:
d
(4位)
4.3 位段的跨平台问题
位段虽然节省空间,但存在严重的跨平台问题:
-
int
位段可能被视为有符号数或无符号数,标准未定义 -
位段最大位数不确定(16位机器最大16,32位机器最大32)
-
成员在内存中的分配方向(从左到右或从右到左)未标准化
-
当空间不足时,是舍弃剩余位还是利用下一单元,标准未定义
总结: 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4.4 位段的应用场景
位段特别适合用于需要精确控制数据位数的场景,如:
-
网络协议:IP数据报头部包含许多只需要几位表示的字段
-
硬件寄存器:与硬件寄存器位对应
-
嵌入式系统:内存受限的环境下节省空间
4.5 位段使用的注意事项
-
不能取地址:位段成员可能不位于字节起始位置(几个成员共有同一个字节),因此没有地址
-
输入处理:不能直接用
scanf
读取位段成员,需要通过临时变量中转 -
可移植性:注重可移植性的代码应避免使用位段
// 错误示例
scanf("%d", &sa._b); // 错误,不能取位段地址// 正确做法
int b = 0;
scanf("%d", &b);
sa._b = b;
总结:
位段是一种强大的内存优化工具,能够将多个小范围值紧凑地存储在内存中,特别适合内存受限的环境或需要精确控制数据位数的场景。
然而,其跨平台问题使得它在需要可移植性的应用中受到限制。在使用位段时,开发者需要在节省空间和保证可移植性之间做出权衡。