在C语言中,基础数据类型(如int
、char
、float
等)难以描述复杂对象(如学生、链表节点等)。结构体(Struct)作为自定义类型的核心,允许我们将不同类型的数据组合成一个整体,极大地扩展了C语言的描述能力。本文将从结构体的声明、初始化、内存对齐,到结构体传参和位段实现,全面解析这一重要知识点。
一、结构体类型的声明
结构体是“值的集合”,其成员可以是不同类型的变量。声明结构体的本质是定义一种新的“数据类型”,后续可通过该类型创建变量。
1.1 基本声明格式
结构体的声明语法如下:
struct tag {
member-list; // 成员列表(类型+变量名)
} variable-list; // 可选:直接声明变量
其中:
tag
是结构体标签(自定义名称),用于标识结构体类型;member-list
是结构体成员,由多个不同类型的变量组成;variable-list
是可选的,可在声明结构体时直接创建变量。
示例:描述学生信息
struct Stu {
char name[20]; // 姓名
int age; // 年龄
char sex[5]; // 性别
char id[20]; // 学号
}; // 分号不能省略!
上述代码声明了一个名为struct Stu
的结构体类型,后续可通过该类型创建变量(如struct Stu s;
)。
1.2 特殊声明:匿名结构体
声明结构体时可省略标签(tag
),称为“匿名结构体”。匿名结构体仅能在声明时创建变量,无法后续复用(除非通过typedef
重命名)。
示例:匿名结构体的使用
// 匿名结构体类型,直接创建变量x
struct {
int a;
char b;
float c;
} x;
// 另一个匿名结构体,成员与上面相同
struct {
int a;
char b;
float c;
} a[20], *p;
⚠️ 注意:编译器会将上述两个匿名结构体视为完全不同的类型,因此以下代码是非法的:
p = &x; // 警告:类型不兼容
1.3 结构体的自引用
结构体中可以包含指向自身类型的指针(用于实现链表、树等数据结构),但不能直接包含自身类型的变量(会导致无限递归,内存大小无法计算)。
错误示例:直接包含自身变量
struct Node {
int data;
struct Node next; // 错误!结构体大小会无限增大
};
正确示例:通过指针自引用
struct Node {
int data; // 数据域
struct Node* next; // 指针域:指向同类型结构体
};
结合typedef的自引用
使用typedef
重命名结构体时,需避免在匿名结构体内部提前使用新名称:
// 错误写法:匿名结构体内部无法识别Node
typedef struct {
int data;
Node* next; // 错误!Node尚未定义
} Node;
// 正确写法:先声明带标签的结构体
typedef struct Node {
int data;
struct Node* next; // 正确:使用struct Node
} Node; // 重命名为Node
二、结构体变量的创建和初始化
声明结构体类型后,可通过该类型创建变量并初始化。初始化方式分为“按顺序初始化”和“指定成员初始化”。
2.1 按顺序初始化
按结构体成员的声明顺序依次赋值,适用于成员较少或顺序明确的场景。
示例代码
#include <stdio.h>
struct Stu {
char name[20];
int age;
char sex[5];
char id[20];
};
int main() {
// 按成员顺序初始化
struct Stu s = {"张三", 20, "男", "20230818001"};
// 访问成员并打印
printf("name: %s\n", s.name); // name: 张三
printf("age : %d\n", s.age); // age : 20
printf("sex : %s\n", s.sex); // sex : 男
printf("id : %s\n", s.id); // id : 20230818001
return 0;
}
2.2 指定成员初始化
通过.成员名
指定初始化的成员,顺序可以任意,未指定的成员会被初始化为0(或空)。
示例代码
int main() {
// 按指定成员初始化(顺序无关)
struct Stu s2 = {
.age = 18,
.name = "lisi",
.id = "20230818002",
.sex = "女"
};
// 访问成员并打印
printf("name: %s\n", s2.name); // name: lisi
printf("age : %d\n", s2.age); // age : 18
printf("sex : %s\n", s2.sex); // sex : 女
printf("id : %s\n", s2.id); // id : 20230818002
return 0;
}
三、结构成员访问操作符
访问结构体成员需使用以下两种操作符:
.
:用于直接访问结构体变量的成员(结构体变量.成员名
);->
:用于通过指针访问结构体成员(结构体指针->成员名
)。
示例代码
struct Stu s = {"张三", 20, "男", "20230818001"};
struct Stu* ps = &s;
// 直接访问
printf("name: %s\n", s.name); // 使用.操作符
// 指针访问
printf("age: %d\n", ps->age); // 使用->操作符
四、结构体内存对齐(核心考点)
计算结构体大小是C语言的高频考点,其核心是内存对齐规则。理解对齐规则需先明确“偏移量”(成员地址与结构体起始地址的差值)和“对齐数”(成员自身大小与编译器默认对齐数的较小值)。
4.1 对齐规则
- 结构体第一个成员的偏移量为0;
- 其他成员的偏移量必须是其“对齐数”的整数倍(对齐数 = min(编译器默认对齐数, 成员大小));
- 结构体总大小是所有成员“最大对齐数”的整数倍;
- 嵌套结构体时,嵌套成员的偏移量需是其内部最大对齐数的整数倍,总大小是所有最大对齐数(含嵌套)的整数倍。
⚠️ 编译器默认对齐数:
- VS中默认是8;
- Linux gcc中默认无对齐数(对齐数 = 成员自身大小)。
4.2 实战练习:计算结构体大小
通过以下练习理解对齐规则:
练习1
struct S1 {
char c1; // 大小1,对齐数min(8,1)=1,偏移量0
int i; // 大小4,对齐数min(8,4)=4,偏移量4(前空3字节)
char c2; // 大小1,对齐数1,偏移量8
};
// 最大对齐数是4,总大小需为4的整数倍 → 12(8+1=9,向上取12)
printf("%d\n", sizeof(struct S1)); // 输出:12
练习2
struct S2 {
char c1; // 偏移量0
char c2; // 偏移量1(对齐数1)
int i; // 对齐数4,偏移量4(前空2字节)
};
// 最大对齐数4,总大小8(4+4=8)
printf("%d\n", sizeof(struct S2)); // 输出:8
练习3(含double类型)
struct S3 {
double d; // 大小8,对齐数8(VS中),偏移量0
char c; // 对齐数1,偏移量8
int i; // 对齐数4,偏移量12(8+1=9,空3字节到12)
};
// 最大对齐数8,总大小16(12+4=16)
printf("%d\n", sizeof(struct S3)); // 输出:16
练习4(嵌套结构体)
struct S4 {
char c1; // 偏移量0
struct S3 s3; // S3最大对齐数8,偏移量8(空7字节)
double d; // 对齐数8,偏移量8+16=24
};
// 最大对齐数8,总大小32(24+8=32)
printf("%d\n", sizeof(struct S4)); // 输出:32
4.3 为什么需要内存对齐?
内存对齐是“空间换时间”的设计:
- 平台兼容性:部分硬件仅能访问特定地址的数据(如只能从4的倍数地址读int);
- 性能优化:对齐数据可减少CPU访问次数(未对齐数据可能需要2次访问)。
4.4 优化结构体空间
为节省空间,应将占用空间小的成员集中存放。例如:
// 优化前:大小12
struct S1 { char c1; int i; char c2; };
// 优化后:大小8(成员c1和c2集中)
struct S2 { char c1; char c2; int i; };
4.5 修改默认对齐数
使用#pragma pack(n)
可修改默认对齐数(n为1、2、4、8等),#pragma pack()
还原默认。
示例代码
#include <stdio.h>
#pragma pack(1) // 设置默认对齐数为1(无对齐)
struct S {
char c1; // 偏移量0
int i; // 偏移量1(对齐数1)
char c2; // 偏移量5
};
#pragma pack() // 还原默认
// 总大小:1+4+1=6
printf("%d\n", sizeof(struct S)); // 输出:6
五、结构体传参
结构体传参有两种方式:传值(结构体变量)和传地址(结构体指针)。
5.1 两种传参方式对比
struct S {
int data[1000]; // 大数组,占用较多空间
int num;
};
struct S s = {{1,2,3,4}, 1000};
// 方式1:传值(复制整个结构体)
void print1(struct S s) {
printf("%d\n", s.num);
}
// 方式2:传地址(仅复制指针)
void print2(struct S* ps) {
printf("%d\n", ps->num);
}
int main() {
print1(s); // 传值:开销大(复制1004*4字节)
print2(&s); // 传地址:开销小(仅复制指针大小)
return 0;
}
5.2 结论
优先选择传地址:函数传参时参数需压栈,结构体过大时传值会导致严重的时间和空间开销。
六、结构体实现位段
位段(Bit Field)是结构体的特殊形式,用于按位分配内存,可极大节省空间(适用于只需要少数bit的场景)。
6.1 位段的声明
位段声明与结构体类似,但成员后多了“:数字
”(表示占用的bit数):
struct A {
int _a:2; // 占用2个bit
int _b:5; // 占用5个bit
int _c:10; // 占用10个bit
int _d:30; // 占用30个bit
};
6.2 位段的内存分配
- 成员类型通常为
int
、unsigned int
、char
等; - 空间按4字节(int)或1字节(char) 开辟;
- 未用完的bit可能被复用或舍弃(取决于编译器)。
示例:计算位段大小
// struct A的成员总bit数:2+5+10+30=47bit
// 按4字节(32bit)开辟:第1个32bit存_a(2)+_b(5)+_c(10)=17bit,剩余15bit不够存_d(30);
// 再开辟第2个32bit存_d(30),总大小为8字节。
printf("%d\n", sizeof(struct A)); // 输出:8
6.3 位段的跨平台问题
位段依赖编译器实现,存在以下跨平台风险:
int
位段的符号性不确定(有符号/无符号);- 最大bit数不确定(16位机器最大16,32位机器最大32);
- 成员分配方向不确定(从左到右或从右到左);
- 剩余bit的复用规则不确定。
⚠️ 注重可移植性的程序应避免使用位段。
6.4 位段的应用场景
位段适合存储“只需少量bit”的信息,例如网络协议(如IP数据报):
- IP数据报中“版本号”仅需4bit,“标志”仅需3bit;
- 使用位段可减少数据报大小,降低网络传输开销。
6.5 位段使用注意事项
位段成员没有独立地址(内存以字节为单位分配地址),因此:
- 不能用
&
取位段成员的地址; - 不能直接用
scanf
输入位段成员(需先存到变量再赋值)。
示例代码
struct A {
int _a:2;
int _b:5;
};
int main() {
struct A sa = {0};
// scanf("%d", &sa._b); // 错误!不能取位段地址
// 正确方式:先输入到变量
int b = 0;
scanf("%d", &b);
sa._b = b; // 赋值给位段成员
return 0;
}
总结
结构体是C语言描述复杂数据的核心工具,掌握其声明、初始化和内存对齐是基础;结构体传参应优先传地址以优化性能;位段作为结构体的扩展,能高效节省空间,但需注意跨平台问题。理解这些知识点,不仅能应对面试考点,更能在实际开发中写出高效、规范的代码。