Dlaczego adres zero jest używany jako wskaźnik zerowy?


121

W C (lub C ++ w tym przypadku) wskaźniki są szczególne, jeśli mają wartość zero: radzę ustawić wskaźniki na zero po zwolnieniu ich pamięci, ponieważ oznacza to, że ponowne zwolnienie wskaźnika nie jest niebezpieczne; kiedy wywołuję malloc, zwraca wskaźnik o wartości zero, jeśli nie może uzyskać pamięci; if (p != 0)Cały czas używam, aby upewnić się, że przekazane wskaźniki są prawidłowe itp.

Ale skoro adresowanie pamięci zaczyna się od 0, czy 0 nie jest tak samo poprawnym adresem jak każdy inny? W jaki sposób można użyć 0 do obsługi wskaźników zerowych, jeśli tak jest? Dlaczego zamiast tego liczba ujemna nie jest równa null?


Edytować:

Kilka dobrych odpowiedzi. Podsumuję to, co zostało powiedziane w odpowiedziach wyrażonych w moim własnym umyśle i mam nadzieję, że społeczność mnie poprawi, jeśli źle zrozumiem.

  • Jak wszystko inne w programowaniu, jest to abstrakcja. Tylko stała, niezupełnie związana z adresem 0. C ++ 0x podkreśla to poprzez dodanie słowa kluczowego nullptr.

  • To nawet nie jest abstrakcja adresu, jest to stała określona przez standard C i kompilator może przetłumaczyć ją na inną liczbę, o ile upewni się, że nigdy nie jest równa "prawdziwemu" adresowi i równa się innym zerowym wskaźnikom, jeśli 0 nie jest najlepsza wartość do wykorzystania dla platformy.

  • W przypadku, gdy nie jest to abstrakcja, co miało miejsce we wczesnych dniach, adres 0 jest używany przez system i nie jest ograniczony dla programisty.

  • Przyznaję, że moja sugestia dotycząca liczby ujemnej była trochę szalona. Używanie liczby całkowitej ze znakiem dla adresów jest trochę marnotrawne, jeśli oznacza to, że oprócz wskaźnika zerowego (-1 lub cokolwiek innego) przestrzeń wartości jest dzielona równo między dodatnimi liczbami całkowitymi, które tworzą prawidłowe adresy, i liczbami ujemnymi, które są po prostu marnowane.

  • Jeśli jakakolwiek liczba jest zawsze reprezentowana przez typ danych, to jest to 0. (Prawdopodobnie 1 to także. Myślę o jednobitowej liczbie całkowitej, która będzie równa 0 lub 1, jeśli nie ma znaku, lub tylko o bicie ze znakiem, jeśli byłoby [-2, 1]. Ale wtedy możesz po prostu ustawić 0 jako null, a 1 to jedyny dostępny bajt w pamięci).

Wciąż mam coś nierozwiązanego. Wskaźnik przepełnienia stosu do określonego stałego adresu mówi mi, że nawet jeśli 0 dla wskaźnika pustego jest abstrakcją, inne wartości wskaźnika niekoniecznie. To prowadzi mnie do opublikowania kolejnego pytania o przepełnienie stosu. Czy kiedykolwiek będę chciał uzyskać dostęp do adresu zero? .


11
Równie łatwo możesz zmienić if (p != 0)na if (p)który jest powszechnym idiomem w C i C ++, chociaż będziesz musiał pozbyć się tego nawyku, jeśli zdecydujesz się na Javę.
Paul Tomblin

14
Usunięcie czegoś dwa razy oznacza, że ​​kod jest nieprawidłowy. Odradzam ustawianie wskaźników na null po, abyś mógł się zawiesić i naprawić problem, a nie go tłumić. W każdym razie popełniasz błąd, zakładając, że adres jest jakąś liczbą całkowitą. Niekoniecznie jest to prawda, a 0 reprezentuje jedynie jakąś rzeczywistą wartość wskaźnika, która jest specyficzna dla implementacji. „Adres ujemny” nie ma sensu koncepcyjnego.
GManNickG

6
@GMan: Może nawet dobrym pomysłem jest ustawienie wskaźnika na adres, który wymusi awarię, taki jak 0xDEADBEEF.
Billy ONeal

5
Pytanie, które nigdy nie umrze!

8
@Noah: Punkt jest ustawiony na null -> ukryj błędy programowania, nie ustaw na null -> znajdź błędy programowania. Nie wiem jak Ty, ale chciałbym, żeby mój kod był poprawny.
GManNickG

Odpowiedzi:


65

2 punkty:

  • tylko stała wartość 0 w kodzie źródłowym jest wskaźnikiem zerowym - implementacja kompilatora może użyć dowolnej wartości, której chce lub potrzebuje w działającym kodzie. Niektóre platformy mają specjalną wartość wskaźnika, która jest „nieprawidłowa”, której implementacja może użyć jako wskaźnika pustego. W C FAQ pojawia się pytanie: „Poważnie, czy jakieś rzeczywiste maszyny rzeczywiście używały niezerowych wskaźników zerowych lub różnych reprezentacji wskaźników do różnych typów?” , co wskazuje na kilka platform, które używały tej właściwości 0 będącej pustym wskaźnikiem w źródle C, ale reprezentowanej inaczej w czasie wykonywania. Standard C ++ ma uwagę, która wyjaśnia, że ​​konwersja „integralnego wyrażenia stałego o wartości zero zawsze daje wskaźnik zerowy,

  • wartość ujemna mogłaby być tak samo użyteczna dla platformy jak adres - standard C musiał po prostu wybrać coś, co będzie wskazywało na pusty wskaźnik, i wybrano zero. Naprawdę nie jestem pewien, czy wzięto pod uwagę inne wartości wartownicze.

