【问题标题】:What exactly is nullptr?nullptr 到底是什么?
【发布时间】:2010-11-19 22:01:52
【问题描述】:

我们现在拥有具有许多新功能的 C++11。一个有趣且令人困惑的(至少对我而言)是新的nullptr

好吧,不再需要讨厌的宏 NULL

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

不过,我不明白nullptr 的工作原理。例如,Wikipedia article 说:

C++11 通过引入一个新的关键字 来纠正这个问题,作为一个可区分的空指针常量:nullptr。它是 type nullptr_t,它可以隐式转换并且与任何指针类型或指向成员的指针类型相当。它不能隐式转换或与整数类型相比较,除了 bool。

它是一个关键字和一个类型的实例?

另外,你有没有另一个例子(除了维基百科)nullptr 优于旧的0

【问题讨论】:

  • 相关事实:nullptr 也用于表示 C++/CLI 中托管句柄的空引用。
  • nullptr_t 是否保证只有一个成员 nullptr?那么,如果一个函数返回nullptr_t,那么编译器就已经知道将返回哪个值,而不管函数体是什么?
  • @AaronMcDaid std::nullptr_t 可以被实例化,但所有实例都将与nullptr 相同,因为类型被定义为typedef decltype(nullptr) nullptr_t。我相信该类型存在的主要原因是,如果有必要,可以专门重载函数以捕获nullptr。有关示例,请参见 here
  • 0 从来都不是空指针,空指针是一个可以通过 casting 零文字到指针类型的指针,它不指向 any 现有对象的定义。
  • @Nils 重点是表达意图!

标签: c++ pointers c++11 nullptr


【解决方案1】:

为什么在 C++11 中使用 nullptr?它是什么?为什么NULL不够?

C++ 专家Alex Allain says it perfectly here(我的重点加粗了):

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

void func(int n); 
void func(char *s);
 
func( NULL ); // guess which function gets called?

虽然看起来第二个函数会被调用——毕竟你是在传递一个看起来像指针的东西——它实际上是第一个被调用的函数!问题在于,因为 NULL 是 0,而 0 是整数,所以会调用第一个版本的 func。 是的,这种事情不会一直发生,但是当它发生时确实发生了,非常令人沮丧和困惑。如果你不知道发生了什么的细节,它很可能看起来像一个编译器错误。 看起来像编译器错误的语言功能并不是你想要的。

输入 nullptr。在 C++11 中,nullptr 是一个新的关键字,可以(并且应该!)用于表示 NULL 指针; 换句话说,无论您以前在哪里写 NULL,都应该使用 nullptr 来代替。 程序员你不太清楚,(每个人都知道 NULL 是什么意思),但它对编译器来说更明确,它不会再习惯于到处看到 0用作指针时有特殊含义。

艾伦在文章的结尾是:

不管这一切——C++11 的经验法则就是在过去使用 NULL 时开始使用 nullptr

(我的话):

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

定义在标题<cstddef>:

见:

  1. https://en.cppreference.com/w/cpp/types/nullptr_t
  2. https://en.cppreference.com/w/cpp/header/cstddef
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: Better types in C++11 - nullptr, enum classes (strongly typed enumerations) and 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

