这段代码在概念上对三个指针做同样的事情(安全指针初始化):
int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;
nullptr
那么,分配指针与分配值NULL
或分配指针相比有什么优势0
?
在该代码中,似乎没有优势。但请考虑以下重载函数:
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
什么?Pre C++11NULL
用于表示没有值的指针或不指向任何有效内容的指针。与流行的概念相反,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
or (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
,则上述表达式都不起作用。
void *
to自动转换std::string
。 void *
转换为指向成员函数的指针。 因此,与 C 不同的是,C++ 标准要求将 NULL 定义为数字文字0
或0L
.
NULL
什么?尽管 C++ 标准委员会提出了适用于 C++ 的 NULL 定义,但该定义也有其自身的问题。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
}
分析上面的片段:
doSomething(char *)
按预期 调用。doSomething(int)
但可能char*
需要版本,因为0
IS 也是一个空指针。 NULL
定义为,可能会在预期的时候0
调用,可能会导致运行时出现逻辑错误。如果定义为,则调用不明确并导致编译错误。doSomething(int)
doSomething(char *)
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;
}
在上述程序中,
char *
版本,属性 2 和 3因此,引入 nullptr 避免了旧 NULL 的所有问题。
nullptr
?C++11 的经验法则是nullptr
在过去使用 NULL 时简单地开始使用。
标准参考:
C++11 标准:C.3.2.4 宏 NULL
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 团队在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 的类型。它是一个 prvalue/rvalue 类型std::nullptr_t
。存在从 nullptr 到任何指针类型的空指针值的隐式转换。
文字 0 是一个 int,而不是一个指针。如果 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 调用 fun——空指针”)和它的实际含义(“我用某种整数调用 fun——而不是 null指针”)。
nullptr 的优点是它没有整数类型。用 nullptr 调用重载函数 fun 调用 void* 重载(即指针重载),因为 nullptr 不能被视为任何积分:
fun(nullptr); // calls fun(void*) overload
因此,使用 nullptr 而不是 0 或 NULL 可以避免重载决议意外。
使用 auto 作为返回类型时nullptr
over的另一个优点NULL(0)
例如,假设您在代码库中遇到这种情况:
auto result = findRecord( /* arguments */ );
if (result == 0) {
....
}
如果您碰巧不知道(或不能轻易找出) findRecord 返回的内容,则可能不清楚 result 是指针类型还是整数类型。毕竟,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)
not forlockAndCall(f3, f3m, nullptr)
为什么编译lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)
失败?
问题是当 0 被传递给 lockAndCall 时,模板类型推断开始计算它的类型。0 的类型是 int,所以这就是这个 lockAndCall 调用的实例化中的参数 ptr 的类型。不幸的是,这意味着在 lockAndCall 内部对 func 的调用中,传递了一个 int,这与预期的std::shared_ptr<int>
参数不兼容。f1
调用中传递的 0lockAndCall
旨在表示一个空指针,但实际传递的是 int。试图将此 int 作为 a 传递给 f1std::shared_ptr<int>
是类型错误。调用lockAndCall
with 0 失败,因为在模板内部,一个 int 被传递给一个需要std::shared_ptr<int>
.
通话涉及的分析NULL
基本相同。当NULL
被传递给lockAndCall
时,会为参数 ptr 推导出一个整数类型,并且当 - 一个ptr
int 或类似 int 的类型 - 被传递给时会发生类型错误f2
,它期望得到一个std::unique_ptr<int>
。
相比之下,呼叫涉及nullptr
没有问题。当nullptr
被传递给lockAndCall
时,for 的类型ptr
被推导出为std::nullptr_t
。当ptr
被传递给 时f3
,有一个从std::nullptr_t
to的隐式转换int*
,因为std::nullptr_t
隐式转换为所有指针类型。
建议,每当您要引用空指针时,请使用 nullptr,而不是 0 或NULL
.
nullptr
以您展示示例的方式没有直接优势。
但是考虑一个情况,你有两个同名的函数;1 次int
,另一个int*
void foo(int);
void foo(int*);
如果你想foo(int*)
通过传递一个NULL来调用,那么方法是:
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 比那些重载问题更重要:在深度嵌套的模板构造中,很难不忘记类型,并且给出明确的签名是一项相当大的努力。因此,对于您使用的所有内容,越精确地关注预期目的越好,它将减少对显式签名的需求,并允许编译器在出现问题时生成更深入的错误消息。