Jedyne wymagania dotyczące wskaźnika pustego to:

  • gwarantuje porównanie nierówności ze wskaźnikiem do rzeczywistego obiektu
  • dowolne dwa zerowe wskaźniki porównują równe (C ++ udoskonala to tak, że musi to zachować tylko dla wskaźników tego samego typu)

12
+1 Podejrzewam, że 0 zostało wybrane jedynie z powodów historycznych. (Przez większość czasu 0 to adres początkowy i nieprawidłowy). Oczywiście takie założenie nie zawsze jest prawdziwe, ale 0 działa całkiem nieźle.
GManNickG

8
Przestrzeń również mogła mieć wpływ. W czasach, gdy po raz pierwszy opracowano C, pamięć była DUŻO droższa niż obecnie. Liczbę zero można wygodnie obliczyć za pomocą instrukcji XOR lub bez konieczności ładowania natychmiastowej wartości. W zależności od architektury może to potencjalnie zaoszczędzić miejsce.
Sparky

6
@GMan - Masz rację. We wczesnych procesorach adres pamięci zero był specjalny i miał sprzętową ochronę przed dostępem z uruchomionego oprogramowania (w niektórych przypadkach był to początek wektora resetowania, a jego modyfikacja mogła uniemożliwić resetowanie lub uruchomienie procesora). Programiści wykorzystali tę ochronę sprzętową jako formę wykrywania błędów w swoim oprogramowaniu, pozwalając logice dekodowania adresu procesora na sprawdzanie niezainicjowanych lub nieprawidłowych wskaźników, zamiast wydawania na to instrukcji procesora. Konwencja pozostaje do dziś, mimo że cel adresu zero mógł się zmienić.
bta

10
16-bitowy kompilator Minix używał 0xFFFF dla NULL.
Joshua

3
W wielu systemach wbudowanych prawidłowym adresem jest 0. Wartość -1 (wszystkie bity jeden) jest również prawidłowym adresem. Sumy kontrolne dla ROM-ów są trudne do obliczenia, gdy dane zaczynają się od adresu 0. :-(
Thomas Matthews

31

Historycznie przestrzeń adresowa zaczynająca się od 0 była zawsze ROM, używana w niektórych systemach operacyjnych lub procedurach obsługi przerwań niskiego poziomu, obecnie, ponieważ wszystko jest wirtualne (w tym przestrzeń adresowa), system operacyjny może mapować dowolną alokację na dowolny adres, więc specjalnie NIE przydzielaj niczego pod adresem 0.


6
To wszystko. Jest to zgodne z konwencją historyczną, a pierwsze adresy były używane do obsługi przerwań, dlatego są bezużyteczne dla normalnych programów. Ponadto 0 jest „puste”, co może być interpretowane jako brak wartości / brak wskaźnika.
TomTom

15

IIRC, wartość „wskaźnika zerowego” nie jest gwarantowana jako zero. Kompilator tłumaczy 0 na dowolną wartość „zerową” odpowiednią dla systemu (która w praktyce jest prawdopodobnie zawsze równa zero, ale niekoniecznie). To samo tłumaczenie jest stosowane zawsze, gdy porównujesz wskaźnik z zerem. Ponieważ możesz porównywać wskaźniki tylko ze sobą i z tą specjalną wartością-0, to izoluje to programistę od wiedzy o reprezentacji pamięci systemu. Co do tego, dlaczego wybrali 0 zamiast 42 lub trochę więcej, zgadnę, że to dlatego, że większość programistów zaczyna liczyć od 0 :) (Ponadto w większości systemów 0 to pierwszy adres pamięci i chcieli, aby był wygodny, ponieważ w ćwicz tłumaczenia, które opisuję, rzadko mają miejsce; język po prostu na to pozwala).


5
@Justin: Źle zrozumiałeś. Stała 0 jest zawsze wskaźnikiem zerowym. @Meador mówi, że możliwe jest, że wskaźnik zerowy (wskazywany przez stałą 0) nie odpowiada adresowi zero. Na niektórych platformach utworzenie wskaźnika pustego ( int* p = 0) może spowodować utworzenie wskaźnika zawierającego wartość 0xdeadbeeflub dowolną inną preferowaną wartość. 0 jest wskaźnikiem zerowym, ale wskaźnik zerowy niekoniecznie jest wskaźnikiem do adresu zero. :)
jalf

Wskaźnik NULL jest zarezerwowaną wartością iw zależności od kompilatora może być dowolnym wzorcem bitowym. Wskaźnik NULL nie oznacza, że ​​wskazuje na adres 0.
Sharjeel Aziz

3
Ale @Jalf, stała 0 nie zawsze jest wskaźnikiem zerowym. To właśnie piszemy, gdy chcemy, aby kompilator wypełnił za nas rzeczywisty pusty wskaźnik platformy . Praktycznie rzecz biorąc, wskaźnik zerowy zazwyczaj nie odpowiadają na adres zera, choć i ja interpretować pytanie Joela jak pytając dlaczego tak jest. Podobno w końcu pod tym adresem znajduje się ważny bajt pamięci, więc dlaczego nie użyć nieistniejącego adresu nieistniejącego bajtu zamiast usunąć ważny bajt z gry? (Piszę to, co wyobrażam sobie Joel, a nie pytanie, które sobie zadaję.)
Rob Kennedy

@Rob: Tak jakby. Wiem, co masz na myśli, i masz rację, ale ja też. Stała liczba całkowita 0 reprezentuje wskaźnik zerowy na poziomie kodu źródłowego. Porównywanie pustego wskaźnika do 0 daje wartość true. Przypisanie 0 do wskaźnika ustawia ten wskaźnik na null. 0 to wskaźnik zerowy. Ale rzeczywista reprezentacja pustego wskaźnika w pamięci może różnić się od zerowego wzorca bitowego. (W każdym razie, mój komentarz był odpowiedzią na usunięty komentarz @ Justina, a nie na pytanie @ Joela. :)
jalf

