学习时间: 2026 年 11 月 13 日晚至 14 日凌晨
学习主题: C++ 指针、运算符优先级、数组指针、函数传参、二级指针与空指针
训练方式: 阅读代码 → 自然语言解释 → 预测执行结果 → 分析错误 → 修正理解
阶段状态: 基础知识学习与状态追踪训练完成,待进入独立 Coding 阶段
一、本次训练的起因与目标
开始训练前,我发现自己对于 C++ 指针的理解存在一个明显问题:
我觉得我现在对于指针的理解差在还不能流畅的转化于人类语言思维链,指针改来改去的就晕了。
问题并不是完全不了解指针,而是虽然知道 *、&、p++ 等基础语法,但遇到多个指针连续修改、地址复制、函数调用时,容易失去对变量状态的追踪。
因此,本次训练的目标不是记住更多语法,而是建立一种思维方式:
- 看见指针表达式,能够翻译成自然语言。
- 区分指针保存的地址和指针指向的数据。
- 理解前置、后置自增以及运算符优先级。
- 连续追踪多个指针的指向和内存数据。
- 理解指针作为函数参数时的行为。
- 掌握一级指针、二级指针和指针引用之间的关系。
第一章:指针与内存的基本关系
1.1 指针的本质
原始代码:
int a = 10;
int* p = &a;
自然语言解释:
a是一个整数变量,保存数值10。&a获取变量a的地址。p是一个int*类型的指针变量。p保存a的地址,因此p指向a。*p表示访问p指向的对象,即变量a。
重点区分:
| 表达式 | 含义 |
|---|---|
a |
变量 a 的值 |
&a |
a 的地址 |
p |
p 保存的地址 |
*p |
p 指向的对象 |
&p |
指针变量 p 自己的地址 |
需要特别注意:
指针本身也是一个变量,因此指针自己也有地址。
例如:
int a = 10;
int* p = &a;
cout << p << endl; // 输出 a 的地址
cout << *p << endl; // 输出 10
cout << &p << endl; // 输出 p 自己的地址
p 和 &p 不是一回事。
1.2 数组与指针的关系
int arr[3] = {10, 20, 30};
int* p = arr;
数组名 arr 在这里会转换为首元素的地址,因此:
int* p = arr;
与下面这句具有相同的初始化效果:
int* p = &arr[0];
可以通过指针访问数组:
*p // arr[0]
*(p + 1) // arr[1]
*(p + 2) // arr[2]
指针算术的移动单位取决于指针指向的类型。
假设 sizeof(int) == 4,那么:
p + 1:地址向后移动 4 字节。p + 2:地址向后移动 8 字节。
但编程时应理解为移动一个或两个 int 元素,而不是手动计算字节。
另外,p + 1 只是产生一个新指针值,不会改变 p;p++ 才会更新 p。
第二章:指针状态追踪基础
2.1 练习一:数组指针移动与修改
原始题目代码
int arr[4] = {5, 10, 15, 20};
int* p = arr;
p++;
*p = 30;
p = p + 2;
(*p)++;
p--;
我的原始分析
现在在arr这个数组里面,声明了一个指针p指向arr数组的第一个数,然后p++移动指针指向数组下一个值,*p是解引用将第二个值也就是10修改为30,p=p+2修改到指向数组的第四个元素,然后将20加一变成21,最后p--指向第三个元素也就是15。
最终回答:
arr = {5, 30, 15, 21}
p 指向 arr[2]
检查结果
全部正确。
逐步验证
| 操作 | p 指向 | 数组状态 |
|---|---|---|
| 初始化 | arr[0] | {5,10,15,20} |
p++ |
arr[1] | {5,10,15,20} |
*p = 30 |
arr[1] | {5,30,15,20} |
p = p + 2 |
arr[3] | {5,30,15,20} |
(*p)++ |
arr[3] | {5,30,15,21} |
p-- |
arr[2] | {5,30,15,21} |
本题知识点
p++修改指针保存的地址。*p = 30修改指针指向的数据。p = p + 2移动两个元素。(*p)++修改当前指向的数据。- 数组元素下标从
0开始。
这道题说明我已经能够完成基本的指针状态追踪。
第三章:运算符优先级与前后置自增
这是本次训练中最容易出现混淆的部分。
3.1 核心概念:后置自增
int a = 10;
int b = a++;
最终:
a = 11;
b = 10;
原因:
a++ 这个表达式产生旧值 10,但它也会让 a 自身增加到 11。
相反:
int a = 10;
int b = ++a;
最终:
a = 11;
b = 11;
因为 ++a 是前置自增,表达式产生更新后的值。
最重要的一条规则:
前置和后置自增都会修改原变量,区别在于表达式产生的是旧值还是新值。
3.2 练习二:连续自增造成的状态混淆
原始题目
int a = 5;
int b = a++;
int c = ++a;
我的原始回答
a = 6
b = 5
c = 7
错误分析
错误在于只计算了 a++ 对 a 的修改,却忽略了 ++a 同样会修改 a。
正确追踪
int a = 5; // a = 5
int b = a++; // b = 5,a = 6
int c = ++a; // a = 7,c = 7
正确结果:
a = 7
b = 5
c = 7
另一个类似错误
原始题目:
int x = 10;
int y = ++x;
int z = x++;
我的原始回答:
x = 12
b = 10
z = 12
其中还将 y 误写成了 b。
正确结果:
x = 12
y = 11
z = 11
修正过程:
int x = 10;
int y = ++x; // x = 11,y = 11
int z = x++; // z = 11,x = 12
随后进行拆分训练时,我已经能够正确回答:
z = 11
x = 12
y 不变
错误原因总结
连续执行多个自增表达式时,我容易把:
- 表达式产生的结果;
- 原变量更新后的值;
- 之前赋值给其他变量的值;
混成同一个状态。
解决方法是在每一行执行结束后,立即更新变量状态,而不是只记住表达式的返回结果。
3.3 理解 *p++
原始代码:
int arr[3] = {10, 20, 30};
int* p = arr;
int a = *p++;
int b = *p;
这道题我最终正确选择了:
a = 10
b = 20
但最初一直不理解:
为什么 ++ 的优先级比 * 高,却依然读取旧位置的数据?
正确理解
*p++
按照运算符优先级解析为:
*(p++)
这里有两个不同的概念:
- 运算符优先级决定表达式如何结合。
- 后置自增的求值规则决定表达式产生旧值,同时更新原变量。
p++ 产生的是更新前的指针值。
因此外面的 * 解引用的是旧地址。
可以将:
int a = *p++;
在这个简单场景下理解为:
int a = *p;
p++;
所以:
a = 10;
p = &arr[1];
数组没有修改。
需要注意,这种拆分是用于理解该例子的等效效果,而不是说所有复杂表达式都可以随意拆分或重排。
3.4 *p++、*++p、(*p)++、++(*p)
这是必须熟练掌握的四种表达式。
| 表达式 | 自增的对象 | 表达式的结果 |
|---|---|---|
*p++ |
指针 p | 旧指针指向的数据 |
*++p |
指针 p | 新指针指向的数据 |
(*p)++ |
p 指向的数据 | 数据自增前的值 |
++(*p) |
p 指向的数据 | 数据自增后的值 |
理解运算符优先级
后缀自增 x++ 的优先级高于前缀解引用 *。
因此:
*p++
解析为:
*(p++)
而:
(*p)++
括号使 *p 成为后置自增的操作对象。
在这里,括号改变的是表达式的结合方式,不能简单把“优先计算括号”理解成“先移动指针”。
练习三:混合自增
原始题目:
int arr[4] = {10, 20, 30, 40};
int* p = arr;
int a = *p++;
int b = (*p)++;
int c = *p;
p++;
*p = a + b;
我的原始推导中:
a = 10,正确。- 将
(*p)++理解为先移动指针到arr[2],错误。 - 因此误判
b = 30、c = 30。
错误原因:
将 (*p)++ 与 *p++ 的作用对象混淆。
正确执行过程
int a = *p++;
// a = 10
// p 指向 arr[1]
int b = (*p)++;
// b = 20
// arr[1] = 21
// p 不变
int c = *p;
// c = 21
p++;
// p 指向 arr[2]
*p = a + b;
// arr[2] = 10 + 20 = 30
最终:
a = 10
b = 20
c = 21
arr = {10, 21, 30, 40}
p 指向 arr[2]
3.5 练习四:前置与后置指针自增
原始题目:
int arr[4] = {10, 20, 30, 40};
int* p = arr;
int a = *++p;
int b = *p++;
int c = *p;
我的原始回答:
a = 20
b = 20
c = 30
数值完全正确,但对第一行的描述不够精确。
修正理解
int a = *++p;
实际过程:
++p将指针从arr[0]移动到arr[1]。++p产生更新后的指针。*解引用这个指针,得到20。
指针只移动一次。
然后:
int b = *p++;
读取 arr[1] 的 20,再让 p 指向 arr[2]。
最后:
int c = *p;
读取 30。
另一次针对性训练
原始代码:
int arr[3] = {10, 20, 30};
int* p = arr;
int a = (*p)++;
int b = *p++;
我的回答:
a = 10
arr[0] = 11
p 在第一步没有移动
b = 11
p 最终指向 arr[1]
arr = {11, 20, 30}
全部正确。
这表明经过重复训练后,我已经能够正确区分指针自增与数据自增。
第四章:多个指针与地址复制
4.1 两个指针指向同一个变量
原始代码:
int a = 10;
int b = 20;
int* p = &a;
int* q = p;
*q = 30;
p = &b;
*p = 40;
我的原始分析
我最开始将:
int* q = p;
理解为:
将指针 p 的地址赋给 q。
并且认为:
p = &b 以后,q 应该也会更改指向吧。
虽然最终 a = 30、b = 40 判断正确,但对 q 的指向存在误解。
错误原因
混淆了:
p
与:
&p
p 保存的是变量 a 的地址。
&p 才是指针变量 p 本身的地址。
因此:
int* q = p;
只是把 p 保存的地址复制给 q。
p 和 q 是两个独立的指针变量,只是最初保存着同一个地址。
正确追踪
| 操作 | a | b | p 指向 | q 指向 |
|---|---|---|---|---|
| 初始化 | 10 | 20 | a | a |
*q = 30 |
30 | 20 | a | a |
p = &b |
30 | 20 | b | a |
*p = 40 |
30 | 40 | b | a |
最终:
a = 30
b = 40
p 指向 b
q 指向 a
核心原理
复制地址,不等于让两个指针变量绑定在一起。
如果两个指针指向同一对象,通过其中一个修改对象,另一个可以观察到变化。
但改变其中一个指针保存的地址,不会自动修改另一个指针。
4.2 练习五:两个指针分别移动
原始题目:
int arr[4] = {10, 20, 30, 40};
int* p = arr;
int* q = p;
p++;
*q = 15;
q = p;
(*q)++;
p++;
*q = *p;
我的原始分析
本题我正确判断了整个过程:
p++:p 指向arr[1],q 仍指向arr[0]。*q = 15:将arr[0]改成15。q = p:q 改为指向arr[1]。(*q)++:将arr[1]从20改成21。p++:p 移动到arr[2]。*q = *p:把arr[2]的值复制到arr[1]。
最终:
arr = {15, 30, 30, 40}
p 指向 arr[2]
q 指向 arr[1]
全部正确。
本题最重要的区分
q = p;
复制的是地址。
*q = *p;
复制的是数据。
这两种操作有完全不同的结果。
4.3 练习六:指针算术与共享指向
原始代码:
int arr[4] = {10, 20, 30, 40};
int* p = arr;
int* q = p + 2;
*p = *q;
p++;
*q = *p + 5;
q--;
*p = *q;
我的原始推导
- p 指向
arr[0],q 指向arr[2]。 *p = *q,将arr[0]修改为30。p++,p 指向arr[1]。*q = *p + 5,将arr[2]修改为25。q--,q 指向arr[1]。*p = *q,相当于将20赋值给20。
我最后提出:
那最后一步不是将20赋值给20吗,那就没变?
这个判断正确。
最终:
arr = {30, 20, 25, 40}
p 指向 arr[1]
q 指向 arr[1]
知识点
当两个指针指向同一个元素时:
*p = *q;
相当于该元素对自身进行赋值,数值不会变化。
但仍应区分指针变量是否为同一个变量。
第五章:指针与函数参数
这是本次训练第二个重要难点。
5.1 普通指针按值传参
原始代码:
#include <iostream>
using namespace std;
void change(int* p)
{
*p = 50;
p++;
}
int main()
{
int arr[3] = {10, 20, 30};
int* p = arr;
change(p);
cout << arr[0] << endl;
cout << *p << endl;
}
我的原始错误分析
最开始,我将 change 内部的代码理解为直接放进 main 中执行:
*p = 50 修改数组,然后 p++ 指向 arr[1],最终输出 50 和 20。
错误原因
忽略了函数参数的按值传递。
change(int* p) 中的 p 是一个新的局部指针变量。
它复制了调用者指针保存的地址,但不是调用者的指针变量本身。
正确理解
可以在脑海中使用不同的名字模拟:
int arr[3] = {10, 20, 30};
int* p = arr;
// 模拟函数内部的参数副本
int* q = p;
*q = 50;
q++;
// main 中的 p 没有发生移动
最终:
arr = {50, 20, 30}
main 中的 p 仍指向 arr[0]
输出:
50
50
核心规则
普通指针按值传参时:
- 可以通过指针修改外部对象。
- 修改函数内部的指针副本,不会改变外部指针变量。
5.2 练习七:函数移动指针后修改数据
原始代码:
void change(int* q)
{
q++;
*q = 100;
}
int main()
{
int arr[3] = {10, 20, 30};
int* p = arr;
change(p);
cout << arr[0] << endl;
cout << arr[1] << endl;
cout << *p << endl;
}
我的原始回答
arr = {10, 100, 20}
p 指向 arr[0]
输出:
10
100
10
错误分析
指针移动和输出都判断正确,但数组第三个元素写错。
原始的 arr[2] = 30 从来没有被修改。
正确结果:
arr = {10, 100, 30}
p 指向 arr[0]
输出:
10
100
10
这次属于状态记录疏漏,而不是指针概念错误。
需要养成的习惯:
每一步只修改真正被代码操作的变量,其他变量必须保留原值。
第六章:指针引用传参 int*&
6.1 指针引用的基本概念
原始题目:
void change(int*& q)
{
q++;
}
int main()
{
int arr[3] = {10, 20, 30};
int* p = arr;
change(p);
cout << *p << endl;
}
我的回答:
20
正确。
原理
int* q
表示普通指针参数,按值接收地址副本。
int*& q
表示 q 是一个 int* 指针变量的引用。
可以把引用理解为给原来的指针变量起另一个名字。
因此:
q++;
会修改调用者的原指针 p。
注意,引用语法中的 & 是引用声明符,不是这里对一个表达式执行取地址运算。
6.2 练习八:普通指针参数与引用参数对比
原始代码:
void func1(int* q)
{
q++;
*q = 50;
}
void func2(int*& q)
{
q++;
*q = 100;
}
int main()
{
int arr[4] = {10, 20, 30, 40};
int* p = arr;
func1(p);
func2(p);
cout << *p << endl;
}
我的原始推导
func1(p):
arr = {10, 50, 30, 40}
p 指向 arr[0]
func2(p):
arr = {10, 100, 30, 40}
p 指向 arr[1]
最终输出:
100
全部正确。
本章总结
| 函数参数 | 能修改外部数据吗 | 能修改调用者的指针指向吗 |
|---|---|---|
int* q |
可以 | 不能直接修改原指针 |
int*& q |
可以 | 可以 |
这里的区别是参数传递机制,不是指针能否访问内存的问题。
第七章:二级指针 int**
7.1 什么是二级指针?
基础代码:
int a = 10;
int* p = &a;
int** pp = &p;
可以理解为:
pp → p → a
变量之间的关系:
| 表达式 | 类型 | 含义 |
|---|---|---|
pp |
int** |
保存指针 p 的地址 |
*pp |
int* |
访问指针变量 p |
**pp |
int |
访问 p 指向的整数 a |
必须注意:
int** pp = &p;
不是直接让 pp 指向 a,而是让 pp 指向指针变量 p。
7.2 练习九:二级指针修改地址和数据
原始题目:
int a = 10;
int b = 20;
int* p = &a;
int** pp = &p;
**pp = 30;
*pp = &b;
**pp = 40;
我的原始推导
**pp = 30,将a改为30。*pp = &b,修改指针p,让它指向b。**pp = 40,现在通过p修改b,因此b = 40。
最终:
a = 30
b = 40
p 指向 b
pp 指向 p
全部正确。
核心规律
pp:修改二级指针自己保存的地址。*pp:访问或修改一级指针p。**pp:访问或修改一级指针指向的数据。
7.3 练习十:二级指针与独立指针变量
原始代码:
int a = 10;
int b = 20;
int* p = &a;
int* q = p;
int** pp = &p;
*pp = &b;
**pp = 50;
*q = 30;
我的原始回答
p 指向 b
q 指向 a
**pp = 50 修改 b
最终:
a = 30
b = 50
全部正确。
逐行解释
初始化后:
pp → p → a
q → a
执行:
*pp = &b;
变成:
pp → p → b
q → a
因此:
**pp = 50;
修改的是 b。
而:
*q = 30;
修改的是 a。
本题将两个知识点结合在一起:
- 普通指针之间复制地址。
- 二级指针直接修改另一个指针变量。
第八章:二级指针作为函数参数
8.1 为什么 int** 可以接收 &p?
原始代码:
void change(int** pp)
{
**pp = 50;
}
int main()
{
int a = 10;
int* p = &a;
change(&p);
}
我的原始回答
因为我们声明的这个change函数接收的值是一个二级指针,二级指针需要一个指针的地址才能生效。
同时正确判断:
a = 50
p 指向不变
更精确的解释
这里关键是参数类型匹配:
int a = 10;
a 的类型是:
int
而:
&a
的类型是:
int*
声明:
int* p = &a;
则:
&p
的类型为:
int**
因此 change(&p) 可以将 &p 传给 int** pp。
函数中的 pp 是 &p 的地址副本,但通过 *pp 可以访问原来的指针 p。
8.2 练习十一:int* 与 int** 修改空指针
原始题目:
void func1(int* q)
{
q = nullptr;
}
void func2(int** q)
{
*q = nullptr;
}
int main()
{
int a = 10;
int* p = &a;
func1(p);
func2(&p);
}
我的原始错误
我认为两个函数执行后,main 中的 p 都会变成空指针。
实际只有 func2 可以修改 main 中的 p。
错误分析
func1:
void func1(int* q)
{
q = nullptr;
}
这里只修改了局部参数 q。
调用者的 p 仍然指向 a。
func2:
void func2(int** q)
{
*q = nullptr;
}
这里 q 保存的是 p 的地址。
因此:
*q = nullptr;
直接修改原来的 p。
正确状态
| 操作 | p 的状态 | a 的值 |
|---|---|---|
| 初始化 | 指向 a | 10 |
func1(p) |
仍指向 a | 10 |
func2(&p) |
nullptr | 10 |
这再次证明:
修改指针副本,与通过另一层指针修改原指针,是两种不同的操作。
第九章:nullptr、空指针与未定义行为
9.1 nullptr 的含义
int* p = nullptr;
表示 p 当前不指向任何对象。
但需要区分:
p = nullptr;
和:
*p = 0;
前者让指针变为空指针。
后者要求 p 有效地指向一个可修改的整数对象,然后将该整数修改为 0。
两者完全不同。
9.2 练习十二:修改指针后,原变量会消失吗?
原始代码:
void change(int** q)
{
**q = 50;
*q = nullptr;
}
int main()
{
int a = 10;
int* p = &a;
change(&p);
cout << a << endl;
}
我的判断:
a = 50
p = nullptr
正确。
但是在回答额外问题:
cout << *p;
时,我最初认为程序应该“什么也没有”。
错误纠正
解引用空指针属于未定义行为(Undefined Behavior,UB)。
可能导致:
- 程序崩溃。
- 访问异常。
- 出现不可预测的程序行为。
不能认为一定没有输出,也不能认为一定输出 0。
安全写法:
if (p != nullptr)
{
cout << *p << endl;
}
else
{
cout << "Cannot dereference nullptr" << endl;
}
另外:
p = nullptr;
不会删除原来的变量 a。
a 仍然存在,数值依旧是 50。
9.3 练习十三:pp = nullptr 与 *pp = nullptr
原始题目:
int a = 10;
int b = 20;
int* p = &a;
int** pp = &p;
pp = nullptr;
我的原始错误
我最初认为:
pp = nullptr;
会使 p 也不再指向 a。
正确理解
pp 和 p 是两个不同的指针变量。
执行:
pp = nullptr;
只让 pp 自己变为空指针。
p 仍然指向 a。
对比:
int a = 10;
int* p = &a;
int** pp = &p;
*pp = nullptr;
这里修改的是 p。
执行后:
pp 仍然指向 p
p = nullptr
a = 10
随后进行的选择题,我正确选择了:
B. pp 仍指向 p,但 p 变成空指针。
说明经过纠正后,已经能够区分这两个操作。
注意:如果已经执行 pp = nullptr,就不能继续执行 *pp = nullptr,因为此时 pp 是空指针。
第十章:三种函数参数的系统对比
本次训练涉及:
int*
int**
int*&
它们之间的区别需要重点复习。
10.1 int*:普通指针参数
void func(int* p)
{
*p = 50;
p = nullptr;
}
如果传入某个整数变量的有效地址:
- 可以修改原来的整数。
- 不能通过
p = nullptr改变调用者的指针变量。
原因:参数按值传递。
10.2 int**:二级指针参数
void func(int** pp)
{
*pp = nullptr;
}
假设调用:
int a = 10;
int* p = &a;
func(&p);
可以把调用者的 p 改为空指针。
原因:pp 指向调用者的指针变量 p,*pp 可以访问它。
10.3 int*&:指针引用参数
void func(int*& p)
{
p = nullptr;
}
调用:
int a = 10;
int* p = &a;
func(p);
同样可以把调用者的 p 改为空指针。
原因:形参 p 引用调用者的指针变量本身。
10.4 对比总结
| 写法 | 参数本质 | 修改调用者指针的方式 |
|---|---|---|
int* q |
指针副本 | 不能通过重新给 q 赋值实现 |
int** q |
指向指针的指针副本 | 修改 *q |
int*& q |
原指针变量的引用 | 直接修改 q |
现代 C++ 中,针对需要修改调用者指针的场景,可以根据接口设计选用指针引用或二级指针。
二者不能简单认为完全相同,它们的类型、调用方式和空值处理都存在区别。
第十一章:本次典型错误汇总
| 错误类型 | 最初的错误理解 | 正确理解 |
|---|---|---|
| 前后置自增 | 忽略变量本身的更新 | 前后置都会使变量增加 |
*p++ |
优先级高意味着先移动完再读取新地址 | 后置自增表达式产生旧指针 |
(*p)++ |
误认为移动指针 | 自增的是 p 指向的数据 |
| 指针赋值 | q = p 使两个指针绑定 |
只是复制地址 |
| 地址概念 | 把 p 当成 &p |
p 保存目标地址,&p 是自身地址 |
| 函数传参 | 函数中的 p 与 main 中的 p 是同一个变量 | 普通指针形参是独立副本 |
| 数组状态 | 修改一个元素时误改其他元素 | 没有被操作的元素保持原值 |
| 二级指针 | 修改 pp 会自动修改 p |
必须区分每一层变量 |
| nullptr | 空指针解引用可能只是没有输出 | 属于未定义行为 |
错误的共同根源
经过这些题目,可以总结出三个主要原因:
第一,缺少独立追踪每个变量的习惯。
以前容易只记住当前指针的位置,而忽略另一个指针保存的地址。
第二,混淆表达式的结果与变量修改后的状态。
尤其是前后置自增:
int b = a++;
b 取得旧值,但 a 本身已经增加。
第三,没有始终区分数据、地址与地址变量。
例如:
p
*p
&p
虽然都围绕同一个指针,但含义不同。
第十二章:建立自己的指针阅读方法
以后看到复杂指针代码,采用下面的固定分析顺序。
第一步:识别变量类型
例如:
int a = 10;
int* p = &a;
int** pp = &p;
先确认:
a :int
p :int*
pp :int**
第二步:画出指向关系
pp → p → a
这一步只考虑地址关系,不急着计算数据。
第三步:确定每个操作修改谁
例如:
pp = nullptr;
修改 pp。
*pp = nullptr;
在 pp 有效指向 p 的前提下,修改 p。
**pp = 50;
在两级解引用都有效的前提下,修改 p 指向的整数。
第四步:遇到 ++ 时,区分表达式值和最终状态
p++
产生旧指针,并更新 p。
++p
更新 p,产生更新后的指针。
(*p)++
产生原数据的旧值,并增加数据。
++(*p)
增加数据,产生更新后的数据值。
第五步:函数调用时单独记录局部变量
例如:
void func(int* q)
{
q++;
}
调用:
func(p);
必须记住 q 是独立的参数副本,不能直接将其当成调用者的 p。
第六步:每执行一行,更新状态表
推荐手写这样的表格:
| 执行语句 | p 指向 | q 指向 | 数组状态 |
|---|---|---|---|
| 初始化 | arr[0] | arr[0] | {10,20,30} |
| p++ | arr[1] | arr[0] | {10,20,30} |
| *q = 50 | arr[1] | arr[0] | {50,20,30} |
这种方法可以有效减少连续推导中的状态丢失。
第十三章:阶段性学习成果与不足
13.1 本次已经理解的知识
经过连续练习,目前已经能够较稳定地理解:
- 普通指针的声明与初始化。
&取地址、*解引用。- 指针与数组元素的关系。
- 指针算术和前后置自增。
*p++、*++p、(*p)++、++(*p)。- 两个指针之间复制地址。
- 通过指针修改数组元素。
- 普通指针按值传参。
- 指针引用传参
int*&。 - 二级指针
int**。 - 通过二级指针修改原指针。
- 空指针和未定义行为的基础知识。
13.2 当前仍然需要加强的能力
虽然概念理解已经明显改善,但仍然存在一些尚未充分验证的能力:
- 在更复杂的代码中连续追踪多个指针。
- 独立编写完整指针程序,而不只是阅读已有程序。
- 在循环中使用指针遍历数组。
- 熟练使用指针处理结构体和链表。
- 掌握动态内存生命周期与相关错误。
- 区分指针越界、悬空指针和空指针。
- 将指针知识应用到实际 C++ 项目。
所以现在不能认为已经完全掌握指针,只能说已经建立了较为完整的基础理解。
第十四章:下一阶段 Coding 训练计划
后续不再大量重复简单选择题,而是转向真正编写代码。
第一阶段:综合状态追踪
目标:混合运用数组、二级指针、函数参数与自增运算符。
待完成题目:
#include <iostream>
using namespace std;
void change(int** pp)
{
(**pp) += 10;
(*pp)++;
}
int main()
{
int arr[4] = {10, 20, 30, 40};
int* p = arr;
int* q = p;
change(&p);
*q = *p;
(*p)++;
cout << arr[0] << " "
<< arr[1] << " "
<< arr[2] << " "
<< arr[3] << endl;
}
状态:待独立分析。
训练时必须写出:
- 函数调用前后 p 的位置。
- q 是否受到影响。
- 每次写入改变哪个元素。
- 最终数组状态。
第二阶段:独立编写指针程序
训练内容:
- 使用指针遍历数组并求和。
- 使用指针寻找最大值及其下标。
- 编写
swap(int* a, int* b)。 - 编写函数通过指针修改数组。
- 使用两个指针完成数组反转。
要求:先独立编写,再编译调试,不直接参考完整答案。
第三阶段:动态内存
学习内容:
new
delete
new[]
delete[]
理解:
- 动态分配对象。
- 对象生命周期。
- 内存泄漏。
- 悬空指针。
- 重复释放。
- RAII 和智能指针。
同时认识现代 C++ 中 std::vector、std::unique_ptr 等工具的作用,避免把手动内存管理当成所有实际程序的默认选择。
第四阶段:链表与结构体指针
主要内容:
struct Node
{
int data;
Node* next;
};
重点理解:
p->data
p->next
p = p->next
然后逐步完成:
- 创建链表。
- 遍历链表。
- 插入节点。
- 删除节点。
- 释放节点资源。
这也是将指针基础知识与数据结构课程连接起来的重要阶段。
第十五章:本次学习总结
今天最重要的收获,并不只是理解某一个指针语法,而是逐渐建立了将指针代码转换为自然语言和内存状态的能力。
刚开始阅读指针代码时,我经常因为指针不断修改而混乱。
后来通过逐行分析,逐渐明确:
- 指针变量保存的是地址。
- 解引用访问的是地址对应的对象。
- 修改指针和修改对象是两回事。
- 两个指针保存相同地址,不代表两个指针变量绑定。
- 函数参数需要区分副本与引用。
- 二级指针可以通过多一层间接访问修改另一个指针。
- 自增表达式的结果与变量更新后的状态需要分别追踪。
目前阅读基础指针代码的准确率已经提高,但还没有经过足够多的独立编程验证。
下一阶段的重点:从理解已有代码,转向独立编写指针程序。
坚持:
理解原理 → 自然语言推导 → 独立 Coding → 编译验证 → 错误复盘 → 博客记录。
这次训练也让我认识到,掌握指针并不是记住多少种语法,而是能否清楚理解每一次操作究竟修改了哪一个变量、哪一个地址或哪一块数据。