把结构体的地址交给函数(void f(Student *p)),函数就能直接改原件,而且只传 4 或 8 字节的地址、不复制那 32 字节内容。访问成员要用新符号 ->(箭头):p->score 就等于 (*p).score。-> 左边必须是指针,. 左边必须是变量——这条分界线背下来,本章一半的报错就不会发生了。教材实例03就在这里。
这是 7.6.1"传递地址"在结构体上的完全重演,也是 6.3.1 那个"换不动的 swap"第三次被回收(第一次在 7.6.1,第二次在 7.8.2)。新东西只有一个符号 ->,它的存在纯粹是因为 . 的优先级比 * 高(3.2 的优先级金字塔又见面了)。学完本节,你就有了一套完整的选择标准:只读用 const &、要改用 * 或 &。
一、语法:形参写成指针
void raiseScore(Student *p) { // 形参是"指向 Student 的指针"
p->score += 5; // 用 -> 访问成员
}
int main() {
Student stu = {"张三", 18, 92.5};
raiseScore(&stu); // 调用处必须写 &
cout << stu.score << endl; // 97.5 —— 原件真被改了
return 0;
}
三处要配套(和 int 版的 swap 一模一样,7.6.1):
| 位置 | 写法 | 说明 |
|---|---|---|
| 形参 | Student *p |
声明成指针 |
| 函数体内 | p->score |
用箭头访问成员 |
| 调用处 | raiseScore(&stu) |
必须加 &(把地址交出去) |
缺一处都不行:形参不写 * 就变成传值(改不动);调用不写 & 类型对不上(编译报错);体内不用 -> 也用不了。
二、新符号 ->:为什么需要它
-> 是"指针专用的点",专门用来"从指针出发访问成员":
Student stu;
Student *p = &stu;
p->score // ✅ 正确:箭头
(*p).score // ✅ 正确:等价写法(先解引用,再点)
*p.score // ❌ 错误!
为什么 *p.score 是错的? 因为 3.2 的优先级金字塔里,. 的优先级高于 *:
*p.score ≡ *(p.score)
// ↑ 先算这个
// p 是指针,指针没有成员 score → 编译报错
要"先解引用、再取成员",就必须加括号:(*p).score。
但
(*p).score写起来费劲、还容易漏括号——所以 C++ 造了->这个符号:p->score≡(*p).score,一个符号搞定两件事(先解引用、再取成员)。
念法:p->score 读作"p 指向的那个东西的 score"。
三、两种访问方式对照表(背下来)
| 左边是什么 | 用哪个 | 例子 |
|---|---|---|
| 结构体变量 | . |
stu.score |
| 结构体指针 | -> |
p->score |
| 数组元素(元素是结构体) | . |
cls[i].score(8.4) |
| 数组元素的地址 | -> |
(p + i)->score(8.4.2) |
口诀:变量点、指针箭头。左边是"一个结构体本身"就用 .;左边是"一个地址"就用 ->。
四、好处一:能改原件
这是传地址的主要动机:
void birthday(Student *p) {
p->age++; // 过生日,年龄 +1
}
birthday(&stu);
cout << stu.age << endl; // 真的涨了 1 岁
注意:p->age++ 等价于 (p->age)++(把 age 的值加 1),不是 p++(让指针挪一格)。这个"先取值还是先挪指针"的坑在 7.5.1 遇到过,在结构体上同样存在。
五、好处二:不复制内容
void f1(Student s); // 每次调用复制 32 字节
void f2(Student *p); // 每次调用只复制 4(或 8)字节的地址
指针的大小和它指向什么无关——int *、Student *、double * 都是 4 字节(32 位)或 8 字节(64 位)。所以无论结构体多大,传地址的代价是恒定的。
这就是 8.4 里"结构体数组必须用指针遍历"的原因:数组有一千个元素,传值是不可想象的。
六、只读的指针参数:写成 const Student *p
如果函数只是看、不改,就应该声明成 const:
void printStudent(const Student *p) { // 承诺不改
cout << p->name << " " << p->age << endl;
// p->age = 20; // ❌ 编译报错:只读
}
三种只读写法的取舍:
| 写法 | 复制吗 | 能改吗 | 特点 |
|---|---|---|---|
Student s |
复制 32 字节 | 不能 | 写法最简,小结构体可用 |
const Student *p |
不复制 | 不能 | 能表示"可能为空"(nullptr) |
const Student &s |
不复制 | 不能 | 写法最干净,但引用不能为空 |
实用建议:只读大结构体,优先 const Student &s;如果这个参数有可能"不存在"(传 nullptr),才用 const Student *p。
七、动手 Demo(实例03 使用结构体指针变量做函数参数)
教材实例03的核心是"用结构体指针做参数"。这里做成一个学生信息维护的小工具,把"能改原件"和"不复制"都验证到:
#include <iostream>
#include <cstring>
using namespace std;
struct Student {
char name[20];
int age;
double score;
};
// ① 只读:const 指针
void printStudent(const Student *p) {
cout << " " << p->name << " " << p->age << " 岁 " << p->score << " 分" << endl;
}
// ② 修改:普通指针,能改原件
void addScore(Student *p, double delta) {
p->score += delta; // 改的是原件
if (p->score > 100) p->score = 100; // 别超 100
}
// ③ 修改:用引用完成同一件事(对照)
void birthday(Student &s) {
s.age++; // 引用版:用点
}
int main() {
Student stu = {"张三", 18, 92.5};
cout << "=== 初始 ===" << endl;
printStudent(&stu); // 传地址
cout << "=== 加 5 分(指针版) ===" << endl;
addScore(&stu, 5.0);
printStudent(&stu); // 97.5
cout << "=== 再加 5 分(会撞上限) ===" << endl;
addScore(&stu, 5.0);
printStudent(&stu); // 100(被夹住了)
cout << "=== 过生日(引用版) ===" << endl;
birthday(stu); // 引用版调用不用 &
printStudent(&stu); // 19 岁
cout << "=== 地址与大小的观察 ===" << endl;
cout << "stu 的地址 = " << &stu << endl;
Student *p = &stu;
cout << "指针 p 指向的地址 = " << p << "(和上面一样)" << endl;
cout << "sizeof(Student) = " << sizeof(Student) << " 字节" << endl;
cout << "sizeof(p) = " << sizeof(p) << " 字节(指针本身,和结构体多大无关)" << endl;
cout << "=== 两种访问写法等价 ===" << endl;
cout << "p->score = " << p->score << endl;
cout << "(*p).score = " << (*p).score << "(完全一样)" << endl;
return 0;
}
看到什么算成功:
=== 初始 ===
张三 18 岁 92.5 分
=== 加 5 分(指针版) ===
张三 18 岁 97.5 分
=== 再加 5 分(会撞上限) ===
张三 18 岁 100 分
=== 过生日(引用版) ===
张三 19 岁 100 分
=== 地址与大小的观察 ===
stu 的地址 = 0x...(每次运行可能不同)
指针 p 指向的地址 = 0x...(和上面一样)
sizeof(Student) = 32 字节
sizeof(p) = 4 字节(指针本身,和结构体多大无关)
=== 两种访问写法等价 ===
p->score = 100
(*p).score = 100(完全一样)
三个观察点:
- addScore 真的改了原件(92.5 → 97.5 → 100);
- sizeof(p) 只有 4 字节,而 sizeof(Student) 是 32——传地址只花了 4 字节的代价就"交出了"那 32 字节的东西;
- p->score 和 (*p).score 输出完全一样,证实 -> 就是"解引用 + 点"的缩写。
注意最后一行的
sizeof(p)在你机器上可能是 8(64 位编译器)。只要是 4 或 8,都说明"指针大小和指向的东西无关"。
30 秒实验:把 printStudent 的形参从 const Student *p 改成 Student *p(去掉 const),然后在函数里加一句 p->score = 0;。编译能过——这提醒你:想去掉 const 保护太容易了,所以"只读"这个承诺靠的是自觉,const 只是替你兜底。
八、常见误区
- "
p->score和*p.score是一回事"——完全不是。*p.score会被解析成*(p.score),而指针没有成员 → 编译报错。要解引用再取成员,必须(*p).score或直接p->score。 - "传指针不用写
&"——必须写。raiseScore(stu)传的是结构体本身,形参要的是地址,类型对不上会报错(或者是编译器做了别的解释)。记住三配套:形参*、体内->、调用&。 - "指针参数和引用参数差不多,随便选"——语义不同:引用"一定有效",指针"可能为空"(
nullptr)。只在需要表达"可能没有"时才用指针(7.8 的结论)。 - "
p->age++会让指针挪一格"——不会。p->age++等价于(p->age)++(值 +1)。要挪指针那是p++(7.5.1 的老区别)。 - "指针参数更高级,所以一律用指针"——不是。只读时
const Student &s更干净;只有在要修改或需要"可能为空"时才轮到指针。
自测三问(点开对答案)
1. `p->score` 展开成"解引用 + 点"的写法是什么?为什么不能写 `*p.score`?——展开是 `(*p).score`。因为 `.` 优先级高于 `*`,`*p.score` 会被当成 `*(p.score)`。 2. 结构体指针传参的三处配套写法是什么?——形参写 `Student *p`、体内用 `p->成员`、调用处写 `&实参`。 3. 传 `Student` 值和传 `Student *` 地址,各自复制多少字节?——传值复制整个 `sizeof(Student)`(本例 32 字节);传地址复制指针本身(4 或 8 字节),**与结构体多大无关**。- ☐ 8.4 结构体数组(下一节)
相关笔记
- 使用结构体变量做函数参数 —— 上一节:传值(含实例02)
- 结构体数组 —— 下一节:一堆结构体
- 传递地址 —— 同一个套路在 int 上
- 结合性和优先级 ——
.为什么比*高 - 🔷 C++基础 · 索引 —— 全课路线图