《Hello C++》4.6 指针

如果说前面学习的数组、结构体和共用体是在“组织数据”,那么这一小节要介绍的指针[1],则是在“找到数据的位置”。这两件事看起来像是两回事,但在程序里,它们其实紧密相连。因为程序除了要知道“有什么数据”,还常常需要知道“数据放在哪里”。

你可以把内存想象成一排排编号清楚的房间,每个房间里都能存放某种数据。变量就像房间里的住客,而指针更像房间的地址牌。它不直接住在房间里,它记住的是房间在哪里。正因为如此,指针在 C++ 中显得既重要又稍微有点“难缠”。

4.6.1 什么是指针

指针本质上是一种特殊的数据类型,它保存的是某个变量的地址[2]。地址可以理解为变量在内存中的位置标记。也就是说,指针不是直接存储数据本身,而是存储数据所在的位置。

1
2
int a = 10;
int* p = &a;

这里 a 是一个整数变量,&a 表示 a 的地址,p 则保存了这个地址。你可以把 p 理解成“指向 a 的指针”。如果要读取 p 指向的值,就可以使用解引用运算符 *

1
cout << *p << endl;

这里的 *p 表示“访问 p 所指向的那个变量的值”。

4.6.2 指针有什么用

初学者刚接触指针时,最常见的疑问就是:既然有普通变量,为什么还要指针?

因为指针有几个非常实用的用途。

  1. 指针可以让我们间接访问变量。通过地址,我们可以找到并修改原来的数据。
    1
    2
    3
    int a = 10;
    int* p = &a;
    *p = 20;
    这段代码最后会让 a 变成 20。也就是说,通过指针修改内容,改到的是原变量本身。
  2. 指针可以帮助我们更灵活地管理内存。后面会学到的 newdelete,就是通过指针来申请和释放内存的。
  3. 指针可以让函数和数据之间的联系更紧密。很多情况下,把地址传给函数,比直接复制一份数据更高效。
  4. 指针是许多高级数据结构和底层操作的基础。数组、字符串、动态内存、链表等内容,后面都会不断用到它。

4.6.3 声明和初始化指针

声明指针时,需要写出它所指向的数据类型。

1
int* p;

这表示 p 是一个指向 int 的指针。有时,我们也会看到另一种写法:

1
int *p;

这两种写法在语义上是一样的,都是声明一个 int 指针。但通常我们更常使用的是 int* p 这种写法,因为这个写法更容易让人一眼就看出 p 是一个指针。指针在使用前最好先进行初始化。最常见的初始化方式是让它指向某个已有变量:

1
2
int a = 10;
int* p = &a;

也可以先把它初始化为空指针[3]

1
int* p = nullptr;

这表示当前这个指针暂时没有指向任何有效对象。nullptr 是 C++11 引入的空指针常量,比旧式的 NULL 更安全,也更清楚。

4.6.4 指针的危险

指针虽然强大,但它也确实是 C++ 中比较容易出问题的知识点。最常见且较多数初学者容易构成的危险就是使用未初始化的指针空指针解引用。前者由于 p 没有被初始化,它里面的值是不确定的,解引用后程序很可能会出错,甚至直接崩溃;后者是指针没有指向有效对象,就不能去访问它所指向的内容。

1
2
3
4
5
6
7
// 未初始化指针解引用
int* p;
cout << *p << endl;

// 空指针解引用
int* p = nullptr;
cout << *p << endl;

还有一种情况,是“悬空指针”[4]。也就是说,指针曾经指向过一块内存,但那块内存已经被释放了,而指针本身还保留着旧地址。这种指针非常危险,因为它看起来像是有值,但实际上已经不能安全使用了。

如果你暂时只记住一件事,那就记住这一句:在使用指针之前,先确认它指向的是有效对象。这句话虽然朴素,但非常重要。

4.6.5 指针和数字

指针保存的是地址,而地址在本质上也是一个数值。也正因为如此,很多初学者会把指针和普通数字混在一起看待。但要注意,指针不是普通数字。虽然它内部确实存着一个数值,但这个数值代表的是内存位置,而不是我们平时意义上的“整数”。

