结构体(struct)是由一系列相同或不同类型的数据构成的数据集合。它与 int、char 这类基础数据类型类似,但更为灵活,允许你将多种数据组合成一个新的自定义类型,以便于管理和使用。
在实际的嵌入式项目中,结构体无处不在。由于C语言无法直接操作数据库,开发者常利用结构体来封装一系列属性,将大量数据组织在内存中,从而完成对数据的存储和操作。当我们面对需要多种数据类型共同描述一个实体的场景时,结构体就变得至关重要。例如,一个学生的信息需要学号(字符串)、姓名(字符串)、年龄(整型)等,这些数据类型不同却同属一个整体,结构体正是解决这个问题的理想工具。
结构体在函数中的作用不仅仅是简化代码,其核心价值在于封装。良好的封装提高了代码的复用性,使用者无需关心内部实现细节,只需按照定义使用即可。
需要注意的是,结构体的大小并非其各成员大小的简单相加。出于性能考虑,现代计算机(如32位CPU)通常以4字节为单位高效地存取数据。因此,如果结构体中每个成员的首地址都是特定字节数(如4字节)的整数倍,那么访问效率会更高,这就是内存对齐的由来。
编译器通常有默认的“对齐系数”。开发者可以通过预编译指令 #pragma pack(n) 来修改这个系数,其中 n 可以是1、2、4、8、16等。
内存对齐规则
- 数据成员对齐规则:结构体或联合体的第一个数据成员放在偏移量(offset)为0的位置。之后每个数据成员的对齐,将按照
#pragma pack 指定的数值和该成员自身长度两者中较小的那个值进行。
- 整体对齐规则:在所有数据成员完成各自对齐后,结构体或联合体本身也要进行对齐。对齐将按照
#pragma pack 指定的数值和结构体中最大数据成员长度两者中较小的那个值进行。
- 推论:当
#pragma pack 的 n 值大于或等于所有数据成员的长度时,这个 n 值将不起任何作用。
C语言的结构体用于描述对象的状态(属性),但不能包含函数来描述行为。而在C++中,为了保持与C语言的过渡连续性,结构体被扩展为可以包含函数,此时它具有了类的功能,但其成员函数默认是 public 访问权限,与 class 的 private 默认权限不同。
结构体声明
下面是一个声明结构体类型的例子:
//声明一个结构体
struct book
{
char title[MAXTITL]; //一个字符串表示的titile 题目;
char author[MAXAUTL]; //一个字符串表示的author作者;
float value; //一个浮点型表示的value价格;
}; //注意分号不能少,这也相当于一条语句;
这个声明描述了一个由两个字符数组和一个 float 变量组成的结构体模板。它本身并未创建任何数据对象,只是定义了这类数据的组织形式。结构体声明也被称为模板,它勾勒了数据存储的蓝图。
声明解析:
- 使用关键字
struct,表明接下来是一个结构体定义。
- 跟随一个可选的标志(或称“标签”,此处是
book),用于后续快速引用该结构体类型。例如:struct book library;。
- 花括号
{} 内是成员列表,每个成员以自己的声明方式描述,以分号结束。
- 结束花括号后的分号表示结构体定义结束。

作用域:
结构体声明的位置决定了其作用域。如果声明在任何函数之外,则其标记在本文件内该声明之后的所有函数中都可以使用。如果声明在某个函数内部,则其标记只能在该函数内部,且必须在声明之后使用。

