12-8 空指针
在上一课(12.7——指针简介)中,我们介绍了指针的基础知识,指针是保存另一个对象地址的对象。可以使用解引用运算符(*)解引用该地址,从而获取该地址处的对象:
#include <iostream>
int main()
{
int x{ 5 };
std::cout << x << '\n'; // print the value of variable x
int* ptr{ &x }; // ptr holds the address of x
std::cout << *ptr << '\n'; // use dereference operator to print the value of the object at the address that ptr is holding (which is x's address)
return 0;
}
以上示例输出如下:

在上一课中,我们也提到指针不必指向任何对象。本节课,我们将进一步探讨这类指针(以及指向空对象所带来的各种影响)。
空指针
除了内存地址之外,指针还可以保存一个额外的值:空值。空值(通常缩写为null)是一个特殊值,表示某个东西没有实际意义。当指针保存空值时,意味着该指针没有指向任何内容。这样的指针被称为空指针。
创建空指针最简单的方法是使用值初始化:
int main()
{
int* ptr {}; // ptr is now a null pointer, and is not holding an address
return 0;
}

最佳实践:
如果您没有使用有效对象的地址初始化指针,则请初始化指针(使其成为空指针)。
因为我们可以使用赋值来改变指针指向的对象,所以最初设置为 null 的指针后来可以指向一个有效的对象:
#include <iostream>
int main()
{
int* ptr {}; // ptr is a null pointer, and is not holding an address
int x { 5 };
ptr = &x; // ptr now pointing at object x (no longer a null pointer)
std::cout << *ptr << '\n'; // print value of x through dereferenced ptr
return 0;
}

nullptr 关键字
与关键字 true 和 false 表示布尔字面值类似,关键字nullptr表示空指针字面值。我们可以使用nullptr它来显式初始化指针或将其赋值为空值。
int main()
{
int* ptr { nullptr }; // can use nullptr to initialize a pointer to be a null pointer
int value { 5 };
int* ptr2 { &value }; // ptr2 is a valid pointer
ptr2 = nullptr; // Can assign nullptr to make the pointer a null pointer
someFunction(nullptr); // we can also pass nullptr to a function that has a pointer parameter
return 0;
}
在上面的例子中,我们使用赋值将 ptr2 的值设置为 nullptr ,从而创建了ptr2一个空指针。
最佳实践
nullptr当您需要空指针字面量进行初始化、赋值或将空指针传递给函数时,请使用此方法。
对空指针进行解引用会导致未定义行为
就像解引用悬空指针(或称自由指针)会导致未定义行为一样,解引用空指针也会导致未定义行为。在大多数情况下,这会导致应用程序崩溃。
以下程序演示了这一点,运行它可能会导致应用程序崩溃或异常终止(尽管尝试一下,不会损坏您的计算机):
#include <iostream>
int main()
{
int* ptr {}; // Create a null pointer
std::cout << *ptr << '\n'; // Dereference the null pointer
return 0;
}

从概念上讲,这很合理。解引用指针意味着“跳转到指针指向的地址并访问那里的值”。空指针保存的是空值,语义上意味着指针没有指向任何东西。那么它能访问什么值呢?
意外地解引用空指针和悬空指针是 C++ 程序员最常犯的错误之一,也可能是 C++ 程序在实践中崩溃的最常见原因。
警告
无论何时使用指针,都需要格外小心,确保代码不会解引用空指针或悬空指针,因为这会导致未定义行为(很可能是应用程序崩溃)。
检查空指针
就像我们可以使用条件语句来测试布尔值是否为 true, false 一样,我们也可以使用条件语句来测试指针值是否为 nullptr:
#include <iostream>
int main()
{
int x { 5 };
int* ptr { &x };
if (ptr == nullptr) // explicit test for equivalence
std::cout << "ptr is null\n";
else
std::cout << "ptr is non-null\n";
int* nullPtr {};
std::cout << "nullPtr is " << (nullPtr==nullptr ? "null\n" : "non-null\n"); // explicit test for equivalence
return 0;
}
上述程序输出:

在第4.9 课——布尔值中,我们注意到整数值将隐式转换为布尔值:整数值0转换为布尔值false,任何其他整数值转换为布尔值true。
类似地,指针也会隐式转换为布尔值:空指针转换为布尔值false,非空指针转换为布尔值true。这使得我们可以跳过显式检查nullptr,直接使用隐式转换为布尔值来判断指针是否为空指针。以下程序与之前的程序等效:
#include <iostream>
int main()
{
int x { 5 };
int* ptr { &x };
// pointers convert to Boolean false if they are null, and Boolean true if they are non-null
if (ptr) // implicit conversion to Boolean
std::cout << "ptr is non-null\n";
else
std::cout << "ptr is null\n";
int* nullPtr {};
std::cout << "nullPtr is " << (nullPtr ? "non-null\n" : "null\n"); // implicit conversion to Boolean
return 0;
}

