3.7 指针类型
指针(pointer)是 C++ 中的一种复合类型。指针类型是一种对象类型,具有大小、地址和对齐要求。指针类型的对象可以存储另一个对象的地址,从而实现对另一个对象的可变引用。
目前而言,读者可以将指针理解为存储了另一个对象的地址。指针类型的主要作用是提供对内存地址的直接访问和操作。
指针的声明与初始化
声明指针时,需要指定它所指向的数据类型,并在类型名后加上 *
:
int* p; // 声明一个指向 int 的指针
double* dp; // 声明一个指向 double 的指针
指针变量的初始化通常使用取地址操作符 &
,这是一种独特的一元表达式,它的结果是一个指向操作数对象的指针值。取地址操作符 &
返回一个对象的地址,这个地址可以被存储在指针变量中。
int a = 10;
int* p = &a; // p 存储变量 a 的地址
也可以使用 auto
关键字来自动推导指针类型:
int a = 10;
auto p1 = &a; // p1 的类型自动推导为 int*
auto* p2 = &a; // p2 的类型明确为 int*,使用 auto* 明确这里是一个指针类型
指针的解引用
通过解引用操作符 *
,可以访问指针所指向的对象,这是一种独特的一元表达式,表达式的结果是指针所指向的对象的值,并且指代这个对象(即是一个左值表达式):
int a = 10;
int* p = &a;
*p = 20; // 修改 a 的值为 20,这和引用绑定是类似的
指针和引用的区别是指针是可变的引用,可以重新指向其他对象,而引用一旦绑定后就不能改变。
int a = 30;
int* p = &a;
int b = 40;
p = &b; // p 现在指向 b,而不是 a
*p = 50; // 修改 b 的值为 50
无效值、空指针与 nullptr
指针可以默认初始化,类似于整数类型,此时指针的值是无效的。使用未初始化的指针导致未定义行为。
int* p; // 未初始化的指针,可能指向一个随机地址
*p = 10; // 错误,未定义行为
指针在未指向任何有效对象时,应该初始化为 nullptr
,对 nullptr
的解引用也是未定义行为,但是在多数的实现中,发生对 nullptr
的解引用会确定性地立即导致程序崩溃,而不是产生完全未知的结果。
int* p = nullptr; // 推荐
*p = 10; // 错误,未定义行为
nullptr
是一个特殊的关键字,用来表示空指针值。它的类型是 std::nullptr_t
,可以用来将任何指针类型值初始化为一个空指针值。
此外,也可以使用 0
或 NULL
来表示空指针值,但推荐使用 nullptr
,因为它更安全且类型明确。
int* p1 = 0; // 旧式写法,表示空指针
int* p2 = NULL; // 旧式写法,表示空指针
C++ 规定,空指针值的指针和 nullptr
、0
、NULL
比较是相等的,例如:
int* p = nullptr;
p == 0 // true
但需要注意,空指针的值表示的字节不需要全都是 0
。
实际使用中不推荐使用 0
或 NULL
,它们可能导致严重的维护问题。原因我们在介绍了更多的内容后再来讨论。
生命期与悬垂指针
在程序执行到某个对象的块作用域结尾时,这个对象的生命期迎来结束,但指向这个对象的指针并不会立即失效。例如:
int * p;
if(...) {
int a = 1;
p = &a; // p 指向 a
}
// 这里 a 的生命期结束了,但 p 仍然指向 a 的地址
*p = 2; // 错误,未定义行为
在上面这段程序的最后,p
成为一个悬垂指针(dangling pointer),或者野指针(wild pointer),它指向一个已经不存在的对象。对悬垂指针的解引用是未定义行为。
为什么我的程序里*p没有直接报错?
不同于空指针,对悬垂指针的解引用无法被确定性地检查。
实际的程序为了效率,可能会保持野指针指向的内存有效,以方便未来复用——届时解引用就会得到恰好占据了这个位置的另一个对象的值。此时程序无法检查这种解引用的有效性。
此外,*p
如果没有进行任何读写操作,编译器可能会优化掉这个解引用操作,从而不会出错。
但注意,上述这些情况没有报错,并不能代表这样的程序行为是正确的。