Czy można zainicjować wskaźnik C na NULL?

90

Pisałem takie rzeczy jak

char *x=NULL;

przy założeniu, że

 char *x=2;

utworzy charwskaźnik do adresu 2.

Ale w samouczku programowania GNU C jest napisane, że int *my_int_ptr = 2;przechowuje wartość całkowitą 2do dowolnego losowego adresu, w my_int_ptrktórym jest przydzielona.

Wydawałoby się to sugerować, że moje własne char *x=NULLprzypisuje jakąkolwiek wartość NULLrzutowania chardo jakiegoś losowego adresu w pamięci.

Podczas

#include <stdlib.h>
#include <stdio.h>

int main()
{
    char *x=NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

faktycznie drukuje

jest NULL

kiedy go kompiluję i uruchamiam, obawiam się, że polegam na niezdefiniowanym zachowaniu lub przynajmniej niedookreślonym zachowaniu i powinienem napisać

char *x;
x=NULL;

zamiast.

fagricipni
źródło
72
Istnieje bardzo myląca różnica między tym, co int *x = whatever;robi, a tym, co int *x; *x = whatever;robi. int *x = whatever;faktycznie zachowuje się jak int *x; x = whatever;nie *x = whatever;.
user2357112 obsługuje Monikę
78
Wydaje się, że ten samouczek źle zrozumiał to zagmatwane rozróżnienie.
user2357112 obsługuje Monikę
51
Tyle gównianych samouczków w sieci! Natychmiast przestań czytać. Naprawdę potrzebujemy TAK czarnej listy, na której moglibyśmy publicznie zawstydzić gówniane książki ...
Lundin
9
@MM Co nie czyni go mniej brzydkim w roku 2017. Biorąc pod uwagę ewolucję kompilatorów i komputerów od lat 80-tych, to w zasadzie to samo, co gdybym był lekarzem i czytał książki medyczne napisane w XVIII wieku.
Lundin
13
Nie sądzę, że ten tutorial kwalifikuje się jako " The GNU C Programming Tutorial" ...
marcelm

Odpowiedzi:

114

Czy można zainicjować wskaźnik C na NULL?

TL; DR Tak, bardzo.


Rzeczywisty argument przedstawiony na prowadnicy czyta się jak

Z drugiej strony, jeśli użyjesz tylko pojedynczego początkowego przypisania, int *my_int_ptr = 2;program spróbuje wypełnić zawartość miejsca pamięci wskazywanego przez my_int_ptrwartość 2. Ponieważ my_int_ptrjest wypełniony śmieciami, może to być dowolny adres. […]

Cóż, oni się mylą, masz rację.

W przypadku instrukcji ( pomijając na razie fakt, że wskaźnik na konwersję liczb całkowitych jest zachowaniem zdefiniowanym w implementacji )

int * my_int_ptr = 2;

my_int_ptrjest zmienną (typu wskaźnik do int), ma swój własny adres (typ: adres wskaźnika do liczby całkowitej), 2w tym adresie przechowujesz wartość .

Teraz, my_int_ptrbędąc typem wskaźnikowym, możemy powiedzieć, że wskazuje on wartość „type” w lokalizacji pamięci wskazywanej przez wartość przechowywaną wmy_int_ptr . Tak, jesteś w zasadzie przypisywania wartości o zmiennej wskaźnik, a nie wartość lokalizacji pamięci wskazywanego przez wskaźnik.

A więc na zakończenie

 char *x=NULL;

inicjalizuje zmienną wskaźnika xdo NULL, a nie wartość pod adresem pamięci wskazywanego przez wskaźnik .

To jest to samo co

 char *x;
 x = NULL;    

Ekspansja:

Teraz, będąc ściśle zgodnym, oświadczenie takie jak

 int * my_int_ptr = 2;

jest nielegalne, ponieważ wiąże się z naruszeniem przymusu. Żeby było jasne,

  • my_int_ptr jest zmienną wskaźnikową, typ int *
  • stała całkowita , z definicji 2ma typ int.

i nie są to typy „kompatybilne”, więc ta inicjalizacja jest nieważna, ponieważ narusza zasady prostego przypisania, o których mowa w rozdziale §6.5.16.1 / P1, opisane w odpowiedzi .

Jeśli ktoś jest zainteresowany, w jaki sposób inicjalizacja jest powiązana z prostymi ograniczeniami przypisania, cytowanie C11, rozdział §6.7.9, P11

Inicjator dla skalara powinien być pojedynczym wyrażeniem, opcjonalnie ujętym w nawiasy klamrowe. Początkową wartością obiektu jest wartość wyrażenia (po konwersji); obowiązują te same ograniczenia typu i konwersje, co w przypadku prostego przypisania, przyjmując typ skalara jako niekwalifikowaną wersję zadeklarowanego typu.

Sourav Ghosh
źródło
@ Random832n Oni się mylą. Zacytowałem powiązaną część w mojej odpowiedzi, proszę poprawić mnie, jeśli jest inaczej. Aha, i nacisk na celowość.
Sourav Ghosh,
„… jest niedozwolone, ponieważ wiąże się z naruszeniem ograniczenia.… literał liczby całkowitej 2 ma typ int, z definicji”. jest problematyczne. Wygląda na to, że ponieważ 2jest int, zadanie jest problemem. Ale to coś więcej. NULLmoże być również an int, an int 0. Po prostu to char *x = 0;jest dobrze zdefiniowane, a char *x = 2;nie jest. 6.3.2.3 Wskaźniki 3 (BTW: C nie definiuje literału liczby całkowitej , tylko literał ciągu i literału złożonego . 0Jest stałą liczbą całkowitą )
chux - Reinstate Monica
@chux Masz rację, ale czy nie jest to char *x = (void *)0;zgodne z zasadami ? czy tylko z innymi wyrażeniami, które dają wartość 0?
Sourav Ghosh
10
@SouravGhosh: stałe całkowite z wartością 0są wyjątkowe: niejawnie konwertują je na wskaźniki o wartości null niezależnie od zwykłych reguł jawnego rzutowania ogólnych wyrażeń całkowitych na typy wskaźników.
Steve Jessop
1
Język opisany w podręczniku C Reference Manual z 1974 roku nie pozwalał deklaracjom na określenie wyrażeń inicjalizacyjnych, a brak takich wyrażeń sprawia, że ​​„użycie luster deklaracji” jest dużo bardziej praktyczne. Składnia int *p = somePtrExpressionjest raczej okropna jak IMHO, ponieważ wygląda na to, że ustawia wartość, *pale w rzeczywistości ustawia wartość p.
supercat
53

Samouczek jest zły. W ISO C int *my_int_ptr = 2;jest to błąd. W GNU C oznacza to samo co int *my_int_ptr = (int *)2;. To konwertuje liczbę całkowitą 2na adres pamięci, w pewien sposób określony przez kompilator.

Nie próbuje zapisywać niczego w lokalizacji, do której odnosi się ten adres (jeśli istnieje). Gdybyś kontynuował pisanie *my_int_ptr = 5;, próbowałby zapisać numer 5w lokalizacji, do której odnosi się ten adres.

MM
źródło
1
Nie wiedziałem, że konwersja liczby całkowitej na wskaźnik jest zdefiniowana jako implementacja. Dzięki za informację.
taskinoor
1
@taskinoor Zwróć uwagę, że konwersja jest możliwa tylko w przypadku wymuszenia jej przez obsadę, jak w tej odpowiedzi. Gdyby nie rzutowanie, kod nie powinien się kompilować.
Lundin
2
@taskinoor: Tak, różne konwersje w C są dość zagmatwane. Ten Q zawiera interesujące informacje na temat konwersji: C: Kiedy rzutowanie między typami wskaźników nie jest niezdefiniowane? .
sleske
17

Aby wyjaśnić, dlaczego samouczek jest błędny, int *my_int_ptr = 2;jest „naruszeniem ograniczeń”, jest to kod, którego nie można kompilować i kompilator musi dać ci diagnostykę po napotkaniu go.

Zgodnie z 6.5.16.1 Proste przypisanie:

Ograniczenia

Jedna z poniższych pozycji musi posiadać:

  • lewy operand ma niepodzielny, kwalifikowany lub niekwalifikowany typ arytmetyczny, a prawy ma typ arytmetyczny;
  • lewy operand ma atomową, kwalifikowaną lub niekwalifikowaną wersję struktury lub typu unii zgodnych z typem po prawej stronie;
  • lewy operand ma niepodzielny, kwalifikowany lub niekwalifikowany typ wskaźnika i (biorąc pod uwagę typ, jaki lewy operand miałby po konwersji lwartości) oba operandy są wskaźnikami do kwalifikowanych lub niekwalifikowanych wersji zgodnych typów, a typ wskazany po lewej stronie ma wszystko kwalifikatory typu wskazanego po prawej stronie;
  • lewy operand ma niepodzielny, kwalifikowany lub niekwalifikowany typ wskaźnika i (biorąc pod uwagę typ, jaki lewy operand miałby po konwersji lwartości) jeden operand jest wskaźnikiem do typu obiektu, a drugi jest wskaźnikiem do kwalifikowanej lub niekwalifikowanej wersji void, a typ wskazany po lewej stronie ma wszystkie kwalifikatory typu wskazanego po prawej stronie;
  • lewy operand jest wskaźnikiem atomowym, kwalifikowanym lub niekwalifikowanym, a prawy jest stałą zerową wskaźnika; lub
  • lewy operand ma typ niepodzielny, kwalifikowany lub niekwalifikowany _Bool, a prawy jest wskaźnikiem.

W tym przypadku lewy operand jest niekwalifikowanym wskaźnikiem. Nigdzie nie wspomniano, że prawy operand może być liczbą całkowitą (typ arytmetyczny). Tak więc kod narusza standard C.

Wiadomo, że GCC zachowuje się słabo, chyba że wyraźnie powiesz, że jest to standardowy kompilator C. Jeśli skompilujesz kod jako -std=c11 -pedantic-errors, da on poprawnie diagnostykę, tak jak musi.

Lundin
źródło
4
głosowano za sugerowaniem błędów -pedantycznych. Chociaż prawdopodobnie użyję powiązanego -Wpedantic.
fagricipni
2
Jedyny wyjątek od twojego stwierdzenia, że ​​prawy operand nie może być liczbą całkowitą: Sekcja 6.3.2.3 mówi: „Wyrażenie stałe będące liczbą całkowitą o wartości 0 lub takie wyrażenie rzutowane na typ void *nazywa się stałą wskaźnika zerowego”. Zwróć uwagę na przedostatni punkt w ofercie. Dlatego int* p = 0;jest to legalny sposób pisania int* p = NULL;. Chociaż ta ostatnia jest wyraźniejsza i bardziej konwencjonalna.
Davislor,
1
Co sprawia, że ​​patologiczne zaciemnianie int m = 1, n = 2 * 2, * p = 1 - 1, q = 2 - 1;również jest legalne.
Davislor,
@Davislor, o którym mowa w punkcie 5 w standardowym cytacie w tej odpowiedzi (zgadzam się, że w podsumowaniu prawdopodobnie powinno o tym wspomnieć)
MM
1
@chux Uważam, że dobrze uformowany program musiałby intptr_tjawnie przekonwertować na jeden z dozwolonych typów po prawej stronie. Oznacza to, że jest void* a = (void*)(intptr_t)b;to zgodne z punktem 4, ale (intptr_t)bnie jest ani zgodnym typem wskaźnika, ani void*stałą a , ani zerowym wskaźnikiem i void* anie jest ani typem arytmetycznym, ani _Bool. Standard mówi, że konwersja jest legalna, ale nie mówi, że jest dorozumiana.
Davislor,
15

int *my_int_ptr = 2

przechowuje wartość całkowitą 2 na dowolny losowy adres w my_int_ptr, kiedy jest przydzielany.

To jest całkowicie błędne. Jeśli to rzeczywiście jest napisane, zdobądź lepszą książkę lub samouczek.

int *my_int_ptr = 2definiuje wskaźnik całkowity, który wskazuje na adres 2. Najprawdopodobniej nastąpi awaria, jeśli spróbujesz uzyskać dostęp do adresu 2.

*my_int_ptr = 2, tj. bez znaku intw linii, przechowuje wartość dwa pod dowolnym losowym adresem, na który my_int_ptrwskazuje. Mówiąc to, możesz przypisać NULLdo wskaźnika, gdy jest zdefiniowany. char *x=NULL;jest całkowicie poprawna C.

Edycja: Podczas pisania tego nie wiedziałem, że konwersja liczb całkowitych na wskaźnik jest zachowaniem zdefiniowanym przez implementację. Szczegółowe informacje można znaleźć w dobrych odpowiedziach @MM i @SouravGhosh.

taskinoor
źródło
1
Jest to całkowicie błędne, ponieważ stanowi naruszenie ograniczenia, a nie z żadnego innego powodu. W szczególności jest to niepoprawne: „int * my_int_ptr = 2 definiuje wskaźnik całkowity, który wskazuje na adres 2”.
Lundin
@Lundin: Twoje zdanie „nie z żadnego innego powodu” jest samo w sobie błędne i mylące. Jeśli naprawisz problem ze zgodnością typów, nadal pozostajesz z faktem, że autor samouczka rażąco błędnie przedstawia sposób działania inicjalizacji wskaźnika i przypisań.
Wyścigi lekkości na orbicie
14

Wiele nieporozumień związanych ze wskaźnikami C wynika z bardzo złego wyboru, którego pierwotnie dokonano w odniesieniu do stylu kodowania, potwierdzonego bardzo złym, niewielkim wyborem w składni języka.

int *x = NULL;ma rację C, ale jest bardzo mylące, powiedziałbym nawet, że bezsensowne i utrudniało zrozumienie języka wielu nowicjuszom. Pozwala to pomyśleć, że później moglibyśmy to zrobić, *x = NULL;co jest oczywiście niemożliwe. Widzisz, typ zmiennej nie jest int, a nazwa zmiennej nie *x, ani *w deklaracji nie odgrywa żadnej funkcjonalnej roli we współpracy z =. Jest czysto deklaratywna. Więc o wiele bardziej sensowne jest to:

int* x = NULL;co jest również poprawne C, choć nie jest zgodne z oryginalnym stylem kodowania K&R. To doskonale wyjaśnia, że ​​typ jest int*, a zmienna wskaźnikowa xtak, więc nawet dla niewtajemniczonych staje się jasne, że wartość NULLjest przechowywana w x, czyli wskaźniku do int.

Ponadto ułatwia wyprowadzenie reguły: gdy gwiazda znajduje się z dala od nazwy zmiennej, jest to deklaracja, podczas gdy gwiazda dołączona do nazwy jest dereferencją wskaźnika.

Tak więc teraz staje się o wiele bardziej zrozumiałe, że dalej możemy to zrobić x = NULL;lub *x = 2;innymi słowy, nowicjuszowi łatwiej jest zobaczyć, jak variable = expressionprowadzi do pointer-type variable = pointer-expressioni dereferenced-pointer-variable = expression. (Dla wtajemniczonych przez „wyrażenie” mam na myśli „wartość r”.)

Niefortunnym wyborem w składni języka jest to, że podczas deklarowania zmiennych lokalnych można powiedzieć, int i, *p;która deklaruje liczbę całkowitą i wskaźnik do liczby całkowitej, więc prowadzi to do przekonania, że *jest to użyteczna część nazwy. Ale tak nie jest, a ta składnia to po prostu dziwaczny przypadek specjalny, dodany dla wygody i moim zdaniem nigdy nie powinien istnieć, ponieważ unieważnia regułę, którą zaproponowałem powyżej. O ile mi wiadomo, nigdzie indziej w języku ta składnia nie ma znaczenia, ale nawet jeśli tak jest, wskazuje ona na rozbieżność w sposobie definiowania typów wskaźników w C. Wszędzie indziej, w deklaracjach pojedynczych zmiennych, na listach parametrów, w elementach strukturalnych itp. możesz zadeklarować swoje wskaźniki jako type* pointer-variablezamiast type *pointer-variable; jest to całkowicie legalne i ma więcej sensu.

Mike Nakis
źródło
int *x = NULL; is correct C, but it is very misleading, I would even say nonsensical,... Muszę się nie zgodzić. It makes one think.... przestań myśleć, najpierw przeczytaj książkę C, bez obrazy.
Sourav Ghosh
^^ to miałoby dla mnie sens. Więc przypuszczam, że jest to subiektywne.
Mike Nakis
5
@SouravGhosh Myślę, że C powinno być zaprojektowane tak, aby int* somePtr, someotherPtrdeklarowało dwie wskazówki, w rzeczywistości pisałem, int* somePtrale to prowadzi do opisanego przez ciebie błędu.
fagricipni
1
@fagricipni Z tego powodu przestałem używać składni deklaracji wielu zmiennych. Deklaruję zmienne jedna po drugiej. Jeśli naprawdę chcę, aby były w tej samej linii, oddzielam je średnikami zamiast przecinkami. „Jeśli miejsce jest złe, nie idź w to miejsce”.
Mike Nakis
2
@fagricipni Cóż, gdybym mógł zaprojektować Linuksa od zera, użyłbym createzamiast niego creat. :) Chodzi o to, że tak jest i musimy się ukształtować, aby się do tego dostosować. Wszystko sprowadza się do osobistego wyboru na koniec dnia, zgadzam się.
Sourav Ghosh,
6

