Przyjrzyjmy się, jak standard C definiuje terminy „zachowanie” i „niezdefiniowane zachowanie”.
Odniesienia dotyczą projektu normy ISO C 2011 N1570 ; Nie znam żadnych istotnych różnic w żadnej z trzech opublikowanych norm ISO C (1990, 1999 i 2011).
Sekcja 3.4:
zachowanie
wygląd zewnętrzny lub działanie
Ok, to trochę niejasne, ale twierdzę, że dana instrukcja nie ma „wyglądu”, a na pewno nie ma „akcji”, chyba że jest faktycznie wykonana.
Sekcja 3.4.3:
niezdefiniowane zachowanie
, po zastosowaniu nieprzenoszalnej lub błędnej konstrukcji programu lub błędnych danych, dla których niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań
Mówi „ po użyciu ” takiej konstrukcji. Słowo „używać” nie jest zdefiniowane w standardzie, więc wracamy do potocznego angielskiego znaczenia. Konstrukcja nie jest „używana”, jeśli nigdy nie została wykonana.
Pod tą definicją jest uwaga:
UWAGA Możliwe niezdefiniowane zachowanie obejmuje zarówno całkowite zignorowanie sytuacji z nieprzewidywalnymi skutkami, zachowanie podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z lub bez wydania komunikatu diagnostycznego), jak i przerwanie tłumaczenia lub wykonania (z wydanie komunikatu diagnostycznego).
Zatem kompilator może odrzucić twój program w czasie kompilacji, jeśli jego zachowanie jest nieokreślone. Ale moja interpretacja jest taka, że może to zrobić tylko wtedy, gdy może udowodnić, że każde wykonanie programu napotka niezdefiniowane zachowanie. Co oznacza, jak sądzę, że:
if (rand() % 2 == 0) {
i = i / 0;
}
które z pewnością mogą mieć niezdefiniowane zachowanie, nie można ich odrzucić w czasie kompilacji.
Z praktycznego punktu widzenia programy muszą mieć możliwość wykonywania testów w czasie wykonywania, aby chronić się przed wywołaniem niezdefiniowanego zachowania, a standard musi im na to zezwalać.
Twój przykład to:
if (0) {
i = 1/0;
}
który nigdy nie wykonuje dzielenia przez 0. Bardzo popularnym idiomem jest:
int x, y;
if (y != 0) {
x = x / y;
}
Podział z pewnością ma niezdefiniowane zachowanie, jeśli y == 0
, ale nigdy nie jest wykonywany, jeśli y == 0
. Zachowanie jest dobrze zdefiniowane iz tego samego powodu, z którego dobrze zdefiniowano twój przykład: ponieważ potencjał niezdefiniowane zachowanie nigdy nie może się wydarzyć.
(Chyba że INT_MIN < -INT_MAX && x == INT_MIN && y == -1
(tak, dzielenie liczb całkowitych może się przepełnić), ale to osobny problem.)
W komentarzu (od czasu usunięcia) ktoś wskazał, że kompilator może oceniać stałe wyrażenia w czasie kompilacji. Co jest prawdą, ale nie ma znaczenia w tym przypadku, ponieważ w kontekście
i = 1/0;
1/0
nie jest wyrażeniem stałym .
Stała ekspresja jest składniowym kategoria, która redukuje się do warunkowej ekspresji (która nie obejmuje zadania i wyrażenia przecinek). Wyrażenie stałe produkcji pojawia się w gramatyce tylko w kontekstach, które faktycznie wymagają stałego wyrażenia, takich jak etykiety przypadków. Więc jeśli napiszesz:
switch (...) {
case 1/0:
...
}
wtedy 1/0
jest wyrażeniem stałym - i takim, które narusza ograniczenie w 6.6p4: „Każde wyrażenie stałe będzie obliczane na stałą, która znajduje się w zakresie reprezentowalnych wartości dla swojego typu.”, dlatego wymagana jest diagnostyka. Ale prawa strona przypisania nie wymaga wyrażenia stałego , a jedynie wyrażenia warunkowego , więc ograniczenia wyrażeń stałych nie mają zastosowania. Kompilator może ocenić dowolne wyrażenie, że to jest w stanie w czasie kompilacji, ale tylko wtedy, gdy zachowanie jest takie samo, jak gdyby były oceniane w trakcie realizacji (lub, w kontekście if (0)
, nie ocenianego w trakcie realizacji ().
(Coś, co wygląda dokładnie jak wyrażenie stałe, niekoniecznie jest wyrażeniem stałym , tak jak w x + y * z
przypadku sekwencja x + y
nie jest wyrażeniem addytywnym ze względu na kontekst, w którym się pojawia).
Co oznacza przypis w N1570 sekcja 6.6, który zamierzałem zacytować:
Zatem w poniższej inicjalizacji
static int i = 2 || 1 / 0;
wyrażenie jest prawidłowym wyrażeniem stałym o wartości całkowitej o wartości jeden.
właściwie nie ma związku z tym pytaniem.
Na koniec jest kilka rzeczy, które są zdefiniowane jako powodujące niezdefiniowane zachowanie, które nie dotyczy tego, co dzieje się podczas wykonywania. Załącznik J, sekcja 2 normy C (ponownie, patrz projekt N1570 ) wymienia rzeczy, które powodują nieokreślone zachowanie, zebrane z pozostałej części normy. Oto kilka przykładów (nie twierdzę, że jest to pełna lista):
- Niepusty plik źródłowy nie kończy się znakiem nowego wiersza, który nie jest bezpośrednio poprzedzony znakiem ukośnika odwrotnego lub kończy się częściowym znacznikiem wstępnego przetwarzania lub komentarzem
- Łączenie tokenów tworzy sekwencję znaków zgodną ze składnią uniwersalnej nazwy znaku
- W pliku źródłowym napotkano znak spoza podstawowego zestawu znaków źródłowych, z wyjątkiem identyfikatora, stałej znakowej, literału ciągu, nazwy nagłówka, komentarza lub tokenu przetwarzania wstępnego, który nigdy nie jest konwertowany na token
- Identyfikator, komentarz, literał ciągu, stała znakowa lub nazwa nagłówka zawiera nieprawidłowy znak wielobajtowy lub nie zaczyna się i nie kończy w początkowym stanie przesunięcia
- Ten sam identyfikator ma powiązania wewnętrzne i zewnętrzne w tej samej jednostce tłumaczeniowej
Te szczególne przypadki to rzeczy, które kompilator może wykryć. Myślę, że ich zachowanie jest nieokreślone, ponieważ komitet nie chciał lub nie mógł narzucić tego samego zachowania we wszystkich implementacjach, a określenie zakresu dozwolonych zachowań po prostu nie było warte wysiłku. Tak naprawdę nie należą one do kategorii „kodu, który nigdy nie zostanie wykonany”, ale wspominam o nich tutaj dla kompletności.