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

结构体数组名就是"第一个元素的地址"(7.5 的老结论),所以 Student *p = cls; 是合法的。之后 p->name 就是 cls[0].name,(p+1)->name 就是 cls[1].name。p++ 不是挪 1 个字节,而是挪 sizeof(Student)——本例是 32 字节,正好跳到下一条记录。教材实例04就在这里。顺带记住:结构体数组传进函数时会退化成指针,长度必须另传一个参数(7.8.3 的老规律)。

📝 本节和前面几节的关系

这是三章知识的总合流:7.5"数组名是首地址 + a[i] ≡ *(a+i)"、7.4.3"指针 +1 按元素跳"、7.4.1~7.4.2"& 与 *"、8.3.2"-> 箭头"——全部在"结构体数组"这个场景里会师。学完这一节,你对"数组、指针、结构体"三件套的理解就闭环了:它们不是三个孤立的东西,而是同一套"用地址操作内存"的三种说法。

一、起点:数组名就是首元素地址

Student cls[3] = { ... };
Student *p = cls;               // 等价于 Student *p = &cls[0];(7.5 的规则)

为什么可以直接赋? 因为 cls 这个数组名会被解释成"首元素的地址",类型是 Student *(指针的类型必须和元素类型一致)。

验证:

cout << cls     << endl;        // 0x7ffd...(首地址)
cout << &cls[0] << endl;        // 完全一样
cout << p       << endl;        // 还是同一个地址

二、两种访问写法,完全等价

cout << cls[0].name << endl;    // 下标法
cout << p->name    << endl;     // 指针法:p 指向 cls[0]

四种写法的等价关系(把 7.5.1 的四写法套到结构体上):

下标法 指针法 说明
cls[i].name (p + i)->name 等价
cls[0].name p->name 等价
cls[i] *(p + i) 取到整条记录

读法:(p + i)->name 读作"p 往后挪 i 条记录,取它的 name"。

注意括号:(p + i)->name 的括号不能省(. -> 的优先级比 + 高)。写成 p + i->name 就完全变味了(i->name 也根本不合法)。

三、p++ 跳多远?—— 32 字节

这是本节最需要"身体记住"的一点:

Student *p = cls;
cout << p       << endl;        // 假设是 0x1000
p++;
cout << p       << endl;        // 0x1020 —— 差了 0x20 = 32 字节!

16 进制里 0x20 = 32,正好是 sizeof(Student)。

为什么不是 +1 字节? 因为 7.4.3 的规则:指针加减的单位是"它指向的类型的大小"。Student * 指向一个 32 字节的结构体,所以 +1 就跳过 32 字节。这正好等于"跳到下一条记录"——这才是我们想要的语义。

指针类型 p + 1 跳多少
char * 1 字节
int * 4 字节
double * 8 字节
Student * 32 字节(sizeof(Student))

四、用指针遍历结构体数组

Student *p = cls;

for (int i = 0; i < n; i++) {
    cout << p->name << endl;        // 用 p 访问"当前这条记录"
    p++;                            // 走到下一条
}

但更地道的是用"首尾夹逼"(不引入额外的 i):

for (Student *p = cls; p < cls + n; p++) {
    cout << p->name << "\t" << p->score << endl;
}

三个要点: 1. 起点 p = cls(首地址)、终点 cls + n(尾后位置,指向"最后一条的再后面",不可解引用——7.5.1 的老规矩); 2. 条件用 p < cls + n,不要用 p <= cls + n - 1(写法更绕且容易错); 3. 循环体里用 p-> 而不是 p.——因为 p 是指针(8.3.2 的口诀:变量点、指针箭头)。

五、把结构体数组传给函数

void printAll(Student cls[], int n) {       // 写成数组或 Student *cls 都一样(7.8.3)
    for (int i = 0; i < n; i++)
        cout << cls[i].name << endl;
}

printAll(myClass, 5);                       // 调用时传数组名 + 长度

两条铁律(7.8.3 的原话,在结构体上同样成立):

  1. 数组传参会"退化成指针"——函数里改 cls[i] 就是改原件,不用加 &;
  2. sizeof(cls) 在函数里失效(得到的是指针大小 4 或 8),所以长度必须另外传一个参数。

想只读:写成 void printAll(const Student cls[], int n)——const 保护内容不被改。

六、动手 Demo(实例04 使用指针访问结构体数组)

教材实例04的核心就是"用指针访问结构体数组"。这里把它和下标法并排跑一遍,亲眼看到结果一致:

#include <iostream>
#include <cstring>
using namespace std;

struct Student {
    char   name[20];
    int    age;
    double score;
};

// ① 指针形参:和 Student cls[] 完全等价(7.8.3)
void printByPointer(const Student *p, int n) {
    for (int i = 0; i < n; i++) {
        cout << "  " << (p + i)->name << "\t" << (p + i)->age
             << "\t" << (p + i)->score << endl;
    }
}

