Was genau ist nullptr?


570

Wir haben jetzt C ++ 11 mit vielen neuen Funktionen. Eine interessante und verwirrende (zumindest für mich) ist die neue nullptr.

Nun, das böse Makro ist nicht mehr nötig NULL.

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

Trotzdem verstehe ich nicht, wie es nullptrfunktioniert. Zum Beispiel sagt der Wikipedia-Artikel :

C ++ 11 korrigiert dies, indem ein neues Schlüsselwort eingeführt wird , das als definierte Nullzeigerkonstante dient: nullptr. Es ist vom Typ nullptr_t , der implizit konvertierbar und mit jedem Zeigertyp oder Zeiger-zu- Element -Typ vergleichbar ist. Es ist nicht implizit konvertierbar oder mit integralen Typen vergleichbar, außer für bool.

Wie ist es ein Schlüsselwort und eine Instanz eines Typs?

Haben Sie auch ein anderes Beispiel (neben dem Wikipedia-Beispiel), bei dem nullptres dem guten alten überlegen ist 0?


23
Verwandte Tatsache: nullptrWird auch verwendet, um eine Nullreferenz für verwaltete Handles in C ++ / CLI darzustellen.
Mehrdad Afshari

3
Denken Sie bei der Verwendung von Visual C ++ daran, dass der Compiler nicht bestimmen kann, ob nullptr einen nativen oder verwalteten Nullzeigerwert angibt, wenn Sie nullptr mit nativem C / C ++ - Code verwenden und dann mit der Compileroption / clr kompilieren. Verwenden Sie nullptr, um einen verwalteten Wert anzugeben, oder __nullptr, um einen nativen Wert anzugeben, um dem Compiler Ihre Absicht klar zu machen. Microsoft hat dies als Komponentenerweiterung implementiert.
Cseder

6
Ist nullptr_tgarantiert nur ein Mitglied nullptr? Wenn also eine Funktion zurückgegeben wird nullptr_t, weiß der Compiler bereits, welcher Wert zurückgegeben wird, unabhängig vom Funktionskörper?
Aaron McDaid

8
@AaronMcDaid std::nullptr_tkann instanziiert werden, aber alle Instanzen sind identisch mit, nullptrda der Typ definiert ist als typedef decltype(nullptr) nullptr_t. Ich glaube, der Hauptgrund, warum der Typ existiert, ist, dass Funktionen bei Bedarf speziell zum Abfangen überladen nullptrwerden können. Siehe hier für ein Beispiel.
Justin Time - Stellen Sie Monica am

5
0 war nie ein Nullzeiger, Nullzeiger ist ein Zeiger, der durch Umwandlung des Nullliteral in den Zeigertyp erhalten werden kann, und er zeigt per Definition nicht auf ein vorhandenes Objekt.
Swift - Friday Pie

Antworten:


403

Wie ist es ein Schlüsselwort und eine Instanz eines Typs?

Das ist nicht überraschend. Beide trueund falsesind Schlüsselwörter und als Literale haben sie einen Typ ( bool). nullptrist ein Zeiger wörtliche vom Typ std::nullptr_t, und es ist ein prvalue (die Adresse der es nicht mit nehmen kann &).

  • 4.10Über die Zeigerkonvertierung heißt es, dass ein Wert vom Typ std::nullptr_teine Nullzeigerkonstante ist und dass eine integrale Nullzeigerkonstante in konvertiert werden kann std::nullptr_t. Die entgegengesetzte Richtung ist nicht erlaubt. Dies ermöglicht das Überladen einer Funktion sowohl für Zeiger als auch für Ganzzahlen und das Übergeben nullptrzur Auswahl der Zeigerversion. Bestehen NULLoder 0würde die intVersion verwirrend auswählen .

  • Eine Umwandlung nullptr_tin einen integralen Typ benötigt a reinterpret_castund hat dieselbe Semantik wie eine Umwandlung (void*)0in einen integralen Typ (Mapping-Implementierung definiert). A reinterpret_castkann nicht nullptr_tin einen Zeigertyp konvertiert werden. Verlassen Sie sich nach Möglichkeit auf die implizite Konvertierung oder verwenden Sie diese static_cast.

  • Der Standard verlangt , dass sizeof(nullptr_t)sei sizeof(void*).