1
2
int a = 10;
int* p = &a;

这里 p 的内容是地址,不是 1020 这种普通数值。你不应该把指针当成随便能加减计算的整数来用。当然,在某些低底层操作里,指针确实可以和地址运算联系起来,但那已经是更深入的内容了。对于现在的我们来说,最重要的是先把它当作“地址的保存者”。

4.6.6 使用 new 来分配内存

到目前为止,我们使用的变量大多是自动存储的,也就是它们在作用域结束时会自动释放。但有些时候,我们需要在程序运行时再决定要多少内存,这就要用到 new 关键字。

1
int* p = new int;

这句代码会在堆上[5]分配一块可以存放 int 的内存,并把这块内存的地址交给指针 p。我们还可以在申请内存时直接初始化:

1
int* p = new int(20);

这样 p 指向的那块内存里,初始值就是 20。在 C++ 中 new 的意义非常大,它让程序可以在运行时灵活申请内存,而不是一开始就把所有空间都固定好。这对于动态数据、变长数据和复杂结构都很有帮助。

4.6.7 使用 delete 释放内存

既然在使用时可以通过 new 申请内存,那么用完之后就要手动释放,这就是 delete 的作用。

1
delete p;

这句代码会把 p 所指向的动态内存归还给系统。释放之后,p 就不应该再继续当作有效指针来使用了。通常我们还会把它重新置空,避免后续误用:

1
2
delete p;
p = nullptr;

这样可以减少悬空指针带来的风险。如果你申请的是动态数组,那么释放时要用 delete[]

1
2
int* arr = new int[5];
delete[] arr;

这一点非常重要,因为单个对象和数组的释放方式并不相同。

4.6.8 使用 new 创建动态数组

有时候我们在程序运行前并不知道数组究竟要多大,这时就可以使用动态数组[6]

创建动态数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

int main()
{
int size = 5;
int* arr = new int[size];

for (int i = 0; i < size; ++i)
{
arr[i] = i * 10;
cout << arr[i] << endl;
}

delete[] arr;
arr = nullptr;

return 0;
}

这段程序先申请了一个大小为 5 的整型数组,然后逐个给元素赋值,最后再释放它。就动态数组而言,它的好处在于,数组大小可以在运行时决定。比如用户输入了要处理的数据个数,程序就可以根据这个数值来申请合适大小的数组。但动态数组也更容易出错,因为它始终需要开发者完成两件事情:一是正确申请,二是正确释放。如果申请了却忘记释放,就可能造成内存泄漏[7]

在现代 C++ 中,很多需要动态数组的场景,更推荐使用标准库容器,比如 vector

不过在学习指针和内存管理时,理解 newdelete 和动态数组仍然非常重要。

4.6.9 小结

指针是 C++ 中非常重要的一类工具。它能帮助我们找到数据的位置、间接访问变量、管理动态内存,还能为后面的数组、函数、类和更复杂的数据结构打基础。

不过,指针也是一把“双刃剑”。用得好,它很强大;用不好,它就容易带来空指针、悬空指针和内存泄漏等问题。所以在初学阶段,我们既要理解它,也要谨慎使用它。后面学习动态数组、结构体指针和更复杂的内存管理时,你会越来越明显地感觉到,指针并不是一个孤立的知识点,而是整个 C++ 体系里非常核心的一环。


  1. 指针(pointer),是一种保存地址的数据类型。 ↩︎

  2. 地址,指变量在内存中的位置标记。 ↩︎

  3. 空指针(null pointer),表示当前不指向任何对象的指针。 ↩︎

  4. 悬空指针(dangling pointer),指已经失效但仍保留旧地址的指针。 ↩︎

  5. 堆(heap),是动态分配内存常用的存储区域。 ↩︎

  6. 动态数组,是运行时通过 new 创建的数组。 ↩︎

  7. 内存泄漏,指申请的内存不再使用却没有正确释放。 ↩︎