Twoje pytanie prawdopodobnie nie brzmiało: „Dlaczego te konstrukcje są niezdefiniowanym zachowaniem w C?”. Twoje pytanie brzmiało prawdopodobnie: „Dlaczego ten kod (użycie ++
) nie dał mi oczekiwanej wartości?”, A ktoś oznaczył twoje pytanie jako duplikat i wysłał cię tutaj.
Ta odpowiedź próbuje odpowiedzieć na to pytanie: dlaczego twój kod nie dał ci oczekiwanej odpowiedzi i jak możesz nauczyć się rozpoznawać (i unikać) wyrażeń, które nie będą działać zgodnie z oczekiwaniami.
Zakładam, że słyszałeś już podstawową definicję C ++
i --
operatorów oraz o tym, jak forma prefiksu ++x
różni się od formy postfiksowej x++
. Ale operatorzy ci są trudni do przemyślenia, więc dla pewności, że zrozumiałeś, być może napisałeś mały, niewielki program testowy obejmujący coś podobnego
int x = 5;
printf("%d %d %d\n", x, ++x, x++);
Ale, ku twojemu zdziwieniu, ten program nie pomógł ci zrozumieć - wydrukował jakieś dziwne, nieoczekiwane, niewytłumaczalne wyjście, sugerując, że może ++
robi coś zupełnie innego, zupełnie nie tak, jak ci się wydawało.
A może patrzysz na trudny do zrozumienia wyraz
int x = 5;
x = x++ + ++x;
printf("%d\n", x);
Być może ktoś dał ci ten kod jako łamigłówkę. Ten kod również nie ma sensu, szczególnie jeśli go uruchomisz - a jeśli skompilujesz i uruchomisz go w dwóch różnych kompilatorach, prawdopodobnie uzyskasz dwie różne odpowiedzi! O co chodzi? Która odpowiedź jest poprawna? (Odpowiedź jest taka, że oboje są lub żaden z nich nie jest).
Jak już słyszałeś, wszystkie te wyrażenia są niezdefiniowane , co oznacza, że język C nie daje żadnej gwarancji, co zrobią. Jest to dziwny i zaskakujący wynik, ponieważ prawdopodobnie myślałeś, że każdy program, który możesz napisać, o ile tylko skompiluje się i uruchomi, wygeneruje unikalne, dobrze zdefiniowane wyjście. Ale w przypadku nieokreślonego zachowania tak nie jest.
Co sprawia, że wyrażenie jest niezdefiniowane? Czy wyrażenia obejmują ++
i --
zawsze są niezdefiniowane? Oczywiście, że nie: są to przydatne operatory, a jeśli użyjesz ich właściwie, są doskonale dobrze zdefiniowane.
Dla wyrażeń mówimy o tym, co sprawia, że są niezdefiniowane, gdy dzieje się zbyt wiele naraz, gdy nie jesteśmy pewni, w jakiej kolejności będą rzeczy, ale kiedy kolejność ma znaczenie dla wyniku, który otrzymujemy.
Wróćmy do dwóch przykładów, których użyłem w tej odpowiedzi. Kiedy pisałem
printf("%d %d %d\n", x, ++x, x++);
pytanie brzmi: przed wywołaniem printf
, czy kompilator oblicza wartość x
najpierw x++
, a może ++x
? Ale okazuje się , że nie wiemy . W C nie ma reguły, która mówi, że argumenty funkcji są oceniane od lewej do prawej, od prawej do lewej lub w innej kolejności. Więc nie możemy powiedzieć, czy kompilator zrobi x
, potem ++x
, potem x++
, albo x++
wtedy ++x
następnie x
, czy jakiś inny porządek. Ale kolejność ma znaczenie, ponieważ w zależności od tego, jakiej kolejności używa kompilator, wyraźnie otrzymamy różne wyniki drukowania printf
.
Co z tym szalonym wyrazem twarzy?
x = x++ + ++x;
Problem z tym wyrażeniem polega na tym, że zawiera trzy różne próby modyfikacji wartości x: (1) x++
część próbuje dodać 1 do x, zapisać nową wartość w x
i zwrócić starą wartość x
; (2) ++x
część próbuje dodać 1 do x, zapisać nową wartość x
i zwrócić nową wartość x
; i (3) x =
część próbuje przypisać sumę pozostałych dwóch z powrotem do x. Które z tych trzech podjętych prób „wygra”? Które z trzech wartości zostaną faktycznie przypisane x
? Znów i być może zaskakujące, w C nie ma reguły, która by nam mówiła.
Możesz sobie wyobrazić, że pierwszeństwo, skojarzenie lub ocena od lewej do prawej mówi ci, w jakiej kolejności rzeczy się dzieją, ale tak nie jest. Możesz mi nie wierzyć, ale uwierz mi na słowo, a powiem to jeszcze raz: pierwszeństwo i asocjatywność nie określają każdego aspektu kolejności oceny wyrażenia w C. W szczególności, jeśli w jednym wyrażeniu jest wiele różne miejsca, w których staramy się przypisać nową wartość do czegoś takiego x
, pierwszeństwo i skojarzenie, nie mówią nam, która z tych prób nastąpi pierwsza, ostatnia lub cokolwiek innego.
Jeśli więc chcesz mieć pewność, że wszystkie twoje programy są dobrze zdefiniowane, jakie wyrażenia możesz napisać, a które nie?
Wszystkie te wyrażenia są w porządku:
y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;
Wszystkie te wyrażenia są niezdefiniowane:
x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);
A ostatnie pytanie brzmi: jak rozpoznać, które wyrażenia są dobrze zdefiniowane, a które wyrażenia są niezdefiniowane?
Jak powiedziałem wcześniej, niezdefiniowane wyrażenia to te, w których jednocześnie dzieje się zbyt wiele, w których nie możesz być pewien, w jakiej kolejności rzeczy się dzieją i gdzie kolejność ma znaczenie:
- Jeśli istnieje jedna zmienna, która jest modyfikowana (przypisywana) w dwóch lub więcej różnych miejscach, skąd wiesz, która modyfikacja nastąpi najpierw?
- Jeśli istnieje zmienna, która jest modyfikowana w jednym miejscu, a jej wartość jest używana w innym miejscu, skąd wiesz, czy używa starej wartości czy nowej wartości?
Jako przykład # 1 w wyrażeniu
x = x++ + ++x;
są trzy próby modyfikacji `x.
Jako przykład # 2 w wyrażeniu
y = x + x++;
oboje używamy wartości x
i modyfikujemy ją.
Oto odpowiedź: upewnij się, że w każdym zapisanym wyrażeniu każda zmienna jest modyfikowana co najwyżej jeden raz, a jeśli zmienna jest modyfikowana, nie próbujesz również użyć wartości tej zmiennej w innym miejscu.