Oh, nach dem Betrachten scheint es mir, dass der bedingte Operator in Fällen wie 0 nicht in nullptr konvertieren kann cond ? nullptr : 0;. Aus meiner Antwort entfernt.
Johannes Schaub - litb

88
Beachten Sie, dass dies NULLnicht einmal garantiert ist 0. Es kann sein 0L, in welchem ​​Fall ein Anruf an void f(int); void f(char *);mehrdeutig ist. nullptrwird immer die Zeigerversion bevorzugen und niemals die inteine aufrufen . Beachten Sie, dass nullptr ist zu convertible bool(der Entwurf sagt , dass bei 4.12).
Johannes Schaub - litb

@litb: also in Bezug auf f (int) und f (void *) - wird f (0) immer noch mehrdeutig sein?
Steve Folly

27
@Steve, nein, das wird die intVersion aufrufen . Ist aber f(0L)mehrdeutig, weil long -> intsowohl als auch long -> void*beide gleich teuer sind. Wenn sich also NULL 0Lauf Ihrem Compiler befindet, ist ein Aufruf f(NULL)angesichts dieser beiden Funktionen nicht eindeutig. Nicht so nullptrnatürlich.
Johannes Schaub - Litb

2
@SvenS Es darf nicht wie (void*)0in C ++ definiert sein. Es kann aber als jede beliebige Nullzeigerkonstante definiert werden, die jede Integralkonstante mit dem Wert 0 nullptrerfüllt und erfüllt. Also definitiv nicht will, aber kann . (Sie haben vergessen , mich btw zu ping ..)
Deduplicator

60

Von nullptr: Ein typsicherer und eindeutiger Nullzeiger :

Das neue Schlüsselwort C ++ 09 nullptr bezeichnet eine rvalue-Konstante, die als universelles Nullzeigerliteral dient und das fehlerhafte und schwach typisierte Literal 0 und das berüchtigte NULL-Makro ersetzt. nullptr beendet damit mehr als 30 Jahre Verlegenheit, Zweideutigkeit und Fehler. In den folgenden Abschnitten wird die Nullptr-Funktion vorgestellt und gezeigt, wie sie die Probleme von NULL und 0 beheben kann.

Weitere Referenzen:


17
C ++ 09? Wurde es nicht vor August 2011 als C ++ 0x bezeichnet?
Michael Dorst

2
@anthropomorphic Nun, das ist sein Zweck. C ++ 0x wurde verwendet, während es noch in Arbeit war, da nicht bekannt war, ob es 2008 oder 2009 fertiggestellt werden würde. Beachten Sie, dass es tatsächlich zu C ++ 0B wurde, was C ++ 11 bedeutet. Siehe stroustrup.com/C++11FAQ.html
mxmlnkn

44

Warum nullptr in C ++ 11? Was ist es? Warum reicht NULL nicht aus?

Der C ++ - Experte Alex Allain sagt es hier perfekt (meine Betonung ist fett gedruckt):

... stellen Sie sich vor, Sie haben die folgenden zwei Funktionsdeklarationen:

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

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

Obwohl es so aussieht, als würde die zweite Funktion aufgerufen - Sie übergeben schließlich einen Zeiger -, ist es wirklich die erste Funktion, die aufgerufen wird! Das Problem ist, dass stattdessen die erste Version von func aufgerufen wird, da NULL 0 und 0 eine Ganzzahl ist. Dies ist die Art von Dingen, die zwar nicht immer passieren, aber wenn sie passieren, sind sie äußerst frustrierend und verwirrend. Wenn Sie die Details der Vorgänge nicht kennen, könnte dies wie ein Compiler-Fehler aussehen. Eine Sprachfunktion, die wie ein Compiler-Fehler aussieht, ist nicht das, was Sie wollen.

