29、函数与结构体:结构体作为参数与返回值,传值与传址的选择
结构体在C语言里是个好东西。它能把一堆相关的数据打包在一起,让代码看起来清爽不少。但问题来了——当你需要把结构体传给函数,或者从函数里返回一个结构体时,到底该传值还是传址?
这个问题,我当年刚入行时也纠结过。后来在一个嵌入式项目里踩了坑,才真正搞明白。今天咱们就把这事彻底说清楚。
传值:简单直接,但有代价
先看传值。说白了就是把整个结构体复制一份,扔给函数。
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
char name[32];
float score;
} Student;
void printStudent(Student s) {
printf("ID: %d, Name: %s, Score: %.1f\n", s.id, s.name, s.score);
}
int main() {
Student stu = {1001, "Zhang San", 89.5};
printStudent(stu); // 传值
return 0;
}
这段代码能跑,没问题。但你想过没有——每次调用printStudent,整个Student结构体(40个字节左右)都要在栈上复制一遍。如果结构体更大,或者函数被频繁调用,这个开销就不容忽视了。
核心要点:传值适合小结构体(一般建议不超过16字节),且你不需要修改原始数据时使用。
我个人习惯是:如果结构体只有两三个int,传值无所谓。但一旦结构体里有了数组、字符串,或者嵌套了其他结构体,我就会警惕起来。
传址:高效灵活,但要小心
传址就不一样了。你传的是结构体的地址,也就是一个指针。函数内部通过指针访问原始数据。
void printStudentPtr(const Student *s) {
if (s == NULL) return;
printf("ID: %d, Name: %s, Score: %.1f\n", s->id, s->name, s->score);
}
void updateScore(Student *s, float newScore) {
if (s == NULL) return;
s->score = newScore;
}
int main() {
Student stu = {1001, "Zhang San", 89.5};
printStudentPtr(&stu); // 传址,只读
updateScore(&stu, 95.0); // 传址,修改
return 0;
}
传址的好处很明显:
- 效率高——只传4或8字节的指针,不管结构体多大
- 能修改原始数据——函数内部可以直接改
- 避免栈溢出——大结构体反复传值,栈空间可能扛不住
但要注意,传址也有坑。我在项目中遇到过一个问题:一个同事传了结构体指针进去,函数内部不小心改了不该改的字段,导致整个模块的状态乱了。排查了半天才发现是这里的问题。
避坑指南:如果函数不需要修改结构体,记得加const修饰。这既是给编译器看的,也是给人看的——告诉调用者:放心,我不会动你的数据。
返回值:结构体也能当返回值
C语言允许函数返回结构体。这个特性在某些场景下非常有用。
Student createStudent(int id, const char *name, float score) {
Student s;
s.id = id;
strncpy(s.name, name, sizeof(s.name) - 1);
s.name[sizeof(s.name) - 1] = '\0';
s.score = score;
return s; // 返回结构体
}
int main() {
Student stu = createStudent(1002, "Li Si", 92.0);
printStudent(stu);
return 0;
}
返回结构体时,编译器会做拷贝。对于小结构体,这没问题。但对于大结构体,你可能要考虑返回指针。
不过返回指针也有风险——你不能返回局部变量的地址。因为函数结束后,局部变量就销毁了。
// 错误示范!不要这样写
Student* badCreateStudent(int id, const char *name, float score) {
Student s;
// ... 初始化 ...
return &s; // 返回局部变量的地址,危险!
}
正确的做法是:要么用malloc动态分配内存,要么让调用者传入一个指针。
// 方式一:动态分配
Student* createStudentByMalloc(int id, const char *name, float score) {
Student *s = (Student*)malloc(sizeof(Student));
if (s == NULL) return NULL;
s->id = id;
strncpy(s->name, name, sizeof(s->name) - 1);
s->name[sizeof(s->name) - 1] = '\0';
s->score = score;
return s;
}
// 方式二:调用者提供缓冲区
void initStudent(Student *s, int id, const char *name, float score) {
if (s == NULL) return;
s->id = id;
strncpy(s->name, name, sizeof(s->name) - 1);
s->name[sizeof(s->name) - 1] = '\0';
s->score = score;
}
我的建议:在嵌入式开发中,尽量少用动态内存分配。用调用者提供缓冲区的方式更安全,也更容易控制内存生命周期。
传值与传址的选择策略
到底什么时候用传值,什么时候用传址?我总结了一个简单的判断流程:
这个流程图是我在实际项目中总结出来的。你想想看,如果结构体很小(比如就两个int),传值完全没问题,代码还更直观。但如果结构体里有个1024字节的数组,你还传值,那每次调用都在栈上复制1K数据,效率可想而知。
实际项目中的选择
我记得在一个物联网网关项目里,有个结构体包含了设备状态、网络配置、传感器数据等,总共200多字节。一开始有人图省事,全部传值。结果系统跑起来后,发现栈空间经常溢出,任务莫名其妙地崩溃。
后来改成传址,问题就解决了。而且我们在不需要修改的地方都加了const,代码的安全性也提高了。
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 小结构体(≤16字节),只读 | 传值 | 简单直观,无副作用 |
| 小结构体,需要修改 | 传址(不加const) | 修改原始数据 |
| 大结构体(>16字节),只读 | 传址(加const) | 避免拷贝开销 |
| 大结构体,需要修改 | 传址(不加const) | 效率和修改兼顾 |
| 函数返回结构体 | 小结构体返回值,大结构体用指针参数 | 避免不必要的拷贝 |
总结一下:传值安全但慢,传址快但需要小心。没有绝对的对错,只有适合不适合。我的原则是——小结构体传值,大结构体传址。需要修改就传非常指针,只读就传const指针。就这么简单。
嗯,关于结构体作为参数和返回值的话题,今天就聊到这里。记住一点:代码是给人看的,也是给机器跑的。在保证正确性的前提下,选择更高效的方式,这是每个工程师都应该养成的习惯。
公众号:蓝海资料掘金营,微信deep3321