nullptr到底是什么?

现在c++ 11有了许多新特性。一个有趣的和令人困惑的(至少对我来说)是新的nullptr

好了,不再需要讨厌的宏NULL了。

int* x = nullptr;
myclass* obj = nullptr;

尽管如此,我还是不明白nullptr是如何工作的。例如,维基百科的文章表示:

c++ 11通过引入一个新的关键字作为一个特殊的空指针常量nullptr来纠正这个错误。它的类型为类型nullptr_t,可隐式转换,可与任何指针类型或指针到成员类型相比较。它不能隐式转换,也不能与整型相比,bool类型除外。

它如何既是关键字又是类型的实例?

此外,你还有其他的例子(除了维基百科的例子),nullptr比旧的0更好吗?

374232 次浏览

其他语言有保留词,它们是类型的实例。例如,Python:

>>> None = 5
File "<stdin>", line 1
SyntaxError: assignment to None
>>> type(None)
<type 'NoneType'>

这实际上是一个相当接近的比较,因为None通常用于尚未初始化的东西,但与此同时,像None == 0这样的比较是错误的。

另一方面,在普通C中,NULL == 0将返回真IIRC,因为NULL只是一个返回0的宏,这总是一个无效地址(AFAIK)。

它是关键字,因为标准将这样指定它。;-)根据最新公开草案(n2914)

2.14.7指针字面量[lex.nullptr]

pointer-literal:
nullptr

指针文字是关键字nullptr。它是类型为std::nullptr_t的右值。

它很有用,因为它没有隐式地转换成一个整数值。

从# EYZ0:

新的c++ 09 nullptr关键字指定了一个右值常量,用作通用空指针字面量,取代了有bug且弱类型的字面量0和臭名昭著的null宏。因此,Nullptr结束了30多年来的尴尬、歧义和错误。下面几节介绍nullptr功能,并展示它如何补救NULL和0的问题。

其他参考资料:

它如何既是关键字又是类型的实例?

这并不奇怪。truefalse都是关键字,作为文字,它们都有一个类型(bool)。nullptrstd::nullptr_t类型的指针文字,它是一个prvalue(您不能使用&获取它的地址)。

  • 关于指针转换的4.10表示类型为std::nullptr_t的prvalue是一个空指针常量,而整型空指针常量可以转换为std::nullptr_t。相反方向是不允许的。这允许重载指针和整数的函数,并传递nullptr来选择指针版本。传递NULL0会令人困惑地选择int版本。

  • nullptr_t强制转换为整型类型需要使用reinterpret_cast,并且与将(void*)0强制转换为整型类型具有相同的语义(已定义映射实现)。reinterpret_cast不能将nullptr_t转换为任何指针类型。如果可能,使用隐式转换或使用static_cast

  • 标准要求sizeof(nullptr_t)sizeof(void*)

当一个函数可以接收指向多个类型的指针时,使用NULL来调用它是不明确的。现在的工作方式非常hack,接受int并假设它是NULL

template <class T>
class ptr {
T* p_;
public:
ptr(T* p) : p_(p) {}


template <class U>
ptr(U* u) : p_(dynamic_cast<T*>(u)) { }


// Without this ptr<T> p(NULL) would be ambiguous
ptr(int null) : p_(NULL)  { assert(null == NULL); }
};

C++11中,您可以重载nullptr_t,这样ptr<T> p(42);将是一个编译时错误,而不是assert

ptr(std::nullptr_t) : p_(nullptr)  {  }

nullptr不能赋值给整型类型,比如int,只能赋值给指针类型;要么是内置指针类型,如int *ptr,要么是智能指针,如std::shared_ptr<T>

我相信这是一个重要的区别,因为NULL仍然可以被分配给整型和指针,因为NULL是一个扩展到0的宏,它可以作为int的初始值以及指针。

此外,你还有其他的例子(除了维基百科的一个),nullptr优于好旧的0?

是的。这也是在我们的生产代码中出现的一个(简化的)真实例子。它之所以突出,是因为gcc能够在交叉编译到具有不同寄存器宽度的平台时发出警告(仍然不确定为什么只有在从x86_64交叉编译到x86时才发出警告warning: converting to non-pointer type 'int' from NULL):

考虑以下代码(c++ 03):

#include <iostream>


struct B {};


struct A
{
operator B*() {return 0;}
operator bool() {return true;}
};


int main()
{
A a;
B* pb = 0;
typedef void* null_ptr_t;
null_ptr_t null = 0;


std::cout << "(a == pb): " << (a == pb) << std::endl;
std::cout << "(a == 0): " << (a == 0) << std::endl; // no warning
std::cout << "(a == NULL): " << (a == NULL) << std::endl; // warns sometimes
std::cout << "(a == null): " << (a == null) << std::endl;
}

它产生如下输出:

