Czy drukowanie wskaźników o wartości null ze specyfikatorem %p
konwersji jest niezdefiniowane ?
#include <stdio.h>
int main(void) {
void *p = NULL;
printf("%p", p);
return 0;
}
Pytanie dotyczy standardu C, a nie implementacji C.
Czy drukowanie wskaźników o wartości null ze specyfikatorem %p
konwersji jest niezdefiniowane ?
#include <stdio.h>
int main(void) {
void *p = NULL;
printf("%p", p);
return 0;
}
Pytanie dotyczy standardu C, a nie implementacji C.
Odpowiedzi:
Jest to jeden z tych dziwnych przypadków narożnych, w których podlegamy ograniczeniom języka angielskiego i niespójnej strukturze w standardzie. W najlepszym razie mogę przedstawić przekonujący kontrargument, którego nie da się udowodnić :) 1
Kod w pytaniu wykazuje dobrze zdefiniowane zachowanie.
Ponieważ podstawą pytania jest [7.1.4] , zacznijmy od tego:
Każda z poniższych instrukcji ma zastosowanie, chyba że wyraźnie określono inaczej w poniższych szczegółowych opisach: Jeśli argument funkcji ma nieprawidłową wartość ( np. Wartość spoza domeny funkcji lub wskaźnik poza przestrzenią adresową programu, lub pusty wskaźnik , [... inne przykłady ...] ) [...] zachowanie jest niezdefiniowane. [... inne oświadczenia ...]
To jest niezdarny język. Jedną z interpretacji jest to, że pozycje na liście są UB dla wszystkich funkcji bibliotecznych, chyba że zostaną zastąpione przez indywidualne opisy. Ale lista zaczyna się od „takich jak”, co oznacza, że ma charakter poglądowy, a nie wyczerpujący. Na przykład nie wspomina o poprawnym zakończeniu zerowania ciągów (krytycznych dla zachowania np strcpy
.).
Dlatego jasne jest, że celem / zakresem 7.1.4 jest po prostu to, że „nieprawidłowa wartość” prowadzi do UB ( chyba że zaznaczono inaczej ). Musimy spojrzeć na opis każdej funkcji, aby określić, co liczy się jako „nieprawidłowa wartość”.
strcpy
[7.21.2.3] mówi tylko tak:
W
strcpy
kopiuje łańcuch znaków wskazywany przezs2
(łącznie z kończącym znakiem null) do tablicy wskazywanej przezs1
. Jeśli kopiowanie odbywa się między nakładającymi się obiektami, zachowanie jest niezdefiniowane.
Nie wspomina wyraźnie o zerowych wskaźnikach, ale nie wspomina też o zerowych terminatorach. Zamiast tego można wywnioskować z „łańcucha wskazywanego przez s2
”, że jedynymi poprawnymi wartościami są łańcuchy (tj. Wskaźniki do tablic znaków zakończonych znakiem null).
Rzeczywiście, ten wzór można zobaczyć w poszczególnych opisach. Kilka innych przykładów:
[7.6.4.1 (fenv)] przechowuje bieżące środowisko zmiennoprzecinkowe w obiekcie wskazywanym przez
envp
[7.12.6.4 (frexp)] przechowuje liczbę całkowitą w obiekcie int wskazywanym przez
exp
[7.19.5.1 (fclose)] strumienia wskazywanego przez
stream
printf
[7.19.6.1] mówi tak o %p
:
p
- Argument będzie wskaźnikiem dovoid
. Wartość wskaźnika jest konwertowana na sekwencję drukowanych znaków w sposób zdefiniowany w implementacji.
Null jest prawidłową wartością wskaźnika, aw tej sekcji nie ma wyraźnej wzmianki, że null jest przypadkiem specjalnym, ani że wskaźnik musi wskazywać na obiekt. Tak więc jest to określone zachowanie.
1. Chyba że zgłosi się autor standardów lub jeśli nie znajdziemy czegoś podobnego do dokumentu uzasadniającego wyjaśnienia.
Tak . Drukowanie wskaźników zerowych ze specyfikatorem %p
konwersji ma niezdefiniowane zachowanie. Powiedziawszy to, nie znam żadnej istniejącej implementacji, która mogłaby się źle zachować.
Odpowiedź dotyczy dowolnego z norm C (C89 / C99 / C11).
Specyfikator %p
konwersji oczekuje argumentu typu wskaźnik na void, konwersja wskaźnika do drukowalnych znaków jest zdefiniowana przez implementację. Nie stwierdza, że oczekiwany jest pusty wskaźnik.
We wprowadzeniu do funkcji biblioteki standardowej stwierdza się, że wskaźniki puste jako argumenty funkcji (biblioteki standardowej) są uważane za nieprawidłowe wartości, chyba że wyraźnie określono inaczej.
C99
/ C11
§7.1.4 p1
[...] Jeśli argument funkcji ma nieprawidłową wartość (np. [...] pusty wskaźnik, [...] zachowanie jest niezdefiniowane.
Przykłady funkcji (biblioteki standardowej), które oczekują zerowych wskaźników jako prawidłowych argumentów:
fflush()
używa pustego wskaźnika do opróżniania „wszystkich strumieni” (które mają zastosowanie).freopen()
używa pustego wskaźnika do wskazania pliku „aktualnie skojarzonego” ze strumieniem.snprintf()
umożliwia przekazanie pustego wskaźnika, gdy „n” jest równe zero.realloc()
używa pustego wskaźnika do przydzielania nowego obiektu.free()
pozwala na przekazanie pustego wskaźnika.strtok()
używa pustego wskaźnika dla kolejnych wywołań.Jeśli weźmiemy przypadek za snprintf()
, sensowne jest zezwolenie na przekazanie wskaźnika zerowego, gdy „n” jest równe zero, ale nie ma to miejsca w przypadku innych funkcji (biblioteki standardowej), które dopuszczają podobne zero „n”. Na przykład: memcpy()
, memmove()
, strncpy()
, memset()
, memcmp()
.
Jest to określone nie tylko we wstępie do biblioteki standardowej, ale także raz jeszcze we wstępie do tych funkcji:
C99 §7.21.1 p2
/ C11 §7.24.1 p2
Gdzie argument zadeklarowany jako
size_t
n określa długość tablicy dla funkcji, n może mieć wartość zero w wywołaniu tej funkcji. O ile wyraźnie nie określono inaczej w opisie konkretnej funkcji w tym podrozdziale, argumenty wskaźników w takim wywołaniu nadal będą miały prawidłowe wartości, jak opisano w 7.1.4.
Nie wiem, czy UB %p
ze wskaźnikiem zerowym jest w rzeczywistości celowe, ale ponieważ standard wyraźnie stwierdza, że wskaźniki zerowe są uważane za nieprawidłowe wartości jako argumenty funkcji biblioteki standardowej, a następnie idzie i wyraźnie określa przypadki, w których wartość null wskaźnik jest ważny argument (snprintf, wolna, itd), a następnie idzie i po raz kolejny powtarza wymóg argumenty ważność nawet w zera „n” przypadki ( memcpy
, memmove
, memset
), to myślę, że to rozsądne założenie, że Komisja normalizacyjna C nie przejmuje się zbytnio niezdefiniowaniem takich rzeczy.
%p
nie powinno być niezdefiniowanym zachowaniem
Autorzy standardu C nie starali się wyczerpująco wyszczególnić wszystkich wymagań behawioralnych, które implementacja musi spełnić, aby nadawała się do określonego celu. Zamiast tego spodziewali się, że ludzie piszący kompilatory wykażą się pewnym zdrowym rozsądkiem, niezależnie od tego, czy standard tego wymaga, czy nie.
Pytanie, czy coś wywołuje UB, rzadko jest samo w sobie użyteczne. Prawdziwe ważne kwestie to:
Czy ktoś, kto próbuje napisać wysokiej jakości kompilator, powinien zachowywać się w przewidywalny sposób? W przypadku opisywanego scenariusza odpowiedź brzmi zdecydowanie tak.
Czy programiści powinni mieć prawo oczekiwać, że wysokiej jakości kompilatory dla wszystkiego, co przypomina zwykłe platformy, będą zachowywać się w przewidywalny sposób? W opisanym scenariuszu powiedziałbym, że tak.
Czy niektórzy tępi autorzy kompilatorów mogą rozciągnąć interpretację standardu, aby usprawiedliwić zrobienie czegoś dziwnego? Mam nadzieję, że nie, ale nie wykluczam tego.
Czy kompilatory dezynfekujące powinny wrzeszczeć o tym zachowaniu? To zależałoby od poziomu paranoi ich użytkowników; kompilator czyszczący prawdopodobnie nie powinien domyślnie skrzeczać o takim zachowaniu, ale być może zapewniać opcję konfiguracji na wypadek, gdyby programy mogły zostać przeniesione do "sprytnych" / głupich kompilatorów, które zachowują się dziwnie.
Jeśli rozsądna interpretacja normy oznaczałaby, że zdefiniowano zachowanie, ale niektórzy autorzy kompilatorów rozciągają tę interpretację, aby uzasadnić postępowanie w inny sposób, czy to naprawdę ma znaczenie, co mówi standard?
printf("%p", (void*) 0)
nieokreślone zachowanie jest zgodne ze standardem? Głęboko zagnieżdżone wywołania funkcji są tak samo istotne jak cena herbaty w Chinach. I tak, UB jest bardzo powszechne w programach w świecie rzeczywistym - co z tego?
%p
każdego możliwego znaczenia pytania.