Geben Sie nullptr ein. In C ++ 11 ist nullptr ein neues Schlüsselwort, das zur Darstellung von NULL-Zeigern verwendet werden kann (und sollte!). Mit anderen Worten, wo immer Sie zuvor NULL geschrieben haben, sollten Sie stattdessen nullptr verwenden. Für Sie, den Programmierer , ist es nicht mehr klar (jeder weiß, was NULL bedeutet), aber für den Compiler ist es expliziter , dass nicht mehr überall Nullen verwendet werden, die eine besondere Bedeutung haben, wenn sie als Zeiger verwendet werden.

Allain beendet seinen Artikel mit:

Unabhängig davon lautet die Faustregel für C ++ 11 einfach, die Verwendung zu beginnen, nullptrwann immer Sie sie NULLin der Vergangenheit anderweitig verwendet hätten .

(Meine Worte):

Vergessen Sie nicht, dass dies nullptrein Objekt ist - eine Klasse. Es kann überall NULLdort verwendet werden, wo es zuvor verwendet wurde, aber wenn Sie seinen Typ aus irgendeinem Grund benötigen, kann es mit extrahiert decltype(nullptr)oder direkt als beschrieben werden std::nullptr_t, was einfach ein typedefvon ist decltype(nullptr).

Verweise:

  1. Cprogramming.com: Bessere Typen in C ++ 11 - nullptr, Aufzählungsklassen (stark typisierte Aufzählungen) und cstdint
  2. https://en.cppreference.com/w/cpp/language/decltype
  3. https://en.cppreference.com/w/cpp/types/nullptr_t

2
Ich muss sagen, Ihre Antwort ist unterschätzt, es war sehr leicht durch Ihr Beispiel zu verstehen.
mss

37

Wenn Sie eine Funktion haben, die Zeiger auf mehr als einen Typ empfangen kann, ist der Aufruf mit nicht NULLeindeutig. Die Art und Weise, wie dies jetzt umgangen wird, ist sehr hackig, wenn man ein int akzeptiert und davon ausgeht, dass es ist 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); }
};

In C++11könnten Sie überladen, nullptr_tso dass ptr<T> p(42);dies eher ein Kompilierungsfehler als eine Laufzeit wäre assert.

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

Was ist, wenn NULLdefiniert ist als 0L?
LF

9

nullptrkann keinem integralen Typ wie einem, intsondern nur einem Zeigertyp zugewiesen werden ; entweder ein eingebauter Zeigertyp wie int *ptroder ein intelligenter Zeiger wiestd::shared_ptr<T>

Ich glaube, dies ist eine wichtige Unterscheidung, da NULLimmer noch sowohl ein integraler Typ als auch ein Zeiger zugewiesen werden können, ebenso wie NULLein erweitertes Makro, 0das sowohl als Anfangswert für einen intals auch als Zeiger dienen kann.


Beachten Sie, dass diese Antwort falsch ist. NULLwird nicht garantiert erweitert 0.
LF

6

Haben Sie auch ein anderes Beispiel (neben dem Wikipedia-Beispiel), bei nullptrdem die gute alte 0 überlegen ist?

Ja. Es ist auch ein (vereinfachtes) Beispiel aus der Praxis, das in unserem Produktionscode vorkommt. Es fiel nur auf, weil gcc beim Crosscompilieren auf eine Plattform mit unterschiedlicher Registerbreite eine Warnung ausgeben konnte (immer noch nicht sicher, warum, nur beim Crosscompilieren von x86_64 auf x86, warnt warning: converting to non-pointer type 'int' from NULL):

Betrachten Sie diesen Code (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;
}

Es ergibt diese Ausgabe:

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