(a == pb): 1
(a == 0): 0
(a == NULL): 0
(a == null): 1

NULL不一定是0。只要你总是使用NULL而不是0,NULL可以是任何值。假设你编写了一个冯诺依曼微控制器的平面内存,它的中断向量为0。如果NULL为0,并且在NULL指针上写入内容,则微控制器崩溃。如果NULL是1024,在1024有一个保留变量,写不会使它崩溃,你可以从程序内部检测NULL指针的赋值。这在个人电脑上是毫无意义的,但对于太空探测器、军事或医疗设备来说,重要的是不要崩溃。

假设你有一个重载的函数(f),它同时接受int和char*。在c++ 11之前,如果你想用空指针调用它,并且你使用了null(即值0),那么你会调用int重载的指针:

void f(int);
void f(char*);


void g()
{
f(0); // Calls f(int).
f(NULL); // Equals to f(0). Calls f(int).
}

这可能不是你想要的。c++ 11用nullptr解决了这个问题;现在你可以这样写:

void g()
{
f(nullptr); //calls f(char*)
}

为什么在c++ 11中使用nullptr ?是什么?为什么NULL是不充分的?

c++专家亚历克斯·阿兰说得很好(我用粗体加了重点):

...假设你有以下两个函数声明:

void func(int n);
void func(char *s);
 

func( NULL ); // guess which function gets called?

虽然它看起来像第二个函数将被调用——毕竟,您传递的是一个指针——但它实际上是第一个将被调用的函数!问题是,因为NULL是0,而0是一个整数,所以func的第一个版本将被调用。这种事情,是的,并不总是发生,但当它发生时,是非常令人沮丧和困惑的。如果您不知道正在发生的事情的细节,它很可能看起来像一个编译器错误。# EYZ1

换句话说,在你之前写NULL的地方,你应该用nullptr代替。作为程序员的你就不清楚了,(每个人都知道NULL的意思),但它对编译器更显式,它将不再看到到处都是0,当用作指针时具有特殊意义。

Allain在文章结尾写道:

不管这些——c++ 11的经验法则是,只要在过去使用NULL,就开始使用nullptr

(我的话):

最后,不要忘记nullptr是一个对象——一个类。它可以在NULL之前使用的任何地方使用,但如果你因为某种原因需要它的类型,它的类型可以用decltype(nullptr)提取,或者直接描述为std::nullptr_t,它只是decltype(nullptr)typedef,如下所示:

在头文件<cstddef>中定义:

看到的:

  1. https://en.cppreference.com/w/cpp/types/nullptr_t
  2. 和# EYZ0
namespace std
{
typedef decltype(nullptr) nullptr_t; // (since C++11)
// OR (same thing, but using the C++ keyword `using` instead of the C and C++
// keyword `typedef`):
using nullptr_t = decltype(nullptr); // (since C++11)
} // namespace std

引用:

  1. Cprogramming.com: c++11中更好的类型-nullptr, enum类(强类型枚举)和cstdint
  2. https://en.cppreference.com/w/cpp/language/decltype
  3. https://en.cppreference.com/w/cpp/types/nullptr_t
  4. https://en.cppreference.com/w/cpp/header/cstddef
  5. https://en.cppreference.com/w/cpp/keyword/using
  6. https://en.cppreference.com/w/cpp/keyword/typedef
0曾经是唯一可以用作指针的无强制转换初始化式的整数值:你不能在没有强制转换的情况下用其他整数值初始化指针。 您可以将0视为一个consexpr单例,语法上类似于整数字面量。它可以初始化任何指针或整数。但令人惊讶的是,您会发现它没有明确的类型:它是int。那么为什么0可以初始化指针,而1不能呢?一个实际的答案是,我们需要一种定义指针空值的方法,而直接将int隐式转换为指针很容易出错。于是0变成了史前时代真正的怪物。 nullptr被提议作为null值的真正单例constexpr表示来初始化指针。它不能直接用于初始化整数,并消除了用0定义NULL所涉及的歧义。nullptr可以定义为一个使用std语法的库,但从语义上看是一个丢失的核心组件。 NULL现在不赞成使用nullptr,除非某些库决定将其定义为nullptr

这是LLVM头文件。

// -*- C++ -*-
//===--------------------------- __nullptr --------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//


#ifndef _LIBCPP_NULLPTR
#define _LIBCPP_NULLPTR


#include <__config>


#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
#pragma GCC system_header
#endif


#ifdef _LIBCPP_HAS_NO_NULLPTR


_LIBCPP_BEGIN_NAMESPACE_STD


