5. C语言 结构体
🧭 一、结构体的本质:把“相关的数据”放到一起
在C语言中,struct 是一种自定义数据类型,用来把多个不同类型的数据组织成一个整体。
struct Student {char name[20];int age;float score;
};
这就定义了一个“学生”类型,它包含:
- 一个字符串
name - 一个整型
age - 一个浮点数
score
✅ 定义变量的几种写法
struct Student s1; // 普通变量
struct Student s2 = {"Tom", 18, 95.5}; // 初始化
struct Student class[30]; // 结构体数组
struct Student *p = &s1; // 指针方式
🧩 二、结构体访问方式
有两种:
| 方式 | 写法 | 含义 |
|---|---|---|
| 普通访问 | s1.age | 访问变量的成员 |
| 指针访问 | p->age | 通过指针访问(等价于 (*p).age) |
struct Student s1 = {"Tom", 18, 95.5};
struct Student *p = &s1;printf("%s %d %.1f\n", s1.name, s1.age, s1.score);
printf("%s %d %.1f\n", p->name, p->age, p->score);
🧱 三、结构体的内存分布与对齐
结构体在内存中是按成员顺序连续存放的,但会有“对齐”规则。
✅ 示例:
struct Data {char a;int b;short c;
};
在32位系统中,可能占用 12字节 而不是7字节。
📊 内存布局图(假设4字节对齐):
| 成员 | 偏移量 | 实际字节 | 说明 |
|---|---|---|---|
a | 0 | 1 | char |
| 填充 | 1-3 | 3 | 对齐到4字节边界 |
b | 4-7 | 4 | int |
c | 8-9 | 2 | short |
| 填充 | 10-11 | 2 | 对齐到4字节倍数 |
结构体最终大小必须是最大对齐单位的整数倍(这里是4)。
⚙️ 嵌入式中应用:内存对齐影响性能
在 ARM Cortex-M 中:
- 非对齐访问可能触发 总线错误
- 或造成 性能下降
所以有时会用编译指令关闭对齐:
#pragma pack(1)
struct Data { char a; int b; short c; };
#pragma pack()
📌 结果: 按1字节对齐,总共7字节。
🧠 四、结构体与指针在嵌入式中的应用
在嵌入式中,结构体最常见用途是:
寄存器映射(Register Mapping)
✅ 例:GPIO外设寄存器映射
typedef struct {volatile unsigned int MODER; // 模式寄存器volatile unsigned int OTYPER; // 输出类型寄存器volatile unsigned int OSPEEDR; // 速度寄存器volatile unsigned int PUPDR; // 上下拉寄存器volatile unsigned int IDR; // 输入数据寄存器volatile unsigned int ODR; // 输出数据寄存器
} GPIO_TypeDef;
💡 然后用宏定义寄存器基地址:
#define GPIOA ((GPIO_TypeDef *)0x40020000)
操作寄存器就像操作结构体成员:
GPIOA->MODER |= (1 << 10); // 设置PA5为输出
GPIOA->ODR |= (1 << 5); // 点亮LED
是不是比直接写:
*(volatile unsigned int *)(0x40020000 + 0x14) |= (1 << 5);
更清晰、更安全?
🧮 五、结构体嵌套与数组
结构体可以嵌套使用:
struct Date {int year, month, day;
};struct Student {char name[20];struct Date birthday;
};
访问方式:
struct Student s = {"Tom", {2000, 5, 20}};
printf("%d-%d-%d\n", s.birthday.year, s.birthday.month, s.birthday.day);
结构体数组:
struct Student class[2] = {{"Tom", {2000, 5, 20}},{"Jerry", {2001, 7, 12}}
};
🧩 六、结构体与函数结合(传参、返回)
✅ 1️⃣ 值传递
void printStu(struct Student s) {printf("%s %d %.1f\n", s.name, s.age, s.score);
}
👉 会复制一份整个结构体。
✅ 2️⃣ 指针传递(嵌入式常用)
void printStu(const struct Student *s) {printf("%s %d %.1f\n", s->name, s->age, s->score);
}
👉 更高效,不复制数据。
✅ 3️⃣ 返回结构体
struct Point { int x, y; };struct Point makePoint(int x, int y) {struct Point p = {x, y};return p;
}
代码解析
-
结构体定义:
struct Point { int x, y; };定义了一个名为Point的结构体,包含两个整型成员x和y(可理解为一个二维坐标点)。 -
函数
makePoint:
函数作用是创建并返回一个Point类型的结构体实例:- 参数
x和y是坐标值; - 函数内部创建局部结构体变量
p,并用{x, y}初始化(成员x赋值为参数x,成员y赋值为参数y); - 通过
return p返回这个结构体。
- 参数
关键:结构体作为返回值的机制
在C语言中,函数可以直接返回结构体(值传递),这与返回基本类型(如 int、char)的逻辑类似:
- 当函数返回结构体时,编译器会将函数内部的局部结构体变量(如
p)完整复制一份,传递给函数调用处的接收变量。 - 函数执行结束后,内部的局部变量
p会被销毁,但返回的“副本”已经被保留,因此不会出现“访问已销毁内存”的问题(这一点与返回指针指向局部变量的错误写法完全不同)。
使用示例
可以在 main 函数中调用 makePoint 接收返回的结构体,并使用其成员:
#include <stdio.h>struct Point { int x, y; };struct Point makePoint(int x, int y) {struct Point p = {x, y};return p; // 返回结构体的副本
}int main() {// 调用函数获取返回的结构体,并用变量接收struct Point pt = makePoint(10, 20);// 使用结构体成员printf("坐标:(%d, %d)\n", pt.x, pt.y); // 输出:坐标:(10, 20)return 0;
}
优缺点分析
-
优点:
简单直接,无需手动管理内存(不像返回指针时可能需要malloc分配内存,后续还要free避免泄漏),适合小型结构体(如Point这种仅包含几个成员的类型)。 -
缺点:
如果结构体很大(例如包含多个数组或复杂成员),返回时的“复制操作”会产生性能开销(因为需要复制整个结构体的内容)。此时更推荐返回结构体指针(但需注意指针指向的内存生命周期,通常用malloc动态分配)。
总结:返回结构体的标准写法,对于小型结构体非常实用,且不存在内存安全问题。
🔧 七、typedef 简化写法
在嵌入式开发中,我们常用 typedef 给结构体起别名:
typedef struct {uint16_t id;char name[16];
} Device_t;
这样使用更简洁:
Device_t dev1 = {1001, "Sensor"};
🧠 八、结构体与内存通信
结构体常用于:
- UART数据帧解析
- SPI/I2C通信帧封装
- CAN报文结构体化
✅ 例:CAN数据包
typedef struct {uint32_t id;uint8_t data[8];uint8_t len;
} CAN_Frame_t;
封装数据:
CAN_Frame_t frame = {0x321, {1,2,3,4,5,6,7,8}, 8};
send_can_frame(&frame);
解包时:
void onCANReceive(const CAN_Frame_t *f) {printf("ID=0x%X, Len=%d\n", f->id, f->len);
}
🧩 九、结构体与位域(Bitfield)
嵌入式中常用位域来操作寄存器的某一位。
typedef struct {uint32_t EN : 1; // 使能uint32_t MODE : 2; // 模式uint32_t SPEED : 3; // 速度uint32_t RESERVED : 26;
} GPIO_CFG_t;
访问方式:
GPIO_CFG_t cfg;
cfg.EN = 1;
cfg.MODE = 3;
💡 对应寄存器位操作,非常直观。
✅ 十、总结总表
| 概念 | 说明 | 嵌入式意义 |
|---|---|---|
struct | 把不同类型数据组合在一起 | 封装硬件数据结构 |
typedef | 给结构体起别名 | 简化代码风格 |
| 指针访问 | 用 -> 操作成员 | 操作寄存器、通信结构 |
| 内存对齐 | 保证性能、兼容性 | 影响结构体大小 |
| 位域 | 按位操作寄存器 | 精确控制硬件位 |
| 嵌套 | 结构体中再包含结构体 | 模块化设计 |
结构体数组在定义的同时也可以进行初始化,并且与结构体变量的初始化规定相同,只能对全局的或静态存储类别的结构体数组初始化。这段话对吗
“结构体数组在定义的同时也可以进行初始化,并且与结构体变量的初始化规定相同,只能对全局的或静态存储类别的结构体数组初始化。”
这句话 部分正确,但不完全准确。
我们来系统分析一下 👇
🧩 一、结构体数组确实可以在定义时初始化 ✅
在 C 语言中,结构体数组可以像普通数组一样,在定义时直接初始化。
#include <stdio.h>struct Student {char name[10];int score;
};int main() {struct Student class[] = {{"Tom", 90},{"Lily", 85},{"Jack", 78}};for (int i = 0; i < 3; i++)printf("%s: %d\n", class[i].name, class[i].score);
}
✅ 输出:
Tom: 90
Lily: 85
Jack: 78
这里 class 是一个局部结构体数组(在函数内定义),
但仍然可以在定义时初始化。
👉 所以说“只能对全局或静态结构体数组初始化”是错误的。
🧠 二、问题的根源:编译期与运行期的区别
其实 C 语言对 “初始化” 的要求是:
只有在 定义时(编译期) 才能进行静态初始化。
如果变量已经定义完毕,则不能再赋初值。
举个例子:
struct Point { int x, y; };// ✅ 正确:定义时初始化
struct Point p1 = {10, 20};// ❌ 错误:不能在定义后初始化
struct Point p2;
p2 = {30, 40}; // ❌ 语法错误
结构体数组同理:
struct Point arr1[] = {{1,2}, {3,4}}; // ✅ 可以
struct Point arr2[2];
arr2 = {{1,2}, {3,4}}; // ❌ 不行
这与是不是“全局或静态”无关,而是取决于是否是定义时。
⚙️ 三、全局 / 静态结构体数组的确有“静态初始化”的特性 ✅
如果结构体数组是全局或静态变量(static),
它们会被放入 静态存储区(.data/.bss 段),
即使你不初始化,也会被自动清零。
struct Point {int x, y;
};struct Point globalArr[2]; // 全局变量,自动初始化为0int main() {static struct Point staticArr[2]; // 静态局部变量,也会自动初始化为0printf("%d %d\n", globalArr[0].x, staticArr[0].x); // 输出 0 0
}
而对于普通(自动)局部变量:
int main() {struct Point localArr[2]; // 自动变量,不会自动初始化!printf("%d\n", localArr[0].x); // ⚠️ 未定义行为!
}
✅ 四、正确说法总结
你那句话可以修改为更严谨的版本:
✅ 结构体数组在定义时可以进行初始化,且初始化方式与结构体变量相同。
全局或静态结构体数组未显式初始化时,会被自动初始化为零;
局部(自动)结构体数组则不会自动初始化。
📘 五、嵌入式实际例子
嵌入式中常见这样的定义:
typedef struct {uint8_t id;uint16_t period;void (*callback)(void);
} Task;Task taskTable[] = {{1, 1000, task1_handler},{2, 500, task2_handler},{3, 200, task3_handler}
};
✅ 这是合法的“静态初始化”,
因为 taskTable 在定义时已经被初始化。
但如果你写:
Task taskTable[3];
taskTable = { ... }; // ❌ 不允许
则编译报错。
🧭 结论总结表:
| 情况 | 能否初始化 | 默认值行为 |
|---|---|---|
| 全局结构体数组 | ✅ 定义时初始化 | 未初始化自动为 0 |
| 静态结构体数组 | ✅ 定义时初始化 | 未初始化自动为 0 |
| 局部结构体数组 | ✅ 定义时初始化 | 未初始化为随机值 |
| 定义后再赋初值 | ❌ 不允许 | - |
什么是静态结构体数组和局部结构体数组
非常好的问题!🎯
这个问题实际上涉及到 C语言中的存储类别(Storage Class) 和 变量生命周期。
理解这两个概念,对于嵌入式系统和系统编程至关重要,能帮助你更加明确数据的存储位置和生命周期,避免常见的内存管理错误。
🧭 一、静态结构体数组(Static Structure Array)
1️⃣ 什么是静态结构体数组?
静态结构体数组 是指那些声明时使用了 static 关键字的结构体数组。
它的 存储位置 位于 静态存储区,它的生命周期 贯穿整个程序的执行过程,即从程序启动到程序退出。
✅ 特性:
- 存储位置:静态存储区(也叫
.data或.bss段)。 - 生命周期:程序开始时分配,程序结束时释放。
- 默认初始化:如果没有显式初始化,自动初始化为零。
- 作用范围:局部静态变量作用域仍然限于定义它的函数内,但它的生命周期从程序开始一直持续到结束。
例子:
#include <stdio.h>typedef struct {int id;char name[20];
} Employee;void printEmployeeInfo() {static Employee employees[2] = {{1, "Alice"},{2, "Bob"}};// 自动初始化for (int i = 0; i < 2; i++) {printf("ID: %d, Name: %s\n", employees[i].id, employees[i].name);}
}int main() {printEmployeeInfo(); // 第一次调用printEmployeeInfo(); // 第二次调用,注意数组不会被重新初始化return 0;
}
关键点:
static关键字:让employees数组保持在静态存储区,并且在函数调用结束后不会销毁,而是保持它的值(不会被重新初始化)。- 生命周期:静态结构体数组
employees在程序的整个运行过程中都会存在。
输出:
ID: 1, Name: Alice
ID: 2, Name: Bob
ID: 1, Name: Alice
ID: 2, Name: Bob
解释:
第二次调用 printEmployeeInfo() 时,employees 数组并没有重新初始化,而是使用了第一次调用时的值。
2️⃣ 静态结构体数组的用途:
在嵌入式编程中,静态结构体数组非常常见,用于存储:
- 固定数量的配置数据。
- 状态机的状态。
- 系统级常量。
🧩 二、局部结构体数组(Automatic Structure Array)
1️⃣ 什么是局部结构体数组?
局部结构体数组 是指那些在 函数内部 声明的 普通结构体数组,并且没有使用 static 关键字。
它的 存储位置 位于 栈区,它的生命周期 仅限于函数调用期间,当函数调用结束时,局部数组就会被销毁。
✅ 特性:
- 存储位置:栈区(stack)。
- 生命周期:在函数调用时创建,函数调用结束时销毁。
- 默认初始化:不进行初始化时,栈内存中的值是随机的(即没有自动初始化)。
- 作用范围:只能在函数内部访问,函数外部无法访问。
例子:
#include <stdio.h>typedef struct {int id;char name[20];
} Employee;void printEmployeeInfo() {Employee employees[2] = {{1, "Alice"},{2, "Bob"}};for (int i = 0; i < 2; i++) {printf("ID: %d, Name: %s\n", employees[i].id, employees[i].name);}
}int main() {printEmployeeInfo(); // 第一次调用printEmployeeInfo(); // 第二次调用,数组被重新初始化return 0;
}
关键点:
- 栈区存储:
employees数组在栈区分配内存,并且在printEmployeeInfo函数执行完后会被销毁。 - 生命周期:每次调用
printEmployeeInfo()时,employees数组都会重新创建并初始化。
输出:
ID: 1, Name: Alice
ID: 2, Name: Bob
ID: 1, Name: Alice
ID: 2, Name: Bob
解释:
每次调用 printEmployeeInfo(),数组 employees 都会重新创建和初始化,且栈空间会在每次函数调用结束时释放。
2️⃣ 局部结构体数组的用途:
局部结构体数组常用于:
- 函数内部临时存储数据。
- 存储每次调用时需要初始化的、临时的结构体数据。
🧠 三、全局结构体数组 vs 静态结构体数组 vs 局部结构体数组
为了更好地理解它们的区别,我们总结了下面的对比表:
| 特性 | 全局结构体数组 | 静态结构体数组 | 局部结构体数组 |
|---|---|---|---|
| 存储位置 | 数据段(.data) | 数据段(.data) | 栈区 |
| 生命周期 | 整个程序运行期间 | 整个程序运行期间 | 函数调用期间 |
| 初始化 | 默认初始化为零(未初始化的会自动清零) | 自动清零或显式初始化 | 未初始化时随机值 |
| 访问 | 可以在程序的任何地方访问 | 只能在定义它的函数内访问 | 只能在定义它的函数内访问 |
| 示例 | struct Employee arr[10]; | static struct Employee arr[10]; | struct Employee arr[10]; |
🧩 四、嵌入式应用实例:寄存器映射
在嵌入式开发中,我们经常会定义结构体数组用于表示寄存器(如GPIO、SPI、I2C等),
而静态结构体数组则特别适合用于这种硬件寄存器映射,保证整个程序的生命周期内这些数据不被丢失。
#define GPIOA_BASE 0x40020000typedef struct {volatile uint32_t MODER; // 0x00: GPIO mode registervolatile uint32_t OTYPER; // 0x04: GPIO output type registervolatile uint32_t OSPEEDR; // 0x08: GPIO output speed registervolatile uint32_t PUPDR; // 0x0C: GPIO pull-up/pull-down register
} GPIO_TypeDef;#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE) // 寄存器基地址映射int main() {// 静态结构体数组static GPIO_TypeDef gpioPorts[3] = {{0x00, 0x00, 0x00, 0x00}, // GPIOA{0x00, 0x00, 0x00, 0x00}, // GPIOB{0x00, 0x00, 0x00, 0x00} // GPIOC};// 访问 GPIOA 寄存器GPIOA->MODER |= (0x01 << 10); // 配置PA5为输出return 0;
}
🧭 五、总结与最佳实践
| 类型 | 说明 | 最佳实践 |
|---|---|---|
| 全局结构体数组 | 生命周期:程序开始到结束;存储在 .data 或 .bss 中; | 用于存储程序中全局共享数据,如配置信息、硬件状态等。 |
| 静态结构体数组 | 生命周期:函数调用期间;存储在静态存储区;自动清零。 | 用于跨多次函数调用保持数据状态,如状态机、任务队列等。 |
| 局部结构体数组 | 生命周期:函数调用期间;存储在栈区;不可自动初始化。 | 用于函数内部的临时数据存储。 |
结构体初始化
本节我们会深入讨论:
- 结构体的初始化方式;
- 如何根据不同存储类别(局部变量、全局变量、静态变量)初始化结构体;
- 如何正确使用结构体初始化器(包括嵌套结构体、数组成员等);
- 如何解决初始化中可能遇到的问题。
🧭 一、结构体初始化概述
在 C 语言中,结构体初始化 就是为结构体的每个成员赋初值。
它可以在定义结构体时直接完成,也可以在定义后手动初始化。
1️⃣ 基本初始化
假设我们有一个简单的结构体类型:
struct Student {char name[20];int age;float score;
};
我们可以通过以下两种方式来初始化结构体:
// 方式1:定义时直接初始化
struct Student s1 = {"Tom", 18, 90.5};// 方式2:先定义,再初始化
struct Student s2;
s2.age = 20;
s2.score = 85.0;
strcpy(s2.name, "Jerry");
🧩 二、结构体成员初始化规则
结构体初始化时,成员的初始化顺序与成员声明顺序一致。
即,第一个成员先初始化,第二个成员其次,依此类推。
struct Student s1 = {"Tom", 18, 90.5};
这里:
"Tom"会初始化name;18会初始化age;90.5会初始化score。
🧠 默认初始化规则:
- 静态/全局结构体变量:如果没有显式初始化,成员会自动初始化为零(0 或 NULL)。
- 局部结构体变量:如果没有显式初始化,成员的值是随机的(垃圾值)。
🧭 三、嵌套结构体的初始化
如果结构体中有另一个结构体成员,你可以采用递归方式初始化。
示例:嵌套结构体初始化
struct Date {int day, month, year;
};struct Student {char name[20];struct Date birthdate; // 嵌套结构体float score;
};// 定义并初始化
struct Student s1 = {"Tom", {20, 5, 1990}, 85.5};
这里:
birthdate是一个struct Date类型的嵌套结构体,我们可以在初始化s1时同时为birthdate的day,month,year赋值。
🧩 四、初始化方式的不同
1️⃣ 按成员顺序初始化(按顺序赋值)
struct Student s1 = {"Tom", 18, 90.5}; // 按顺序初始化
这里:
"Tom"会初始化name;18会初始化age;90.5会初始化score。
2️⃣ 通过指定成员名初始化(命名初始化器)
你也可以通过指定成员名的方式来初始化结构体,这种方式能够避免顺序错误的问题,且更具可读性。
struct Student s2 = {.name = "Jerry", .age = 20, .score = 85.0};
这样初始化的好处是,即使你不按照结构体成员声明的顺序写,也可以正确初始化。
🧭 五、结构体数组初始化
结构体数组的初始化方式与单个结构体相似,只不过是为数组中的每个结构体逐一赋值。
示例:
struct Student class[3] = {{"Tom", 18, 90.5},{"Jerry", 20, 85.0},{"Alice", 19, 88.5}
};
这里我们初始化了一个含有 3 个元素的结构体数组 class,每个元素是一个 struct Student。
手动初始化:
struct Student class[3];
class[0] = (struct Student){"Tom", 18, 90.5};
class[1] = (struct Student){"Jerry", 20, 85.0};
class[2] = (struct Student){"Alice", 19, 88.5};
🧩 六、部分初始化(未初始化成员的处理)
如果你只为部分成员赋初值,其他成员将自动初始化为零。
struct Student s1 = {"Tom"}; // 只初始化 name
这里:
s1.name被初始化为"Tom";s1.age和s1.score会自动初始化为零。
注意:如果初始化时没有给某些成员赋值,零初始化 只对全局和静态存储类别的结构体有效。局部变量在没有显式初始化时会含有垃圾值。
🧭 七、结构体初始化的注意事项
1️⃣ 全局和静态结构体变量自动初始化为零
struct Student s1; // 全局变量,自动初始化为零
在全局或静态区域定义的结构体,如果没有显式初始化,所有成员都会自动初始化为零。
int型成员初始化为0char[]型成员初始化为空字符串""指针型成员初始化为NULL
2️⃣ 局部结构体变量不自动初始化
int main() {struct Student s1; // 局部变量,不会自动初始化printf("Name: %s\n", s1.name); // 可能是垃圾值
}
局部变量如果没有显式初始化,成员的值是 不确定的,会包含垃圾值,这是 C 语言的一个常见陷阱。
🧩 八、结构体的内存分配与初始化
1️⃣ 栈内存中的结构体
当你声明一个结构体时,它的成员会被保存在栈上。栈上的结构体成员不会自动初始化,除非显式地进行初始化。
void func() {struct Student s = {"Tom", 18, 90.5}; // 局部变量printf("Name: %s, Age: %d, Score: %.1f\n", s.name, s.age, s.score);
}
2️⃣ 堆内存中的结构体(动态分配)
当你使用 malloc 动态分配结构体时,结构体成员的值是未定义的(也是垃圾值)。如果你想初始化它们,必须手动进行。
struct Student *s = malloc(sizeof(struct Student));
if (s != NULL) {s->age = 20;strcpy(s->name, "Alice");s->score = 88.5;
}
🧭 九、总结与最佳实践
| 情况 | 说明 | 最佳实践 |
|---|---|---|
| 结构体数组初始化 | 结构体数组可以在定义时进行初始化。 | 对结构体数组进行逐个初始化,避免漏项。 |
| 部分初始化 | 未初始化的成员会自动初始化为零。 | 始终显式初始化成员,避免错误。 |
| 全局/静态变量 | 全局变量和静态变量会自动初始化为零。 | 对于静态/全局结构体,初始化时可以省略部分项。 |
| 局部变量 | 局部结构体变量不会自动初始化,必须显式初始化。 | 局部结构体变量必须初始化,否则会有垃圾值。 |
结构体数组
🧭 一、结构体数组的基本概念
1️⃣ 什么是结构体数组?
结构体数组 是一个由多个结构体元素组成的数组。
每个数组元素都是一个结构体,结构体的各个成员可以是不同的数据类型。
你可以通过数组下标来访问数组中的每个结构体。
定义方式:
struct Student {char name[20];int age;float score;
};struct Student class[3]; // 定义一个包含3个学生的结构体数组
class[0]、class[1]和class[2]是struct Student类型的结构体。- 你可以通过
class[i]来访问每个学生的具体信息。
2️⃣ 结构体数组的初始化
结构体数组可以像普通数组一样在定义时进行初始化。
例:初始化结构体数组
struct Student class[3] = {{"Tom", 18, 90.5},{"Jerry", 20, 85.0},{"Alice", 19, 88.5}
};
class[0]被初始化为{"Tom", 18, 90.5};class[1]被初始化为{"Jerry", 20, 85.0};class[2]被初始化为{"Alice", 19, 88.5}。
手动初始化:
struct Student class[3];class[0].age = 18;
class[0].score = 90.5;
strcpy(class[0].name, "Tom");class[1].age = 20;
class[1].score = 85.0;
strcpy(class[1].name, "Jerry");class[2].age = 19;
class[2].score = 88.5;
strcpy(class[2].name, "Alice");
🧩 二、访问结构体数组成员
你可以通过数组下标访问结构体数组的元素,然后再通过点操作符 . 来访问结构体的成员。
例:
#include <stdio.h>struct Student {char name[20];int age;float score;
};int main() {struct Student class[3] = {{"Tom", 18, 90.5},{"Jerry", 20, 85.0},{"Alice", 19, 88.5}};// 访问第一个学生的信息printf("Name: %s, Age: %d, Score: %.2f\n", class[0].name, class[0].age, class[0].score);// 访问第二个学生的信息printf("Name: %s, Age: %d, Score: %.2f\n", class[1].name, class[1].age, class[1].score);return 0;
}
输出:
Name: Tom, Age: 18, Score: 90.50
Name: Jerry, Age: 20, Score: 85.00
在这个例子中,我们通过 class[0].name、class[0].age 和 class[0].score 访问 class[0] 这个学生的各个成员。
🧭 三、结构体数组的内存布局
每个结构体数组元素占据一定的内存空间,内存空间大小等于单个结构体的大小。
结构体数组的内存是 按结构体成员顺序依次排列 的。
示例:内存布局
假设结构体定义如下:
struct Student {char name[20];int age;float score;
};
每个 struct Student 占据的内存大小是:
name[20]:20 字节age:4 字节(通常是int类型的大小)score:4 字节(通常是float类型的大小)
在 32 位系统中,假设没有对齐,struct Student 大小为 20 + 4 + 4 = 28 字节。
结构体数组 class[3] 占用的内存:
class[0] ──────────────► [ "Tom" ] [ 18 ] [ 90.5 ]
class[1] ──────────────► [ "Jerry" ] [ 20 ] [ 85.0 ]
class[2] ──────────────► [ "Alice" ] [ 19 ] [ 88.5 ]
🧩 四、结构体数组的动态内存分配
有时,我们需要在程序运行时动态分配结构体数组的内存。
这可以通过 malloc 函数实现。
例:动态分配结构体数组
#include <stdio.h>
#include <stdlib.h>struct Student {char name[20];int age;float score;
};int main() {// 动态分配内存struct Student *class = malloc(3 * sizeof(struct Student));if (class == NULL) {printf("Memory allocation failed!\n");return 1;}// 初始化数据strcpy(class[0].name, "Tom");class[0].age = 18;class[0].score = 90.5;strcpy(class[1].name, "Jerry");class[1].age = 20;class[1].score = 85.0;strcpy(class[2].name, "Alice");class[2].age = 19;class[2].score = 88.5;// 打印数据for (int i = 0; i < 3; i++) {printf("Name: %s, Age: %d, Score: %.2f\n", class[i].name, class[i].age, class[i].score);}// 释放内存free(class);return 0;
}
关键点:
- 使用
malloc动态分配内存来存储结构体数组。 - 必须使用
free()来释放内存,防止内存泄漏。
🧭 五、结构体数组与指针
结构体数组本质上是一个 指向结构体的指针,所以可以使用指针来操作结构体数组。
对于结构体数组,可以用指针加偏移量的方式来访问各个元素。
示例:
struct Student class[3] = {{"Tom", 18, 90.5},{"Jerry", 20, 85.0},{"Alice", 19, 88.5}
};// 使用指针访问结构体数组
struct Student *p = class;printf("%s\n", p->name); // 访问 class[0]
p++;
printf("%s\n", p->name); // 访问 class[1]
p++;
printf("%s\n", p->name); // 访问 class[2]
输出:
Tom
Jerry
Alice
这里,我们将结构体数组 class 的首地址赋给指针 p,然后通过指针偏移访问数组中的元素。
🧩 六、结构体数组的常见应用场景
1️⃣ 存储多个对象
结构体数组常用于 存储多个相同类型的对象,例如:
- 存储多个 学生 数据;
- 存储多个 传感器 的状态;
- 存储多个 任务 的信息。
2️⃣ 操作硬件寄存器
在嵌入式开发中,结构体数组用于操作多个硬件寄存器。例如,多个 GPIO端口的配置。
#define GPIOA_BASE 0x40020000
#define GPIOB_BASE 0x40020400typedef struct {volatile uint32_t MODER; // GPIO模式寄存器volatile uint32_t OTYPER; // GPIO输出类型寄存器volatile uint32_t OSPEEDR; // GPIO输出速度寄存器volatile uint32_t PUPDR; // GPIO上下拉寄存器
} GPIO_TypeDef;GPIO_TypeDef *GPIOs[2] = {(GPIO_TypeDef *)GPIOA_BASE, (GPIO_TypeDef *)GPIOB_BASE};
📘 七、总结
| 特性 | 描述 |
|---|---|
| 结构体数组的定义 | struct Student class[3]; |
| 结构体数组的初始化 | 在定义时可以直接初始化,或者手动逐一赋值 |
| 结构体数组的访问 | 通过下标访问元素,class[0].name 或 class[i].name |
| 动态分配内存 | 使用 malloc 和 free 动态分配内存 |
| 结构体数组与指针 | 数组名即为首元素的指针,可通过指针访问数组元素 |
| 应用场景 | 存储多个对象、硬件寄存器操作等 |