关于标志名(标签)的省略:
何时可以省略标志名?当你在一个地方定义结构体类型,在另一个地方定义该类型的变量时,必须使用标志名。如果定义结构体类型的同时就创建变量,那么标志名可以省略,但这种定义是一次性的,无法在其他地方复用该类型。
一般格式为:
struct 结构体名 {
成员变量;
};
C语言结构体定义的三种方式
-
最标准的方式(类型说明与定义分开):
#include <stdio.h>
struct student //结构体类型的说明与定义分开。声明
{
int age; /*年龄*/
float score; /*分数*/
char sex; /*性别*/
};
int main ()
{
struct student a = {20, 79, 'f'}; //定义
printf("年龄:%d 分数:%.2f 性别:%c\n", a.age, a.score, a.sex);
return 0;
}
-
声明时直接定义变量(不推荐,复用性差):
#include <stdio.h>
struct student /*声明时直接定义*/
{
int age; /*年龄*/
float score; /*分数*/
char sex; /*性别*/
/*这种方式不环保,只能用一次*/
} a = {21, 80, 'n'};
int main ()
{
printf("年龄:%d 分数:%.2f 性别:%c\n", a.age, a.score, a.sex);
}
-
直接定义结构体变量(无类型名,最不推荐):
#include <stdio.h>
struct //直接定义结构体变量,没有结构体类型名。这种方式最烂
{
int age;
float score;
char sex;
} t = {21, 79, 'f'};
int main ()
{
printf("年龄:%d 分数:%f 性别:%c\n", t.age, t.score, t.sex);
return 0;
}
定义结构体变量
结构体类型的定义只是告诉编译器数据的表示方法,并不会分配内存。要使用结构体,必须创建变量,即结构体变量。
struct book library; 这条语句创建了一个 struct book 类型的变量 library,此时编译器才按照 book 模板为其分配内存空间。
分析:
struct book 在声明中的作用类似于 int 这样的基础类型名。struct book s1, s2, *ss; 定义了两个结构体变量和一个指向该结构体的指针。
struct book library; 等效于:
struct book{
char …;
….;
…..;
} library;
第一种写法可以减少重复代码量。
标志符何时可以省略?
情况一:
struct {
char title[MAXTITL];
char author[MAXAUTL];
float value;
} library;
这里直接创建了结构体变量 library,编译器会分配内存。这本质上是将声明类型和定义变量合并了,且成员未初始化。由于没有类型名,此结构体“模板”无法在其他地方复用。
情况二:使用 typedef 为已有类型(包括结构体)定义新名称。
一般格式:typedef 已有类型 新类型名;
typedef int Elem;
typedef struct {
int date;
.....
} STUDENT;
STUDENT stu1, stu2; // 使用新类型名定义变量

结构体变量定义总结
- 先定义类型,后定义变量:
struct 结构体名 变量名列表;
struct book s1, s2, *ss; // 需先定义struct book类型
- 定义类型的同时定义变量:
struct 结构体名 {
成员列表;
} 变量名列表; // 结构体名可以省略,但不建议
- 直接定义结构体类型变量(即方式二省略结构体名):
这种方式无法指明结构体类型名,因此该类型无法重复使用。除非再次书写完全相同的 struct { ... } 定义。
结构体变量的初始化
初始化结构体变量与初始化数组类似,使用花括号括起逗号分隔的初始化列表。每个初始化项目必须与对应成员的类型匹配。
struct book s1 = { //对结构体初始化
"yuwen", //title为字符串
"guojiajiaoyun", //author为字符数组
22.5 //value为float型
};
注意:
C99标准支持指定初始化(Designated Initializers),可以不按顺序初始化成员,但某些编译器(特别是嵌入式领域的旧版本编译器)可能不支持。

访问结构体成员
使用结构成员运算符点(.) 访问成员:结构体变量名.成员名。
该运算符结合性为自左向右,优先级最高。
printf(“%s\n%s\n%f”, s1.title, s1.author, s1.value); //访问结构体变量元素
s1.value 是 float 类型,可以像普通 float 变量一样使用。
注意 scanf(“%d”, &s1.value); 中,. 的优先级高于 &,因此无需括号。
嵌套访问:如果成员本身是结构体类型,可通过多级 . 运算符访问。
结构体变量名.成员.子成员……最低一级子成员;
struct date {
int year;
int month;
int day;
};
struct student {
char name[10];
struct date birthday;
} student1;
// 引用出生年份:student1.birthday.year;
结构体变量的整体与分开操作
- 整体赋值:允许将同类型的一个结构体变量整体赋值给另一个。
- 禁止整体I/O:不能将结构体变量作为一个整体进行输入 (
scanf) 或输出 (printf),必须分别指明其各成员。

