云计算百科
云计算领域专业知识百科平台

C语言:结构体进阶

文章目录

  • 前言
    • 本文旨在系统性地介绍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个字节
};

赞(0)
未经允许不得转载:网硕互联帮助中心 » C语言:结构体进阶
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!