文章目录
- 前言
-
- 本文旨在系统性地介绍C语言中结构体的进阶知识。
- 一、结构体类型声明
-
- 1.1、嵌套结构体类型
- 1.2、结构体的自引用
- 1.3、typedef简化结构体类型
- 1.4、匿名结构体类型
-
- 1.4.1、匿名结构体类型唯一
- 1.4.2、匿名结构体作为成员
- 二、结构体数组
- 三、结构体内存对齐
-
- 3.1、对齐规则
- 3.2、存在内存对齐原因
- 3.3、结构体设计技巧
- 3.4、修改默认对齐数
- 四、结构体的位段(位域)成员
-
- 4.1、位段的内存分配
- 4.2、位段的跨平台不确定因素
- 4.3、位段使用的注意事项
- 4.4、位段的特殊写法
前言
本文旨在系统性地介绍C语言中结构体的进阶知识。
一、结构体类型声明
1.1、嵌套结构体类型
在一个结构体中使用另一个结构体类型作为成员。 代码示例:
//学生有生日,生日包含年/月/日 → 把"生日"单独设计成一个结构体,再放进"学生"结构体里。
// 第一步:定义日期结构体
struct Date
{
int year;
int month;
int day;
};
// 第二步:在学生结构体中嵌套日期结构体
struct Student
{
char name[20];
struct Date birthday; // 嵌套
char id[20];
};
int main()
{
// 嵌套结构体的初始化
struct Student s = {"aoyun", {2008, 8, 8}, "2026010102"};
// 成员的访问
printf("⽣⽇:%d-%d-%d\\n",s.birthday.year,s.birthday.month,s.birthday.day);
return 0;
}
1.2、结构体的自引用
结构体内部包含一个指向同类型结构体的指针。 代码示例:
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点(自引用结构体)
struct Node
{
int data;
struct Node* next; // 指向下一个节点
};
int main()
{
// 创建 3 个节点
struct Node n1, n2, n3;
// 存储数据
n1.data = 1;
n2.data = 2;
n3.data = 3;
// 连接成链表
n1.next = &n2; // n1 指向 n2
n2.next = &n3; // n2 指向 n3
n3.next = NULL; // n3 是最后一个节点
// 遍历链表并打印
struct Node* p = &n1;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
// 输出:1 2 3
return 0;
}
1.3、typedef简化结构体类型
写法1:定义结构体 + typedef 分开写 代码示例:
// 先定义结构体
struct Student {
char name[20];
int age;
};
// 再用 typedef 起别名
typedef struct Student Student;
// 使用
Student s1; // 等价于 struct Student s1;
写法2:定义结构体的同时使用 typedef 代码示例:
typedef struct Student {
char name[20];
int age;
} Student;
// 使用
Student s1;
1.4、匿名结构体类型
匿名结构体是指省略了结构体标签(tag) 的结构体类型。它没有名字,因此通常只能在定义时直接创建变量。 代码示例:
// 普通结构体(有名字)
struct X
{
int a;
char b;
float c;
};
struct X x; // 可以多次使用
// 匿名结构体(没有名字)
struct
{
int a;
char b;
float c;
} x; // 只能使用这一次
使用 typedef 重命名后,它就变成了一个可复用的类型,解决了匿名结构体的限制。 代码示例:
// 使用 typedef 给匿名结构体重命名
typedef struct
{
int a;
char b;
float c;
} X; // X 是新的类型名
X x1; // 创建全局变量 x1(类型 X)
X arr[20]; // 创建数组
X *p = &x1; // 类型匹配
int main()
{
X x2; // 创建局部变量 x2
return 0;
}
1.4.1、匿名结构体类型唯一
即使两个匿名结构体的成员完全相同,编译器也视为不同类型,不能相互赋值。 代码示例:
struct
{
int a;
char b;
float c;
} x; // 创建变量 x,类型是“匿名结构体类型A”
struct
{
int a;
char b;
float c;
}*p = &x; // 错误,类型A ≠ 类型B
1.4.2、匿名结构体作为成员
情况1:匿名结构体成员直接展开 代码示例:
struct S
{
int a;
char b;
struct // 匿名结构体(未创建变量)
{
float c;
int d;
};
};
int main()
{
struct S s;
s.a = 10;
s.c = 3.5f; // 直接访问,成员 c 和 d 会“上浮”成为外层结构体的成员。
s.d = 100;
return 0;
}
情况2:匿名结构体作为具名成员 代码示例:
struct S
{
int a;
char b;
struct // 创建变量 x
{
float c;
int d;
} x;
};
int main()
{
struct S s;
s.a = 10;
s.x.c = 3.5f; // 需要通过 x 访问
s.x.d = 100;
return 0;
}
二、结构体数组
结构体数组就是元素类型为结构体的数组,用于存储多个结构体数据。 代码示例:
// 定义日期结构体
struct Date
{
int year;
int month;
int day;
};
// 定义学生结构体(包含嵌套结构体)
typedef struct Student
{
char name[20];
struct Date birthday; // 嵌套结构体
char id[20];
} Student;
int main()
{
// 结构体数组初始化
Student stu_arr[3] =
{
{"zhangsan", {2008, 8, 8}, "2026010101"},
{"lisi", {2009, 1, 10}, "2026010102"},
{"wangwu", {2009, 3, 20}, "2026010103"}
};
// 遍历访问结构体数组
for (int i = 0; i < 3; i++)
{
printf("name:%s\\tbirth:%d-%d-%d\\tid:%s\\n",stu_arr[i].name,
stu_arr[i].birthday.year,stu_arr[i].birthday.month,stu_arr[i].birthday.day,
stu_arr[i].id);
}
return 0;
}
三、结构体内存对齐
结构体内存对齐是指:编译器为了提升 CPU 访问内存的效率,会在结构体成员之间插入“填充字节”(padding),使得每个成员的起始地址都位于其“对齐边界”上。
内存对齐是用少量内存填充字节(空间),换取 CPU 一次读取完成(时间)。
3.1、对齐规则
规则1:结构体的第1个成员对齐到0偏移处(结构体起始位置) 规则2:从第2个成员开始,每个成员对齐到其对应的对齐数的整数倍偏移处,对齐数 = min(编译器默认对齐数, 成员大小) 规则3:结构体的总大小必须是最大对齐数(包含嵌套结构体的最大对齐数)的整数倍 规则4:嵌套的结构体类型成员对齐到其自身最大对齐数的整数倍偏移处
VS 默认对齐数固定为 8,Linux环境中GCC 没有默认对齐数(对齐数 = 成员自身大小)。
代码示例:
#include <stdio.h>
struct s1
{
double d; // 大小 8,对齐数 = min(8, 8) = 8
// 放在偏移 0(第1个成员,从0开始)
char c; // 大小 1,对齐数 = min(8, 1) = 1
// 放在偏移 8
int i; // 大小 4,对齐数 = min(8, 4) = 4
// 放在偏移 12
// 当前已占:0~15(共 16 字节)
// 最大对齐数 = max(8, 1, 4) = 8
// 总大小必须是 8 的倍数 → 16 已是 8 的倍数
};
struct s2
{
char c1; // 大小 1,对齐数 = min(8, 1) = 1
// 放在偏移 0
struct s1 s1_member;// 嵌套结构体,对齐数 = 自身最大对齐数 = 8
// 放在偏移 8
// s1_member 内部占 16 字节(偏移 8~23)
double d; // 大小 8,对齐数 = min(8, 8) = 8
// 放在偏移 24,占 8 字节(24~31)
// 当前已占:0~31(共 32 字节)
// 最大对齐数 = max(1, 8, 8) = 8
// 总大小必须是 8 的倍数 → 32 已是 8 的倍数
};
int main()
{
struct s1 s1 = {0};
struct s2 s2 = {0};
printf("sizeof(struct s1) = %zu\\n", sizeof(struct s1)); // 16
printf("sizeof(struct s2) = %zu\\n", sizeof(struct s2)); // 32
return 0;
}
3.2、存在内存对齐原因
平台原因(移植性):某些 CPU 只能从特定地址读取特定类型的数据,否则会抛出硬件异常 性能原因(效率):CPU 以总线宽度(4/8 字节)为粒度发起内存访问,对齐的数据一次读完,未对齐的数据需要多次读取+拼接
| CPU总线宽度 | CPU与内存之间一次传输的数据位数 | 硬件 | 决定 CPU 能一次读多少数据 |
| 默认对齐数 | 编译器设定的对齐上限 | 编译器 | 决定结构体成员最大按多少字节对齐 |
默认对齐数 ≤ CPU总线宽度,这样才能保证对齐后的数据不会超过 CPU 一次读取的能力。
3.3、结构体设计技巧
将占用空间小的成员集中放在一起,可以有效减少填充字节,节省内存。
核心原理:char 类型对齐数为 1,可以放在任意偏移位置,不会产生填充。将多个 char 放在一起,它们会连续排列,共用同一个填充“间隙”。 代码示例:
#include <stdio.h>
struct S1
{
char c1; // 偏移 0
int i; // 偏移 4(对齐数4,偏移1~3填充)
char c2; // 偏移 8
};
// 总大小:12 字节(最大对齐数4,12是4的倍数,9→12)
struct S2
{
char c1; // 偏移 0
char c2; // 偏移 1(对齐数1,紧挨着)
int i; // 偏移 4(对齐数4,偏移2~3填充)
};
// 总大小:8 字节(最大对齐数4,8是4的倍数)
3.4、修改默认对齐数
#pragma pack(n) 可以临时修改默认对齐数,控制结构体的紧凑程度。pack(1) 消除填充节省空间,但可能降低访问速度;用完后及时 #pragma pack() 恢复默认。
代码示例:
#include <stdio.h>
#pragma pack(1) // 设置默认对齐数为 1
struct S
{
char c1; // 对齐数 = min(1, 1) = 1
int i; // 对齐数 = min(1, 4) = 1
char c2; // 对齐数 = min(1, 1) = 1
};
#pragma pack() // 取消设置,恢复默认对齐数
int main()
{
printf("%zu\\n", sizeof(struct S)); // 6(1+4+1,无填充)
return 0;
}
四、结构体的位段(位域)成员
在结构体定义中,如果成员名后面加上一个冒号和数字,就称为位段成员。这个数字表示该成员占用的比特位数,而不是字节数。
C语言标准规定:C89/C90:位段成员的类型必须是 int、unsigned int 或 signed int。C99:新增支持 _Bool 类型。
现代编译器的扩展,大多数编译器(GCC、Clang、MSVC)在实际实现中放宽了类型限制,额外支持:char、unsigned char、short、unsigned short、long、unsigned long、long long、unsigned long long、枚举类型(enum)。 注意:这些扩展支持取决于具体编译器,不是C标准强制要求的。如果追求可移植性,建议只使用标准规定的类型。
代码示例:
struct A {
unsigned int a : 2; // 占用2位
unsigned int b : 5; // 占用5位
unsigned int c : 10; // 占用10位
unsigned int d : 30; // 占用30位
};
4.1、位段的内存分配
①成员类型:位段成员可以是 int、unsigned int、signed int 或 char 等类型(现代编译器通常支持更丰富的类型)。
②按类型申请空间:成员为 unsigned int / signed int 时,每次按 4 字节申请内存空间。成员为 unsigned char / signed char 时,每次按 1 字节申请内存空间。按需扩展,当前单元不够用时申请下一个单元。
③实现依赖:位段对内存的使用细节(填充、跨单元、顺序等)高度依赖编译器实现,C标准未作严格规定。
代码示例:
//分配方向:从低位到高位分配
// 剩余位策略:当前单元剩余位不够时,舍弃填充,申请新单元
#include <stdio.h>
// 定义位段结构体
// 成员类型为 unsigned char → 每次按 1 字节(8位)申请空间
struct S
{
unsigned char a : 3; // a 占用 3 位(bit0~bit2)
unsigned char b : 4; // b 占用 4 位(bit3~bit6),当前字节剩余5位,足够存放
unsigned char c : 5; // c 占用 5 位,当前字节只剩1位(bit7),不够
// → 申请第2个字节,c 从新字节的 bit0 开始存放
// → 第1字节剩余 bit7 被填充废弃
unsigned char d : 4; // d 占用 4 位,当前字节(第2字节)剩余3位(bit3~bit5),不够
// → 申请第3个字节,d 从新字节的 bit0 开始存放
// → 第2字节剩余 bit3~bit7 被填充废弃
};
// 总大小 = 3 字节
int main() {
struct S s = {0}; // 将所有字节初始化为 0
// 赋值注意:值不能超过位段能容纳的范围
// a 占 3 位,最大能存 2^3 – 1 = 7
s.a = 10; // 10 = 二进制 1010,但 a 只有 3 位 → 截断为 010(即 2)
// b 占 4 位,最大能存 2^4 – 1 = 15
s.b = 12; // 12 = 二进制 1100,完整存入 b 的 4 位
// c 占 5 位,最大能存 2^5 – 1 = 31
s.c = 3; // 3 = 二进制 00011,完整存入 c 的 5 位
// d 占 4 位,最大能存 2^4 – 1 = 15
s.d = 4; // 4 = 二进制 0100,完整存入 d 的 4 位
return 0;
}
4.2、位段的跨平台不确定因素
| 符号性 | int 位段被当作有符号还是无符号,由编译器决定 | 语义(数据解释) |
| 最大位数 | 16位机器最大16位,32位机器最大32位,写成27在16位机器上会出问题 | 语义(数据范围) |
| 分配顺序 | 从高位还是低位开始分配,标准未定义 | 布局(顺序) |
| 剩余位处理 | 当前单元剩余位不够时,是填充废弃还是跨单元利用,标准未定义 | 布局(填充/跨单元) |
注:①填充:剩余位被废弃不用,称为填充。②跨单元:剩余位不够时,编译器可选择将成员放入下一个单元(舍弃剩余位),也可选择跨单元存储(部分在当前单元,部分在下一个单元)。③顺序:小端系统从低位(bit0)开始分配,大端系统从高位(bit7)开始分配。
位段能节省内存,但存在大量实现定义(编译器必须在文档中说明)行为,跨平台性差。注重可移植性的程序应避免使用位段,或针对平台做专门处理。
4.3、位段使用的注意事项
①sizeof 返回的是字节数,而位段成员可能只占几个 bit,不够 1 个字节。C 标准规定 sizeof 不能作用于位段成员。
②内存中每个字节分配一个地址,而字节内部的 bit 位没有独立地址。位段成员可能从某个字节的中间位置开始,没有对应的地址,因此不能使用 & 操作符。
代码示例:
struct A {
unsigned int _a : 2;
unsigned int _b : 5;
unsigned int _c : 10;
unsigned int _d : 30;
};
int main() {
struct A sa = {0};
// printf("%zu\\n", sizeof(sa._a)); // 错误,位段成员不能作为 sizeof 的操作数
// scanf("%d", &sa._b); // 错误,不能对位段成员取地址
//正确,先读入普通变量,再赋值给位段成员
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
4.4、位段的特殊写法
① 无名位段(跳过指定位数) 作用:在当前存储单元中跳过指定位数,不分配给任何成员。跳过的位被填充废弃,且无法被程序访问。
代码示例:
struct Example {
unsigned char a : 4; // 占低4位
unsigned char : 4; // 匿名位段,跳过4位(不命名,无法访问)
unsigned char b : 8; // 从下一个字节开始
};
② 零宽度位段(强制对齐到下一个存储单元) 作用:告诉编译器当前存储单元剩余位全部废弃,下一个成员从新的存储单元起始位置开始分配。
代码示例:
struct Example2
{
unsigned char a : 4; // 占当前字节的低4位
unsigned char : 0; // 零宽度位段:强制结束当前存储单元
unsigned char b : 1; // 从新存储单元(下一个字节)开始
unsigned char c : 8; // 再占1个字节
};
网硕互联帮助中心


评论前必须登录!
注册