结构体(二):结构体指针、结构体作为函数参数、结构体嵌套、typedef的使用

好,咱们接着聊结构体。上一章我们把结构体的基本概念讲清楚了,这一章要深入一点,聊聊实际开发中真正高频使用的几个技巧。说白了,结构体这东西,你要是只会定义和赋值,那跟没用过没啥区别。真正让它发挥威力的,是指针、传参、嵌套,还有那个能让你少打很多字的 typedef

结构体指针:别把整个结构体搬来搬去

先问一个问题:你有一个结构体变量,里面存了 100 个字段,你想把它传给一个函数处理。你会怎么做?

新手可能会直接把整个结构体传过去。嗯,语法上没错,但效率上……我跟你讲,我当年刚入行时就这么干过。结果一个嵌入式项目里,结构体里有个 4KB 的数组,每次传参都要复制 4KB 数据,函数调用一多,栈空间直接爆了。从那以后,我再也不敢这么写了。

正确的做法是什么?传指针。

struct Student {
    char name[32];
    int age;
    float score;
};

void printStudent(struct Student *pStu) {
    printf("姓名:%s,年龄:%d,成绩:%.1f\n", pStu->name, pStu->age, pStu->score);
}

int main() {
    struct Student stu = {"张三", 20, 88.5};
    printStudent(&stu);
    return 0;
}

看到没?pStu->name 这种写法,就是通过指针访问结构体成员。箭头符号 -> 是专门干这个的。我个人习惯是:只要结构体不是特别小(比如就两三个 int),一律传指针。省内存、省时间、还安全。

核心要点:

  • 结构体变量名不是指针,取地址用 &
  • 通过指针访问成员用 ->
  • 传指针比传值效率高得多,尤其是结构体较大时

结构体作为函数参数:传值 vs 传指针

咱们再细说一下传参这件事。C 语言里,函数参数传递有两种方式:传值和传址。结构体也不例外。

传值方式:

void modifyStudent(struct Student s) {
    s.age = 30;  // 只修改了副本,原数据不变
}

这种方式,函数内部拿到的是结构体的一个完整副本。你改它,原数据纹丝不动。而且,复制整个结构体是有开销的。我记得有一次调试一个 bug,发现某个函数执行特别慢,查了半天,发现就是传了一个 1KB 的结构体,而且这个函数还被频繁调用。改成传指针后,速度提升了不止一个量级。

传址方式:

void modifyStudent(struct Student *p) {
    p->age = 30;  // 直接修改原数据
}

传指针,函数内部操作的就是原始数据。想改就改,不想改就加个 const 保护起来:

void printStudent(const struct Student *p) {
    // p->age = 100;  // 编译报错,防止误修改
    printf("%d\n", p->age);
}

我的建议:

  • 如果函数只需要读取结构体数据,用 const 指针
  • 如果函数需要修改结构体数据,用普通指针
  • 只有结构体非常小(比如就一个 int)且不需要修改时,才考虑传值

结构体嵌套:把复杂的东西组织起来

实际项目中,很少有结构体是扁平的。比如你要描述一个员工,他除了基本信息,还有所属部门、部门经理、部门地址……这时候就需要结构体嵌套了。

struct Date {
    int year;
    int month;
    int day;
};

struct Employee {
    char name[32];
    struct Date birthDate;      // 嵌套一个日期结构体
    struct Date hireDate;       // 入职日期
    float salary;
};

访问嵌套成员时,一层一层点下去就行:

struct Employee emp;
emp.birthDate.year = 1990;
emp.hireDate.month = 3;

如果嵌套的是指针,那就箭头和点混着用。举个例子:

struct Department {
    char name[32];
    struct Employee *manager;   // 指向经理的指针
};

struct Department dept;
dept.manager->salary = 15000.0;  // 先点后箭头

我在项目中遇到过一种情况:结构体嵌套了三层,访问一个字段要写 a.b.c.d 这么长。说实话,可读性确实差。后来我一般会控制在两层以内,如果实在要更深,就考虑用指针或者重新设计结构。

注意:结构体不能包含自身类型的非指针成员。比如:

struct Node {
    int data;
    struct Node next;  // 编译错误!无限递归
};

但可以包含指向自身类型的指针,这是链表的基础:

struct Node {
    int data;
    struct Node *next;  // 正确
};

typedef 的使用:给类型起个顺手的名字

每次写 struct Student 你不觉得烦吗?反正我是觉得烦。尤其是结构体名字长的时候,写起来又累又容易错。

typedef 就是用来解决这个问题的。它给已有的类型起个新名字,说白了就是别名。

typedef struct Student {
    char name[32];
    int age;
} Student_t;

// 现在可以直接用 Student_t 定义变量
Student_t stu1;
Student_t *pStu;

你看,少写了 struct 关键字,代码清爽多了。我个人习惯是在类型名后面加 _t 后缀,一眼就能看出这是个类型别名。当然也有人不加,这个看团队规范。

还有一种更简洁的写法,定义结构体时直接匿名:

typedef struct {
    char name[32];
    int age;
} Student_t;

这种写法省去了结构体标签名,直接用 Student_t 来定义变量。不过要注意,匿名结构体不能自引用(比如链表节点就不行),因为没名字可引用。

typedef 的常见用法:

原始写法typedef 后
struct Student s;Student_t s;
struct Student *p;Student_t *p;
unsigned charuint8_t(常见于嵌入式)
void (*func)(int)FuncPtr(函数指针)

说到函数指针,typedef 在这里特别有用。比如:

typedef void (*Callback)(int event);

void registerCallback(Callback cb) {
    // 注册回调函数
}

没有 typedef 的话,那个函数声明会写成 void registerCallback(void (*cb)(int)),读起来费劲多了。

知识体系图

下面这张图把本章的核心知识点串起来了,你可以对照着回顾一下:

结构体(二) 结构体指针 - 用 -> 访问成员 - 传指针效率高 作为函数参数 传值:复制副本,不改原数据 传址:直接操作,加 const 保护 结构体嵌套 - 一层层点下去 - 避免过深嵌套 typedef 使用 - 给类型起别名 - 匿名结构体 + typedef - 函数指针场景特别好用 核心:指针提效率,嵌套组数据,typedef 省代码

避坑指南

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

  • 忘记初始化指针: 我曾经定义了一个 struct Student *p 就直接用 p->age = 20,结果程序直接崩溃。记住,指针必须先指向一个有效的结构体变量,或者用 malloc 分配内存。
  • 传指针却忘了取地址: 调用函数时写成 printStudent(stu) 而不是 printStudent(&stu)。编译器可能会报类型不匹配,但有时候会隐式转换,导致诡异的结果。
  • typedef 和 #define 搞混: typedef 是类型别名,#define 是文本替换。比如 typedef char *String#define String char * 在声明多个变量时行为完全不同。这个坑我见过不少人掉进去过。

好了,这一章的内容就这些。结构体指针、传参、嵌套、typedef,这四个东西你掌握了,写 C 代码的时候会顺手很多。下一章我们聊聊共用体和枚举,也是实战中经常碰到的老朋友。


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