@jalf @Rob Myślę, że potrzebujesz kilku terminów do wyjaśnienia. :) Od §4.10 / 1: „Stała wskaźnika o wartości zerowej jest integralnym wyrażeniem stałym rvalue typu całkowitego, którego wynikiem jest zero. Stała wskaźnika o wartości zerowej może zostać przekonwertowana na typ wskaźnika; wynikiem jest wartość wskaźnika zerowego tego typu i można odróżnić od każdej innej wartości wskaźnika do obiektu lub wskaźnika do typu funkcji. "
GManNickG

15

Musisz źle rozumieć znaczenie stałego zera w kontekście wskaźnika.

Ani w C, ani w C ++ wskaźniki nie mogą mieć „wartości zero”. Wskaźniki nie są obiektami arytmetycznymi. Nie mogą mieć wartości liczbowych, takich jak „zero”, „ujemny” ani niczego podobnego. Więc twoje stwierdzenie o „wskaźnikach… mają wartość zero” po prostu nie ma sensu.

W C i C ++ wskaźniki mogą mieć zarezerwowaną wartość wskaźnika zerowego . Rzeczywista reprezentacja wartości wskaźnika null nie ma nic wspólnego z żadnymi „zerami”. Może to być absolutnie wszystko, co jest odpowiednie dla danej platformy. Prawdą jest, że na większości plaform wartość wskaźnika null jest fizycznie reprezentowana przez rzeczywistą zerową wartość adresu. Jeśli jednak na niektórych platformach adres 0 jest rzeczywiście używany do jakiegoś celu (np. Może być konieczne utworzenie obiektów pod adresem 0), wartość wskaźnika zerowego na takiej platformie najprawdopodobniej będzie inna. Może być na przykład fizycznie reprezentowany jako 0xFFFFFFFFwartość adresu lub jako 0xBAADBAADwartość adresu.

Niemniej jednak, niezależnie od tego, jak wartość wskaźnika null jest reprezentowana na danej platformie, w swoim kodzie nadal będziesz oznaczać wskaźniki null za pomocą stałej 0. Aby przypisać wartość pustego wskaźnika do danego wskaźnika, będziesz nadal używać wyrażeń takich jak p = 0. Obowiązkiem kompilatora jest uświadomienie sobie, czego chcesz i przetłumaczenie tego na odpowiednią reprezentację wartości wskaźnika zerowego, tj. Przetłumaczenie tego na kod, który umieści wartość adresu 0xFFFFFFFFwe wskaźniku p, na przykład.

Krótko mówiąc, fakt, że używasz 0w swoim kodzie sorce do generowania wartości wskaźnika null nie oznacza, że ​​wartość wskaźnika null jest w jakiś sposób powiązana z adresem 0. To 0, czego używasz w swoim kodzie źródłowym, jest po prostu „cukrem składniowym”, który nie ma absolutnie żadnego związku z rzeczywistym adresem fizycznym, na który wskazuje wartość wskaźnika zerowego.


3
<quote> Wskaźniki nie są obiektami arytmetycznymi </quote> Arytmetyka wskaźników jest dość dobrze zdefiniowana w C i C ++. Częścią wymagań jest to, aby oba wskaźniki wskazywały na ten sam kompozyt. Wskaźnik zerowy nie wskazuje na żadną złożoną, więc używanie go w wyrażeniach arytmetycznych wskaźnika jest niedozwolone. Na przykład nie ma takiej gwarancji (p1 - nullptr) - (p2 - nullptr) == (p1 - p2).
Ben Voigt

5
@Ben Voigt: Specyfikacja języka definiuje pojęcie typu arytmetycznego . Mówię tylko, że typy wskaźnikowe nie należą do kategorii typów arytmetycznych. Arytmetyka wskaźnikowa to inna i zupełnie niepowiązana historia, zwykły zbieg okoliczności językowy.
AnT

1
Jak ktoś, kto czyta obiekty arytmetyczne, ma wiedzieć, że oznacza to „w sensie typów arytmetycznych”, a nie „w sensie operatorów arytmetycznych” (z których kilka jest użytecznych na wskaźnikach) lub „w sensie arytmetyki wskaźnikowej”. Jeśli chodzi o zbieżności językowe, obiekt arytmetyczny ma więcej wspólnych liter z arytmetyką wskaźnikową niż typy arytmetyczne . Jednocześnie standard mówi o wartości wskaźnika . Oryginalny plakat prawdopodobnie oznaczał całkowitą reprezentację wskaźnika, a nie wartość wskaźnika , i NULLwyraźnie nie musi być reprezentowany przez 0.
Ben Voigt

Na przykład termin obiekty skalarne w terminologii C / C ++ jest tylko skrótem dla obiektów typów skalarnych (tak jak obiekty POD = obiekty typów POD ). Dokładnie w ten sam sposób użyłem terminu obiekty arytmetyczne , czyli obiekty typu arytmetycznego . Oczekuję, że „ktoś” to zrozumie w ten sposób. Ktoś, kto nie może, zawsze może poprosić o wyjaśnienie.
AnT

1
Pracowałem na systemie, w którym (jeśli chodzi o sprzęt) null to 0xffffffff, a 0 to całkowicie poprawny adres
pm100

8

Ale skoro adresowanie pamięci zaczyna się od 0, czy 0 nie jest tak samo poprawnym adresem jak każdy inny?

W niektórych / wielu / wszystkich systemach operacyjnych adres pamięci 0 jest w pewien sposób szczególny. Na przykład często jest mapowany na nieprawidłową / nieistniejącą pamięć, co powoduje wyjątek, jeśli próbujesz uzyskać do niej dostęp.