Do wielu doskonałych odpowiedzi chciałbym dodać coś ortogonalnego. W rzeczywistości inicjalizacja do NULLjest daleka od złej praktyki i może być przydatna, jeśli ten wskaźnik może, ale nie musi, być używany do przechowywania dynamicznie przydzielanego bloku pamięci.

int * p = NULL;
...
if (...) {
    p = (int*) malloc(...);
    ...
}
...
free(p);

Ponieważ zgodnie z normą ISO-IEC 9899 free jest to nop, gdy argumentem jest NULL, powyższy kod (lub coś bardziej znaczącego w tych samych liniach) jest prawidłowy.

Luca Citi
źródło
5
Rzutowanie wyniku malloc w C jest zbędne, chyba że ten kod C powinien również kompilować się jako C ++.
kot
Masz rację, void*jest konwertowany w razie potrzeby. Ale posiadanie kodu, który działa z kompilatorem C i C ++, może przynieść korzyści.
Luca Citi
1
@LucaCiti C i C ++ to różne języki. Błędy czekają tylko na Ciebie, jeśli spróbujesz skompilować plik źródłowy napisany dla jednego za pomocą kompilatora zaprojektowanego dla drugiego. To tak, jakby próbować pisać kod w C, który można skompilować za pomocą narzędzi Pascal.
Evil Dog Pie
1
Dobra rada. Ja (staram się) zawsze inicjalizować moje stałe wskaźnika na coś. We współczesnym C zwykle może to być ich ostateczna wartość i mogą to być constwskaźniki zadeklarowane w medias res , ale nawet gdy wskaźnik musi być zmienny (jak ten używany w pętli lub przez realloc()), ustawienie go tak, aby NULLłapał błędy tam, gdzie był używany wcześniej jest ustawiony z jego prawdziwą wartością. W większości systemów wyłuskiwanie odwołań NULLpowoduje segfault w punkcie awarii (chociaż są wyjątki), podczas gdy niezainicjowany wskaźnik zawiera śmieci, a zapisywanie do nich powoduje uszkodzenie dowolnej pamięci.
Davislor,
1
Ponadto bardzo łatwo jest zobaczyć w debugerze, że wskaźnik zawiera NULL, ale może być bardzo trudno odróżnić wskaźnik śmieci od prawidłowego. Dlatego warto upewnić się, że wszystkie wskaźniki są zawsze prawidłowe lub NULLod momentu deklaracji.
Davislor,
1

