跳转至

04-指针与引用:理解内存与地址

目录页:见 C++ 小白教程目录

目标

  • 理解“变量在内存里有地址”,并能用指针保存与使用地址
  • 分清 & 取地址、* 解引用、nullptr 空指针
  • 学会引用 T&:用它安全地修改外部变量
  • 看懂“数组为什么经常要和长度一起传”

先决知识

  • 会写函数并理解值传递(见 02 章 2、4)
  • 理解作用域/生命周期(见 02 章 5)
  • 会用数组与字符串下标(见 03 章 1、4)

1. 内存与地址:变量不是“飘在空中”的

当你写:

int x = 10;

这不是“空气里的 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;
}
int& r = x; 这是重点。 - int& 表示 引用类型 - r 是 x 的引用,可以理解为:r 是 x 的别名

你会看到 xr 永远同步变化,因为它们是同一块数据。


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)
  • 类比:地址还在,但房子没了;你去敲门会出大事
  • 作用:这是一个“警告概念”,你必须知道它存在
  • 规则/坑点:不要返回局部变量的地址;不要保存指向已销毁对象的指针

下面这种写法不要做(这段不要运行):

int* bad() {
    int x = 10;
    return &x;
}

正确做法会在后续章节讲(类、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 表示空
  • 引用:变量别名,适合做“必须能修改外部变量”的参数
  • 数组退化:数组传参常常变成起点地址,所以长度必须额外传
  • 悬空指针:指向已销毁对象的地址纸条非常危险

下一篇会进入类与对象:你会看到“把数据和操作绑在一起”以后,很多资源管理问题能更优雅地解决。