🔷 第八章 结构体与共用体
📄 一句话概括

把结构体的地址交给函数(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 只是替你兜底。

八、常见误区

  1. "p->score 和 *p.score 是一回事"——完全不是。*p.score 会被解析成 *(p.score),而指针没有成员 → 编译报错。要解引用再取成员,必须 (*p).score 或直接 p->score。
  2. "传指针不用写 &"——必须写。raiseScore(stu) 传的是结构体本身,形参要的是地址,类型对不上会报错(或者是编译器做了别的解释)。记住三配套:形参 *、体内 ->、调用 &。
  3. "指针参数和引用参数差不多,随便选"——语义不同:引用"一定有效",指针"可能为空"(nullptr)。只在需要表达"可能没有"时才用指针(7.8 的结论)。
  4. "p->age++ 会让指针挪一格"——不会。p->age++ 等价于 (p->age)++(值 +1)。要挪指针那是 p++(7.5.1 的老区别)。
  5. "指针参数更高级,所以一律用指针"——不是。只读时 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 字节),**与结构体多大无关**。

相关笔记