6. 结构体与函数:传参的艺术
结构体跟函数打交道,是嵌入式开发里最频繁的操作之一。我见过不少新手,甚至工作两三年的工程师,在这上面栽过跟头。说白了,就是没搞清楚「传值」和「传址」的区别。
今天咱们就把这事儿彻底聊透。
6.1 结构体作为函数参数
先看最基础的情况——把结构体传给函数。这里有两种方式:传值和传址。
6.1.1 传值调用
传值,就是把整个结构体拷贝一份给函数。函数里怎么改,都不影响原来的数据。
#include <stdio.h>
#include <string.h>
typedef struct {
char name[32];
int age;
float score;
} Student;
void printStudent(Student s) {
printf("Name: %s, Age: %d, Score: %.1f\n", s.name, s.age, s.score);
s.age = 100; // 改的是副本,不影响原数据
}
int main() {
Student stu = {"Alice", 20, 88.5};
printStudent(stu);
printf("After call: age = %d\n", stu.age); // 还是20
return 0;
}
嗯,这里要注意:传值会复制整个结构体。如果结构体很大(比如几百字节),每次调用都要拷贝,性能就下来了。我在一个数据采集项目里遇到过,结构体里有个 1KB 的缓冲区,传值调用导致栈空间暴涨,最后直接栈溢出。从那以后,我对大结构体传值就特别敏感。
⚠️ 性能警告: 结构体大小超过 64 字节时,建议优先考虑传址。嵌入式环境栈空间有限,别让拷贝拖垮性能。
6.1.2 传址调用
传址,就是传结构体的指针。函数里通过指针直接操作原数据。
void updateStudent(Student *s) {
if (s == NULL) return; // 一定要判空!
s->age = 25;
strcpy(s->name, "Bob");
}
int main() {
Student stu = {"Alice", 20, 88.5};
updateStudent(&stu);
printf("Name: %s, Age: %d\n", stu.name, stu.age); // Bob, 25
return 0;
}
传址的好处很明显:快,而且能改原数据。但有个坑——指针可能为空。我习惯在函数入口处加个断言或者判空,这是血的教训换来的。
💡 个人习惯: 如果函数不修改结构体内容,参数用
const Student *s。这样既享受传址的效率,又保证数据不被意外修改。
6.2 结构体作为函数返回值
函数返回结构体,这事儿比传参更微妙。先看个例子:
Student createStudent(const char *name, int age, float score) {
Student s;
strncpy(s.name, name, sizeof(s.name) - 1);
s.name[sizeof(s.name) - 1] = '\0';
s.age = age;
s.score = score;
return s; // 返回结构体副本
}
int main() {
Student stu = createStudent("Charlie", 22, 91.0);
printf("%s, %d, %.1f\n", stu.name, stu.age, stu.score);
return 0;
}
返回结构体,本质上是返回一个副本。编译器可能会做 RVO(返回值优化),但别依赖这个。如果结构体很大,返回副本的开销不小。
那返回指针呢?
Student *createStudentPtr(const char *name, int age, float score) {
Student *s = (Student *)malloc(sizeof(Student));
if (s == NULL) return NULL;
strncpy(s->name, name, sizeof(s->name) - 1);
s->name[sizeof(s->name) - 1] = '\0';
s->age = age;
s->score = score;
return s;
}
int main() {
Student *stu = createStudentPtr("David", 23, 92.5);
if (stu) {
printf("%s, %d, %.1f\n", stu->name, stu->age, stu->score);
free(stu); // 别忘了释放!
}
return 0;
}
返回指针,效率高,但引入了内存管理问题。谁分配,谁释放,必须约定清楚。我曾经在一个团队项目里,因为返回指针后忘记释放,导致内存泄漏查了两天。后来我们定了个规矩:函数返回动态分配的结构体指针,命名必须带 _alloc 后缀,提醒调用者要释放。
🔑 核心原则:
- 小结构体(≤ 64 字节)→ 传值或返回值,简单直接
- 大结构体 → 传址,避免拷贝开销
- 返回指针 → 明确所有权,谁 malloc 谁 free
- const 修饰 → 只读指针,安全第一
6.3 知识体系图
下面这张图,帮你理清结构体与函数交互的完整脉络:
6.4 避坑指南
最后,分享几个我踩过的坑:
- 别返回局部变量的地址。 函数返回后,栈上的局部变量就失效了。返回指针指向它,就是野指针。
- 结构体里有指针成员时,浅拷贝要小心。 传值复制了指针值,但两个结构体指向同一块内存。改一个,另一个也变。
- 嵌入式环境注意对齐。 结构体传到函数时,如果编译器对齐方式不同,可能出诡异问题。跨模块调用时,记得用
__attribute__((packed))或者明确指定对齐。
嗯,结构体与函数的交互,说白了就是「值 vs 指针」的选择题。选对了,代码干净高效;选错了,调试到怀疑人生。希望今天的内容能帮你少走弯路。