前言
在之前的笔记中,我们学习了数组(包括多维数组和指针数组)在存储同类型数据时的用法。但实际开发中,常需要记录由不同类型数据组成的 “整体信息”—— 比如学生的姓名(字符串)、年龄(整数)、性别(字符)。这时,用多个数组分别存储会显得零散且难管理,而结构体正是为解决这类 “异构数据整合” 问题而生的。这篇笔记将通过具体例子,讲讲结构体如何整合不同类型数据,以及它的定义、使用和初始化方法。
结构体:异构数据的整合工具
📌 从数组的局限看结构体的优势
若要记录 3 个学生的姓名、年龄、性别,用数组需要定义多个不同类型的数组,例如:
#include<stdio.h>
#include<string.h>
int mymain(void)
{
char names[3][20]; //存储姓名(字符串数组)
int ages[3]; //存储年龄(整数数组)
char sex[3]; //存储性别(字符数组)
// 记录第一个学生信息
strcpy(names[0], "zhangsan");
ages[0] = 18;
sex[0] = 'M';
return 0;
}
这种方式的缺点很明显:每类信息需要一个独立数组,新增信息(如成绩)就得再定义数组,且数据之间的关联性被割裂(很难直观看出 “names [0]、ages [0]、sex [0] 属于同一个学生”)。
而结构体可以将这些不同类型的信息 “打包” 成一个整体,让数据管理更清晰。用结构体实现同样功能:
补充strcpy的作用:复制整个字符串(含
'\0'
)
#include<stdio.h>
#include<string.h>
// 定义结构体类型,描述学生的组成信息
struct Student{
char name[20]; //姓名(字符串)
int age; //年龄(整数)
char sex; //性别(字符)
int score; //成绩(整数,新增信息更方便)
};
int mymain(void)
{
struct Student a; //声明一个Student类型的结构体变量a
// 为结构体成员赋值
strcpy(a.name, "zhangsan"); //字符串赋值需用strcpy
a.age = 18;
a.sex = 'M';
a.score = 60;
return 0;
}
对比可见,结构体通过struct Student
将学生的各类信息整合在一起,新增信息只需在结构体中添加成员,且通过 “结构体变量。成员”(如a.age
)能直观访问,数据关联性更强。
🔗 结构体与数组的结合:批量管理同类对象
若要记录多个学生的信息,可将结构体与数组结合,定义 “结构体数组”—— 数组的每个元素都是一个结构体变量。例如:
#include<stdio.h>
#include<string.h>
struct Student{
char name[20];
int age;
char sex;
int score;
};
int mymain(void)
{
struct Student students[3]; //包含3个Student结构体的数组
// 为第一个学生赋值
strcpy(students[0].name, "zhangsan");
students[0].age = 18;
students[0].sex = 'M';
students[0].score = 60;
return 0;
}
🔗 结构体数组:批量管理同类对象的优势
对比前面 “用struct Student a
定义单个结构体变量” 的方式,这里使用结构体数组(如struct Student students[3]
)的好处很明显:通过[0]、[1]、[2]
的索引编号,就能分别表示 3 个学生,无需再定义b、c
等多个变量。这种方式既保留了结构体 “整合信息” 的特性,又借助数组的 “连续存储” 优势,让批量管理同类对象(如多个学生、多个传感器)变得简洁高效。
📌 结构体变量与数组的类比理解
我们可以通过已学知识类比理解结构体:
char a
是单个字符变量,char a[]
是字符数组(多个字符的集合);- 同理,
struct Student a
是单个结构体变量(一个学生的完整信息),struct Student a[]
是结构体数组(多个学生的信息集合)。
这种类比能帮我们快速上手:结构体变量像 “单个复合数据单元”,结构体数组则是 “多个复合数据单元的连续集合”,和我们对 “变量” 与 “数组” 的基本认知逻辑一致。
📏 结构体的存储空间与内存对齐
既然结构体是 “定义的类型”,那么它和普通变量一样,会占据一定的存储空间。接下来我们以struct Student
为例,详细看看它的内存分布:
结构体Student
的成员及各自占用的字节数如下:
name[20]
:20 字节(字符串,含结束符);age
:4 字节(int 类型);sex
:1 字节(char 类型);score
:4 字节(int 类型)。
从图中能看到,结构体成员在内存中按定义顺序依次存储,但有个细节:sex
只占 1 字节,其后却留出了 3 字节的空闲空间,并没有紧接着存储score
。这并非浪费,而是编译器遵循 “四字节对齐原则” 的结果。
对于 32 位单片机来说,它读取数据时习惯访问 “4 的整数倍地址”(如 0x00、0x04、0x08)。如果score
紧跟在sex
后(即从 “20+4+1=25 字节” 处开始),其地址不是 4 的倍数,单片机需要分两次读取(先读 25-28 字节,再取其中有效部分),效率会降低。而留出 3 字节空闲后,score
从 28 字节(4 的 7 倍)开始存储,单片机一次就能读完整,大大提升访问效率。
✅ 验证结构体的存储与访问
为了验证上述特性,我们可以通过代码打印结构体成员的信息和地址:
#include<stdio.h>
#include<string.h>
struct Student{
char name[20];
int age;
char sex;
int score;
};
int mymain(void)
{
struct Student a;
strcpy(a.name, "zhangsan");
a.age = 18;
a.sex = 'M';
a.score = 60;
// 打印成员值,验证信息是否正确存储
printf("a's name : %s\n\r", a.name);
printf("a's age : %d\n\r", a.age);
printf("a's sex : %c\n\r", a.sex);
printf("a's score : %d\n\r", a.score);
// 打印成员地址,验证内存对齐
printf("a's name address : 0x%x\n\r", a.name); // name首地址(结构体起始地址)
printf("a's age address : 0x%x\n\r", &a.age); // age地址(紧跟name,20字节处,符合4字节对齐)
printf("a's sex address : 0x%x\n\r", &a.sex); // sex地址(紧跟age,24字节处)
printf("a's score address : 0x%x\n\r", &a.score); // score地址(28字节处,验证4字节对齐)
return 0;
}
我一开始误用
printf("a's sex : %s\n\r", a.sex);
时出现了错误,原因是a.sex = 'M'
赋值的是单个字符,需用%c
格式打印;若用%s
,则需赋值字符串(如a.sex = "M"
,此时"M"
包含结束符'\0'
,符合字符串格式)。
📝 结构体的定义与初始化方法
最后我们再梳理结构体的定义格式,加深记忆:
struct 结构体名{
数据类型 成员1;
数据类型 成员2;
...
数据类型 成员n;
};
针对事先定义好的struct Student
(包含name、age、sex
),以下是三种常用的初始化方法:
方式一:逐个成员初始化
适用于需要分步赋值或只初始化部分成员的场景:
struct Student student1;
strcpy(student1.name, "bb");
student1.age = 18;
student1.sex = 'M';
方式二:声明时按顺序初始化
适用于成员顺序明确,且需一次性初始化所有成员的场景:
struct Student student2 = {"mm", 18, 'F'};
方式三:指定成员初始化器(适用于 C99 及以上标准)
可灵活指定初始化的成员,无需严格按顺序,更清晰:
struct Student student3 = {
.name, "jj";
.age = 18;
.sex = 'F';
};
结尾
这篇笔记中,我们通过对比数组的局限,认识了结构体在整合异构数据时的优势:它能将不同类型的信息打包成一个整体,结合数组还能高效管理多个同类对象。同时,我们也了解了结构体的内存对齐特性(为提高访问效率)和三种初始化方法。
结构体在嵌入式开发中应用广泛 —— 比如描述传感器的 “型号(字符串)+ 采样值(浮点数)+ 状态(整数)”,或外设的 “地址(整数)+ 配置参数(结构体嵌套)” 等。掌握结构体,能让我们更自然地映射现实中的 “复杂对象”。
Hello_Embed 会继续带你探索结构体的进阶用法,比如结构体嵌套和结构体指针。下篇笔记见。