Ich kann nicht sehen, wie sich dies bei Verwendung von nullptr (und C ++ 11) verbessert. Wenn Sie pb auf nullptr setzen, wird der erste Vergleich immer noch als wahr ausgewertet (beim Vergleichen von Äpfeln mit Birnen ..). Der zweite Fall ist noch schlimmer: Wenn Sie a mit nullptr vergleichen, wird a in B * konvertiert und dann erneut in true ausgewertet (bevor es in bool umgewandelt und der Ausdruck in false ausgewertet wurde). Das Ganze erinnert mich an JavaScript und ich frage mich, ob wir in Zukunft === in C ++ bekommen werden :(
Nils

5

Nun, andere Sprachen haben reservierte Wörter, die Instanzen von Typen sind. Python zum Beispiel:

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

Dies ist eigentlich ein ziemlich enger Vergleich, da er Nonenormalerweise für etwas verwendet wird, das nicht initialisiert wurde, aber gleichzeitig Vergleiche wieNone == 0 falsch sind.

Andererseits würde in einfachem C NULL == 0wahres IIRC zurückgeben, da NULLes sich nur um ein Makro handelt, das 0 zurückgibt , was immer eine ungültige Adresse (AFAIK) ist.


4
NULList ein Makro, das auf eine Null erweitert wird. Eine konstante Null, die in einen Zeiger umgewandelt wird, erzeugt einen Nullzeiger. Ein Nullzeiger muss nicht Null sein (ist es aber oft), Null ist nicht immer eine ungültige Adresse, und eine nicht konstante Null, die in einen Zeiger umgewandelt wird, muss nicht Null sein, und ein Nullzeiger, der in einen Zeiger umgewandelt wird Ganzzahl muss nicht Null sein. Ich hoffe, ich habe das in Ordnung gebracht, ohne etwas zu vergessen. Eine Referenz: c-faq.com/null/null2.html
Samuel Edwin Ward

3

Es ist ein Schlüsselwort, da der Standard es als solches spezifiziert. ;-) Nach dem neuesten öffentlichen Entwurf (n2914)

2.14.7 Zeigerliterale [lex.nullptr]

pointer-literal:
nullptr

Das Zeigerliteral ist das Schlüsselwort nullptr. Es ist ein Wert vom Typ std::nullptr_t.

Dies ist nützlich, da es nicht implizit in einen ganzzahligen Wert konvertiert wird.


2

Angenommen, Sie haben eine Funktion (f), die überladen ist, um sowohl int als auch char * zu übernehmen. Wenn Sie es vor C ++ 11 mit einem Nullzeiger aufrufen wollten und NULL (dh den Wert 0) verwendeten, würden Sie das für int überladene aufrufen:

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

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

Dies ist wahrscheinlich nicht das, was Sie wollten. C ++ 11 löst dies mit nullptr; Jetzt können Sie Folgendes schreiben:

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

1

Lassen Sie mich zunächst eine Implementierung von ungekünstelt geben 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;

nullptrist ein subtiles Beispiel für die Rückgabetyp-Resolver-Sprache, mit der automatisch ein Nullzeiger des richtigen Typs abgeleitet wird, abhängig vom Typ der Instanz, der er zugewiesen wird.

int *ptr = nullptr;                // OK
void (C::*method_ptr)() = nullptr; // OK
  • Wie oben beschrieben, wird beim nullptrZuweisen zu einem Ganzzahlzeiger eine intTypinstanziierung der Vorlagenkonvertierungsfunktion erstellt. Gleiches gilt auch für Methodenzeiger.
  • Auf diese Weise erstellen wir durch die Nutzung der Vorlagenfunktionalität jedes Mal, wenn wir eine neue Typzuweisung durchführen, den entsprechenden Typ eines Nullzeigers.
  • Da nullptres sich um ein ganzzahliges Literal mit dem Wert Null handelt, können Sie seine Adresse nicht verwenden, was wir durch Löschen von & operator erreicht haben.