Dlaczego zamiast tego liczba ujemna nie jest równa null?

Myślę, że wartości wskaźnika są zwykle traktowane jako liczby bez znaku: w przeciwnym razie na przykład 32-bitowy wskaźnik byłby w stanie zaadresować tylko 2 GB pamięci zamiast 4 GB.


4
Zakodowałem na urządzeniu, w którym adres zero był prawidłowym adresem i nie było ochrony pamięci. Wskaźniki zerowe również były zerami; jeśli przypadkowo napisałeś do pustego wskaźnika, to przeleciałeś nad ustawieniami systemu operacyjnego, które były pod adresem zerowym; wesołość zwykle nie następowała.
MM

1
Tak: na przykład na niezabezpieczonym trybie procesora x86 adres 0 jest tablicą wektorów przerwań .
ChrisW

@ChrisW: W niechronionym trybie x86 adres zero w szczególności jest wektorem przerwania dzielenia przez zero, którego niektóre programy mogą mieć całkowicie uzasadnione powody do pisania.
supercat

Nawet na platformach, na których użyteczna pamięć zaczynałaby się od adresu fizycznego, zero, implementacja C mogłaby z łatwością użyć adresu zero do przechowywania obiektu, którego adres nigdy nie jest zajęty, albo po prostu pozostawić pierwsze słowo pamięci niewykorzystane. Na większości platform funkcja „porównaj z zerem” zapisuje instrukcję, a nie „porównaj z czymkolwiek”, więc nawet zmarnowanie pierwszego słowa w pamięci byłoby tańsze niż użycie niezerowego adresu dla wartości null. Zwróć uwagę, że nie ma wymogu, aby adresy rzeczy nieobjętych standardem C (np. Porty I / O lub wektory przerwań) były równe zeru, ani że ...
supercat

... wskaźnik zerowy procesu systemowego uzyskuje dostęp w inny sposób niż każdy inny, więc wszystkie bity-zero są ogólnie dobrym adresem dla „null”, nawet w systemach, w których dostęp do zerowej lokalizacji fizycznej byłby użyteczny i sensowny.
supercat

5

Domyślam się, że magiczna wartość 0 została wybrana do zdefiniowania nieprawidłowego wskaźnika, ponieważ można go przetestować przy użyciu mniejszej liczby instrukcji. Niektóre języki maszynowe automatycznie ustawiają flagi zera i znaku zgodnie z danymi podczas ładowania rejestrów, dzięki czemu można przetestować wskaźnik zerowy z prostym ładowaniem i rozgałęzieniem instrukcji bez wykonywania oddzielnych instrukcji porównania.

(Większość ISA ustawia flagi tylko dla instrukcji ALU, a nie ładuje. I zazwyczaj nie tworzysz wskaźników przez obliczenia, z wyjątkiem kompilatora podczas analizowania źródła C. Ale przynajmniej nie potrzebujesz dowolnej stałej szerokości wskaźnika do porównać z.)

Na Commodore Pet, Vic20 i C64, które były pierwszymi maszynami, nad którymi pracowałem, pamięć RAM zaczynała się w miejscu 0, więc można było czytać i pisać przy użyciu wskaźnika zerowego, jeśli naprawdę chcesz.


3

Myślę, że to tylko konwencja. Musi istnieć jakaś wartość, aby oznaczyć nieprawidłowy wskaźnik.

Po prostu tracisz jeden bajt przestrzeni adresowej, co rzadko powinno stanowić problem.

Nie ma negatywnych wskaźników. Wskaźniki są zawsze bez znaku. Również jeśli mogłyby być ujemne, twoja konwencja oznaczałaby utratę połowy przestrzeni adresowej.


Uwaga: w rzeczywistości nie tracisz przestrzeni adresowej; można uzyskać wskaźnik do adresu 0 wykonując: char *p = (char *)1; --p;. Ponieważ zachowanie wskaźnika zerowego jest nieokreślone przez standard, ten system może w przeczywistości odczytywać i zapisywać adres 0, inkrementować w celu podania adresu 1itp.
MM

@MattMcNabb: implementacja, w której adres zero jest prawidłowym adresem sprzętowym, może w pełni zasadnie definiować zachowanie, char x = ((char*)0);aby odczytać adres zero i zapisać tę wartość w x. Taki kod dałby Undefined Behavior każdej implementacji, która nie definiuje jej zachowania, ale fakt, że standard mówi, że coś jest niezdefiniowanym zachowaniem, w żaden sposób nie zabrania implementacjom oferowania własnych specyfikacji tego, co zrobi.
supercat

@supercat ITYM *(char *)0. To prawda, ale moim zdaniem implementacja nie musi definiować zachowania *(char *)0ani żadnych innych operacji wskaźnika zerowego.
MM

1
@MattMcNabb: zachowanie char *p = (char*)1; --p;byłoby zdefiniowane przez standard tylko wtedy, gdyby ta sekwencja została wykonana po tym, jak wskaźnik na coś innego niż pierwszy bajt obiektu zostałby rzutowany na obiekt an intptr_t, a wynik tego rzutowania miałby wartość 1 iw tym konkretnym przypadku wynik of --pdałby wskaźnik do bajtu poprzedzającego bajt, którego wartość wskaźnika po rzutowaniu na intptr_tniego dała 1.
supercat

3

Chociaż C używa 0 do reprezentowania wskaźnika zerowego, należy pamiętać, że wartość samego wskaźnika może nie być zerowa. Jednak większość programistów będzie używać tylko systemów, w których wskaźnik zerowy wynosi w rzeczywistości 0.

Ale dlaczego zero? Cóż, to jeden adres wspólny dla każdego systemu. Często niskie adresy są zarezerwowane dla celów systemu operacyjnego, dlatego wartość działa również jako niedostępna dla programów użytkowych. Przypadkowe przypisanie wartości całkowitej do wskaźnika może skończyć się zerem tak samo, jak cokolwiek innego.


