在 C 语言中,结构体(struct)是一种非常重要的自定义数据类型,它允许我们将不同类型的数据组合在一起,形成一个有机的整体。无论是在嵌入式开发、数据结构实现还是日常编程中,结构体都扮演着不可或缺的角色。本文将从结构体的基础概念出发,逐步深入到变量的创建与初始化,并重点剖析结构体中一个容易被忽视但至关重要的特性 ——内存对齐,通过图示化的方式帮助大家彻底理解其原理与计算方法。

一、结构体类型:自定义数据的 “容器”

结构体的本质是一种 “数据打包” 机制,它可以将 int、char、float 等基本数据类型,甚至其他结构体类型组合起来,定义出符合实际需求的复杂数据类型。

1.1 结构体类型的定义格式

结构体的定义遵循以下语法:

struct 结构体名 {

数据类型1 成员变量1;

数据类型2 成员变量2;

// ... 更多成员变量

};

  • struct:关键字,用于声明结构体类型;
  • 结构体名:自定义的类型名称(如Student、Book),遵循 C 语言标识符规则;
  • 大括号内:结构体的 “成员变量”,每个成员都有独立的数据类型和名称;
  • 注意:结构体定义的末尾必须加上分号(;),这是容易遗漏的细节。

1.2 示例:定义一个 “学生” 结构体

以学生信息为例,我们需要存储学号(int)、姓名(char 数组)、成绩(float),可以定义如下结构体:

// 定义结构体类型struct Student

struct Student {

int id; // 学号(4字节,32位系统下)

char name[20]; // 姓名(20字节)

float score; // 成绩(4字节)

};

这里的struct Student并不是一个变量,而是一个数据类型模板,它规定了该类型变量应包含的成员及各成员的类型。

二、结构体变量的创建与初始化

有了结构体类型模板后,我们就可以像使用 int、char 等基本类型一样,创建结构体变量并为其初始化。

2.1 结构体变量的创建方式

结构体变量的创建主要有三种方式,根据实际场景选择即可:

方式 1:先定义类型,再创建变量(最常用)

先通过struct 结构体名定义类型,再用该类型声明变量:

// 步骤1:定义结构体类型

struct Student {

int id;

char name[20];

float score;

};

// 步骤2:创建结构体变量(s1、s2为struct Student类型的变量)

struct Student s1;

struct Student s2;

方式 2:定义类型的同时创建变量

在结构体定义的末尾直接声明变量,适用于变量较少的场景:

struct Student {

int id;

char name[20];

float score;

} s1, s2; // 定义类型时直接创建s1、s2变量

方式 3:匿名结构体(无结构体名,仅创建变量)

如果不需要重复使用该结构体类型(仅创建一次变量),可以省略结构体名,即 “匿名结构体”:

struct { // 无结构体名,仅一次性创建s变量

int id;

char name[20];

float score;

} s;

注意:匿名结构体无法后续再创建新的变量,因为没有类型名可以引用。

2.2 结构体变量的初始化

结构体变量的初始化与数组类似,通过 “初始化列表”({})为成员变量赋值,遵循 “顺序匹配” 或 “指定成员” 的规则。

方式 1:按成员顺序初始化(推荐,简洁)

初始化列表中的值按结构体成员的定义顺序依次赋值:

// 按id→name→score的顺序初始化s1

struct Student s1 = {2023001, "Zhang San", 95.5};

  • 字符串赋值:name是 char 数组,直接用字符串常量"Zhang San"赋值即可;
  • 未完全初始化:如果初始化列表中的值少于成员数量,未赋值的成员会被默认初始化为 0(数值类型)或空字符(字符类型):

// id=2023002,name未赋值(默认空字符串),score=0.0

struct Student s2 = {2023002};

方式 2:指定成员初始化(灵活,不受顺序限制)

C99 标准支持通过 “成员名: 值” 的方式指定成员赋值,顺序可以任意调整:

