结构体数组名就是"第一个元素的地址"(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 的原话,在结构体上同样成立):
- 数组传参会"退化成指针"——函数里改
cls[i]就是改原件,不用加&; 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 才指向最后一条。搞错这一个位置,就是经典的"少打印一条"或"越界多打印一条"。
七、常见误区
- "
p++让地址加 1"——加sizeof(Student)。这是 7.4.3 的老规矩,在结构体上体现得最明显(32 字节 vs 1 字节,天差地别)。 - "
(p + i)->name可以写成(p + i).name"——不行。p + i还是指针,指针用->(口诀:变量点、指针箭头)。 - "
cls + n就是最后一条记录"——是"最后一条的再后面"(尾后位置),不能解引用。循环统一写p < cls + n。 - "结构体数组传函数里,长度不用传,函数里
sizeof能算出来"——算不出来。数组名在函数里退化成指针,sizeof得到 4 或 8。长度必须另传(7.8.3)。 - "用指针遍历比下标快得多"——现代编译器对两种写法优化结果几乎一样。选哪种看可读性:能用下标就用下标(不容易错),指针遍历主要用在"只需要往前走"的场合和教学演示。
自测三问(点开对答案)
1. `Student *p = cls;` 之后,`p + 1` 的地址比 `p` 大多少?——大 `sizeof(Student)` 字节(本例 32)。**按元素跳,不按字节跳。** 2. `(p + i)->name` 用下标法怎么写?——`cls[i].name`。两者完全等价。 3. 结构体数组传进函数后,为什么必须另传长度?——数组名退化成指针(7.8.3),函数里的 `sizeof` 算的是指针大小(4 或 8),算不出元素个数。- ☐ 8.5 共用体(下一节)
相关笔记
- 结构体数组的声明与引用 —— 上一节:下标法与遍历
- 共用体 —— 下一节:共享内存的 union
- 指针与一维数组 —— 四种等价写法的出处
- 指针运算 —— 指针 +1 的单位
- 使用数组作为函数参数 —— 长度为什么必须另传
- 🔷 C++基础 · 索引 —— 全课路线图