to jest wskaźnik zerowy

int * nullPtr = (void*) 0;
Ahmed Nabil El-Gawahergy
źródło
1
To odpowiedź na tytuł, ale nie na treść pytania.
Fabio mówi Przywróć Monikę
1

To jest poprawne.

int main()
{
    char * x = NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

Ta funkcja jest odpowiednia do tego, co robi. Przypisuje adres 0 do wskaźnika znaku x. Oznacza to, że wskazuje wskaźnik x na adres pamięci 0.

Alternatywny:

int main()
{
    char* x = 0;

    if ( !x )
        printf(" x points to NULL\n");

    return EXIT_SUCCESS;
}

Domyślam się, czego chciałeś:

int main()
{
    char* x = NULL;
    x = alloc( sizeof( char ));
    *x = '2';

    if ( *x == '2' )
        printf(" x points to an address/location that contains a '2' \n");

    return EXIT_SUCCESS;
}

x is the street address of a house. *x examines the contents of that house.
Vanderdecken
źródło
„Przypisuje adres 0 do wskaźnika znaku x”. -> Może. C nie określa wartości wskaźnika, tylko to char* x = 0; if (x == 0)będzie prawdziwe. Wskaźniki niekoniecznie są liczbami całkowitymi.
chux - Przywróć Monikę
Nie „wskazuje wskaźnika x na adres pamięci 0”. Ustawia wartość wskaźnika na nieokreśloną nieprawidłową wartość, którą można przetestować , porównując ją z 0 lub NULL. Faktyczna operacja jest zdefiniowana w ramach implementacji. Nie ma tu nic, co odpowiadałoby na rzeczywiste pytanie.
Markiz Lorne