小结:除“同类型结构体变量可整体赋值”外,其他情况下不能整体引用,必须对成员分别引用。
结构体长度与内存对齐
数据类型的字节数因编译器位数而异(常见16位/32位/64位)。思考下例结构体占几个字节?
typedef struct {
char addr;
char name;
int id;
} PERSON;
通过 printf("PERSON长度=%d字节\n", sizeof(PERSON)); 可看到结果为 8字节,而非 1+1+4=6 字节。这就是内存对齐的结果。

内存对齐实验
- 定义字符数组并初始化:
char ss[20] = {0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29};
- 用
PERSON 指针指向该数组:
PERSON *ps = (PERSON *)ss;
- 打印成员:
printf(“0x%02x,0x%02x,0x%02x\n”, ps->addr, ps->name, ps->id);
printf(“PERSON长度=%d字节\n”, sizeof(PERSON));

可以看到 addr 和 name 各占1字节,但因 id 是 int 型需要4字节对齐,所以编译器在它们之后插入了2字节的“空洞”(Padding),然后才是 id 的值 0x17161514。

调整成员顺序以优化空间:
如果将结构体定义为:
typedef struct {
char addr;
int id;
char name;
} PERSON;
输出显示总长度变为 12字节。


如果定义为:
typedef struct {
int id;
char addr;
char name;
} PERSON;
输出显示总长度变回 8字节。


结论:合理安排结构体成员的顺序,可以节省内存空间。通常将要求对齐字节数大的成员(如 int, double)放在前面,对齐字节数小的成员(如 char)放在后面。
如果所有成员都是 char 型,则会按照1字节对齐,总大小就是成员数量之和。
typedef struct {
char addr;
char name;
char id;
} PERSON;


结构体嵌套
结构体可以嵌套另一个结构体。
typedef struct {
char addr;
char name;
int id;
} PERSON;
typedef struct {
char age;
PERSON ps1;
} STUDENT;
通过指针和数组进行内存布局分析:
STUDENT *stu = (STUDENT *)ss;
printf(“0x%02x,0x%02x,0x%02x,0x%02x\n”, stu->ps1.addr, stu->ps1.name, stu->ps1.id, stu->age);
printf(“STUDENT长度=%d字节\n”, sizeof(STUDENT));


调整 STUDENT 成员顺序:
typedef struct {
PERSON ps1;
char age;
} STUDENT;


嵌套结构体访问遵循规律:
// 方式一:定义时同时创建变量
struct A {
struct B {
int c;
} b;
} a;
a.b.c = 10;
// 方式二:定义内部结构体B后,再定义B的变量
struct A {
struct B {
int c;
};
struct B sb;
} a;
a.b.c = 11; // 访问匿名结构体成员b
a.sb.c = 22; // 访问具名结构体变量sb
结构体占用内存空间总结
- 结构体定义不分配内存,定义变量时才分配。
- 大小通常是各成员大小之和,但受内存对齐影响。对齐系数通常是4字节(32位机)或8字节(64位机),以提高访问效率。可使用
#pragma pack(n) 修改对齐方式,n=1 时即为紧凑排列。
- 和C++类不同,C语言结构体不能在定义时直接初始化成员变量(C99的指定初始化除外)。
为什么函数参数常使用结构体指针?
当函数参数较多时,使用多个独立参数会导致代码重复和调用繁琐。
int handle_video(char *name, long address, int size, time_t time, int alg);
更优雅的方式是使用结构体封装所有相关参数:
struct video_info {
char *name;
long address;
int size;
int alg;
time_t time;
};
int handle_video(struct video_info *vinfo);
使用指针的好处:
- 避免数据拷贝:当结构体较大时,传值(
struct video_info vinfo)会导致整个结构体数据被复制到函数栈帧中,开销大。传递指针只复制一个地址,效率更高。
- 允许函数修改实参:如果函数需要修改结构体内容,必须使用指针。
- 代码简洁:调用时只需传递一个结构体变量的地址,大大简化了代码。
因此,即使函数(如 handle_video, send_video)不需要修改参数内容,从性能角度考虑,也常使用 const struct video_info *vinfo 形式的指针参数。
嵌入式开发中的位结构体(位域)
在嵌入式开发中,位结构体常用于高效表示硬件寄存器状态或进行紧凑的数据通信。它允许我们按位(bit)来定义结构体成员的长度。
1. 位结构体类型设计
typedef struct symbol_struct {
uint_32 SYMBOL_TYPE : 5; // 5位
uint_32 reserved_1 : 4; // 4位
uint_32 SYMBOL_NUMBER : 7; // 7位
uint_32 SYMBOL_ACTIVE : 1; // 1位
uint_32 SYMBOL_INDEX : 8; // 8位
uint_32 reserved_2 : 8; // 8位
} SYMBOL_STRUCT, *SYMBOL_STRUCT_PTR;
关键点:成员的数据类型(如 uint_32)决定了编译器分配内存的基本单位,而后面的位域指定了在该单位内使用的有效位数。