struct _LIBCPP_TEMPLATE_VIS nullptr_t
{
void* __lx;


struct __nat {int __for_bool_;};


_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t() : __lx(0) {}
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t(int __nat::*) : __lx(0) {}


_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR operator int __nat::*() const {return 0;}


template <class _Tp>
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
operator _Tp* () const {return 0;}


template <class _Tp, class _Up>
_LIBCPP_INLINE_VISIBILITY
operator _Tp _Up::* () const {return 0;}


friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator==(nullptr_t, nullptr_t) {return true;}
friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator!=(nullptr_t, nullptr_t) {return false;}
};


inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t __get_nullptr_t() {return nullptr_t(0);}


#define nullptr _VSTD::__get_nullptr_t()


_LIBCPP_END_NAMESPACE_STD


#else  // _LIBCPP_HAS_NO_NULLPTR


namespace std
{
typedef decltype(nullptr) nullptr_t;
}


#endif  // _LIBCPP_HAS_NO_NULLPTR


#endif  // _LIBCPP_NULLPTR

# EYZ1

跳出来的一件事是操作符*重载(返回0比分段故障友好得多…) 另一件事是它看起来与存储地址在所有不兼容。这与它如何抛出void*和将NULL结果作为哨兵值传递给普通指针相比,将明显减少“永远不要忘记,这可能是一个炸弹”因素

首先让我给您一个简单的nullptr_t的实现

struct nullptr_t
{
void operator&() const = delete;  // Can't take address of nullptr


template<class T>
inline operator T*() const { return 0; }


template<class C, class T>
inline operator T C::*() const { return 0; }
};


nullptr_t nullptr;

nullptr返回类型解析器习语的一个微妙例子,它根据分配给它的实例的类型自动推导出正确类型的空指针。

int *ptr = nullptr;                // OK
void (C::*method_ptr)() = nullptr; // OK
  • 如上所述,当nullptr被赋值给一个整数指针时,模板化转换函数的int类型实例化将被创建。方法指针也是一样。
  • 通过这种方式利用模板功能,我们实际上每次都创建了适当类型的空指针,这是一个新的类型赋值。
  • 因为nullptr是一个值为0的整数字面值,你不能使用它的地址,我们通过删除&操作符。

为什么我们首先需要nullptr ?

  • 你可以看到传统的NULL有一些问题,如下:

1️⃣隐式转换

char *str = NULL; // Implicit conversion from void * to char *
int i = NULL;     // OK, but `i` is not pointer type

2️⃣函数调用歧义

void func(int) {}
void func(int*){}
void func(bool){}


func(NULL);     // Which one to call?
  • 编译会产生以下错误:
error: call to 'func' is ambiguous
func(NULL);
^~~~
note: candidate function void func(bool){}
^
note: candidate function void func(int*){}
^
note: candidate function void func(int){}
^
1 error generated.
compiler exit status 1

3️⃣构造函数重载

struct String
{
String(uint32_t)    {   /* size of string */    }
String(const char*) {       /* string */        }
};


String s1( NULL );
String s2( 5 );
  • 在这种情况下,您需要显式强制转换(即String s((char*)0))

根据cppreferencenullptr是一个关键字,它:

表示指针文字。它是一个类型为std::nullptr_t的右值。 存在< >强隐式转换< / >强从nullptr到null的指针值 任何指针类型指向成员类型的指针。类似的转换 存在于任何空指针常量,其中包括类型的值 std::nullptr_t和宏NULL.

所以nullptr是一个不同类型的值std::nullptr_t,而不是int。它隐式转换为任何指针类型的空指针值。这个神奇的事情发生在您的引擎盖下,您不必担心它的实现。然而,NULL是一个宏,它是一个实现定义的空指针常量。它通常是这样定义的:

#define NULL 0

也就是一个整数。

这是一个微妙但重要的区别,可以避免歧义。

例如:

int i = NULL;     //OK
int i = nullptr;  //error
int* p = NULL;    //OK
int* p = nullptr; //OK

当你有两个像这样的函数重载:

void func(int x);   //1)
void func(int* x);  //2)

func(NULL)调用1)因为NULL是一个整数。 func(nullptr)调用2),因为nullptr隐式转换为int*类型的指针

另外,如果你看到这样的语句:

auto result = findRecord( /* arguments */ );


if (result == nullptr)
{
...
}

你不容易找到findRecord返回什么,你可以确定result一定是一个指针类型;nullptr使它更具可读性。

在一个推断的背景下,事情的运作有点不同。如果你有一个这样的模板函数:

template<typename T>
void func(T *ptr)
{
...
}

你试着用nullptr来调用它:

func(nullptr);

你会得到一个编译器错误,因为nullptr的类型是nullptr_t。您必须显式地将nullptr转换为特定的指针类型,或者使用nullptr_tfunc提供重载/专门化。


使用nulptr的优点:
  • 避免函数重载之间的模糊
  • 使您能够进行模板专门化
  • 更安全、直观和富有表现力的代码,例如if (ptr == nullptr)取代if (ptr == 0)