使用 nullptr 有什么优点

What are the advantages of using nullptr?

本文关键字:什么 nullptr 使用      更新时间:2023-10-16

这段代码在概念上对三个指针(安全指针初始化)执行相同的操作:

int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;

那么,分配指针nullptr比分配NULL0值有什么优势?

在该代码中,似乎没有优势。但请考虑以下重载函数:

void f(char const *ptr);
void f(int v);
f(NULL);  //which function will be called?

将调用哪个函数?当然,这里的意图是称呼f(char const *),但实际上f(int)会被召唤!这是一个大问题1,不是吗?

因此,此类问题的解决方案是使用nullptr

f(nullptr); //first function is called

当然,这不是nullptr的唯一优势。 这是另一个:

template<typename T, T *ptr>
struct something{};                     //primary template
template<>
struct something<nullptr_t, nullptr>{};  //partial specialization for nullptr

由于在模板中,nullptr的类型被推导出为nullptr_t,所以你可以这样写:

template<typename T>
void f(T *ptr);   //function to handle non-nullptr argument
void f(nullptr_t); //an overload to handle nullptr argument!!!

1.在C++中,NULL被定义为#define NULL 0,所以基本上是int,这就是为什么f(int)被称为。

C++11 引入了nullptr,它被称为Null指针常量,它提高了类型安全性并解决了与现有实现相关的空指针常量NULL不同的歧义情况。为了能够了解nullptr的优势。我们首先需要了解什么是NULL以及与之相关的问题是什么。


NULL到底是什么?

C++11 之前NULL用于表示没有值的指针或不指向任何有效内容的指针。与流行的观念相反NULL不是C++中的关键字。它是在标准库标头中定义的标识符。简而言之,如果不包含一些标准库标头,就无法使用NULL。请考虑示例程序

int main()
{ 
int *ptr = NULL;
return 0;
}

输出:

prog.cpp: In function 'int main()':
prog.cpp:3:16: error: 'NULL' was not declared in this scope

C++ 标准将 NULL 定义为在某些标准库头文件中定义的实现定义的宏。 NULL 的起源来自 C,C++从 C 继承了它。C 标准将 NULL 定义为0(void *)0。但在C++中,有一个微妙的区别。

C++无法接受这个规范。与 C 不同,C++ 是一种强类型语言(C 不需要从void*显式强制转换为任何类型,而C++强制要求显式强制转换)。这使得 C 标准指定的 NULL 定义在许多C++表达式中毫无用处。例如:

std::string * str = NULL;         //Case 1
void (A::*ptrFunc) () = &A::doSomething;
if (ptrFunc == NULL) {}           //Case 2

如果将 NULL 定义为(void *)0,则上述表达式都不起作用。

  • 情况 1:不会编译,因为需要从void *std::string进行自动转换。
  • 情况 2:不会编译,因为需要从void *强制转换为指向成员函数的指针。

因此,与C不同,C++标准强制要求将NULL定义为数字文字00L


那么,当我们已经NULL时,还需要另一个空指针常量吗?

尽管C++标准委员会提出了一个适用于C++的空定义,但该定义也有其自身的公平问题。NULL 在几乎所有情况下都足够好,但不是全部。对于某些罕见的情况,它给出了令人惊讶和错误的结果。例如

#include<iostream>
void doSomething(int)
{
std::cout<<"In Int version";
}
void doSomething(char *)
{
std::cout<<"In char* version";
}
int main()
{
doSomething(NULL);
return 0;
}

输出:

In Int version

显然,意图似乎是调用以char*作为参数的版本,但是当输出显示采用int版本的函数时,被调用。这是因为 NULL 是数字文本。

此外,由于 NULL 是 0 还是 0L 都是实现定义的,因此在函数重载解析中可能会有很多混淆。

示例程序:

#include <cstddef>
void doSomething(int);
void doSomething(char *);
int main()
{
doSomething(static_cast <char *>(0));    // Case 1
doSomething(0);                          // Case 2
doSomething(NULL)                        // Case 3
}

分析上面的代码片段:

  • 情况 1:按预期调用doSomething(char *)
  • 情况 2:调用doSomething(int)但也许需要char*版本0因为 IS 也是一个空指针。
  • 情况 3:如果NULL定义为0,则在可能doSomething(char *)预期时调用doSomething(int),这可能会导致运行时的逻辑错误。 如果NULL定义为0L,则调用是不明确的,并导致编译错误。

