22、结构体(二):结构体指针、指向结构体的指针、结构体作为函数参数(传值与传址)
上一节我们把结构体定义、声明和初始化聊透了。这一节咱们来点硬核的——指针和结构体怎么配合。说实话,指针这东西,刚学的时候觉得它玄乎,用多了你会发现,它其实就是C语言的“遥控器”。
结构体本身可以很大,比如一个学生结构体,里面塞了姓名、学号、成绩数组……你要是每次都用值传递,那效率可就太感人了。我当年刚入行时,就因为这个被老同事调侃过:“你这是在拷贝整个数据库啊?”
好,咱们直接进入正题。
结构体指针:指向结构体的指针
结构体指针,说白了就是一个指针变量,它存的是结构体变量的地址。定义方式很简单:
struct Student {
char name[20];
int id;
float score;
};
struct Student stu1;
struct Student *pStu = &stu1; // 指向结构体的指针
这里 pStu 就是结构体指针。它指向 stu1 的起始地址。你可能会问:“我直接用 stu1 不就行了,干嘛多此一举?”
嗯,问得好。我举个例子你就明白了。
假设你写了一个函数,要修改学生成绩。如果你传的是结构体本身,函数里改的是副本,原数据纹丝不动。但如果你传的是指针,函数就能直接操作原数据。这就是“传址”的魅力。
访问结构体成员:两种方式
通过结构体指针访问成员,有两种写法:
- 用
*解引用:(*pStu).score = 95.5; - 用
->箭头运算符:pStu->score = 95.5;
我个人习惯用箭头运算符。为什么?因为简洁、直观。你看 (*pStu).score 这写法,括号不能少,点号优先级比星号高,写起来容易出错。箭头运算符就是专门为结构体指针设计的,读起来也顺口:“pStu 指向的 score”。
#include <stdio.h>
#include <string.h>
struct Student {
char name[20];
int id;
float score;
};
int main() {
struct Student stu1;
struct Student *p = &stu1;
// 用箭头赋值
strcpy(p->name, "张三");
p->id = 1001;
p->score = 88.5;
// 用箭头输出
printf("姓名:%s\n", p->name);
printf("学号:%d\n", p->id);
printf("成绩:%.1f\n", p->score);
return 0;
}
输出结果:
姓名:张三
学号:1001
成绩:88.5
p->name 这种写法,心里默念“p指向的name”,就不会搞混了。
结构体作为函数参数:传值 vs 传址
这是面试高频考点。咱们直接对比着看。
传值(值传递)
传值就是把整个结构体拷贝一份给函数。函数里改的是副本,不影响原数据。
void printStudent(struct Student s) {
printf("姓名:%s,成绩:%.1f\n", s.name, s.score);
s.score = 0; // 只改了副本,原数据不变
}
int main() {
struct Student stu = {"李四", 1002, 92.0};
printStudent(stu);
printf("原成绩:%.1f\n", stu.score); // 还是 92.0
return 0;
}
输出:
姓名:李四,成绩:92.0
原成绩:92.0
看到了吧?函数里把成绩改成0,原数据纹丝不动。这就是传值的特点——安全,但效率低。如果结构体很大(比如里面有个大数组),每次传值都要拷贝一整块内存,性能开销不小。
传址(指针传递)
传址就是传结构体的地址,函数通过指针直接操作原数据。
void updateScore(struct Student *p, float newScore) {
p->score = newScore; // 直接修改原数据
}
int main() {
struct Student stu = {"王五", 1003, 78.0};
updateScore(&stu, 95.0);
printf("更新后成绩:%.1f\n", stu.score); // 变成 95.0
return 0;
}
输出:
更新后成绩:95.0
传址的好处很明显:效率高、能改原数据。但缺点也有——如果你不小心在函数里改了不该改的数据,那就麻烦了。所以,如果你只是想读取数据,不想修改,可以加 const 修饰:
void printStudent(const struct Student *p) {
printf("姓名:%s,成绩:%.1f\n", p->name, p->score);
// p->score = 0; // 编译报错,const 保护了数据
}
const,函数内部可能会意外修改原数据。我曾经在项目里遇到过,一个同事写的打印函数里顺手改了成绩,排查了半天才发现。所以,只读函数一定要加 const。
什么时候用传值,什么时候用传址?
我总结了一个简单的原则:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 结构体很小(如只含几个int) | 传值 | 拷贝开销小,代码简单 |
| 结构体较大(含数组、嵌套结构体) | 传址 | 避免拷贝,提升性能 |
| 需要修改原数据 | 传址 | 传值无法修改原数据 |
| 只读访问,不修改 | 传址 + const | 高效且安全 |
你想想看,如果你写一个链表操作函数,节点里存了上千个字符,传值的话每次调用都要拷贝一遍,那程序跑起来得多慢。所以实际项目中,传址几乎是标配。
避坑指南:结构体指针的常见错误
我见过不少新手在结构体指针上栽跟头。这里列几个典型问题:
- 未初始化指针就使用:
struct Student *p; p->score = 90;—— 这会导致野指针,程序直接崩溃。记住:指针必须先指向一个有效的结构体变量,或者用malloc分配内存。 - 混淆
.和->:结构体变量用.,结构体指针用->。混用的话编译器会报错。 - 传址时忘记取地址:
updateScore(stu, 95.0);而不是updateScore(&stu, 95.0);—— 这会把结构体内容当成地址,编译可能不报错,但运行结果完全不对。
核心总结:
- 结构体指针用
->访问成员 - 传值安全但低效,传址高效但需小心
- 只读函数用
const保护数据 - 指针必须初始化后再使用
知识体系图
下面这张图帮你理清结构体指针和传值传址的关系:
这张图把结构体指针和函数参数的关系梳理得很清楚。左边是指针的定义和访问方式,右边是传值和传址的对比。你多看几遍,脑子里就有框架了。
好了,这一节的内容就到这儿。结构体指针和传值传址,说白了就是“怎么高效、安全地操作结构体数据”。我个人建议你多写几个小例子,比如写一个学生成绩管理系统,用结构体指针和传址来实现增删改查。代码写多了,这些概念自然就刻在脑子里了。