Chính xác thì nullptr là gì?


570

Bây giờ chúng ta có C ++ 11 với nhiều tính năng mới. Một điều thú vị và khó hiểu (ít nhất là đối với tôi) là cái mới nullptr.

Vâng, không cần nữa cho macro khó chịu NULL.

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

Tuy nhiên, tôi không nhận được làm thế nào nullptrhoạt động. Ví dụ, bài viết Wikipedia nói:

C ++ 11 sửa lỗi này bằng cách giới thiệu một từ khóa mới để phục vụ như một hằng con trỏ null phân biệt: nullptr. Nó thuộc loại nullptr_t , có thể chuyển đổi hoàn toàn và có thể so sánh với bất kỳ loại con trỏ hoặc loại con trỏ thành thành viên. Nó không hoàn toàn có thể chuyển đổi hoặc so sánh với các loại tích phân, ngoại trừ bool.

Làm thế nào nó là một từ khóa và một thể hiện của một loại?

Ngoài ra, bạn có một ví dụ khác (bên cạnh Wikipedia không) ở đâu nullptrtốt hơn cũ 0?


23
thực tế liên quan: nullptrcũng được sử dụng để thể hiện tham chiếu null cho các thẻ điều khiển được quản lý trong C ++ / CLI.
Mehrdad Afshari

3
Khi sử dụng Visual C ++, hãy nhớ rằng nếu bạn sử dụng nullptr với mã C / C ++ gốc và sau đó biên dịch với tùy chọn trình biên dịch / clr, trình biên dịch có thể xác định xem nullptr chỉ ra giá trị con trỏ null được quản lý hay không. Để làm rõ ý định của bạn với trình biên dịch, hãy sử dụng nullptr để chỉ định giá trị được quản lý hoặc __nullptr để chỉ định giá trị gốc. Microsoft đã thực hiện điều này như một phần mở rộng thành phần.
cseder

6
Được nullptr_tđảm bảo chỉ có một thành viên , nullptr? Vì vậy, nếu một hàm được trả về nullptr_t, thì trình biên dịch đã biết giá trị nào sẽ được trả về, bất kể phần thân của hàm là gì?
Aaron McDaid

8
@AaronMcDaid std::nullptr_tcó thể được khởi tạo, nhưng tất cả các trường hợp sẽ giống hệt nhau nullptrvì loại được xác định là typedef decltype(nullptr) nullptr_t. Tôi tin rằng lý do chính loại tồn tại là để các chức năng có thể bị quá tải đặc biệt để bắt nullptr, nếu cần thiết. Xem ở đây cho một ví dụ.
Thời gian của Justin - Phục hồi Monica

5
0 không bao giờ là một con trỏ null, null pointer là một con trỏ có thể được lấy bằng cách đúc zero đen để loại con trỏ, và nó không trỏ đến bất kỳ đối tượng hiện theo định nghĩa.
Swift - Thứ Sáu Pie

Câu trả lời:


403

Làm thế nào nó là một từ khóa và một thể hiện của một loại?

Điều này không đáng ngạc nhiên. Cả hai truefalselà từ khóa và dưới dạng chữ, chúng có một loại ( bool). nullptrlà một con trỏ nghĩa đen của loại std::nullptr_tvà nó là một giá trị (bạn không thể lấy địa chỉ của nó bằng cách sử dụng &).

  • 4.10về chuyển đổi con trỏ nói rằng một giá trị loại std::nullptr_tlà hằng số con trỏ null và hằng số con trỏ null tích hợp có thể được chuyển đổi thành std::nullptr_t. Hướng ngược lại không được phép. Điều này cho phép nạp chồng một hàm cho cả con trỏ và số nguyên và chuyển qua nullptrđể chọn phiên bản con trỏ. Vượt qua NULLhoặc 0sẽ nhầm lẫn chọn intphiên bản.

  • Việc truyền nullptr_tvào một kiểu tích phân cần a reinterpret_castvà có cùng ngữ nghĩa với một (void*)0kiểu kết hợp thành một kiểu tích phân (thực hiện ánh xạ được xác định). A reinterpret_castkhông thể chuyển đổi nullptr_tthành bất kỳ loại con trỏ. Dựa vào chuyển đổi ngầm nếu có thể hoặc sử dụng static_cast.

  • Tiêu chuẩn yêu cầu đó sizeof(nullptr_t)sizeof(void*).


