Ten fragment kodu koncepcyjnie robi to samo dla trzech wskaźników (bezpieczna inicjalizacja wskaźnika):
int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;
A więc jakie są zalety przypisywania wskaźników nullptr
nad przypisywaniem im wartości NULL
lub 0
?
int
ivoid *
nie wybieraint
wersji zamiastvoid *
wersji podczas używanianullptr
.f(nullptr)
różni się odf(NULL)
. Ale jeśli chodzi o powyższy kod (przypisanie do zmiennej lokalnej), wszystkie trzy wskaźniki są dokładnie takie same. Jedyną zaletą jest czytelność kodu.nullptr
ale to jest różnica między 0 aNULL
Odpowiedzi:
Wydaje się, że w tym kodzie nie ma żadnej przewagi. Ale rozważ następujące przeciążone funkcje:
Która funkcja zostanie wywołana? Oczywiście intencją tutaj jest zadzwonić
f(char const *)
, ale w rzeczywistościf(int)
zostanie wezwany! To jest duży problem 1 , prawda?Tak więc rozwiązaniem takich problemów jest użycie
nullptr
:Oczywiście to nie jedyna zaleta
nullptr
. Oto kolejny:Ponieważ w szablonie typ
nullptr
jest wydedukowany jakonullptr_t
, więc możesz napisać to:1. W C ++
NULL
jest definiowane jako#define NULL 0
, więc jest w zasadzieint
, dlategof(int)
jest wywoływane.źródło
nullptr
? (Nie, nie jestem wymagający)NULL
jest wymagany przez standard, aby mieć typ całkowity, dlatego jest zwykle definiowany jako0
lub0L
. Nie jestem też pewien, czy podoba mi się tonullptr_t
przeciążenie, ponieważ wyłapuje tylko wywołania znullptr
, a nie ze wskaźnikiem zerowym innego typu, jak(void*)0
. Ale mogę uwierzyć, że ma on pewne zastosowania, nawet jeśli jedyne, co robi, to poza zdefiniowaniem własnego typu elementu zastępczego o pojedynczej wartości, który oznacza „brak”.nullptr
ma dobrze zdefiniowaną wartość liczbową, podczas gdy stałe wskaźnika zerowego nie. Stała wskaźnika zerowego jest konwertowana na wskaźnik zerowy tego typu (cokolwiek to jest). Wymagane jest, aby dwa wskaźniki zerowe tego samego typu były porównywane identycznie, a konwersja wartości logicznych zamienia wskaźnik o wartości null nafalse
. Nic więcej nie jest wymagane. Dlatego możliwe jest, że kompilator (głupi, ale możliwy) użyje np.0xabcdef1234
Lub innej liczby jako wskaźnika pustego. Z drugiej stronynullptr
wymagana jest konwersja na numeryczne zero.f(nullptr)
nie wywoła zamierzonej funkcji? Motywacji było więcej niż jedna. W nadchodzących latach sami programiści mogą odkryć wiele innych przydatnych rzeczy. Nie można więc powiedzieć, że istnieje tylko jeden prawdziwy Wykorzystanie odnullptr
.C ++ 11 wprowadza
nullptr
, jest znany jakoNull
stała wskaźnika i poprawia bezpieczeństwo typów i rozwiązuje niejednoznaczne sytuacje w przeciwieństwie do istniejącej stałej wskaźnika zerowego zależnej od implementacjiNULL
. Aby móc zrozumieć zaletynullptr
. najpierw musimy zrozumieć, co jestNULL
i jakie są problemy z tym związane.Co to jest
NULL
dokładnie?Pre C ++ 11
NULL
był używany do reprezentowania wskaźnika, który nie ma wartości lub wskaźnika, który nie wskazuje na nic prawidłowego. W przeciwieństwie do popularnego pojęciaNULL
nie jest słowem kluczowym w C ++ . Jest to identyfikator zdefiniowany w standardowych nagłówkach bibliotek. Krótko mówiąc, nie można używaćNULL
bez dołączenia niektórych standardowych nagłówków bibliotek. Rozważ przykładowy program :Wynik:
Standard C ++ definiuje NULL jako makro zdefiniowane w implementacji, zdefiniowane w niektórych plikach nagłówkowych bibliotek standardowych. Pochodzenie NULL pochodzi z C, a C ++ odziedziczyło je po C. Standard C zdefiniował NULL jako
0
lub(void *)0
. Ale w C ++ istnieje subtelna różnica.C ++ nie może zaakceptować tej specyfikacji w obecnej postaci. W przeciwieństwie do C, C ++ jest językiem silnie typizowanym (C nie wymaga jawnego rzutowania z
void*
do żadnego typu, podczas gdy C ++ wymusza jawne rzutowanie). To sprawia, że definicja NULL określona przez standard C jest bezużyteczna w wielu wyrażeniach C ++. Na przykład:Gdyby NULL zostało zdefiniowane jako
(void *)0
, żadne z powyższych wyrażeń nie zadziałałoby.void *
dostd::string
.void *
wymagane jest rzutowanie wskaźnika z do na funkcję składową.Tak więc w przeciwieństwie do C, C ++ Standard nakazał zdefiniować NULL jako literał numeryczny
0
lub0L
.Więc jaka jest potrzeba innej stałej wskaźnika zerowego, skoro
NULL
już mamy ?Chociaż komitet ds. Standardów C ++ wymyślił definicję NULL, która będzie działać dla C ++, ta definicja miała swój własny udział w problemach. NULL działał wystarczająco dobrze w prawie wszystkich scenariuszach, ale nie we wszystkich. Dało zaskakujące i błędne wyniki dla niektórych rzadkich scenariuszy. Na przykład :
Wynik:
Oczywiście intencją wydaje się być wywołanie wersji, która przyjmuje
char*
jako argument, ale gdy dane wyjściowe pokazują, funkcja, która przyjmujeint
wersję, zostaje wywołana. Dzieje się tak, ponieważ NULL jest literałem numerycznym.Ponadto, ponieważ jest zdefiniowane w implementacji, czy NULL jest 0 czy 0L, może być wiele nieporozumień w rozwiązywaniu przeciążeń funkcji.
Przykładowy program:
Analiza powyższego fragmentu:
doSomething(char *)
zgodnie z oczekiwaniami.doSomething(int)
ale być możechar*
pożądana była wersja, ponieważ0
JEST również pustym wskaźnikiem.NULL
jest zdefiniowane jako0
, wywołuje,doSomething(int)
gdy być możedoSomething(char *)
było zamierzone, być może powodując błąd logiczny w czasie wykonywania. JeśliNULL
jest zdefiniowane jako0L
, wywołanie jest niejednoznaczne i powoduje błąd kompilacji.Tak więc, w zależności od implementacji, ten sam kod może dawać różne wyniki, co jest oczywiście niepożądane. Oczywiście komitet normalizacyjny C ++ chciał to naprawić i to jest główna motywacja dla nullptr.
Więc co to jest
nullptr
i jak pozwala uniknąć problemówNULL
?C ++ 11 wprowadza nowe słowo kluczowe,
nullptr
które służy jako stała wskaźnika zerowego. W przeciwieństwie do NULL, jego zachowanie nie jest zdefiniowane w ramach implementacji. Nie jest to makro, ale ma swój własny typ. nullptr ma typstd::nullptr_t
. C ++ 11 odpowiednio definiuje właściwości nullptr, aby uniknąć wad NULL. Podsumowując jego właściwości:Właściwość 1: ma swój własny typ
std::nullptr_t
iWłaściwość 2: jest niejawnie konwertowalna i porównywalna z dowolnym typem wskaźnika lub typem wskaźnika do elementu członkowskiego, ale
Właściwość 3: nie jest niejawnie konwertowalna ani porównywalna z typami całkowitymi, z wyjątkiem
bool
.Rozważmy następujący przykład:
W powyższym programie
char *
wersja połączeń , właściwość 2 i 3W ten sposób wprowadzenie nullptr pozwala uniknąć wszystkich problemów starego dobrego NULL.
Jak i gdzie powinieneś używać
nullptr
?Praktyczna zasada dotycząca C ++ 11 jest taka, że po prostu zacznij używać
nullptr
zawsze, gdy w przeszłości używałbyś NULL.Standardowe odniesienia:
C ++ 11 Standard: C.3.2.4 Macro NULL
C ++ 11 Standard: 18.2 Typy
C ++ 11 Standard: 4.10 Konwersje wskaźników
C99 Standard: 6.3.2.3 Wskaźniki
źródło
nullptr
zorientowałem, chociaż nie wiedziałem, jaką różnicę robi z moim kodem. Dzięki za świetną odpowiedź, a zwłaszcza za wysiłek. Dało mi dużo światła na ten temat.0xccccc....
ale zmienna bez wartości jest wewnętrzną sprzecznością.bool flag = nullptr;
). Nie, nie OK, podczas kompilacji z g ++ 6error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Prawdziwą motywacją jest tutaj doskonała spedycja .
Rozważać:
Mówiąc najprościej, 0 jest wartością specjalną , ale wartości nie mogą być propagowane przez system - tylko typy mogą. Funkcje przekazujące są niezbędne i 0 nie może sobie z nimi poradzić. Stąd absolutnie konieczne było wprowadzenie
nullptr
, gdzie typ jest tym, co jest szczególne, a typ rzeczywiście może się rozmnażać. W rzeczywistości zespół MSVC musiał wprowadzićnullptr
przed terminem po zaimplementowaniu odniesień rvalue, a następnie odkrył tę pułapkę dla siebie.Jest kilka innych narożnych przypadków, w których
nullptr
życie może być łatwiejsze - ale nie jest to sprawa podstawowa, ponieważ odlew może rozwiązać te problemy. RozważaćWywołuje dwa oddzielne przeciążenia. Ponadto rozważ
To jest niejednoznaczne. Ale dzięki nullptr możesz zapewnić
źródło
forward((int*)0)
Pracuje. Czy coś mi brakuje?Podstawy nullptr
std::nullptr_t
jest typem literału pustego wskaźnika, nullptr. Jest to prvalue / rvalue typustd::nullptr_t
. Istnieją niejawne konwersje z nullptr na wartość wskaźnika o wartości null dowolnego typu wskaźnika.Literał 0 to int, a nie wskaźnik. Jeśli C ++ znajdzie 0 w kontekście, w którym można użyć tylko wskaźnika, niechętnie zinterpretuje 0 jako wskaźnik zerowy, ale jest to pozycja rezerwowa. Podstawową zasadą C ++ jest to, że 0 jest liczbą int, a nie wskaźnikiem.
Zaleta 1 - Usuń niejednoznaczność podczas przeciążania typów wskaźników i typów całkowitych
W C ++ 98 główną tego konsekwencją było to, że przeciążanie typów wskaźnikowych i całkowitych mogło prowadzić do niespodzianek. Przekazywanie 0 lub NULL do takich przeciążeń nigdy nie jest nazywane przeciążeniem wskaźnika:
Interesującą rzeczą w tym wywołaniu jest sprzeczność między pozornym znaczeniem kodu źródłowego („Wywołuję zabawę z NULL - wskaźnikiem zerowym”) a jego rzeczywistym znaczeniem („Wzywam zabawę jakąś liczbą całkowitą - nie zerową wskaźnik").
Zaletą nullptr jest to, że nie ma typu całkowitego. Wywołanie przeciążonej funkcji fun z nullptr wywołuje void * overload (tj. Przeciążenie wskaźnika), ponieważ nullptr nie może być postrzegane jako coś integralnego:
Użycie nullptr zamiast 0 lub NULL pozwala uniknąć niespodzianek związanych z rozwiązywaniem przeciążenia.
Kolejną zaletą
nullptr
overNULL(0)
podczas korzystania auto na typ zwracanyNa przykład załóżmy, że napotkasz to w bazie kodu:
Jeśli nie wiesz (lub nie możesz łatwo dowiedzieć się), co zwraca findRecord, może nie być jasne, czy wynik jest typem wskaźnikowym, czy typem całkowitym. W końcu 0 (na podstawie jakiego wyniku jest testowany) może pójść w obie strony. Z drugiej strony, jeśli zobaczysz następujące informacje,
nie ma dwuznaczności: wynik musi być typem wskaźnikowym.
Zaleta 3
Powyższy program jest kompilowany i wykonywany pomyślnie, ale lockAndCallF1, lockAndCallF2 i lockAndCallF3 mają nadmiarowy kod. Szkoda pisać taki kod, jeśli możemy napisać dla nich szablon
lockAndCallF1, lockAndCallF2 & lockAndCallF3
. Więc można to uogólnić za pomocą szablonu. Napisałem funkcję szablonulockAndCall
zamiast wielu definicjilockAndCallF1, lockAndCallF2 & lockAndCallF3
dla nadmiarowego kodu.Kod jest ponownie rozkładany, jak poniżej:
Szczegółowa analiza, dlaczego kompilacja nie powiodła się dla, a
lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)
nie dlalockAndCall(f3, f3m, nullptr)
Dlaczego kompilacja
lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)
nie powiodła się?Problem polega na tym, że po przekazaniu wartości 0 do lockAndCall rozpoczyna się odliczenie typu szablonu, aby określić jego typ. Typ 0 to int, więc jest to typ parametru ptr wewnątrz instancji tego wywołania lockAndCall. Niestety oznacza to, że w wywołaniu funkcji func wewnątrz lockAndCall przekazywana jest wartość int, która nie jest zgodna z oczekiwanym
std::shared_ptr<int>
parametremf1
. Wartość 0 przekazana w wywołaniu dolockAndCall
miała reprezentować wskaźnik zerowy, ale faktycznie przekazano wartość int. Próba przekazania tego int do f1 jako astd::shared_ptr<int>
jest błędem typu. WywołanielockAndCall
z wartością 0 kończy się niepowodzeniem, ponieważ wewnątrz szablonu int jest przekazywany do funkcji, która wymaga rozszerzeniastd::shared_ptr<int>
.Analiza rozmowy z udziałem
NULL
jest zasadniczo taka sama. GdyNULL
jest przekazywany dolockAndCall
, dla parametru ptr wyprowadzany jest typ całkowity, a błąd typu występuje, gdyptr
przekazywany jest typ typu int lub typu intf2
, który oczekuje, że zostanie pobrany plikstd::unique_ptr<int>
.W przeciwieństwie do połączenia z udziałem
nullptr
nie ma problemu. Kiedynullptr
jest przekazywanelockAndCall
, typ forptr
jest wydedukowany jakostd::nullptr_t
. Gdyptr
jest przekazywany dof3
, następuje niejawna konwersja zstd::nullptr_t
naint*
, ponieważstd::nullptr_t
niejawnie konwertuje do wszystkich typów wskaźników.Zalecane jest, aby zawsze, gdy chcesz odwołać się do pustego wskaźnika, użyj nullptr, a nie 0 lub
NULL
.źródło
Nie ma bezpośredniej korzyści z
nullptr
pokazania przykładów.Ale rozważ sytuację, w której masz 2 funkcje o tej samej nazwie; 1 bierze,
int
a kolejnyint*
Jeśli chcesz zadzwonić
foo(int*)
, przekazując NULL, sposób jest następujący:nullptr
sprawia, że jest to łatwiejsze i bardziej intuicyjne :Dodatkowe łącze ze strony internetowej Bjarne'a.
Nieistotne, ale w C ++ 11 uwaga dodatkowa:
źródło
decltype(nullptr)
jeststd::nullptr_t
.typedef decltype(nullptr) nullptr_t;
. Chyba mogę zajrzeć do standardu. Ach, znalazłem to: Uwaga: std :: nullptr_t to odrębny typ, który nie jest ani typem wskaźnikowym, ani wskaźnikiem do typu składowego; raczej wartość prvalue tego typu jest stałą wskaźnika o wartości null i można ją przekonwertować na wartość wskaźnika o wartości null lub wartość wskaźnika do elementu członkowskiego o wartości null.nullptr
.Jak już powiedzieli inni, jego podstawową zaletą są przeciążenia. I chociaż
int
przeciążenia jawne i wskaźnikowe mogą być rzadkie, rozważ standardowe funkcje biblioteczne, takie jakstd::fill
(które ugryzło mnie więcej niż raz w C ++ 03):Nie kompilacji:
Cannot convert int to MyClass*
.źródło
IMO ważniejsze niż te przeciążenia: w głęboko zagnieżdżonych konstrukcjach szablonów trudno nie stracić orientacji w typach, a podawanie wyraźnych podpisów jest nie lada wyzwaniem. Tak więc w przypadku wszystkiego, czego używasz, im bardziej precyzyjnie koncentrujesz się na zamierzonym celu, tym lepiej, zmniejszy to potrzebę stosowania jawnych podpisów i pozwoli kompilatorowi na generowanie bardziej wnikliwych komunikatów o błędach, gdy coś pójdzie nie tak.
źródło