3
Bardziej prawdopodobnym powodem tego wszystkiego jest to, że: tanie jest rozdawać pamięć wstępnie zainicjowaną do zera i wygodne, aby wartości w tej pamięci reprezentowały coś znaczącego, jak liczba całkowita 0, zmiennoprzecinkowa 0,0 i wskaźniki zerowe. Dane statyczne w C, które są inicjowane na zero / null, nie muszą zajmować żadnej przestrzeni w pliku wykonywalnym i są mapowane do bloku wypełnionego zerami po załadowaniu. Zero może być również traktowane w specjalny sposób w językach maszynowych: łatwe porównania zera, takie jak "rozgałęzienie, jeśli równe zero", itp. MIPS ma nawet fikcyjny rejestr, który jest po prostu zerową stałą.
Kaz

2

W przeszłości mała ilość pamięci aplikacji była zajęta przez zasoby systemowe. W tamtych czasach zero stało się domyślną wartością zerową.

Chociaż niekoniecznie jest to prawdą w przypadku nowoczesnych systemów, nadal złym pomysłem jest ustawianie wartości wskaźnika na cokolwiek innego niż to, co zapewniła alokacja pamięci.


2

Odnośnie argumentu dotyczącego nie ustawiania wskaźnika na wartość null po jego usunięciu, aby w przyszłości usuwać „ujawniać błędy” ...

Jeśli naprawdę się tym martwisz, lepszym podejściem, które na pewno zadziała, jest wykorzystanie assert ():


...
assert(ptr && "You're deleting this pointer twice, look for a bug?");
delete ptr;
ptr = 0;
...

Wymaga to dodatkowego wpisania i jednego dodatkowego sprawdzenia podczas kompilacji debugowania, ale z pewnością da ci to, czego chcesz: zauważ, że ptr jest usuwany „dwukrotnie”. Alternatywa podana w dyskusji o komentarzach, a nie ustawianie wskaźnika na wartość null, aby uzyskać awarię, po prostu nie gwarantuje sukcesu. Co gorsza, w przeciwieństwie do powyższego, może spowodować awarię (lub znacznie gorzej!) Użytkownika, jeśli jeden z tych „błędów” dostanie się na półkę. Wreszcie, ta wersja umożliwia dalsze uruchamianie programu, aby zobaczyć, co się naprawdę dzieje.

Zdaję sobie sprawę, że to nie odpowiada na zadane pytanie, ale martwiłem się, że ktoś czytający komentarze może dojść do wniosku, że za `` dobrą praktykę '' NIE należy ustawiać wskaźników na 0, jeśli jest możliwe, że zostaną wysłane na bezpłatne () lub usuń dwukrotnie. W tych kilku przypadkach, gdy jest to możliwe, NIGDY nie jest dobrą praktyką używanie niezdefiniowanego zachowania jako narzędzia do debugowania. Nikt, kto nigdy nie musiał szukać błędu, który został ostatecznie spowodowany usunięciem nieprawidłowego wskaźnika, nie zaproponowałby tego. Wytropienie tego rodzaju błędów zajmuje wiele godzin i prawie zawsze wpływa na program w całkowicie nieoczekiwany sposób, który jest trudny lub niemożliwy do wyśledzenia pierwotnego problemu.


2

Ważnym powodem, dla którego wiele systemów operacyjnych wykorzystuje wszystkie bity-zero do reprezentacji wskaźnika zerowego, jest to, że oznacza to memset(struct_with_pointers, 0, sizeof struct_with_pointers)i podobne, ustawi wszystkie wskaźniki wewnątrz struct_with_pointersna wskaźniki zerowe. Standard C nie gwarantuje tego, ale wiele, wiele programów tak zakłada.


1

Na jednej ze starych maszyn DEC (myślę, że PDP-8), środowisko wykonawcze C chroniłoby pamięć pierwszej strony pamięci, tak aby każda próba uzyskania dostępu do pamięci w tym bloku spowodowałaby zgłoszenie wyjątku.


PDP-8 nie miał kompilatora C. PDP-11 nie miał ochrony pamięci, a VAX był niesławny z powodu cichego zwracania 0 do NULL dereferencji wskaźnika. Nie jestem pewien, której maszyny to dotyczy.
fuz

1

Wybór wartości wartowniczej jest arbitralny i faktycznie jest on adresowany przez następną wersję C ++ (nieformalnie znaną jako „C ++ 0x”, najprawdopodobniej w przyszłości znaną jako ISO C ++ 2011) wraz z wprowadzeniem słowo kluczowe nullptrreprezentujące wskaźnik o wartości null. W C ++ wartość 0 może być użyta jako wyrażenie inicjalizujące dla dowolnego POD i dowolnego obiektu z domyślnym konstruktorem i ma specjalne znaczenie przypisywania wartości wartowniczej w przypadku inicjalizacji wskaźnika. Jeśli chodzi o to, dlaczego nie wybrano wartości ujemnej, adresy zwykle mieszczą się w zakresie od 0 do 2 N.-1 dla pewnej wartości N. Innymi słowy, adresy są zwykle traktowane jako wartości bez znaku. Gdyby wartość maksymalna została użyta jako wartość wartownicza, musiałaby zmieniać się w zależności od systemu w zależności od rozmiaru pamięci, podczas gdy 0 jest zawsze reprezentowalnym adresem. Jest również używany ze względów historycznych, ponieważ adres pamięci 0 był zwykle bezużyteczny w programach, a obecnie większość systemów operacyjnych ma części jądra załadowane na dolne strony pamięci, a takie strony są zwykle chronione w taki sposób, że jeśli dotknięty (usunięty) przez program (zapisz jądro) spowoduje błąd.


1

