04-指针与引用:理解内存与地址¶
目录页:见 C++ 小白教程目录
目标¶
- 理解“变量在内存里有地址”,并能用指针保存与使用地址
- 分清
&取地址、*解引用、nullptr空指针 - 学会引用
T&:用它安全地修改外部变量 - 看懂“数组为什么经常要和长度一起传”
先决知识¶
- 会写函数并理解值传递(见 02 章 2、4)
- 理解作用域/生命周期(见 02 章 5)
- 会用数组与字符串下标(见 03 章 1、4)
1. 内存与地址:变量不是“飘在空中”的¶
当你写:
这不是“空气里的 10”,而是“内存里某个位置存着 10”,这个位置有一个编号,这个编号就叫地址。
- 专业名称:内存地址(memory address)
- 类比:你家住址;数据住在内存里也有“门牌号”
- 作用:程序可以通过地址找到并操作那块内存里的数据
- 规则/坑点:地址是给机器看的,不要把它当成普通数字去乱算(后面会讲正确方式)
2. 指针(pointer):专门用来存地址的变量¶
- 专业名称:指针(pointer)
- 类比:一张“地址纸条”,上面写着某个变量的门牌号
- 作用:通过地址间接访问/修改那块内存里的数据
- 规则/坑点:指针本身也是变量;它可以为空(nullptr);乱用会导致崩溃或玄学 bug
2.1 & 取地址:把门牌号拿出来¶
- 专业名称:取地址运算符(address-of operator)
- 类比:把“住址”抄在纸上
- 作用:得到变量在内存中的地址
- 规则/坑点:只能对“有具体存储位置”的对象取地址(新手阶段你先理解:对普通变量取地址没问题)
2.2 * 解引用:按地址找到房子并进去拿东西¶
- 专业名称:解引用运算符(dereference operator)
- 类比:拿着地址去敲门,进屋拿到屋里的东西
- 作用:通过指针访问它指向的值
- 规则/坑点:指针为空或指向无效位置时解引用会出大事
可运行例子(看懂“指针 = 地址纸条”):
#include <iostream>
int main() {
int x = 10;
int* p = &x;
std::cout << "x=" << x << '\n';
std::cout << "p=" << p << '\n';
std::cout << "*p=" << *p << '\n';
*p = 99;
std::cout << "x=" << x << '\n';
return 0;
}
你应该观察到:
p输出的是一个看起来像“十六进制”的地址(门牌号)*p输出的是地址对应的值- 改
*p就等于改x
3. nullptr:空指针 = “没有地址”¶
- 专业名称:空指针(null pointer)
- 类比:地址纸条是空白的,你还没写任何门牌号
- 作用:明确表示“目前不指向任何有效对象”
- 规则/坑点:对
nullptr解引用必崩;使用前先判断
可运行例子:
#include <iostream>
int main() {
int* p = nullptr;
if (p == nullptr) {
std::cout << "p is null\n";
}
return 0;
}
4. 引用(reference):给同一个变量起“别名”¶
如果说指针是一张“地址纸条”,引用更像是“同一个人有两个名字”。
- 专业名称:引用(reference)
- 类比:别名/外号,叫法不同,但指的是同一个人(同一块内存)
- 作用:让函数能安全地修改外部变量(不用写
*,也不需要判空) - 规则/坑点:引用必须在定义时绑定到一个对象;绑定后不能再改绑
可运行例子:
#include <iostream>
int main() {
int x = 10;
int& r = x;
r = 20;
std::cout << "x=" << x << '\n';
std::cout << "r=" << r << '\n';
return 0;
}
你会看到 x 和 r 永远同步变化,因为它们是同一块数据。
5. 用引用修复“值传递改不动”的问题¶
02 章里你见过:值传递像传复印件,函数改不到外面。引用就是“直接拿到原件”。
可运行例子:写一个真正能交换两个数的 swapInt。
#include <iostream>
void swapInt(int& a, int& b) {
int t = a;
a = b;
b = t;
}
int main() {
int x = 3;
int y = 8;
swapInt(x, y);
std::cout << "x=" << x << ", y=" << y << '\n';
return 0;
}
这里你只记住一个结论就够用:
- 你希望函数“改外面的变量”,优先用
T&(引用参数)
6. 指针参数:当你需要“可能为空”或“遍历一段连续数据”¶
引用很安全,但它有一个天然限制:它不能表示“空”。当你希望“有时候有、有时候没有”这种意思,指针更合适。
6.1 指针当参数:可空 + 需要判空¶
#include <iostream>
bool setToZero(int* p) {
if (p == nullptr) {
return false;
}
*p = 0;
return true;
}
int main() {
int x = 5;
setToZero(&x);
std::cout << "x=" << x << '\n';
int* p = nullptr;
bool ok = setToZero(p);
std::cout << "ok=" << ok << '\n';
return 0;
}
你会看到:指针参数更灵活,但你必须承担“判空”的责任。
7. 数组为什么经常要和长度一起传:数组会“退化”¶
03 章我们先让你记了结论:数组传给函数时常见写法是“数组 + 长度”。现在讲原因。
- 专业名称:数组退化为指针(array-to-pointer decay)
- 类比:你把“一整排储物格”交给别人时,实际上只给了“第一个格子的门牌号”
- 作用:函数拿到的是起点地址,想遍历后面的格子就需要知道有多少格
- 规则/坑点:函数内部通常无法凭空知道数组长度,所以必须额外传
n
看一个可运行例子:用指针遍历数组的连续内存。
#include <iostream>
int sumOf(const int* p, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += p[i];
}
return sum;
}
int main() {
int a[5] = {1, 2, 3, 4, 5};
std::cout << "sum=" << sumOf(a, 5) << '\n';
return 0;
}
这里的 p[i] 你可以理解成“指针版的下标访问”:
p[i]等价于*(p + i)
你不需要现在就去手算地址,但你要知道:
- “连续内存 + 起点地址 + 偏移量” 就能定位到任何一个元素
8. 最大的坑:悬空指针(dangling pointer)¶
你在 02 章学过:局部变量离开作用域会销毁。指针如果还记着它的地址,就会变成“写着已经拆迁的房子的门牌号”。
- 专业名称:悬空指针(dangling pointer)
- 类比:地址还在,但房子没了;你去敲门会出大事
- 作用:这是一个“警告概念”,你必须知道它存在
- 规则/坑点:不要返回局部变量的地址;不要保存指向已销毁对象的指针
下面这种写法不要做(这段不要运行):
正确做法会在后续章节讲(类、RAII、智能指针),你现在先记住“局部变量会消失”就行。
常见坑¶
-
错误:对
nullptr解引用*p
结果:崩溃
正确:解引用前判空,或用引用参数表达“不能为空” -
错误:把
&和*的含义搞反
结果:要么编译不过,要么逻辑乱
正确:&取地址(拿门牌号),*解引用(按地址找值) -
错误:想修改外部变量却用值传递
结果:外部变量不变
正确:用引用参数T&(见第 5 节) -
错误:数组遍历写成
i <= n
结果:越界访问
正确:循环条件用i < n -
错误:返回局部变量地址或保存它
结果:悬空指针,行为不稳定
正确:不要这么做;后续用更安全的资源管理方式
小练习¶
练习 1:用引用写 3 个函数¶
- 题目:实现
swapInt(int& a, int& b)、clamp(int& x, int low, int high)、makePositive(int& x) - 输入/输出:在
main里读入数据,调用函数后输出结果 - 约束:只用 if,不用库函数
- 提示:
clamp:小于 low 变 low;大于 high 变 high;否则不变 - 目标:练引用参数修改外部变量
练习 2:写一个“安全设置”函数(指针可空)¶
- 题目:实现
bool trySet(int* p, int value) - 输入/输出:传入指针,如果为空返回 false;否则写入并返回 true
- 约束:必须判空
- 提示:把返回值当成“有没有成功写入”
- 目标:练指针判空与解引用
练习 3:数组查找(数组退化)¶
- 题目:实现
int findFirst(const int* a, int n, int x),返回 x 第一次出现的下标;找不到返回 -1 - 输入/输出:输入 n、数组、x,输出下标
- 约束:用循环遍历
- 提示:一旦找到就可以提前 return
- 目标:练“指针/数组参数 + 长度”的典型接口
练习 4:用指针遍历字符串¶
- 题目:读入一行字符串,统计其中数字字符的个数
- 输入/输出:输入一行,输出
digits=... - 约束:必须用
const char* p = s.c_str()和指针移动来遍历(不准用下标) - 提示:
s.c_str()会给你一个 C 风格字符串的起点地址,以'\0'结尾 - 目标:练指针移动与终止条件
练习 5:解释题(强制理解)¶
- 题目:用自己的话解释:为什么函数里通常不知道数组长度
- 输入/输出:无
- 约束:必须提到“数组退化为指针”
- 提示:从“传进去的到底是什么”入手解释
- 目标:把核心概念讲清楚
小结¶
你现在应该掌握:
- 指针:存地址,
&取地址,*解引用,nullptr表示空 - 引用:变量别名,适合做“必须能修改外部变量”的参数
- 数组退化:数组传参常常变成起点地址,所以长度必须额外传
- 悬空指针:指向已销毁对象的地址纸条非常危险
下一篇会进入类与对象:你会看到“把数据和操作绑在一起”以后,很多资源管理问题能更优雅地解决。