因此,根据实现的不同,相同的代码可能会产生不同的结果,这显然是不希望的。当然,C++标准委员会希望纠正这一点,这是nullptr的主要动机。


那么什么是nullptr,它如何避免NULL的问题?

C++11 引入了一个新的关键字nullptr作为空指针常量。与 NULL 不同,它的行为不是实现定义的。它不是宏,但它有自己的类型。nullptr 的类型为std::nullptr_t。C++11 适当地定义了 nullptr 的属性,以避免 NULL 的缺点。总结其属性:

属性 1:它有自己的类型std::nullptr_t
属性2:它是隐式可转换的,可与任何指针类型或指向成员的指针类型相当,但
属性 3:它不是隐式可转换的或与整型类型相当的,除了bool

请考虑以下示例:

#include<iostream>
void doSomething(int)
{
std::cout<<"In Int version";
}
void doSomething(char *)
{
std::cout<<"In char* version";
}
int main()
{
char *pc = nullptr;      // Case 1
int i = nullptr;         // Case 2
bool flag = nullptr;     // Case 3
doSomething(nullptr);    // Case 4
return 0;
}

在上面的程序中,

  • 案例 1:确定 - 属性 2
  • 案例 2:不行 - 属性 3
  • 案例 3:确定 - 属性 3
  • 案例 4:没有混淆 - 调用char *版本,属性 2 和 3

因此,引入 nullptr 避免了旧 NULL 的所有问题。

您应该如何以及在哪里使用nullptr

C++11 的经验法则是,只要您过去使用 NULL 就开始使用nullptr


标准参考:

C++11 标准:C.3.2.4 宏空
C++11 标准:18.2 类型
C++11 标准:4.10 指针转换
C99 标准:6.3.2.3 指针

这里真正的动机是完美的转发

考虑:

void f(int* p);
template<typename T> void forward(T&& t) {
f(std::forward<T>(t));
}
int main() {
forward(0); // FAIL
}

简单地说,0 是一个特殊的值,但不能通过系统传播 - 只有类型可以。转发功能是必不可少的,0 无法处理它们。因此,绝对有必要引入nullptr,其中类型是特殊的,并且类型确实可以传播。事实上,MSVC 团队在实施 rvalue 引用后不得不提前引入nullptr,然后自己发现了这个陷阱。

还有其他一些极端情况,nullptr可以让生活更轻松——但这不是一个核心案例,因为演员可以解决这些问题。考虑

void f(int);
void f(int*);
int main() { f(0); f(nullptr); }

调用两个单独的重载。此外,请考虑

void f(int*);
void f(long*);
int main() { f(0); }

这是模棱两可的。但是,使用 nullptr,您可以提供

void f(std::nullptr_t)
int main() { f(nullptr); }

nullptr 的基础知识

std::nullptr_t是空指针文本 nullptr 的类型。它是类型std::nullptr_t的 prvalue/rvalue。存在从 nullptr 到任何指针类型的 null 指针值的隐式转换。

文字 0 是一个整数,而不是一个指针。如果 C++ 发现自己在只能使用指针的上下文中查看 0,它会勉强将 0 解释为空指针,但这是一个回退位置。C++的主要策略是 0 是 int,而不是指针。

优势 1 - 在指针和整数类型上重载时消除歧义

在 C++98 中,这的主要含义是指针和整数类型的重载可能会导致意外。将 0 或 NULL 传递给此类重载从不称为指针重载:

void fun(int); // two overloads of fun
void fun(void*);
fun(0); // calls f(int), not fun(void*)
fun(NULL); // might not compile, but typically calls fun(int). Never calls fun(void*)

关于这个调用的有趣之处在于源代码的明显含义("我用 NULL-the null 指针调用乐趣")与其实际含义("我用某种整数调用乐趣——而不是空指针")之间的矛盾。

nullptr 的优点是它没有整数类型。 用 nullptr 调用重载函数 fun 调用 void* 重载(即指针重载),因为 nullptr 不能被视为任何积分:

fun(nullptr); // calls fun(void*) overload 

因此,使用 nullptr 而不是 0 或 NULL 可以避免过载分辨率意外。

将 auto 用于返回类型时nullptr优于NULL(0)的另一个优点

例如,假设您在代码库中遇到此问题:

auto result = findRecord( /* arguments */ );
if (result == 0) {
....
}

如果您碰巧不知道(或无法轻松找到)findRecord 返回的内容,则可能不清楚结果是指针类型还是整数类型。毕竟,0(测试结果)可以采用任何一种方式。另一方面,如果您看到以下内容,

