一、结构体类型的声明:打造自己的数据模板
结构体的本质是 "数据模板"—— 它定义了一组数据的组合方式,但本身不占用内存空间。声明一个结构体的基本语法如下:
struct 标签名
{
成员类型 成员名;
//更多成员...
} 变量列表; //分号不能省略!
1.1 基本声明:描述具体对象
比如我们要描述一个学生,需要包含姓名、年龄、性别和学号,结构体声明如下:
struct Stu
{
char name[20]; //姓名
int age; //年龄
char sex[5]; //性别
char id[20]; //学号
}; //分号必须有!
这里的struct Stu就是一个结构体类型,我们可以用它来创建具体的变量。
1.2 特殊声明:匿名结构体
如果结构体只需要使用一次,可以省略 "标签名",称为匿名结构体:
struct
{
int a;
char b;
float c;
} x; //直接创建变量x,该结构体类型无法再次使用
注意:匿名结构体的类型是唯一的,即使成员完全相同,也会被编译器视为不同类型。例如下面的代码是非法的:
struct { int a; char b; } x;
struct { int a; char b; } *p;
p = &x; // 错误!编译器认为两者类型不同
1.3 结构体自引用:构建链表等递归结构
在实现链表、树等数据结构时,我们需要结构体中包含自身类型的成员,这称为自引用。但注意不能直接包含结构体本身(会导致无限嵌套,大小无法计算),而应该用指针:
//错误示例:包含自身变量,大小无限大
struct Node
{
int data;
struct Node next; //错误!
};
//正确示例:包含自身指针
struct Node
{
int data;
struct Node* next; //指针大小固定(4或8字节),合理
};
如果用typedef给结构体重命名,需避免匿名结构体提前使用新名称:
//错误示例:匿名结构体中提前使用Node
typedef struct
{
int data;
Node* next; //此时Node还未定义
} Node;
//正确示例:先声明带标签的结构体
typedef struct Node
{
int data;
struct Node* next;
} Node; //正确,Node是struct Node的别名
二、结构体变量的创建和初始化
定义好结构体类型后,就可以创建变量并初始化了,主要有两种方式:
2.1 按成员顺序初始化
按结构体中成员的声明顺序依次赋值:
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
int main()
{
//按顺序初始化:姓名、年龄、性别、学号
struct Stu s = {"张三", 20, "男", "20230818001"};
//访问成员(用.操作符)
printf("姓名:%s,年龄:%d\n", s.name, s.age);
return 0;
}
2.2 指定成员初始化
可以通过成员名指定赋值,顺序不限:
int main()
{
//指定成员初始化,顺序可以打乱
struct Stu s2 = {.age = 18, .name = "李四", .id = "20230818002", .sex = "女"};
printf("姓名:%s,年龄:%d\n", s2.name, s2.age); //输出"李四,18"
return 0;
}
三、重点:结构体内存对齐
结构体的大小计算是 C 语言的高频考点,也是难点。为什么struct {char a; int b;}的大小不是1+4=5字节?这就要理解内存对齐的规则。
3.1 对齐规则:决定成员的存储位置
内存对齐是编译器为了提高访问效率而采用的策略,具体规则如下:
- 第一个成员永远对齐到结构体起始位置(偏移量为 0)。
- 其他成员需对齐到 "对齐数" 的整数倍位置。
对齐数 = min (编译器默认对齐数,成员自身大小)。- VS 默认对齐数为 8,Linux(gcc)默认无对齐数(对齐数 = 成员大小)。
- 结构体总大小必须是所有成员 "最大对齐数" 的整数倍。
- 嵌套结构体时,嵌套的结构体成员需对齐到自身最大对齐数的整数倍;整个结构体的总大小是所有成员(含嵌套结构体)最大对齐数的整数倍。
3.2 实例分析:用图理解内存布局
我们结合例子,用内存布局图直观展示(以 VS 环境为例,默认对齐数 8)。
例 1:struct S1
struct S1
{
char c1; //大小1
int i; //大小4
char c2; //大小1
};
- 步骤 1:
c1是第一个成员,放在偏移量 0 处,占 1 字节(0~0)。 - 步骤 2:
i的对齐数 = min (8, 4) = 4,需放在 4 的整数倍位置(偏移量 4),占 4 字节(4~7)。 - 步骤 3:
c2的对齐数 = min (8, 1) = 1,放在 8(7+1)处,占 1 字节(8~8)。 - 总大小:最大对齐数是 4,需满足 4 的整数倍。当前已用 9 字节,向上取整到 12 字节。
偏移量:0 1 2 3 4 5 6 7 8 9 10 11
成员: c1 空 空 空 i i i i c2 空 空 空
sizeof(struct S1) = 12。
例 2:struct S2(成员顺序优化)
struct S2
{
char c1; //1字节
char c2; //1字节
int i; //4字节
};
- 步骤 1:
c1在 0(0~0),c2对齐数 1,接在 1(1~1)。 - 步骤 2:
i对齐数 4,放在 4(4~7)。 - 总大小:最大对齐数 4,总大小 8(0~7),刚好是 4 的倍数。
偏移量:0 1 2 3 4 5 6 7
成员: c1 c2 空 空 i i i i
sizeof(struct S2) = 8(比 S1 节省 4 字节,可见成员顺序的重要性)。
例 3:嵌套结构体 struct S4
struct S3
{
double d; //8字节
char c; //1字节
int i; //4字节
};
struct S4
{
char c1; //1字节
struct S3 s3; //嵌套S3
double d; //8字节
};
先算struct S3的大小:
d在 0(0~7),对齐数 8。c对齐数 1,放在 8(8~8)。i对齐数 4,需放在 12(8+1=9,最近的 4 的倍数是 12),占 12~15。- 最大对齐数 8,总大小需是 8 的倍数(当前 16 字节,满足)。所以
sizeof(S3)=16。
再算struct S4:
c1在 0(0~0)。s3是嵌套结构体,最大对齐数 8,需放在 8 的倍数(偏移 8),占 8~23(16 字节)。d对齐数 8,放在 24(23+1=24,8 的倍数),占 24~31(8 字节)。- 最大对齐数 8,总大小 32(31-0+1=32,是 8 的倍数)。
偏移量:0 1-7 8-23(s3) 24-31(d)
成员: c1 空 S3的16字节 d的8字节
sizeof(S4)=32。
3.3 为什么需要内存对齐?
- 平台兼容性:某些硬件只能访问特定地址的数据(如只能从 4 的倍数地址读 int),否则抛出异常。
- 性能优化:对齐的内存只需 1 次访问,未对齐可能需要 2 次。例如:若 int 放在偏移 1 处,处理器需先读 0~3 字节,再读 4~7 字节,拼接后才能得到完整 int。
本质是用空间换时间的策略。
3.4 如何优化:平衡对齐和空间
要减少内存浪费,应将小成员集中在一起(如 S2 比 S1 节省空间)。例如:
//差:小成员分散
struct Bad
{
char a; //1
double b; //8
char c; //1
}; //大小:24(0+8+8+8,最大对齐数8)
//好:小成员集中
struct Good
{
char a; //1
char c; //1
double b; //8
}; //大小:16(0+1+6空+8,总16)
3.5 修改默认对齐数
用#pragma pack(n)可以修改默认对齐数(n 为 1、2、4、8 等),#pragma pack()恢复默认。
#pragma pack(1) //设置默认对齐数1(无对齐)
struct S
{
char c1; //1
int i; //4
char c2; //1
};
#pragma pack() //恢复默认
此时对齐数 = 成员大小,总大小 = 1+4+1=6 字节。
四、结构体传参:传地址更优
结构体传参时有两种方式:传值和传地址。推荐传地址:
struct S
{
int data[1000];
int num;
};
//传值:复制整个结构体,开销大
void print1(struct S s)
{
printf("%d", s.num);
}
//传地址:只复制指针(4/8字节),高效
void print2(struct S* ps)
{
printf("%d", ps->num);
}
原因:函数传参时参数需压栈,结构体过大时压栈开销大,导致性能下降。
总结
结构体是 C 语言中自定义复杂类型的核心工具,掌握以下几点:
- 声明时注意匿名结构体的限制和自引用的正确方式(用指针)。
- 初始化支持按顺序和指定成员两种方式。
- 内存对齐是重点:理解 4 条规则,通过实例画图分析大小,记住 "空间换时间" 的本质,合理安排成员顺序优化空间。
- 传参优先传地址,减少开销。
通过练习结构体大小计算,能更深入理解内存对齐的细节,应对考试和实际开发中的问题。
2444

被折叠的 条评论
为什么被折叠?