// ② 用自增指针遍历(首尾夹逼)
void printByWalk(Student *begin, Student *end) {
    for (Student *p = begin; p < end; p++)
        cout << "  " << p->name << " -> " << p->score << endl;
}

// ③ 用指针修改数组内容
void addAll(Student *p, int n, double delta) {
    for (int i = 0; i < n; i++) (p + i)->score += delta;
}

int main() {
    Student cls[3] = {
        {"张三", 18, 92.5},
        {"李四", 19, 85.0},
        {"王五", 20, 78.5}
    };
    int n = sizeof(cls) / sizeof(cls[0]);

    // —— 地址关系 ——
    cout << "cls      = " << cls     << endl;
    cout << "&cls[0]  = " << &cls[0] << "(相同)" << endl;
    Student *p = cls;
    cout << "p        = " << p       << "(还是相同)" << endl;

    // —— 指针 +1 跳多远 ——
    cout << "p        = " << p     << endl;
    cout << "p + 1    = " << p + 1 << "  (相差 " << sizeof(Student) << " 字节)" << endl;

    // —— 下标法 vs 指针法:结果一致 ——
    cout << "=== 下标法 ===" << endl;
    for (int i = 0; i < n; i++)
        cout << "  " << cls[i].name << "\t" << cls[i].score << endl;

    cout << "=== 指针法((p+i)->) ===" << endl;
    printByPointer(cls, n);

    cout << "=== 指针自增遍历 ===" << endl;
    printByWalk(cls, cls + n);

    // —— 用指针改内容 ——
    addAll(cls, n, 2.5);
    cout << "=== 全体加 2.5 分后 ===" << endl;
    printByPointer(cls, n);

    cout << "sizeof(cls)   = " << sizeof(cls)
         << ",sizeof(cls[0]) = " << sizeof(cls[0])
         << ",个数 = " << sizeof(cls) / sizeof(cls[0]) << endl;
    return 0;
}

看到什么算成功:

cls      = 0x...(某地址)
&cls[0]  = 0x...(相同)
p        = 0x...(还是相同)
p        = 0x...
p + 1    = 0x...  (相差 32 字节)
=== 下标法 ===
  张三    92.5
  李四    85
  王五    78.5
=== 指针法((p+i)->) ===
  张三    18  92.5
  李四    19  85
  王五    20  78.5
=== 指针自增遍历 ===
  张三 -> 92.5
  李四 -> 85
  王五 -> 78.5
=== 全体加 2.5 分后 ===
  张三    18  95
  李四    19  87.5
  王五    20  81
sizeof(cls)   = 96,sizeof(cls[0]) = 32,个数 = 3

四个看点: - cls、&cls[0]、p 三个地址完全相同——证实"数组名就是首元素地址"; - p + 1 比 p 大 32(十六进制差 0x20)——指针跳动的单位是"一条记录"; - 下标法和指针法输出一模一样——两种写法等价; - addAll 直接改了原数组(92.5 → 95),因为传数组就是传地址。

30 秒实验:把 printByWalk(cls, cls + n) 改成 printByWalk(cls, cls + n - 1),重新运行——最后一条不见了。这提醒你:cls + n 是"尾后位置",cls + n - 1 才指向最后一条。搞错这一个位置,就是经典的"少打印一条"或"越界多打印一条"。

七、常见误区

  1. "p++ 让地址加 1"——加 sizeof(Student)。这是 7.4.3 的老规矩,在结构体上体现得最明显(32 字节 vs 1 字节,天差地别)。
  2. "(p + i)->name 可以写成 (p + i).name"——不行。p + i 还是指针,指针用 ->(口诀:变量点、指针箭头)。
  3. "cls + n 就是最后一条记录"——是"最后一条的再后面"(尾后位置),不能解引用。循环统一写 p < cls + n。
  4. "结构体数组传函数里,长度不用传,函数里 sizeof 能算出来"——算不出来。数组名在函数里退化成指针,sizeof 得到 4 或 8。长度必须另传(7.8.3)。
  5. "用指针遍历比下标快得多"——现代编译器对两种写法优化结果几乎一样。选哪种看可读性:能用下标就用下标(不容易错),指针遍历主要用在"只需要往前走"的场合和教学演示。
自测三问(点开对答案) 1. `Student *p = cls;` 之后,`p + 1` 的地址比 `p` 大多少?——大 `sizeof(Student)` 字节(本例 32)。**按元素跳,不按字节跳。** 2. `(p + i)->name` 用下标法怎么写?——`cls[i].name`。两者完全等价。 3. 结构体数组传进函数后,为什么必须另传长度?——数组名退化成指针(7.8.3),函数里的 `sizeof` 算的是指针大小(4 或 8),算不出元素个数。

相关笔记