Musi mieć jakąś wartość. Oczywiście nie chcesz wkraczać na wartości, których użytkownik mógłby zasadnie chcieć użyć. Spekuluję, że skoro środowisko wykonawcze C zapewnia segment BSS dla danych zainicjowanych zerem, w pewnym stopniu sensowne jest interpretowanie zera jako niezainicjowanej wartości wskaźnika.


0

Rzadko gdy system operacyjny pozwala na pisanie pod adresem 0. Często zdarza się, że pliki specyficzne dla systemu operacyjnego są zapisywane w małej ilości pamięci; mianowicie IDT, tabele stron itp. (Tabele muszą być w pamięci RAM i łatwiej jest je umieścić na dole, niż próbować określić, gdzie jest górna część pamięci RAM). I żaden system operacyjny przy zdrowych zmysłach nie pozwoli ci edytuj tabele systemowe chcąc nie chcąc.

Być może K & R nie myślał o tym, kiedy tworzyli C, ale (wraz z faktem, że 0 == null jest dość łatwe do zapamiętania) sprawia, że ​​0 jest popularnym wyborem.


Nie jest to prawdą w trybie chronionym i faktycznie w niektórych konfiguracjach Linuksa możesz pisać na adres wirtualny 0.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

0

Wartość 0jest specjalną wartością, która w określonych wyrażeniach przybiera różne znaczenia. W przypadku wskaźników, jak wielokrotnie wskazywano, używa się go zapewne dlatego, że w tamtych czasach był to najwygodniejszy sposób powiedzenia „wstaw tutaj domyślną wartość wartowniczą”. Jako wyrażenie stałe nie ma tego samego znaczenia co bitowe zero (tj. Wszystkie bity ustawione na zero) w kontekście wyrażenia wskaźnikowego. W C ++ istnieje kilka typów, które nie mają bitowej reprezentacji zerowej, NULLtakich jak element członkowski wskaźnika i wskaźnik do funkcji elementu członkowskiego.

Na szczęście, C ++ 0x ma nowego słowa kluczowego dla „wyrażenia, które oznacza znaną nieprawidłowy wskaźnik, który nie jest również mapować do bitowym zera do wyrażenia integralnych”: nullptr. Chociaż istnieje kilka systemów, na które można kierować za pomocą C ++, które pozwalają na wyłuskiwanie adresu 0 bez barfingu, należy więc uważać.


0

W tym wątku jest już wiele dobrych odpowiedzi; prawdopodobnie istnieje wiele różnych powodów preferowania wartości 0wskaźników zerowych, ale dodam jeszcze dwa:

  • W C ++ inicjalizacja wskaźnika zerowego ustawi go na null.
  • W przypadku wielu procesorów bardziej wydajne jest ustawienie wartości na 0 lub sprawdzenie, czy jest równa / nie równa 0, niż dla dowolnej innej stałej.

0

Zależy to od implementacji wskaźników w C / C ++. Nie ma konkretnego powodu, dla którego NULL jest równoważne w przypisaniach do wskaźnika.


-1

Istnieją historyczne powody, ale są też powody optymalizacji.

System operacyjny często udostępnia proces ze stronami pamięci zainicjowanymi na 0. Jeśli program chce zinterpretować część tej strony pamięci jako wskaźnik, to jest to 0, więc program może łatwo określić, że ten wskaźnik jest nie zainicjowano. (nie działa to tak dobrze, gdy jest stosowane do niezainicjowanych stron Flash)

Innym powodem jest to, że na wielu, wielu procesorach bardzo łatwo jest przetestować równoważność wartości do 0. Czasami jest to bezpłatne porównanie wykonane bez dodatkowych instrukcji i zwykle można to zrobić bez konieczności podawania wartości zerowej w innym rejestrze lub jako literał w strumieniu instrukcji do porównania.

Tanie porównania dla większości procesorów są ze znakiem mniejszym niż 0 i równym 0 (ze znakiem większym niż 0 i różnym od 0 są implikowane przez oba te sposoby)

Ponieważ 1 wartość ze wszystkich możliwych wartości musi być zarezerwowana jako zła lub niezainicjowana, równie dobrze możesz uczynić ją tą, która ma najtańszy test równoważności ze złą wartością. Dotyczy to również ciągów znaków zakończonych '\ 0'.

Gdybyś spróbował użyć do tego celu więcej lub mniej niż 0, skończyłoby się na przecięciu zakresu adresów o połowę.


-2

Stała 0jest używany zamiast NULLponieważ C została wykonana przez niektórych jaskiniowców bilionów lat temu NULL, NIL, ZIP, lub NADDAmiałby wszystko miało znacznie więcej sensu niż 0.

Ale skoro adresowanie pamięci zaczyna się od 0, czy 0 nie jest tak samo poprawnym adresem jak każdy inny?

W rzeczy samej. Chociaż wiele systemów operacyjnych nie zezwala na mapowanie czegokolwiek pod adresem zero, nawet w wirtualnej przestrzeni adresowej (ludzie zdali sobie sprawę, że C jest niezabezpieczonym językiem i odzwierciedlając to, że błędy wyłuskiwania zerowego wskaźnika są bardzo częste, zdecydowano się je „naprawić”, odrzucając kod przestrzeni użytkownika do mapowania na stronę 0; Tak więc, jeśli wywołasz wywołanie zwrotne, ale wskaźnik wywołania zwrotnego ma wartość NULL, nie wykonasz dowolnego kodu).

W jaki sposób można użyć 0 do obsługi wskaźników zerowych, jeśli tak jest?

Ponieważ 0używany w porównaniu ze wskaźnikiem zostanie zastąpiony pewną wartością specyficzną dla implementacji , która jest wartością zwracaną przez malloc w przypadku awarii malloc.

Dlaczego zamiast tego liczba ujemna nie jest równa null?

