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;
}

我的建议:在嵌入式开发中,尽量少用动态内存分配。用调用者提供缓冲区的方式更安全,也更容易控制内存生命周期。

传值与传址的选择策略

到底什么时候用传值,什么时候用传址?我总结了一个简单的判断流程:

结构体作为参数 结构体大小 > 16字节? 使用传址(指针) 需要修改原始数据? 传非常指针(不加const) 使用传值 传const指针

这个流程图是我在实际项目中总结出来的。你想想看,如果结构体很小(比如就两个int),传值完全没问题,代码还更直观。但如果结构体里有个1024字节的数组,你还传值,那每次调用都在栈上复制1K数据,效率可想而知。

实际项目中的选择

我记得在一个物联网网关项目里,有个结构体包含了设备状态、网络配置、传感器数据等,总共200多字节。一开始有人图省事,全部传值。结果系统跑起来后,发现栈空间经常溢出,任务莫名其妙地崩溃。

后来改成传址,问题就解决了。而且我们在不需要修改的地方都加了const,代码的安全性也提高了。

场景 推荐方式 原因
小结构体(≤16字节),只读 传值 简单直观,无副作用
小结构体,需要修改 传址(不加const) 修改原始数据
大结构体(>16字节),只读 传址(加const) 避免拷贝开销
大结构体,需要修改 传址(不加const) 效率和修改兼顾
函数返回结构体 小结构体返回值,大结构体用指针参数 避免不必要的拷贝

总结一下:传值安全但慢,传址快但需要小心。没有绝对的对错,只有适合不适合。我的原则是——小结构体传值,大结构体传址。需要修改就传非常指针,只读就传const指针。就这么简单。

嗯,关于结构体作为参数和返回值的话题,今天就聊到这里。记住一点:代码是给人看的,也是给机器跑的。在保证正确性的前提下,选择更高效的方式,这是每个工程师都应该养成的习惯。


公众号:蓝海资料掘金营,微信deep3321