警告
条件语句只能用于区分空指针和非空指针。目前没有便捷的方法来判断一个非空指针是指向有效对象还是悬空指针(指向无效对象)。
使用 nullptr 来避免悬空指针
前面我们提到过,解引用空指针或悬空指针会导致未定义行为。因此,我们需要确保代码不会出现这两种情况。
我们可以通过使用条件语句来确保指针非空,从而轻松避免对空指针进行解引用:
// Assume ptr is some pointer that may or may not be a null pointer
if (ptr) // if ptr is not a null pointer
std::cout << *ptr << '\n'; // okay to dereference
else
// do something else that doesn't involve dereferencing ptr (print an error message, do nothing at all, etc...)
但是悬空指针怎么办呢?由于无法检测指针是否悬空,我们首先需要避免程序中出现任何悬空指针。我们通过确保任何未指向有效对象的指针都被设置为 nullptr 来实现这一点。
这样,在解引用指针之前,我们只需要测试它是否为空——如果它不为空,我们就假设该指针不是悬空的。
最佳实践
指针要么应该指向有效对象的地址,要么应该被设置为 nullptr。这样我们就只需要检查指针是否为空,并且可以假设任何非空指针都是有效的。
不幸的是,避免悬空指针并非易事:当一个对象被销毁时,指向该对象的所有指针都会变成悬空指针。这些指针不会自动被置为空!程序员有责任确保所有指向刚刚被销毁的对象的指针都被正确置为空nullptr。
警告
当一个对象被销毁时,指向该对象的所有指针都会变成悬空指针(不会自动置空nullptr)。您有责任检测这些情况,并确保这些指针随后被置空nullptr。
旧式空指针字面量:0 和 NULL
在较旧的代码中,您可能会看到使用另外两个字面值而不是nullptr。
第一种是字面量0。在指针的上下文中,字面量0被特殊定义为表示空值,并且是唯一可以将整数字面量赋值给指针的情况。
int main()
{
float* ptr { 0 }; // ptr is now a null pointer (for example only, don't do this)
float* ptr2; // ptr2 is uninitialized
ptr2 = 0; // ptr2 is now a null pointer (for example only, don't do this)
return 0;
}
顺便提一下……
在现代架构中,该地址0通常用于表示空指针。然而,C++ 标准并未保证此值,某些架构使用其他值。0当字面量x用于表示空指针时,它会被转换为架构用于表示空指针的任何地址。
此外,还有一个名为 null 的预处理器宏NULL(定义在 <cstddef> 头文件中)。该宏继承自 C 语言,在 C 语言中通常用于指示空指针。
#include <cstddef> // for NULL
int main()
{
double* ptr { NULL }; // ptr is a null pointer
double* ptr2; // ptr2 is uninitialized
ptr2 = NULL; // ptr2 is now a null pointer
return 0;
}
在现代 C++ 中,应该避免使用NULL和0(而是使用 nullptr)。我们将在第12.11 课——按地址传递(第 2 部分)中讨论原因。
尽可能优先使用引用而不是指针
指针和引用都使我们能够间接地访问其他对象。
指针还具有改变自身指向对象以及指向空指针的额外能力。然而,这些指针能力也存在固有的危险:空指针存在被解引用的风险,而改变指针指向对象的能力则使得创建悬空指针变得更加容易。
int main()
{
int* ptr { };
{
int x{ 5 };
ptr = &x; // assign the pointer to an object that will be destroyed (not possible with a reference)
} // ptr is now dangling and pointing to invalid object
if (ptr) // condition evaluates to true because ptr is not nullptr
std::cout << *ptr; // undefined behavior
return 0;
}
由于引用不能绑定到空值,所以我们不必担心空引用的问题。而且,由于引用在创建时必须绑定到一个有效的对象,并且之后不能被重新绑定,因此产生悬空引用的几率更低。
由于引用更安全,因此应优先使用引用而不是指针,除非需要指针提供的额外功能。
最佳实践
除非需要指针提供的额外功能,否则应优先使用引用而不是指针。
一个笑话
你听过关于空指针的笑话吗?
没关系,不会出现反引用的情况。
测验时间
问题 1
1a) 我们能否判断一个指针是否为空指针?如果可以,该如何判断?
是的,我们可以对指针使用条件语句(if 语句或条件运算符)。如果指针为空指针,则会将其转换为布尔值false;否则,将转换为布尔值true。
1b) 我们能否判断一个非空指针是有效的还是悬空的?如果可以,该如何判断?
没有简单的办法来确定这一点。
问题 2
对于每个子项,请回答所描述的行为会导致以下哪种结果:可预测的、未定义的,还是可能未定义的。如果答案是“可能未定义的”,请具体说明何时发生。
假设所提及的任何对象都是指针可以指向的类型。
2a) 将对象的地址赋值给非常量指针
显示解决方案
不出所料,这只是将地址复制到指针对象中。
2b) 将 nullptr 赋值给一个指针
显示解决方案
可预见的。
2c) 解引用指向有效对象的指针
显示解决方案
可预见的。
2d) 解引用悬空指针
显示解决方案
未定义的
2e) 解引用空指针
显示解决方案
未定义的
2f) 解引用非空指针
显示解决方案
如果指针悬空,则可能未定义。
问题 3
为什么要将指向无效对象的指针设置为“nullptr”?
显示解决方案
我们无法判断一个非空指针是有效指针还是悬空指针,而访问悬空指针会导致未定义行为。因此,我们需要确保程序中没有任何悬空指针。
如果我们确保所有指针要么指向有效对象,要么设置为 `nullptr`,那么我们可以使用条件语句来测试 null 值,以确保我们不会解引用 null 指针,并假设所有非 null 指针都指向有效对象。

浙公网安备 33010602011771号