Byłoby to jeszcze bardziej zagmatwane.


Twój punkt widzenia na temat „jaskiniowców” itp. Prawdopodobnie leży u podstaw tego, chociaż myślę, że szczegóły są inne. Najwcześniejsze formy tego, co wyewoluowało w C, zostały zaprojektowane do działania w jednej konkretnej architekturze, w której wskaźnik intbył nie tylko tego samego rozmiaru co wskaźnik - w wielu kontekstach wskaźniki inti wskaźniki mogły być używane zamiennie. Jeśli procedura oczekiwałaby wskaźnika i została przekazana jako liczba całkowita 57, procedura użyłaby adresu z tym samym wzorem bitowym co liczba 57. Na tych konkretnych maszynach wzorzec bitowy oznaczający wskaźnik zerowy wynosił 0, więc przekazywanie wartości int 0 przekazałby pusty wskaźnik.
supercat

Od tego czasu C ewoluowało, dzięki czemu można go używać do pisania programów dla wielu innych maszyn z różnymi reprezentacjami liczb i wskaźników. Podczas gdy niezerowe stałe numeryczne były rzadko używane jako wskaźniki, stałe zera numeryczne były szeroko stosowane do reprezentowania wskaźników zerowych. Zablokowanie takiego użycia spowodowałoby uszkodzenie istniejącego kodu, więc od kompilatorów oczekiwano, że przetłumaczy numeryczne zero na cokolwiek, czego implementacja używa do reprezentowania wskaźnika zerowego.
supercat

-4

( Przeczytaj ten akapit przed przeczytaniem postu.Wszystkich zainteresowanych przeczytaniem tego posta proszę o uważne przeczytanie i oczywiście nie neguj go, dopóki nie zrozumiesz go całkowicie, dzięki. )

Jest to teraz wiki społeczności, jako taka, jeśli ktoś nie zgadza się z którymkolwiek z pojęć, należy go zmodyfikować, podając jasne i szczegółowe wyjaśnienie, co jest nie tak i dlaczego, a jeśli to możliwe, zacytować źródła lub przedstawić dowody, które można odtworzyć.

Odpowiedź

Oto kilka innych powodów, które mogą być podstawowymi czynnikami dla NULL == 0

  1. Fakt, że zero jest fałszywe, więc można to zrobić bezpośrednio if(!my_ptr)zamiast if(my_ptr==NULL).
  2. Fakt, że niewtajemniczone globalne liczby całkowite są domyślnie inicjowane ze wszystkimi zerami i jako taki wskaźnik wszystkich zer byłby uważany za niezainicjowany.

Tutaj chciałbym powiedzieć słowo na temat innych odpowiedzi

Nie z powodu cukru syntaktycznego

Mówienie, że NULL jest równe zero z powodu cukru składniowego, nie ma zbytniego sensu, jeśli tak, dlaczego nie użyć indeksu 0 tablicy do przechowywania jej długości?

W rzeczywistości C jest językiem, który najbardziej przypomina wewnętrzną implementację, czy ma sens powiedzieć, że C wybrał zero tylko z powodu cukru syntaktycznego? Woleliby raczej podać słowo kluczowe null (jak wiele innych języków) niż mapować zero na NULL!

Jako taki, chociaż na dzień dzisiejszy może to być po prostu cukier syntaktyczny, jasne jest, że pierwotnym zamiarem twórców języka C nie był cukier syntaktyczny, co pokażę dalej.

1) Specyfikacja

Chociaż prawdą jest, że specyfikacja C mówi o stałej 0 jako zerowym wskaźniku (sekcja 6.3.2.3), a także definiuje NULL jako definicję implementacji (sekcja 7.19 w specyfikacji C11 i 7.17 w specyfikacji C99), Faktem jest, że w książce „The C Programming Language” napisanej przez wynalazców C w sekcji 5.4 znajduje się, co następuje:

C gwarantuje, że zero nigdy nie jest prawidłowym adresem dla danych, więc wartość zwracana zero może zostać użyta do zasygnalizowania nieprawidłowego zdarzenia, w tym przypadku bez spacji.

Wskaźnik i liczby całkowite nie są zamienne, zero jest jedynym wyjątkiem: stałe zero może być przypisane do wskaźnika, a wskaźnik można porównać ze stałym zerem. Symboliczna stała NULL jest często używana zamiast zera, jako mnemonik wskazujący wyraźniej, że jest to specjalna wartość wskaźnika. NULL jest zdefiniowane w. Odtąd będziemy używać NULL.

Jak widać (ze słów „adres zerowy”) co najmniej pierwotnym zamysłem autorów C był adres zero, a nie stałe zero, ponadto z tego fragmentu wynika, że ​​powód, dla którego specyfikacja przemawia z stałe zero prawdopodobnie nie wyklucza wyrażenia, którego wynikiem jest zero, ale zamiast tego dołącza stałą zero będącą liczbą całkowitą, która będzie jedyną stałą całkowitą dozwoloną do użycia w kontekście wskaźnika bez rzutowania.

2) Podsumowanie

Chociaż specyfikacja nie mówi wprost, że adres zerowy może być traktowany inaczej niż stała zerowa, nie mówi, że nie, a fakt, że mając do czynienia ze stałą wskaźnika zerowego , nie twierdzi, że jest to implementacja zdefiniowana jako robi przez stałą zdefiniowaną przez NULL , zamiast twierdzić, że jest równe zero, pokazuje, że może istnieć różnica między stałą zerową a adresem zerowym.

(Jeśli jednak tak jest, zastanawiam się tylko, dlaczego NULL jest zdefiniowana w implementacji, skoro w takim przypadku NULL może być również stałym zerem, ponieważ kompilator i tak musi przekonwertować wszystkie stałe zerowe na rzeczywistą implementację zdefiniowaną NULL?)