Ồ, sau khi tìm kiếm, dường như với tôi rằng toán tử có điều kiện không thể chuyển đổi 0 thành nullptr trong các trường hợp như cond ? nullptr : 0;. Xóa khỏi câu trả lời của tôi.
Julian Schaub - litb

88
Lưu ý rằng NULLthậm chí không được đảm bảo 0. Nó có thể 0L, trong trường hợp một cuộc gọi đến void f(int); void f(char *);sẽ mơ hồ. nullptrsẽ luôn ưu tiên phiên bản con trỏ và không bao giờ gọi phiên bản đó int. Cũng lưu ý rằng nullptr có thể chuyển đổi thành bool(dự thảo nói rằng tại 4.12).
Julian Schaub - litb

@litb: vậy liên quan đến f (int) và f (void *) - f (0) có còn mơ hồ không?
Steve Folly

27
@Steve, không có cái đó sẽ gọi intphiên bản. Nhưng f(0L)là mơ hồ, vì long -> intlà tốt như long -> void*là cả hai không kém phần tốn kém. Vì vậy, nếu NULL nằm 0Ltrong trình biên dịch của bạn, thì một cuộc gọi f(NULL)sẽ mơ hồ với hai hàm đó. Tất nhiên là không như vậy nullptr.
Julian Schaub - litb

2
@SvenS Không được định nghĩa như (void*)0trong C ++. Nhưng nó có thể được định nghĩa là bất kỳ hằng số con trỏ null tùy ý, bất kỳ hằng số tích phân nào có giá trị 0 và nullptrthực hiện. Vì vậy, chắc chắn nhất không sẽ nhưng có thể . (Bạn đã quên ping tôi btw ..)
Ded

60

Từ nullptr: Con trỏ Null loại an toàn và rõ ràng :

Từ khóa nullptr C ++ 09 mới chỉ định một hằng số giá trị đóng vai trò là một con trỏ null phổ quát theo nghĩa đen, thay thế cho lỗi 0 và gõ chữ yếu và macro NULL khét tiếng. nullptr do đó chấm dứt hơn 30 năm bối rối, mơ hồ và lỗi. Các phần sau đây trình bày cơ sở nullptr và chỉ ra cách nó có thể khắc phục các bệnh của NULL và 0.

Tài liệu tham khảo khác:


17
C ++ 09? Không phải nó được gọi là C ++ 0x trước tháng 8 năm 2011 sao?
Michael Dorst

2
@anthropomorphic Vâng đó là mục đích của nó. C ++ 0x đã được sử dụng trong khi nó vẫn đang hoạt động, vì không biết liệu nó sẽ kết thúc năm 2008 hay 2009. Lưu ý rằng nó thực sự đã trở thành C ++ 0B có nghĩa là C ++ 11. Xem stroustrup.com/C++11FAQ.html
mxmlnkn

44

Tại sao nullptr trong C ++ 11? Nó là gì? Tại sao NULL không đủ?

Chuyên gia C ++ Alex Allain nói rằng nó hoàn hảo ở đây (nhấn mạnh của tôi được in đậm):

... hãy tưởng tượng bạn có hai khai báo hàm sau:

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

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

