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 知识体系图

下面这张图,帮你理清结构体与函数交互的完整脉络:

结构体与函数 作为函数参数 传值调用 拷贝整个结构体 传址调用 传递结构体指针 作为函数返回值 返回结构体 返回副本(RVO优化) 返回指针 动态分配,需释放 选择建议 小结构体(≤64B)→ 传值/返回值 | 大结构体 → 传址 返回指针 → 明确所有权 | 只读参数 → const 指针

6.4 避坑指南

最后,分享几个我踩过的坑:

  • 别返回局部变量的地址。 函数返回后,栈上的局部变量就失效了。返回指针指向它,就是野指针。
  • 结构体里有指针成员时,浅拷贝要小心。 传值复制了指针值,但两个结构体指向同一块内存。改一个,另一个也变。
  • 嵌入式环境注意对齐。 结构体传到函数时,如果编译器对齐方式不同,可能出诡异问题。跨模块调用时,记得用 __attribute__((packed)) 或者明确指定对齐。

嗯,结构体与函数的交互,说白了就是「值 vs 指针」的选择题。选对了,代码干净高效;选错了,调试到怀疑人生。希望今天的内容能帮你少走弯路。


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