Jednak nie widzę tego w prawdziwym działaniu, a na ogólnych platformach adres zero i stałe zero są traktowane tak samo i generują ten sam komunikat o błędzie.

Ponadto faktem jest, że dzisiejsze systemy operacyjne faktycznie rezerwują całą pierwszą stronę (zakres od 0x0000 do 0xFFFF), aby uniemożliwić dostęp do adresu zerowego z powodu wskaźnika C NULL (patrz http://en.wikipedia.org/wiki/ Zero_page , a także „Windows Via C / C ++ autorstwa Jeffrey'a Richtera i Christophe'a Nasarre'a (opublikowane przez Microsoft Press)”).

Dlatego prosiłbym każdego, kto twierdzi, że rzeczywiście widział to w akcji, o określenie platformy i kompilatora oraz dokładnego kodu, który faktycznie zrobił (chociaż z powodu niejasnej definicji w specyfikacji [jak pokazałem] każdy kompilator a platforma może robić, co chce).

Wygląda jednak na to, że autorzy C nie mieli tego na myśli i mówili o „adresie zerowym”, a „C gwarantuje, że nigdy nie jest to prawidłowy adres”, a także „NULL to tylko mnemonic ”, wyraźnie pokazując, że jego pierwotna intencja nie dotyczyła„ cukru syntaktycznego ”.

Nie z powodu systemu operacyjnego

Twierdząc również, że system operacyjny odmawia dostępu do adresu zerowego z kilku powodów:

1) Kiedy napisano C, nie było takiego ograniczenia, co można zobaczyć na tej stronie http://en.wikipedia.org/wiki/Zero_page .

2) Faktem jest, że kompilatory C miały dostęp do adresu pamięci zero.

Wydaje się, że jest to fakt z następującego artykułu BellLabs ( http://www.cs.bell-labs.com/who/dmr/primevalC.html )

Oba kompilatory różnią się szczegółami, jak sobie z tym radzą. We wcześniejszym, początek znajduje się przez nazwanie funkcji; później, początek jest po prostu równy 0. Wskazuje to, że pierwszy kompilator został napisany, zanim mieliśmy maszynę z mapowaniem pamięci, więc początek programu nie znajdował się w lokalizacji 0, podczas gdy w czasie drugiego, mieliśmy PDP-11, który zapewniał mapowanie.

(W rzeczywistości na dzień dzisiejszy (jak cytowałem powyżej odniesienia z Wikipedii i microsoft press), powodem ograniczenia dostępu do adresu zerowego są wskaźniki C's NULL! Więc na końcu okazuje się, że jest odwrotnie!)

3) Pamiętaj, że C jest również używany do pisania systemów operacyjnych, a nawet kompilatorów C!

W rzeczywistości C został opracowany w celu napisania z nim systemu operacyjnego UNIX i jako taki nie wydaje się być powodem, dla którego mieliby ograniczać się do adresu zero.

(Sprzęt) Wyjaśnienie, w jaki sposób komputery (fizycznie) mogą uzyskać dostęp do adresu zerowego

Jest jeszcze jedna kwestia, którą chcę tutaj wyjaśnić, jak w ogóle można odwołać się do adresu zero?

Pomyśl o tym przez chwilę, adresy są pobierane przez procesor, a następnie wysyłane jako napięcia na magistrali pamięci, która jest następnie używana przez system pamięci, aby dostać się do właściwego adresu, a jednak adres zerowy będzie oznaczał brak napięcia , więc w jaki sposób fizyczny sprzęt systemu pamięci uzyskuje dostęp do adresu zero?

Wydaje się, że odpowiedź brzmi, że adres zerowy jest adresem domyślnym, a innymi słowy adres zero jest zawsze dostępny dla systemu pamięci, gdy magistrala pamięci jest całkowicie wyłączona, i jako takie każde żądanie odczytu lub zapisu bez określenia rzeczywistego adresu (który tak jest w przypadku adresu zero) automatycznie uzyskuje dostęp do adresu zero.


1
Nie przegłosowałem Cię, ale Twój post zawiera kilka merytorycznych nieścisłości, np. że fizyczna pamięć na pozycji 0 jest niemożliwa do uzyskania (ponieważ wszystkie przełączniki są wyłączone? Naprawdę?), 0, a stała 0 jest wymienna (mogą nie być) i inne.
Hasturkun

Jeśli chodzi o 0 i stałe zero, tak właśnie mówi oryginalna książka i co pokazują rzeczywiste testy, czy odkryłeś prawdziwą różnicę między nimi? Jeśli tak, który kompilator i platformę? Podczas gdy wiele odpowiedzi sugeruje, że istnieje różnica, której nie znalazłem, i nie ma w nich odniesienia, aby pokazać różnicę. W rzeczywistości według en.wikipedia.org/wiki/Zero_page A także „Windows przez C / C ++ autorstwa Jeffreya Richtera i Christophe'a Nasarre'a (opublikowane przez Microsoft Press)” cała pierwsza strona! jest chroniony w nowoczesnych komputerach tylko po to, aby zapobiec
zerowaniu

Oczywiście wzorzec bitów adresu jest używany do wybierania tego, co jest odczytywane. Generalnie tak jest. w każdym razie nie chcę się z tobą kłócić, tylko wskazywałem, dlaczego mogłeś zostać odrzucony.
Hasturkun,

Nie zgadzam się z Twoimi roszczeniami. Nie jestem również zainteresowany kontynuowaniem tej dyskusji.
Hasturkun,

6
Reklamacja sprzętu to bzdura. Aby odczytać adres zero, jedź! Chip Select low,! RAS high,! CAS low,! WE high i wszystkie linie adresu low. Gdy autobus jest wyłączony,! CS jest wysoki.
MSalters
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.