#include <stdio.h>
int main(void)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 3
i = 1;
i = (i++);
printf("%d\n", i); // 2 Should be 1, no ?
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 1
u = 1;
u = (u++);
printf("%d\n", u); // 2 Should also be one, no ?
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); // 3 (Should be the same as u ?)
int w = 0;
printf("%d %d\n", ++w, w); // shouldn't this print 1 1
int x[2] = { 5, 8 }, y = 0;
x[y] = y ++;
printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
814
(i++)
nadal ocenia na 1, niezależnie od nawiasówi = (i++);
zamierzano zrobić, z pewnością istnieje wyraźniejszy sposób na napisanie tego. Byłoby to prawdą, nawet gdyby było dobrze zdefiniowane. Nawet w Javie, która określa zachowaniei = (i++);
, wciąż jest to zły kod. Po prostu napiszi++;
Odpowiedzi:
C ma pojęcie niezdefiniowanego zachowania, tzn. Niektóre konstrukcje językowe są poprawne pod względem składniowym, ale nie można przewidzieć zachowania po uruchomieniu kodu.
O ile mi wiadomo, standard nie mówi wprost, dlaczego istnieje pojęcie niezdefiniowanego zachowania. Moim zdaniem, po prostu dlatego, że projektanci języków chcieli mieć pewną swobodę w semantyce, zamiast np. Wymagać, aby wszystkie implementacje obsługiwały przepełnienie liczb całkowitych w dokładnie taki sam sposób, co najprawdopodobniej wiązałoby się z poważnymi kosztami wydajności, po prostu porzucili zachowanie niezdefiniowany, więc jeśli napiszesz kod powodujący przepełnienie liczb całkowitych, wszystko może się zdarzyć.
Mając to na uwadze, dlaczego te „problemy”? Język wyraźnie mówi, że pewne rzeczy prowadzą do nieokreślonego zachowania . Nie ma problemu, nie ma w tym „powinności”. Jeśli niezdefiniowane zachowanie ulegnie zmianie, gdy zadeklarowana zostanie jedna ze zmiennych
volatile
, to niczego nie udowodni ani nie zmieni. Jest niezdefiniowany ; nie możesz uzasadnić tego zachowania.Twój najbardziej interesujący przykład, ten z
to podręcznikowy przykład niezdefiniowanego zachowania (patrz wpis Wikipedii o punktach sekwencyjnych ).
źródło
i = ++i + 1;
.Wystarczy skompilować i zdemontować linię kodu, jeśli jesteś tak skłonny wiedzieć, jak dokładnie otrzymujesz to, co dostajesz.
Oto, co otrzymuję na moim komputerze, wraz z tym, co myślę, że się dzieje:
(Przypuszczam, że instrukcja 0x00000014 była pewnego rodzaju optymalizacją kompilatora?)
źródło
gcc evil.c -c -o evil.bin
igdb evil.bin
→disassemble evil
lub cokolwiek to jest Windows odpowiedniki :)Why are these constructs undefined behavior?
.gcc -S evil.c
), co jest tutaj wszystkim, czego potrzeba. Montaż, a następnie demontaż to tylko ronda.Myślę, że odpowiednimi częściami normy C99 są 6.5 Wyrażenia, §2
i 6.5.16 Operatorzy przydziałów, §4:
źródło
i=i=5
jest także niezdefiniowanym zachowaniemA=B=5;
jak „Blokada zapisu A; Blokada zapisu B; Przechowywanie 5 do A; Przechowywanie 5 do B; Odblokowywanie B ; Odblokuj A; ”oraz wyrażenie takieC=A+B;
jak„ Blokada odczytu A; Blokada odczytu B; Oblicz A + B; Odblokuj A i B; Blokada zapisu C; Zapisz wynik; Odblokuj C; ”. Zapewniłoby to, że jeśli jeden wątek zrobiłby to samo,A=B=5;
aC=A+B;
drugi zrobiłby ten drugi, albo zobaczyłby, że oba zapisy miały miejsce, albo żaden. Potencjalnie przydatna gwarancja. Gdyby jednak jeden wątekI=I=5;
...Większość odpowiedzi przytoczonych tutaj ze standardu C podkreśla, że zachowanie tych konstruktów jest niezdefiniowane. Aby zrozumieć, dlaczego zachowanie tych konstrukcji jest niezdefiniowane , najpierw zrozummy te terminy w świetle standardu C11:
Sekwencjonowany: (5.1.2.3)
Bez konsekwencji:
Oceny mogą być jedną z dwóch rzeczy:
Punkt sekwencji:
Teraz dochodzę do pytania, dla takich wyrażeń jak
standard mówi, że:
6.5 Wyrażenia:
Dlatego powyższe wyrażenie wywołuje UB, ponieważ dwa skutki uboczne na tym samym obiekcie nie
i
są względem siebie konsekwentne. Oznacza to, że nie jest ustalane, czy efekt uboczny przez przypisanie doi
zostanie wykonany przed czy po efektem ubocznym do++
.W zależności od tego, czy przypisanie nastąpi przed przyrostem, czy po nim, zostaną wygenerowane różne wyniki, i to jest przypadek nieokreślonego zachowania .
Pozwala zmienić nazwę po
i
lewej stronie przypisania be,il
a po prawej stronie przypisania (w wyrażeniui++
) byćir
, wtedy wyrażenie będzie jakWażną kwestią dotyczącą
++
operatora Postfix jest to, że:Oznacza to, że wyrażenie
il = ir++
może być ocenione jakolub
co daje dwa różne wyniki
1
i2
które zależy od sekwencji efektów ubocznych według przypisania,++
a zatem wywołuje UB.źródło
Nie można tak naprawdę wyjaśnić tego zachowania , ponieważ wywołuje ono zarówno zachowanie nieokreślone, jak i zachowanie nieokreślone , więc nie możemy poczynić żadnych ogólnych prognoz dotyczących tego kodu, chociaż jeśli czytasz pracę Olve Maudal, taką jak Deep C oraz Nieokreślone i Nieokreślone, czasami możesz zrobić coś dobrego zgaduje w bardzo konkretnych przypadkach z określonym kompilatorem i środowiskiem, ale proszę, nie rób tego nigdzie w pobliżu produkcji.
Przechodząc do nieokreślonego zachowania , w projekcie standardowej sekcji c99
6.5
paragraf 3 mówi ( podkreślenie moje ):Więc kiedy mamy taką linię:
Nie wiemy, czy
i++
czy++i
najpierw będą oceniane. Ma to na celu przede wszystkim dać kompilatorowi lepsze opcje optymalizacji .Mamy też niezdefiniowanej zachowanie również tutaj, ponieważ program jest zmodyfikowanie zmiennych (
i
,u
itp ..) więcej niż raz pomiędzy punktami sekwencji . Z projektu standardowego6.5
ustępu 2 ( wyróżnienie moje ):przytacza następujące przykłady kodu jako niezdefiniowane:
We wszystkich tych przykładach kod próbuje zmodyfikować obiekt więcej niż jeden raz w tym samym punkcie sekwencji, co zakończy się
;
w każdym z następujących przypadków:Nieokreślone zachowanie jest zdefiniowane w projekcie standardu c99 w sekcji
3.4.4
jako:a niezdefiniowane zachowanie jest zdefiniowane w sekcji
3.4.3
jako:i zauważa, że:
źródło
Innym sposobem odpowiedzi na to pytanie, zamiast zagubienia się w tajemniczych szczegółach punktów sekwencji i nieokreślonym zachowaniu, jest po prostu pytanie, co one mają znaczyć? Co programista próbował zrobić?
Pierwszy fragment, o który pytam
i = i++ + ++i
, jest dość szalony w mojej książce. Nikt nigdy nie napisałby tego w prawdziwym programie, nie jest oczywiste, co robi, nie ma możliwego do pomyślenia algorytmu, który ktoś próbowałby kodować, co skutkowałoby tą szczególną wymyśloną sekwencją operacji. A ponieważ nie jest dla ciebie i dla mnie oczywiste, co powinien zrobić, w mojej książce jest w porządku, jeśli kompilator nie jest w stanie ustalić, co powinien zrobić.Drugi fragment
i = i++
jest nieco łatwiejszy do zrozumienia. Ktoś najwyraźniej próbuje zwiększyć i i przypisać wynik z powrotem do i. Ale jest kilka sposobów na zrobienie tego w C. Najbardziej podstawowy sposób dodania 1 do i i przypisania wyniku do i, jest taki sam w prawie każdym języku programowania:C ma oczywiście przydatny skrót:
Oznacza to: „dodaj 1 do i i przypisz wynik z powrotem do i”. Więc jeśli zbudujemy kombinację tych dwóch, pisząc
tak naprawdę mówimy: „dodaj 1 do i i przypisz wynik z powrotem do i, a następnie przypisz wynik z powrotem do i”. Jesteśmy zdezorientowani, więc nie komplikuje mnie to zbytnio, jeśli kompilator też się zdezorientuje.
Realnie, te szalone wyrażenia są pisane tylko wtedy, gdy ludzie używają ich jako sztucznych przykładów tego, jak powinien działać ++. I oczywiście ważne jest, aby zrozumieć, jak działa ++. Ale jedną praktyczną zasadą używania ++ jest: „Jeśli nie jest oczywiste, co oznacza wyrażenie używające ++, nie pisz go”.
Spędziliśmy niezliczone godziny na comp.lang.c, dyskutując o takich wyrażeniach i dlaczego są niezdefiniowane. Dwie z moich dłuższych odpowiedzi, które naprawdę wyjaśniają, dlaczego, są archiwizowane w Internecie:
Zobacz także pytanie 3.8 i pozostałe pytania w sekcji 3 listy C FAQ .
źródło
*p=(*q)++;
oznaczaćif (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;
, że nie jest to przypadek. Hipernowoczesne C wymagałoby napisania czegoś w rodzaju drugiego sformułowania (chociaż nie ma standardowego sposobu wskazania, że kod nie dba o to, co jest w środku*p
), aby osiągnąć poziom wydajności kompilatorów zastosowanych w tym pierwszym (else
klauzula jest konieczna, aby pozwolić kompilator zoptymalizuje to,if
czego wymagałyby niektóre nowsze kompilatory).assert
instrukcje, aby programista mógł poprzedzić dany wiersz prostymassert(p != q)
. (Oczywiście wzięcie tego kursu wymagałoby również przepisania,<assert.h>
aby nie usuwać asercji wprost w wersjach bez debugowania, ale raczej zamienić je w coś__builtin_assert_disabled()
, co właściwy kompilator może zobaczyć, a następnie nie emitować kodu.)Często pytanie to jest połączone jako duplikat pytań związanych z podobnym kodem
lub
lub podobne warianty.
Chociaż jest to również niezdefiniowane zachowanie, jak już powiedziano, istnieją subtelne różnice, gdy
printf()
występują w porównaniu do stwierdzenia takiego jak:W następującym oświadczeniu:
kolejność oceny argumentów na
printf()
to nieokreślone . Oznacza to, wyrażeniai++
i++i
mogą być oceniane w dowolnej kolejności. Standard C11 zawiera kilka odpowiednich opisów na ten temat:Załącznik J, nieokreślone zachowania
3.4.4, nieokreślone zachowanie
Zachowanie nieokreślona sama w sobie nie jest problemem. Rozważ ten przykład:
To również ma nieokreślone zachowanie, ponieważ kolejność oceny
++x
iy++
jest nieokreślona. Ale jest to całkowicie legalne i prawidłowe oświadczenie. W tym oświadczeniu nie ma niezdefiniowanego zachowania. Ponieważ modyfikacje (++x
iy++
) są wykonywane dla różnych obiektów.Co renderuje następujące oświadczenie
ponieważ nieokreślonym zachowaniem jest fakt, że te dwa wyrażenia modyfikują ten sam obiekt
i
bez pośredniego punktu sekwencji .Innym szczegółem jest to, że przecinek biorący udział w wywołaniu printf () jest separatorem , a nie operatorem przecinka .
Jest to ważne rozróżnienie, ponieważ operator przecinka wprowadza punkt sekwencyjny między oceną ich operandów, co sprawia, że następujące legalne:
Operator przecinka ocenia operandy od lewej do prawej i zwraca tylko wartość ostatniego operandu. Więc
j = (++i, i++);
,++i
przyrostyi
do6
ii++
plony stara wartośći
(6
), która jest przypisanaj
. Następniei
staje się z7
powodu przyrostu.Więc jeśli przecinek w wywołaniu funkcji miałby być przecinkiem, to wtedy
nie będzie problemu. Ale wywołuje niezdefiniowane zachowanie, ponieważ przecinek tutaj jest separatorem .
Dla tych, którzy są nowi w nieokreślonym zachowaniu , skorzystaliby na czytaniu Co każdy programista C powinien wiedzieć o nieokreślonym zachowaniu, aby zrozumieć koncepcję i wiele innych wariantów nieokreślonego zachowania w C.
Ten post: Nieokreślone, nieokreślone i zdefiniowane w implementacji zachowanie jest również istotne.
źródło
int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));
wydaje się dawać stabilne zachowanie (ocena argumentów od prawej do lewej w gcc v7.3.0; wynik „a = 110 b = 40 c = 60”). Czy to dlatego, że przypisania są uważane za „pełne zdania” i tym samym wprowadzają punkt sekwencyjny? Czy nie powinno to prowadzić do oceny argumentów / stwierdzeń od lewej do prawej? Czy może to tylko przejaw niezdefiniowanego zachowania?b
ic
w 3rd i 4th argumentów odpowiednio i czytania w 2. argument. Ale nie ma sekwencji między tymi wyrażeniami (drugi, trzeci i czwarty argument). gcc / clang ma również opcję,-Wsequence-point
która może pomóc w ich znalezieniu.Chociaż jest mało prawdopodobne, aby jakiekolwiek kompilatory i procesory faktycznie to zrobiły, zgodnie ze standardem C legalne byłoby, aby kompilator zaimplementował „i ++” za pomocą sekwencji:
Chociaż nie sądzę, aby jakikolwiek procesor obsługiwał sprzęt, aby umożliwić wydajne wykonanie takiej czynności, można łatwo wyobrazić sobie sytuacje, w których takie zachowanie ułatwiłoby kod wielowątkowy (np. Gwarantowałoby to, że jeśli dwa wątki spróbują wykonać powyższe czynności sekwencja naraz
i
zwiększy się o dwa) i nie jest całkowicie wykluczone, że jakiś przyszły procesor może zapewnić coś takiego.Jeśli kompilator miałby pisać
i++
jak wskazano powyżej (legalny zgodnie ze standardem) i miałby przeplatać powyższe instrukcje podczas oceny ogólnego wyrażenia (również legalnego), a jeśli nie zdarzyłby się komunikat, że jedna z pozostałych instrukcji się wydarzyła aby uzyskać dostępi
, kompilator może (i legalnie) wygenerować sekwencję instrukcji, która zablokuje się. Z pewnością kompilator niemal na pewno wykryłby problem w przypadku, gdy ta sama zmiennai
jest używana w obu miejscach, ale jeśli procedura akceptuje odwołania do dwóch wskaźnikówp
iq
, i używa(*p)
oraz(*q)
w powyższym wyrażeniu (zamiast używaći
dwa razy) kompilator nie byłby zobowiązany do rozpoznania lub uniknięcia impasu, który wystąpiłby, gdyby adres tego samego obiektu został przekazany zarówno dla, jakp
i dlaq
.źródło
Podczas gdy składnia wyrażeń podoba
a = a++
luba++ + a++
jest prawną, zachowanie tych konstruktów jest niezdefiniowany , ponieważ będą w standardzie C nie jest przestrzegane. C99 6.5p2 :W przypisie 73 wyjaśniono to dalej
Różne punkty sekwencji są wymienione w załączniku C do C11 (i C99 ):
Brzmienie tego samego akapitu w C11 jest następujące:
Możesz wykryć takie błędy w programie, na przykład używając najnowszej wersji GCC z
-Wall
i-Werror
, a wtedy GCC wprost odmówi skompilowania twojego programu. Poniżej przedstawiono dane wyjściowe gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:Ważne jest, aby wiedzieć, co to jest punkt sekwencyjny - a co to jest punkt sekwencyjny, a co nie . Na przykład operator przecinka jest punktem sekwencyjnym, więc
jest dobrze zdefiniowany i zwiększy się
i
o jeden, dając starą wartość, odrzuć tę wartość; następnie u operatora przecinka rozstrzygnij skutki uboczne; a następnie przyrosti
o jeden, a wynikowa wartość staje się wartością wyrażenia - tzn. jest to po prostu wymyślony sposób pisania,j = (i += 2)
który po raz kolejny jest „sprytnym” sposobem pisaniaJednak
,
listy argumentów funkcji nie są są przecinkami i nie ma punktu sekwencji między ocenami różnych argumentów; zamiast tego ich oceny nie odnoszą się do siebie; więc wywołanie funkcjima nieokreśloną problem , ponieważ nie ma sensu sekwencji między oceną
i++
i++i
w jej argumenty , zaś wartośći
jest modyfikowane w dwa, zarówno przezi++
i++i
pomiędzy poprzednim i następnym punkcie sekwencji.źródło
Standard C mówi, że zmienna powinna być przypisana najwyżej raz między dwoma punktami sekwencji. Na przykład średnik to punkt sekwencyjny.
Więc każde oświadczenie formularza:
i tak dalej naruszają tę zasadę. Standard mówi również, że zachowanie jest niezdefiniowane i nieokreślone. Niektóre kompilatory wykrywają je i dają pewne wyniki, ale nie jest to zgodne ze standardem.
Jednak dwie różne zmienne mogą być zwiększane między dwoma punktami sekwencji.
Powyżej jest powszechną praktyką kodowania podczas kopiowania / analizy ciągów.
źródło
W /programming/29505280/incrementing-array-index-in-c ktoś zapytał o zdanie takie jak:
który drukuje 7 ... OP oczekiwał, że wydrukuje 6.
Te
++i
przyrosty nie są gwarantowane wszystkim kompletna przed resztą obliczeniach. W rzeczywistości różne kompilatory uzyskają tutaj różne wyniki. W przykładzie, który podałeś, pierwsze 2++i
wykonany, wówczas wartościk[]
zostały odczytane, to ostatnia++i
wtedyk[]
.Nowoczesne kompilatory bardzo dobrze to zoptymalizują. W rzeczywistości być może lepszy niż kod, który pierwotnie napisałeś (zakładając, że zadziałał tak, jak chciałeś).
źródło
Dobre wyjaśnienie tego, co dzieje się w tego rodzaju obliczeniach, znajduje się w dokumencie n1188 ze strony ISO W14 .
Wyjaśniam pomysły.
Główną zasadą normy ISO 9899, która ma zastosowanie w tej sytuacji, jest 6,5p2.
Punkty sekwencji w wyrażeniu jak
i=i++
są przedi=
i poi++
.W artykule, który cytowałem powyżej, wyjaśniono, że można dowiedzieć się, że program składa się z małych pól, z których każde zawiera instrukcje między 2 kolejnymi punktami sekwencji. Punkty sekwencji są zdefiniowane w załączniku C normy, w przypadku
i=i++
2 punktów sekwencji, które ograniczają pełne wyrażenie. Takie wyrażenie jest syntaktycznie równoważne wpisowiexpression-statement
w formie gramatyki Backus-Naur (gramatyka znajduje się w załączniku A do normy).Tak więc kolejność instrukcji w pudełku nie ma wyraźnej kolejności.
można interpretować jako
lub jako
ponieważ obie te formy interpretacji kodu
i=i++
są prawidłowe i ponieważ obie generują różne odpowiedzi, zachowanie jest niezdefiniowane.Tak więc punkt sekwencji można zobaczyć na początku i na końcu każdego pola, z którego składa się program [pola są jednostkami atomowymi w C], a wewnątrz pola kolejność instrukcji nie jest zdefiniowana we wszystkich przypadkach. Zmieniając tę kolejność, czasami można zmienić wynik.
EDYTOWAĆ:
Innym dobrym źródłem do wyjaśnienia takich dwuznaczności są wpisy ze strony c-faq (również opublikowane jako książka ), a mianowicie tutaj i tutaj i tutaj .
źródło
i=i++
są bardzo podobne do tej odpowiedzi .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 postfiksowejx++
. 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ś podobnegoAle, 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
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
pytanie brzmi: przed wywołaniem
printf
, czy kompilator oblicza wartośćx
najpierwx++
, 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 zrobix
, potem++x
, potemx++
, albox++
wtedy++x
następniex
, 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 drukowaniaprintf
.Co z tym szalonym wyrazem twarzy?
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ść wx
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 przypisanex
? 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:
Wszystkie te wyrażenia są niezdefiniowane:
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:
Jako przykład # 1 w wyrażeniu
są trzy próby modyfikacji `x.
Jako przykład # 2 w wyrażeniu
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.
źródło
Powodem jest to, że program działa w nieokreślony sposób. Problem leży w kolejności oceny, ponieważ nie są wymagane żadne punkty sekwencji zgodnie ze standardem C ++ 98 (żadne operacje nie są sekwencjonowane przed kolejną lub po innej zgodnie z terminologią C ++ 11).
Jeśli jednak pozostaniesz przy jednym kompilatorze, zachowanie będzie trwałe, dopóki nie dodasz wywołań funkcji ani wskaźników, co spowodowałoby, że zachowanie byłoby bardziej nieuporządkowane.
Więc najpierw GCC: Używając Nuwen MinGW 15 GCC 7.1 otrzymasz:
}
Jak działa GCC? ocenia wyrażenia podrzędne w kolejności od lewej do prawej dla prawej strony (RHS), a następnie przypisuje wartość do lewej strony (LHS). Dokładnie tak zachowują się Java i C # i definiują ich standardy. (Tak, równoważne oprogramowanie w Javie i C # ma zdefiniowane zachowania). Ocenia każde podwyrażenie jeden po drugim w oświadczeniu RHS w kolejności od lewej do prawej; dla każdego wyrażenia podrzędnego: najpierw obliczana jest ++ c (wstępna inkrementacja), następnie do operacji używana jest wartość c, a następnie inkrementacja c ++).
zgodnie z GCC C ++: Operators
równoważny kod w zdefiniowanym zachowaniu C ++, jak GCC rozumie:
Następnie przechodzimy do Visual Studio . Visual Studio 2015, otrzymujesz:
Jak działa studio wizualne, przyjmuje inne podejście, ocenia wszystkie wyrażenia wstępne w pierwszym przejściu, następnie wykorzystuje wartości zmiennych w operacjach w drugim przejściu, przypisuje od RHS do LHS w trzecim przejściu, a na koniec przechodzi do oceny wszystkich wyrażenia po inkrementacji w jednym przebiegu.
Więc odpowiednik w zdefiniowanym zachowaniu C ++ jako Visual C ++ rozumie:
zgodnie z dokumentacją Visual Studio w Precedence and Order of Evaluation :
źródło