Odpowiedź zależy od twojego punktu widzenia:
Jeśli oceniasz według standardu C ++, nie możesz uzyskać zerowego odwołania, ponieważ najpierw otrzymujesz niezdefiniowane zachowanie. Po tym pierwszym przypadku niezdefiniowanego zachowania norma zezwala na wszystko. Tak więc, jeśli piszesz *(int*)0
, masz już niezdefiniowane zachowanie, tak jak jesteś, z punktu widzenia standardu języka, dereferencjonowanie pustego wskaźnika. Reszta programu jest nieistotna, po wykonaniu tego wyrażenia wypadasz z gry.
Jednak w praktyce odwołania o wartości null można łatwo utworzyć na podstawie wskaźników o wartości null i nie zauważysz tego, dopóki faktycznie nie spróbujesz uzyskać dostępu do wartości za odwołaniem zerowym. Twój przykład może być nieco zbyt prosty, ponieważ każdy dobry optymalizujący kompilator zobaczy niezdefiniowane zachowanie i po prostu zoptymalizuje wszystko, co od niego zależy (odniesienie zerowe nawet nie zostanie utworzone, zostanie zoptymalizowane).
Jednak ta optymalizacja zależy od kompilatora, aby udowodnić niezdefiniowane zachowanie, co może nie być możliwe. Rozważ tę prostą funkcję w pliku converter.cpp
:
int& toReference(int* pointer) {
return *pointer;
}
Kiedy kompilator widzi tę funkcję, nie wie, czy wskaźnik jest wskaźnikiem zerowym, czy nie. Więc po prostu generuje kod, który zamienia dowolny wskaźnik w odpowiednie odniesienie. (Btw: to jest noop, ponieważ wskaźniki i referencje są dokładnie tą samą bestią w asemblerze.) Teraz, jeśli masz inny plik user.cpp
z kodem
#include "converter.h"
void foo() {
int& nullRef = toReference(nullptr);
cout << nullRef; //crash happens here
}
kompilator nie wie, że toReference()
wyłuskuje przekazany wskaźnik i zakłada, że zwraca prawidłowe odwołanie, które w praktyce będzie odwołaniem zerowym. Wywołanie kończy się powodzeniem, ale gdy próbujesz użyć odwołania, program ulega awarii. Ufnie. Standard pozwala na wszystko, w tym pojawienie się różowych słoni.
Możesz zapytać, dlaczego jest to istotne, w końcu niezdefiniowane zachowanie zostało już wywołane w środku toReference()
. Odpowiedź brzmi: debugowanie: zerowe odwołania mogą się propagować i mnożyć, tak jak robią to puste wskaźniki. Jeśli nie jesteś świadomy tego, że mogą istnieć odwołania zerowe i nauczysz się ich unikać, możesz spędzić trochę czasu próbując dowiedzieć się, dlaczego funkcja członkowska wydaje się zawieszać, gdy tylko próbuje odczytać zwykły stary int
element członkowski (odpowiedź: instancja w wywołaniu członka była odwołaniem o wartości null, więc this
jest to wskaźnik zerowy, a Twój element członkowski jest obliczany jako adres 8).
A co powiesz na sprawdzenie zerowych odwołań? Podałeś linię
if( & nullReference == 0 ) // null reference
w twoim pytaniu. Cóż, to nie zadziała: zgodnie ze standardem, masz niezdefiniowane zachowanie, jeśli wyłuskujesz pusty wskaźnik i nie możesz utworzyć zerowego odniesienia bez wyłuskiwania pustego wskaźnika, więc zerowe odwołania istnieją tylko w sferze niezdefiniowanego zachowania. Ponieważ Twój kompilator może założyć, że nie wyzwalasz niezdefiniowanego zachowania, może założyć, że nie ma czegoś takiego jak odwołanie o wartości null (nawet jeśli z łatwością wyemituje kod, który generuje odwołania o wartości null!). W związku z tym widzi if()
warunek, stwierdza, że nie może być prawdziwy, i po prostu odrzuca całe if()
stwierdzenie. Wraz z wprowadzeniem optymalizacji czasu łącza sprawdzenie zerowych odwołań w solidny sposób stało się po prostu niemożliwe.
TL; DR:
Puste odniesienia są trochę upiorne:
Ich istnienie wydaje się niemożliwe (= standardowo),
ale istnieją (= przez wygenerowany kod maszynowy),
ale nie możesz ich zobaczyć, jeśli istnieją (= Twoje próby zostaną zoptymalizowane),
ale i tak mogą cię zabić nieświadomego (= Twój program ulega awarii w dziwnych momentach lub gorzej).
Twoją jedyną nadzieją jest to, że one nie istnieją (= napisz swój program, aby ich nie tworzyć).
Mam nadzieję, że nie będzie cię to prześladować!