Menu

C++ 指针教程:取地址、解引用与 nullptr

从零讲解 C++ 指针:如何声明指针,取地址运算符 & 和解引用运算符 *,nullptr,指向数组的指针,以及会让程序崩溃的悬空指针和未初始化指针陷阱。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

存放地址的变量

每个变量都存放在内存中的某个位置,这个带编号的位置叫做它的地址。大多数时候你并不关心它在哪里,只管用变量名就行。指针把这件事反了过来:它是一个变量,而它的值就是一个地址。它存的不是 42,而是“42 所在的那个位置”。

正是这层间接让指针如此强大。函数可以通过指针修改调用者的变量,链表这样的数据结构靠指针把节点串起来,而且(你会在动态内存一节看到)指针也是你访问运行时分配的内存的方式。

&score 中的 & 是取地址运算符,它得到 score 所在的位置。*p 中的 * 是解引用运算符,它顺着地址找回存放在那里的值。

两个运算符:& 和 *

初学者最容易混淆的一点是:* 放在不同的位置,含义完全不同。下面这几行一定要分清:

int* p;     // DECLARATION: "p is a pointer to int"
p = &x;     // & = address-of: store the address of x in p
int y = *p; // * = dereference: read the value p points at
*p = 99;    // dereference on the left: write through the pointer

在声明中,* 是类型的一部分;在表达式中,* 才真正执行操作。指针设置好之后,对它解引用就能完整地读写原来的变量:

注意,第 1 行之后你再也没有通过名字碰过 health,可它的值却一直在变。这正是关键所在:hp 是同一块存储的另一个入口。空格写法(int* p、int *p、int*p)只是风格问题,对编译器来说完全相同,本指南统一写成 int* p。

nullptr:什么都不指向

一个不指向任何地方的指针,应该设为 nullptr(C++11)。它清楚且类型安全地表示“还没有目标”,也让你在解引用之前有东西可以检查。

优先使用 nullptr,而不是遗留的 NULL 宏或一个光秃秃的 0。因为 nullptr 拥有真正的指针类型,它在重载决议时绝不会被误读成整数 0,而老写法可能引发这种不易察觉的 bug。

陷阱:解引用空指针。 通过空指针(或未初始化的指针)读写是未定义行为,通常会让程序立刻崩溃:

int* p = nullptr;
cout << *p;   // CRASH - dereferencing null is undefined behavior

对任何可能为空的指针,解引用之前都要先用 if (p)(或 if (p != nullptr))检查一下。

指针与数组

数组的名字会退化为指向第一个元素的指针,所以指针和数组密不可分。给指针加 1 并不是加一个字节,而是前进一个元素,指针运算正是这样工作的:

p[i] 和 *(p + i) 就是同一个表达式,这种等价关系也正是数组下标从零开始的原因。这里的经典 bug 是越过末尾:nums + 4 是一个合法的“尾后”标记,可以拿来比较,但解引用 *(nums + 4) 就是越界读取。指针的差一错误是程序崩溃和数据悄悄损坏的主要原因之一,所以写停止条件时一定要想清楚。

const 与指针

const 可以作用于指针指向的东西、指针本身,或者两者都有。从右往左读声明,就能弄清它的含义:

const int* p;        // pointer to const int  - can't change *p, can repoint p
int* const p = &x;   // const pointer to int  - can change *p, can't repoint p
const int* const p = &x; // both locked

这在实际代码中随处可见。一个承诺不修改你数据的函数,会接收指向 const 的指针:

把被指向的值标为 const 既写明了意图,又能让编译器拦下意外的写入:零运行时开销,白得一份安全。

大坑:悬空指针

悬空指针指向的内存已经不再存放你期望的值:变量离开了作用域,或者内存已经被释放。对它解引用是未定义行为,更麻烦的是,它常常看起来能正常工作,直到某一天突然出问题。

int* makeBad() {
    int local = 5;
    return &local;   // BUG: local dies when the function returns
}                    // the returned pointer now dangles

这个地址仍然是一个合法的数字,但它指向的栈空间已经被回收,读取它会得到垃圾值或者直接崩溃。如果你保留了指向已被 delete 的堆对象的指针,或者指向某个 vector 中元素的指针而这个 vector 后来重新分配了内存,也会发生同样的事情。

三条规则能保你平安:

  • 永远不要返回局部变量的地址。按值返回,或者让调用者持有存储。
  • 指针指向的东西不在了之后,把指针设为 nullptr,使用前先检查。
  • 涉及所有权和生命周期时,用智能指针代替裸 new/delete:它们会自动释放内存,让这一整类 bug 大大减少。

下一节:引用与指针

指针并不是间接访问另一个变量的唯一方式。C++ 还有引用,它们用起来很像指针,但不能为空、不能重新绑定,语法也更简洁。下一节我们会在引用与指针的对比中把两者放在一起,让你清楚地知道该选哪个,以及为什么现代 C++ 在能用引用的地方更偏爱引用。

常见问题

C++ 中的指针是什么?

指针是一种变量,它存储的是另一个值的内存地址,而不是这个值本身。用 * 声明指针(例如 int* p),用 & 运算符取得地址(p = &x),再通过 *p 解引用来读写它指向的值。

C++ 指针中的 & 和 * 有什么区别?

在指针的语境下,& 是取地址运算符:&x 给出 x 的地址。* 则身兼两职:在声明中(int* p)它表示这个变量是指针,在表达式中(*p)它对指针解引用,取到该地址上存储的值。

C++ 中的 nullptr 是什么?为什么用它代替 NULL?

nullptr 是 C++11 引入的类型安全的空指针字面量,意思是“不指向任何东西”。优先用它,而不是老式的 NULL 或 0,因为 nullptr 拥有真正的指针类型,在重载决议时绝不会被误当成整数。解引用之前一定要先检查 if (p):解引用空指针是未定义行为。

Coddy 编程语言插图

和 Coddy 一起学编程

开始