auto result = findRecord( /* arguments */ );
if (result == nullptr) {
...
}

没有歧义:结果必须是指针类型。

优势3

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
//do something
return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
//do something
return 0.0;
}
bool f3(int* pw) // mutex is locked
{
return 0;
}
std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;
void lockAndCallF1()
{
MuxtexGuard g(f1m); // lock mutex for f1
auto result = f1(static_cast<int>(0)); // pass 0 as null ptr to f1
cout<< result<<endl;
}
void lockAndCallF2()
{
MuxtexGuard g(f2m); // lock mutex for f2
auto result = f2(static_cast<int>(NULL)); // pass NULL as null ptr to f2
cout<< result<<endl;
}
void lockAndCallF3()
{
MuxtexGuard g(f3m); // lock mutex for f2
auto result = f3(nullptr);// pass nullptr as null ptr to f3 
cout<< result<<endl;
} // unlock mutex
int main()
{
lockAndCallF1();
lockAndCallF2();
lockAndCallF3();
return 0;
}

上面的程序编译并成功执行,但 lockAndCallF1、lockAndCallF2 和 lockAndCallF3 有冗余代码。如果我们可以为所有这些lockAndCallF1, lockAndCallF2 & lockAndCallF3编写模板,那么编写这样的代码是很遗憾的。所以可以用模板概括。我已经编写了模板函数lockAndCall而不是冗余代码的多个定义lockAndCallF1, lockAndCallF2 & lockAndCallF3

代码重构如下:

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
//do something
return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
//do something
return 0.0;
}
bool f3(int* pw) // mutex is locked
{
return 0;
}
std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;
template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
//decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr)
{
MuxtexGuard g(mutex);
return func(ptr);
}
int main()
{
auto result1 = lockAndCall(f1, f1m, 0); //compilation failed 
//do something
auto result2 = lockAndCall(f2, f2m, NULL); //compilation failed
//do something
auto result3 = lockAndCall(f3, f3m, nullptr);
//do something
return 0;
}

详细分析为什么编译失败lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)而不是lockAndCall(f3, f3m, nullptr)

为什么编译lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)失败?

问题是,当 0 传递给 lockAndCall 时,模板类型推导会启动以找出其类型。0 的类型是 int,因此这是此调用 lockAndCall 实例化中的参数 ptr 的类型。不幸的是,这意味着在 lockAndCall 内部对 func 的调用中,传递了一个 int,这与f1期望的std::shared_ptr<int>参数不兼容。在对lockAndCall的调用中传递的 0 旨在表示空指针,但实际传递的是 int。尝试将此 int 作为std::shared_ptr<int>传递给 f1 是类型错误。对 0lockAndCall的调用失败,因为在模板内部,int 正在传递给需要std::shared_ptr<int>的函数。

涉及NULL的呼叫的分析基本相同。当NULL传递给lockAndCall时,会为参数ptr推导出一个整型类型,当ptr(一个int或类似int的类型)传递给f2时,会发生类型错误,期望得到一个std::unique_ptr<int>

相比之下,涉及nullptr的呼叫没有问题。当nullptr传递给lockAndCall时,ptr的类型被推导出为std::nullptr_t。当ptr传递给f3时,有一个从std::nullptr_tint*的隐式转换,因为std::nullptr_t隐式转换为所有指针类型。

建议,每当要引用空指针时,请使用 nullptr,而不是 0 或NULL

以您展示示例的方式进行nullptr没有直接优势。
但是考虑一个情况,即您有 2 个同名函数;一个需要int,另一个需要int*

void foo(int);
void foo(int*);

如果要通过传递 NULL 来调用foo(int*),则方法是:

foo((int*)0); // note: foo(NULL) means foo(0)

nullptr使它更加简单直观

foo(nullptr);

来自Bjarne网页的附加链接。
无关紧要,但在 C++11 旁注:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)

正如其他人已经说过的那样,它的主要优势在于过载。虽然显式int与指针重载可能很少见,但请考虑像std::fill这样的标准库函数(它在 C++03 中不止一次咬过我):

MyClass *arr[4];
std::fill_n(arr, 4, NULL);

不编译:Cannot convert int to MyClass*.

IMO比那些重载问题更重要:在深度嵌套的模板结构中,很难不忘记类型,并且提供显式签名是一项艰巨的任务。因此,对于您使用的所有内容,越精确地专注于预期目的越好,它将减少对显式签名的需求,并允许编译器在出现问题时生成更有见地的错误消息。