3. 结构体指针与函数:结构体指针、结构体作为函数参数、指向结构体的指针数组
好,咱们接着聊结构体。前面几章我们把结构体定义、初始化、内存对齐这些基础打牢了。这一章,我带你看看结构体在实际工程中怎么用——说白了,就是结构体怎么跟指针、函数打交道。
你想想看,一个结构体里可能装着几十个字段,如果每次传参都整个拷贝一份,那效率得多低?我早年做嵌入式项目时,就吃过这个亏。当时一个数据包结构体有 200 多字节,我直接传值,结果栈空间爆了,程序跑飞。嗯,从那以后,我养成了用结构体指针的习惯。
3.1 结构体指针:轻量级操作的核心
结构体指针,说白了就是一个指向结构体变量的指针。它存的是结构体的首地址。通过这个指针,我们可以间接访问结构体的成员。
定义方式很简单:
struct Student {
char name[32];
int age;
float score;
};
struct Student stu = {"张三", 20, 95.5f};
struct Student *pStu = &stu; // 结构体指针
访问成员有两种写法。我个人习惯用箭头运算符 ->,因为它更直观:
// 方式一:解引用后取成员
(*pStu).age = 21;
// 方式二:箭头运算符(推荐)
pStu->age = 21;
pStu->score = 98.0f;
->。代码更干净,也不容易漏括号。你想想看,(*pStu).age 这种写法,括号一多就容易乱。
3.2 结构体作为函数参数:传值 vs 传指针
这是面试高频题,也是实际开发中容易踩坑的地方。我们直接看对比:
| 传参方式 | 语法 | 内存开销 | 能否修改原数据 | 适用场景 |
|---|---|---|---|---|
| 传值 | void func(struct Student s) |
整个结构体拷贝(大) | 否(副本操作) | 结构体很小(≤8字节) |
| 传指针 | void func(struct Student *ps) |
4/8字节(指针大小) | 是(直接操作原数据) | 结构体较大或需要修改 |
| 传 const 指针 | void func(const struct Student *ps) |
4/8字节 | 否(只读保护) | 大结构体只读访问 |
为什么我强调用指针?举个例子。假设你有一个传感器数据结构体,里面包含 10 个浮点数和 5 个整型:
struct SensorData {
float temperature[10];
float humidity[10];
int status[5];
uint32_t timestamp;
};
// 传值 —— 栈上拷贝 100+ 字节
void process_by_value(struct SensorData data) {
// 操作的是副本,原数据不变
}
// 传指针 —— 只传 4 字节地址
void process_by_pointer(struct SensorData *pdata) {
// 直接操作原数据,零拷贝开销
}
3.3 传 const 指针:只读保护的好习惯
如果你只是想读取结构体数据,不想修改它,那就加个 const。这既是文档,也是编译器帮你做的安全检查。
void print_student(const struct Student *ps) {
// 可以读
printf("Name: %s, Age: %d\n", ps->name, ps->age);
// 编译器报错:不能修改
// ps->age = 30; // ❌ 编译错误
}
我个人写库函数时,所有只读参数都加 const。这能防止调用方不小心改了不该改的数据。你想想看,如果某个模块偷偷改了你的结构体,排查起来多痛苦?
3.4 指向结构体的指针数组:管理多个对象
这个技巧在实际项目中非常常用。比如你要管理多个设备的状态,每个设备是一个结构体,用指针数组来组织它们:
struct Device {
int id;
char name[16];
int status; // 0: offline, 1: online
};
struct Device dev1 = {1, "Sensor_A", 1};
struct Device dev2 = {2, "Sensor_B", 0};
struct Device dev3 = {3, "Sensor_C", 1};
// 指针数组:每个元素指向一个 Device 结构体
struct Device *devices[] = {&dev1, &dev2, &dev3};
int device_count = sizeof(devices) / sizeof(devices[0]);
遍历和操作就非常灵活了:
void scan_devices(struct Device *devs[], int count) {
for (int i = 0; i < count; i++) {
if (devs[i]->status == 1) {
printf("Device %d (%s) is online\n",
devs[i]->id, devs[i]->name);
}
}
}
// 调用
scan_devices(devices, device_count);
为什么用指针数组而不是结构体数组?两个原因:
- 节省内存:指针数组只存地址,结构体数组存完整数据。如果结构体很大,指针数组优势明显。
- 灵活排序:你可以只交换指针,不用搬动整个结构体。排序效率高得多。
3.5 动态分配结构体指针数组
如果设备数量在运行时才能确定,那就需要动态分配:
#include <stdlib.h>
struct Device **create_device_array(int count) {
// 分配指针数组
struct Device **arr = (struct Device **)malloc(
count * sizeof(struct Device *));
if (arr == NULL) {
return NULL; // 分配失败
}
// 为每个指针分配结构体
for (int i = 0; i < count; i++) {
arr[i] = (struct Device *)malloc(sizeof(struct Device));
if (arr[i] == NULL) {
// 清理已分配的内存
for (int j = 0; j < i; j++) {
free(arr[j]);
}
free(arr);
return NULL;
}
arr[i]->id = i + 1;
snprintf(arr[i]->name, 16, "Device_%d", i + 1);
arr[i]->status = 0;
}
return arr;
}
// 记得释放
void free_device_array(struct Device **arr, int count) {
for (int i = 0; i < count; i++) {
free(arr[i]);
}
free(arr);
}
3.6 结构体指针与函数指针结合
这个稍微进阶一点,但非常实用。结构体里可以放函数指针,实现类似面向对象的多态效果:
struct Animal {
char name[16];
void (*speak)(void); // 函数指针
};
void dog_speak(void) {
printf("汪汪!\n");
}
void cat_speak(void) {
printf("喵喵~\n");
}
int main() {
struct Animal dog = {"旺财", dog_speak};
struct Animal cat = {"咪咪", cat_speak};
struct Animal *p = &dog;
p->speak(); // 输出:汪汪!
p = &cat;
p->speak(); // 输出:喵喵~
return 0;
}
这种写法在嵌入式驱动层很常见。比如不同型号的传感器,初始化、读取数据的函数不同,但接口一致。用结构体指针 + 函数指针,就能统一管理。
3.7 本章知识体系
下面这张图帮你梳理结构体指针与函数的核心脉络:
这张图把本章的核心内容串起来了。中心是结构体指针,三个分支分别对应函数传参、指针数组管理、函数指针结合。底部是核心原则——记住它,能帮你避开 90% 的坑。
好了,这一章就到这里。结构体指针是 C 语言里非常实用的工具,用好了能让你的代码既高效又优雅。下一章我们聊聊结构体嵌套和自引用,那又是另一番天地了。
公众号:蓝海资料掘金营,微信deep3321