观察发现,前5个成员共占33位,超过了4字节(32位),因此 reserved_2 被分配到了新的地址空间(0x1fff0830)。同时,reserved_2 虽然只用了8位,但因其类型是 uint_32,所以实际占用了4个字节。
2. 修改数据类型为 uint_8
typedef struct symbol_struct {
uint_8 SYMBOL_TYPE : 5;
uint_8 reserved_1 : 4;
uint_8 SYMBOL_NUMBER : 7;
uint_8 SYMBOL_ACTIVE : 1;
uint_8 SYMBOL_INDEX : 8;
uint_8 reserved_2 : 8;
} SYMBOL_STRUCT, *SYMBOL_STRUCT_PTR;
将类型改为 uint_8(1字节)后,编译器以1字节为单位进行分配。

此时,reserved_2 只占用1个字节。SYMBOL_TYPE 和 reserved_1 共9位,超过1字节,因此它们被分配在两个不同的字节地址上。
3. 分析位域的紧凑性
当所有位域的总位数不超过其基础类型(如 uint_32)的大小时,编译器会尝试将它们紧凑地排列在同一块内存中。

此例中,所有位域加起来正好32位,因此它们被紧密打包在4个字节内。通过联合体(union)和直接赋值可以验证位域的跨越存储。
typedef union {
uint32 WORD;
struct {
uint32 LAYER_LEVEL : 2;
uint32 OBJECT_ACTIVE_CUR : 4;
uint32 OBJECT_ACTIVE_PRE : 4; // 赋值为 0b1111
uint32 NUMBER_ACTIVE : 4; // 赋值为 0b0101
// ... 其他位域
} BIT;
} STATUS_STRUCT;

可以看到 OBJECT_ACTIVE_PRE 和 NUMBER_ACTIVE 的位域在内存中是连续紧凑存放的,可能跨越字节边界。
4. 混合类型与空隙
如果位域的基础类型混合了 uint_8 和 uint_32,或者位域无法紧凑放入当前分配单元,就会产生“空隙”。

图中 RESERVED 成员已不在前4字节的地址空间内,说明中间存在空隙。
5. 结构体/联合体组合的影响
外层是普通结构体还是联合体,并不影响内部位结构体的内存分配策略。


总结与注意事项
- 空间分配单位:位域的基础数据类型(
uint_8、uint_32等)决定了编译器分配内存的最小单位。位域只是指在该单位内使用的有效位数。
- 紧凑存储:编译器会尽可能将位域紧密排列在当前分配的内存单位内,直到放不下,再分配新的单位。
- 位域大小限制:位域长度不能超过其基础类型的位数(如
uint_8 test : 15 会编译报错)。
- 平台与编译器差异:最重要的一点:位结构体的具体内存布局高度依赖于编译器、操作系统以及编译选项。上述结论基于特定环境(CodeWarrior 10.2, MQX 3.8)得出,在其他环境下可能不同。在涉及跨平台或硬件直接操作时,务必通过调试工具(如查看变量地址)仔细验证内存布局。
掌握结构体,特别是理解其内存对齐和位域机制,对于编写高效、紧凑的嵌入式C代码至关重要。如果你想了解更多关于内存管理或底层开发的细节,可以关注云栈社区,那里有更多深入的讨论和资源分享。