结构体(二):结构体指针、结构体作为函数参数、结构体嵌套、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 char | uint8_t(常见于嵌入式) |
void (*func)(int) | FuncPtr(函数指针) |
说到函数指针,typedef 在这里特别有用。比如:
typedef void (*Callback)(int event);
void registerCallback(Callback cb) {
// 注册回调函数
}
没有 typedef 的话,那个函数声明会写成 void registerCallback(void (*cb)(int)),读起来费劲多了。
知识体系图
下面这张图把本章的核心知识点串起来了,你可以对照着回顾一下:
避坑指南
最后,分享几个我踩过的坑:
- 忘记初始化指针: 我曾经定义了一个
struct Student *p就直接用p->age = 20,结果程序直接崩溃。记住,指针必须先指向一个有效的结构体变量,或者用malloc分配内存。 - 传指针却忘了取地址: 调用函数时写成
printStudent(stu)而不是printStudent(&stu)。编译器可能会报类型不匹配,但有时候会隐式转换,导致诡异的结果。 - typedef 和 #define 搞混:
typedef是类型别名,#define是文本替换。比如typedef char *String和#define String char *在声明多个变量时行为完全不同。这个坑我见过不少人掉进去过。
好了,这一章的内容就这些。结构体指针、传参、嵌套、typedef,这四个东西你掌握了,写 C 代码的时候会顺手很多。下一章我们聊聊共用体和枚举,也是实战中经常碰到的老朋友。
公众号:蓝海资料掘金营,微信 deep3321