// 不按顺序,直接指定成员赋值

struct Student s3 = {

.score = 88.0, // 先赋值score

.id = 2023003, // 再赋值id

.name = "Li Si" // 最后赋值name

};

这种方式的优势在于:当结构体成员较多时,可读性更强,且后续修改成员顺序时无需调整初始化列表。

2.3 结构体成员的访问

创建并初始化变量后,通过 “变量名.成员名” 的方式访问或修改成员变量:

struct Student s1 = {2023001, "Zhang San", 95.5};

// 访问成员:输出s1的学号和成绩

printf("ID: %d, Score: %.1f\n", s1.id, s1.score); // 输出:ID: 2023001, Score: 95.5

// 修改成员:将s1的成绩改为98.0

s1.score = 98.0;

printf("Updated Score: %.1f\n", s1.score); // 输出:Updated Score: 98.0

三、重点:结构体的内存对齐(原理 + 图示)

当我们用sizeof(struct 结构体名)计算结构体的内存大小时,会发现一个有趣的现象:结构体的总大小不等于各成员变量大小之和。例如,下面的结构体:

struct Test {

char a; // 1字节

int b; // 4字节

};

如果按 “成员之和” 计算,大小应为1+4=5字节,但实际用sizeof(struct Test)计算的结果却是8字节。这背后的原因,就是 C 语言为了提高内存访问效率而设计的内存对齐机制

3.1 内存对齐的核心规则

要理解结构体的内存大小,需掌握以下 3 条核心规则(以 32 位系统为例,默认对齐系数为 4):

规则 1:成员变量的对齐

结构体中每个成员变量的起始地址,必须是该成员 “自身大小” 的整数倍。

  • 例如:char类型(1 字节)的起始地址可以是任意地址(1 的整数倍);int类型(4 字节)的起始地址必须是 4 的整数倍(如 0、4、8、12...);float类型(4 字节)同理。
规则 2:结构体的整体对齐

结构体的总大小,必须是 “结构体中最大成员大小” 的整数倍(或对齐系数的整数倍,取两者较小值,默认对齐系数为 4)。

  • 例如:若结构体中最大成员是int(4 字节),则总大小必须是 4 的整数倍;若最大成员是double(8 字节),且对齐系数为 4,则总大小取 4 的整数倍。
规则 3:填充字节(Padding)

当成员变量的起始地址不满足 “规则 1” 时,编译器会在两个成员之间自动插入 “填充字节”,确保后续成员的地址符合对齐要求。填充字节不存储任何有效数据,仅用于占位。

3.2 示例 1:单成员 + 填充(struct Test1)

先看一个简单的例子,理解 “成员对齐” 和 “填充字节”:

struct Test1 {

char a; // 1字节

int b; // 4字节

};

内存布局图示(地址从 0 开始):

地址

0

1

2

3

4

5

6

7

内容

a

填充

填充

填充

b

b

b

b

说明

存储 char a(1 字节)

填充 3 字节(因 int b 需从 4 的整数倍地址开始)

存储 int b(4 字节,占 4-7 地址)

分析过程:
  1. 成员 a 的布局:char a占 1 字节,起始地址 0(1 的整数倍,符合规则 1),存储在地址 0
  2. 成员 b 的对齐:int b需从 4 的整数倍地址开始,但地址 1 不符合(1 不是 4 的整数倍),因此编译器在地址 1-3 插入 3 个填充字节。
  3. 成员 b 的布局:int b从地址 4 开始存储,占 4-7 共 4 字节(符合规则 1)。
  4. 整体对齐:结构体中最大成员是int(4 字节),总大小 8 字节(8 是 4 的整数倍,符合规则 2)。
  5. 最终大小:sizeof(struct Test1) = 8字节(1 字节 a + 3 字节填充 + 4 字节 b)。

3.3 示例 2:多成员 + 整体对齐(struct Test2)

再看一个包含 3 个成员的例子,完整覆盖 3 条规则:

struct Test2 {

char a; // 1字节

short b; // 2字节

int c; // 4字节

};

内存布局图示:

地址

0

1

2

3

4

5

6

7

内容

a

填充

b

b

c

c

c

c

说明

存储 char a(地址 0)

填充 1 字节(short b 需从 2 的整数倍地址开始)

存储 short b(地址 2-3)

存储 int c(地址 4-7)

分析过程:
  1. 成员 a:地址 0(1 字节,符合规则 1),占 0 地址。
  2. 成员 b:short(2 字节)需从 2 的整数倍地址开始,地址 1 不符合,填充 1 字节(地址 1),b存储在地址 2-3(符合规则 1)。
  3. 成员 c:int(4 字节)需从 4 的整数倍地址开始,地址 4 符合,存储在 4-7(符合规则 1)。
  4. 整体对齐:最大成员是int(4 字节),总大小 8 字节(8 是 4 的整数倍,符合规则 2)。
  5. 最终大小:sizeof(struct Test2) = 8字节(1a + 1 填充 + 2b + 4c)。

3.4 示例 3:成员顺序影响总大小(关键!)

结构体的总大小不仅取决于成员类型,还与成员的定义顺序密切相关。调整成员顺序,可能会减少填充字节,从而减小结构体的总大小。

反例:顺序不合理,填充字节多

struct BadOrder {

char a; // 1字节

int b; // 4字节

char c; // 1字节

};

内存布局:

地址

0

1-3

4-7

8

9-11

内容

a

填充 3

b

c

填充 3

大小

1 + 3 + 4 + 1 + 3 = 12 字节

正例:顺序优化,填充字节少

调整成员顺序,将相同大小的成员放在一起:

struct GoodOrder {

char a; // 1字节

char c; // 1字节

int b; // 4字节

};

内存布局:

地址

0

1

2-3

4-7

内容

a

c

填充 2

b

大小

1 + 1 + 2 + 4 = 8 字节

结论:

通过优化成员顺序(将小字节成员集中在一起),结构体的总大小从 12 字节减小到 8 字节,节省了 4 字节内存。这在大量创建结构体变量(如结构体数组)时,能显著减少内存占用。

3.5 如何修改默认对齐系数?

C 语言允许通过#pragma pack(n)预处理指令修改默认对齐系数(n 为 2 的幂,如 1、2、4、8),格式如下:

#pragma pack(2) // 设置对齐系数为2

struct Test3 {

char a; // 1字节

int b; // 4字节

};

#pragma pack() // 恢复默认对齐系数

此时sizeof(struct Test3)的结果为6字节(1a + 1 填充 + 4b,总大小 6 是 2 的整数倍),而非默认的 8 字节。

注意:修改对齐系数需谨慎,过小的对齐系数(如 1)会消除填充字节,节省内存,但可能降低 CPU 的内存访问效率;过大的对齐系数则会增加填充字节,浪费内存。通常建议使用默认对齐系数(4 或 8),仅在内存资源极度紧张的场景(如嵌入式开发)中调整。

四、总结

  1. 结构体类型:是自定义的数据类型模板,通过struct关键字定义,包含多个不同类型的成员变量。
  2. 变量创建与初始化:支持 “先定义类型再创建变量”“定义类型时创建变量”“匿名结构体” 三种方式;初始化可按顺序或指定成员,未赋值成员默认初始化为 0。
  3. 内存对齐核心:为提高访问效率,成员需按 “自身大小” 对齐,结构体总大小需按 “最大成员大小” 对齐,中间通过填充字节补位。
  4. 优化技巧:合理调整成员顺序(小字节成员集中),可减少填充字节,降低内存占用。

掌握结构体的内存对齐,不仅能帮助我们理解sizeof的计算结果,更能在实际开发中(如内存敏感场景)写出更高效、更节省内存的代码。希望本文的图示和示例能让你对结构体的理解更上一层楼!

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