Mặc dù có vẻ như hàm thứ hai sẽ được gọi - sau tất cả, bạn chuyển qua cái dường như là một con trỏ - đó thực sự là hàm đầu tiên sẽ được gọi! Vấn đề là vì NULL bằng 0 và 0 là số nguyên, nên phiên bản đầu tiên của func sẽ được gọi thay thế. Đây là loại điều mà, vâng, không phải lúc nào cũng xảy ra, nhưng khi nó xảy ra, thì cực kỳ bực bội và khó hiểu. Nếu bạn không biết chi tiết về những gì đang xảy ra, nó có thể trông giống như một lỗi trình biên dịch. Một tính năng ngôn ngữ trông giống như một lỗi trình biên dịch, tốt, không phải là thứ bạn muốn.

Nhập nullptr. Trong C ++ 11, nullptr là một từ khóa mới có thể (và nên!) Được sử dụng để thể hiện các con trỏ NULL; nói cách khác, bất cứ nơi nào bạn viết NULL trước đây, bạn nên sử dụng nullptr thay thế. Điều đó không rõ ràng hơn đối với bạn, lập trình viên , (mọi người đều biết NULL nghĩa là gì), nhưng nó rõ ràng hơn với trình biên dịch , sẽ không còn thấy số 0 ở mọi nơi được sử dụng có ý nghĩa đặc biệt khi được sử dụng như một con trỏ.

Allain kết thúc bài viết của mình bằng:

Bất kể tất cả điều này - quy tắc của C ++ 11 chỉ đơn giản là bắt đầu sử dụng nullptrbất cứ khi nào bạn có thể sử dụng NULLtrong quá khứ.

(Lời của tôi):

Cuối cùng, đừng quên đó nullptrlà một đối tượng - một lớp. Nó có thể được sử dụng ở bất cứ đâu NULLđã được sử dụng trước đây, nhưng nếu bạn cần loại của nó vì một số lý do, loại này có thể được trích xuất với decltype(nullptr), hoặc được mô tả trực tiếp như std::nullptr_t, chỉ đơn giản là một typedeftrongdecltype(nullptr) .

Người giới thiệu:

  1. Cprogramming.com: Các loại tốt hơn trong C ++ 11 - nullptr, các lớp enum (liệt kê được gõ mạnh) và cstdint
  2. https://en.cppreference.com/w/cpp/lingu/decltype
  3. https://en.cppreference.com/w/cpp/types/nullptr_t

2
Tôi phải nói rằng câu trả lời của bạn bị đánh giá thấp, nó rất dễ hiểu thông qua ví dụ của bạn.
mss

37

Khi bạn có một chức năng có thể nhận con trỏ tới nhiều loại, việc gọi nó bằng NULLkhông rõ ràng. Cách thức này được thực hiện xung quanh bây giờ là rất khó khăn bằng cách chấp nhận một int và giả định nó 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); }
};

Trong C++11bạn sẽ có thể quá tải nullptr_tvì vậy đó ptr<T> p(42);sẽ là một lỗi thời gian biên dịch thay vì thời gian chạy assert.

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

Điều gì nếu NULLđược định nghĩa là 0L?
LF

9

nullptrkhông thể được gán cho một loại tích phân, chẳng hạn như intnhưng chỉ một loại con trỏ; hoặc một loại con trỏ tích hợp như int *ptrhoặc một con trỏ thông minh nhưstd::shared_ptr<T>

Tôi tin rằng đây là một sự khác biệt quan trọng vì NULLvẫn có thể được gán cho cả loại tích phân và con trỏ dưới dạng NULLmacro được mở rộng để 0có thể đóng vai trò là giá trị ban đầu cho intcả con trỏ cũng như con trỏ.


Lưu ý rằng câu trả lời này là sai. NULLkhông được đảm bảo để được mở rộng 0.
LF

6

Ngoài ra, bạn có một ví dụ khác (bên cạnh Wikipedia) ở đâu nullptrtốt hơn 0 cũ không?

Đúng. Đó cũng là một ví dụ trong thế giới thực (đơn giản hóa) xảy ra trong mã sản xuất của chúng tôi. Nó chỉ nổi bật vì gcc có thể đưa ra cảnh báo khi biên dịch chéo sang nền tảng có độ rộng đăng ký khác nhau (vẫn không chắc chắn chính xác tại sao chỉ khi biên dịch chéo từ x86_64 đến x86, cảnh báowarning: converting to non-pointer type 'int' from NULL ):