【讨论】:

    【解决方案2】:

    我先给你一个简单的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;
    

    nullptrReturn Type Resolver 习语的一个微妙示例,它根据分配给它的实例的类型自动推断出正确类型的空指针。

    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))

    【讨论】:

      【解决方案3】:

      根据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)

      【讨论】:

        【解决方案4】:

        nullptr 不能分配给整数类型,例如 int,而只能分配给指针类型;可以是内置指针类型,例如int *ptr,也可以是智能指针,例如std::shared_ptr&lt;T&gt;

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

        【讨论】:

        • 请注意,这个答案是错误的。 NULL 不保证扩展为0
        【解决方案5】:

        这是 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
        

        (可以通过grep -r /usr/include/*` 快速发现很多内容)

        跳出来的一件事是运算符* 重载(返回0 比segfaulting 友好得多......)。 另一件事是它看起来与存储地址完全不兼容。与它如何使用 void*'s 并将 NULL 结果作为标记值传递给普通指针相比,这显然会减少“永远不要忘记,它可能是炸弹”的因素。

        【讨论】:

          【解决方案6】:

          0 曾经是唯一可以用作指针的无强制转换初始化器的整数值:您不能在没有强制转换的情况下使用其他整数值初始化指针。 您可以将 0 视为语法上类似于整数文字的 conexpr 单例。它可以启动任何指针或整数。但令人惊讶的是,您会发现它没有不同的类型:它是一个int。那么为什么 0 可以初始化指针而 1 不能呢?一个实际的答案是我们需要一种定义指针空值的方法,并且将int 直接隐式转换为指针是容易出错的。于是0变成了史前时代真正的怪胎怪兽。 nullptr 被提议作为一个真正的单例 constexpr 表示空值来初始化指针。它不能用于直接初始化整数并消除用 0 定义 NULL 所涉及的歧义。nullptr 可以定义为使用 std 语法的库,但在语义上看起来是缺少的核心组件。 NULL 现在已弃用,取而代之的是 nullptr,除非某些库决定将其定义为 nullptr

          【讨论】:

            【解决方案7】:

            假设您有一个函数 (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*)
            }
            

            【讨论】:

              【解决方案8】:

              当你有一个函数可以接收指向多个类型的指针时,用NULL 调用它是不明确的。通过接受一个 int 并假设它是 NULL,现在解决这个问题的方式非常 hacky。

              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&lt;T&gt; p(42); 将是编译时错误,而不是运行时assert

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

              【讨论】:

              • 如果NULL被定义为0L怎么办?
              【解决方案9】:

              NULL 不必为 0。只要您始终使用 NULL 而从不使用 0,NULL 可以是任何值。假设您对具有平坦内存的冯诺依曼微控制器进行编程,其中断向量为 0。如果 NULL 为 0,并且在 NULL 指针处写入某些内容,则微控制器崩溃。如果 NULL 可以说是 1024 并且在 1024 有一个保留变量,则写入不会使其崩溃,并且您可以从程序内部检测 NULL 指针分配。这在 PC 上毫无意义,但对于太空探测器、军事或医疗设备来说,重要的是不要崩溃。

              【讨论】:

              • 嗯,空指针在内存中的实际值可能不为零,但 C(和 C++)标准要求编译器将整数 0 文字转换为空指针。
              • 传说在霍尼韦尔的一些计算机中,NULL 不是零而是 06000。见stackoverflow.com/questions/2597142/…
              【解决方案10】:

              来自nullptr: A Type-safe and Clear-Cut Null Pointer

              新的 C++09 nullptr 关键字指定了一个右值常量,用作通用空指针文字,取代了有缺陷的弱类型文字 0 和臭名昭著的 NULL 宏。因此,nullptr 结束了 30 多年的尴尬、模棱两可和错误。以下部分介绍了 nullptr 工具,并展示了它如何解决 NULL 和 0 的问题。

              其他参考资料:

              【讨论】:

              • C++09? 2011 年 8 月之前不是被称为 C++0x 吗?
              • @anthropomorphic 这就是它的目的。 C++0x 在它还在进行中的时候就被使用了,因为不知道它会在 2008 年还是 2009 年完成。注意它实际上变成了 C++0B,意思是 C++11。见stroustrup.com/C++11FAQ.html
              【解决方案11】:

              另外,你有另一个例子(除了维基百科)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
              

              【讨论】:

              • 我看不到使用 nullptr(和 C++11)时这种情况如何改善。如果您将 pb 设置为 nullptr,则第一个比较评估仍然为真(在比较苹果和梨时..)。第二种情况更糟:如果将 a 与 nullptr 进行比较,它会将 a 转换为 B*,然后它会再次评估为 true(在它被强制转换为 bool 并且 expr 评估为 false 之前)。整个事情让我想起了 JavaScript,我想知道我们将来是否会在 C++ 中得到 === :(
              【解决方案12】:

              它是一个关键字和一个类型的实例?

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

              • 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*)

              【讨论】:

              • 哦,看了之后,在我看来,条件运算符在cond ? nullptr : 0;这样的情况下无法将0转换为nullptr。从我的答案中删除。
              • 请注意,NULL 甚至不能保证是0。它可以是0L,在这种情况下,对void f(int); void f(char *); 的调用将是不明确的。 nullptr 将始终支持指针版本,并且永远不会调用 int 之一。另请注意,nullptr 可转换为 bool(草案在 4.12 上说)。
              • @litb: 所以关于 f(int) 和 f(void*) - f(0) 还会模棱两可吗?
              • @Steve,不,这将调用int 版本。但是f(0L) 是模棱两可的,因为long -&gt; intlong -&gt; void* 都同样昂贵。因此,如果您的编译器上的 NULL 是 0L,那么考虑到这两个函数,调用 f(NULL) 将是模棱两可的。 nullptr 当然不是这样。
              • @SvenS 在 C++ 中不能定义为(void*)0。但它可以定义为任意空指针常量,任何值为 0 和 nullptr 的整型常量都可以满足。所以,绝对不是will,而是can。 (顺便说一句,你忘了 ping 我……)
              【解决方案13】:

              嗯,其他语言有保留字作为类型的实例。例如 Python:

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

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

              另一方面,在纯 C 中,NULL == 0 将返回 true IIRC,因为 NULL 只是一个返回 0 的宏,它始终是一个无效地址 (AFAIK)。

              【讨论】:

              • NULL 是一个扩展为零的宏,将常量零转换为指针会产生一个空指针。空指针不必为零(但通常是),零并不总是无效地址,非常数零转换为指针不必为空,空指针转换为整数不必为零。我希望我没有忘记任何事情。参考:c-faq.com/null/null2.html
              【解决方案14】:

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

              2.14.7 指针字面量 [lex.nullptr]

              pointer-literal:
              nullptr
              

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

              它很有用,因为它不会隐式转换为整数值。

              【讨论】:

                猜你喜欢
                • 2013-06-08
                • 1970-01-01
                • 2013-02-06
                • 2013-05-05
                • 2012-06-16
                • 2011-04-02
                • 2016-04-13
                相关资源
                最近更新 更多