Warum brauchen wir überhaupt nullptr?

  • Sie sehen, traditionell NULLhat ein Problem damit wie folgt:

1️⃣ Implizite Konvertierung

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

2️⃣ Mehrdeutigkeit von Funktionsaufrufen

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

func(NULL);     // Which one to call?
  • Die Kompilierung erzeugt den folgenden Fehler:
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️⃣ Konstruktorüberlastung

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

String s1( NULL );
String s2( 5 );
  • In solchen Fällen benötigen Sie eine explizite Besetzung (dh ,  String s((char*)0)).

0

0 war früher der einzige ganzzahlige Wert, der als umwandlungsfreier Initialisierer für Zeiger verwendet werden konnte: Sie können Zeiger ohne Umwandlung nicht mit anderen ganzzahligen Werten initialisieren. Sie können 0 als einen Consexpr-Singleton betrachten, der syntaktisch einem ganzzahligen Literal ähnlich ist. Es kann einen beliebigen Zeiger oder eine Ganzzahl initiieren. Aber überraschenderweise werden Sie feststellen, dass es keinen bestimmten Typ hat: Es ist ein int. Wie kommt es also, dass 0 Zeiger initialisieren kann und 1 nicht? Eine praktische Antwort war, dass wir ein Mittel zum Definieren des Zeiger-Nullwerts benötigen und die direkte implizite Konvertierung intin einen Zeiger fehleranfällig ist. So wurde 0 ein echtes Freak-Weirdo-Biest aus der prähistorischen Zeit. nullptrEs wurde vorgeschlagen, eine echte Singleton-Constexpr-Darstellung des Nullwerts zu sein, um Zeiger zu initialisieren. Es kann nicht verwendet werden, um Ganzzahlen direkt zu initialisieren, und beseitigt Unklarheiten, die mit der Definition NULLin Bezug auf 0 verbunden sind. Es nullptrkönnte als Bibliothek mit Standardsyntax definiert werden, wird jedoch semantisch als fehlende Kernkomponente angesehen. NULLwird jetzt zugunsten von abgelehnt nullptr, es sei denn, eine Bibliothek beschließt, es als zu definieren nullptr.


-1

Hier ist der LLVM-Header.

// -*- 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

(viel kann mit einem schnellen aufgedeckt werden grep -r /usr/include/*`)

Eine Sache, die herausspringt, ist die *Überlastung des Operators (die Rückgabe von 0 ist viel freundlicher als Segfaulting ...). Eine andere Sache ist es nicht kompatibel sieht mit Speichern einer Adresse überhaupt . Was im Vergleich dazu, wie es geht, Leere * zu schleudern und NULL-Ergebnisse als Sentinel-Werte an normale Zeiger zu übergeben, offensichtlich den Faktor "Niemals vergessen, es könnte eine Bombe sein" reduzieren würde.


-2

NULL muss nicht 0 sein. Solange Sie immer NULL und niemals 0 verwenden, kann NULL ein beliebiger Wert sein. Angenommen, Sie programmieren einen von Neuman-Mikrocontroller mit flachem Speicher, dessen Interrupt vektoren bei 0 liegt. Wenn NULL 0 ist und etwas an einem NULL-Zeiger schreibt, stürzt der Mikrocontroller ab. Wenn NULL beispielsweise 1024 ist und bei 1024 eine reservierte Variable vorhanden ist, stürzt der Schreibvorgang nicht ab, und Sie können NULL-Zeigerzuweisungen im Programm erkennen. Dies ist auf PCs sinnlos, aber für Raumsonden, militärische oder medizinische Geräte ist es wichtig, nicht abzustürzen.


2
Nun, der tatsächliche Wert des Nullzeigers im Speicher ist möglicherweise nicht Null, aber der C- (und C ++) Standard verlangt von Compilern, das Integral-0-Literal in den Nullzeiger umzuwandeln.
Bzim
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.