Hãy xem xét mã này (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;
}

Nó mang lại sản lượng này:

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

Tôi không thấy cách này cải thiện khi sử dụng nullptr (và C ++ 11). Nếu bạn đặt pb thành nullptr, so sánh đầu tiên sẽ đánh giá vẫn đúng (trong khi so sánh táo với lê ..). Trường hợp thứ hai thậm chí còn tệ hơn: Nếu bạn so sánh a với nullptr, nó sẽ chuyển đổi a thành B * và sau đó nó sẽ đánh giá lại thành đúng (trước khi nó được chuyển thành bool và expr được đánh giá thành false). Toàn bộ điều này làm tôi nhớ đến JavaScript và tôi tự hỏi liệu chúng ta sẽ nhận được === trong C ++ trong tương lai :(
Nils

5

Vâng, các ngôn ngữ khác có các từ dành riêng là trường hợp của các loại. Python, ví dụ:

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

Đây thực sự là một so sánh khá gần bởi vì Nonethường được sử dụng cho một cái gì đó chưa được xác định, nhưng đồng thời so sánh nhưNone == 0 là sai.

Mặt khác, ở đồng bằng C, NULL == 0sẽ trả về IIRC thực vì NULLchỉ là macro trả về 0, luôn là địa chỉ không hợp lệ (AFAIK).


4
NULLlà một macro mở rộng về 0, một số 0 không đổi cho một con trỏ tạo ra một con trỏ null. Một con trỏ null không phải là số không (nhưng thường là), số 0 không phải luôn luôn là một địa chỉ không hợp lệ và một số 0 không liên tục cho một con trỏ không phải là null và một con trỏ null được chuyển thành một số nguyên không phải bằng không. Tôi hy vọng tôi có được điều đó mà không quên bất cứ điều gì. Tham khảo: c-faq.com/null/null2.html
Samuel Edwin Ward

3

Nó là một từ khóa vì tiêu chuẩn sẽ chỉ định nó như vậy. ;-) Theo dự thảo công khai mới nhất (n2914)

2.14.7 Con trỏ bằng chữ [lex.nullptr]

pointer-literal:
nullptr

Các con trỏ bằng chữ là từ khóa nullptr. Nó là một giá trị của loại std::nullptr_t.

Nó hữu ích vì nó không hoàn toàn chuyển đổi thành một giá trị tích phân.


2

Giả sử bạn có một hàm (f) bị quá tải để lấy cả int và char *. Trước C ++ 11, nếu bạn muốn gọi nó bằng một con trỏ null và bạn đã sử dụng NULL (tức là giá trị 0), thì bạn sẽ gọi một số bị quá tải cho int:

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

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

Đây có lẽ không phải là những gì bạn muốn. C ++ 11 giải quyết điều này với nullptr; Bây giờ bạn có thể viết như sau:

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

1

Hãy để tôi đầu tiên cung cấp cho bạn một thực hiện không tinh vi 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;

nullptrlà một ví dụ tinh tế của thành ngữ Resolver Kiểu trả về để tự động suy ra một con trỏ null của loại đúng tùy thuộc vào loại của thể hiện mà nó được gán.

int *ptr = nullptr;                // OK
void (C::*method_ptr)() = nullptr; // OK
  • Như bạn có thể ở trên, khi nullptrđược gán cho một con trỏ nguyên, mộtint kiểu khởi tạo của hàm chuyển đổi templatized được tạo. Và con trỏ phương thức cũng vậy.
  • Bằng cách này bằng cách tận dụng chức năng mẫu, chúng tôi thực sự đang tạo ra loại con trỏ null thích hợp mỗi khi chúng tôi thực hiện, một phép gán kiểu mới.
  • nullptrmột số nguyên có giá trị bằng 0, bạn không thể sử dụng địa chỉ mà chúng tôi đã thực hiện bằng cách xóa & toán tử.

Tại sao chúng ta cần nullptrở nơi đầu tiên?

  • Bạn thấy truyền thống NULLcó một số vấn đề với nó như dưới đây:

1️⃣ Chuyển đổi ngầm định

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

2️⃣ Chức năng gọi mơ hồ

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

func(NULL);     // Which one to call?
  • Quá trình biên dịch tạo ra các lỗi sau:
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️⃣ Quá tải xây dựng

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

String s1( NULL );
String s2( 5 );
  • Trong những trường hợp như vậy, bạn cần diễn viên rõ ràng (nghĩa là  String s((char*)0)).

0

0 từng là giá trị số nguyên duy nhất có thể được sử dụng làm công cụ khởi tạo không có cast cho con trỏ: bạn không thể khởi tạo con trỏ với các giá trị nguyên khác mà không cần cast. Bạn có thể coi 0 là một đơn vị consexpr về mặt cú pháp tương tự như một số nguyên. Nó có thể khởi tạo bất kỳ con trỏ hoặc số nguyên. Nhưng đáng ngạc nhiên, bạn sẽ thấy rằng nó không có loại khác biệt: nó là một int. Vậy làm thế nào đến 0 có thể khởi tạo con trỏ và 1 không thể? Một câu trả lời thực tế là chúng ta cần một phương tiện xác định giá trị null của con trỏ và chuyển đổi ngầm định trực tiếp intthành một con trỏ dễ bị lỗi. Do đó, 0 trở thành một con quái vật kỳ dị thực sự ra khỏi thời kỳ tiền sử. nullptrđã được đề xuất để trở thành một đại diện constexpr singleton thực sự của giá trị null để khởi tạo con trỏ. Nó không thể được sử dụng để trực tiếp khởi tạo các số nguyên và loại bỏ sự mơ hồ liên quan đến việc xác định NULLtheo 0. nullptrcó thể được định nghĩa là một thư viện sử dụng cú pháp std nhưng về mặt ngữ nghĩa là một thành phần cốt lõi bị thiếu. NULLbây giờ không được ủng hộ nullptr, trừ khi một số thư viện quyết định định nghĩa nó là nullptr.


-1

Đây là tiêu đề 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

(một thỏa thuận tuyệt vời có thể được phát hiện nhanh chóng grep -r /usr/include/*`)

Một điều nhảy ra là *quá tải toán tử (trả về 0 thân thiện hơn nhiều so với segfaulting ...). Một điều nữa là nó không giống phù hợp với lưu trữ một địa chỉ nào cả . Điều này, so với cách nó đi trượt khoảng trống * và chuyển kết quả NULL cho các con trỏ bình thường như các giá trị canh gác, rõ ràng sẽ làm giảm yếu tố "không bao giờ quên, nó có thể là một quả bom".


-2

NULL không cần phải bằng 0. Miễn là bạn luôn sử dụng NULL và không bao giờ 0, NULL có thể là bất kỳ giá trị nào. Khi bạn lập trình một Vi điều khiển von Neuman với bộ nhớ phẳng, có vektor ngắt của nó ở mức 0. Nếu NULL bằng 0 và một cái gì đó ghi vào Con trỏ NULL thì Vi điều khiển gặp sự cố. Nếu NULL cho phép 1024 và tại 1024 có một biến dành riêng, thì ghi sẽ không bị lỗi và bạn có thể phát hiện các bài tập Con trỏ NULL từ bên trong chương trình. Điều này là vô nghĩa trên PC, nhưng đối với tàu thăm dò không gian, thiết bị quân sự hoặc y tế, điều quan trọng là không bị rơi.


2
Chà, giá trị thực của con trỏ null trong bộ nhớ có thể không bằng 0, nhưng tiêu chuẩn C (và C ++) bắt buộc các trình biên dịch chuyển đổi tích phân 0 bằng chữ sang con trỏ null.
bzim
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.