Dlaczego te konstrukcje wykorzystują niezdefiniowane zachowanie przed i po zwiększeniu?

814
#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?
}
Szkatułka
źródło
12
@Jarett, nie, potrzebowałem tylko wskaźników do „sekwencjonowania punktów”. Podczas pracy znalazłem fragment kodu w i = i ++, myślę: „To nie modyfikuje wartości i”. Testowałem i zastanawiałem się dlaczego. Od tego czasu usunąłem tę instrukcję i zastąpiłem ją i ++;
PiX
198
Myślę, że to interesujące, że ZAWSZE wszyscy zakładają, że takie pytania są zadawane, ponieważ pytający chce UŻYWAĆ danego konstruktu. Moje pierwsze założenie było takie, że PiX wie, że są złe, ale jestem ciekawy, dlaczego zachowują się tak, jak robią to na kompilatorze Whataver, którego używał ... I tak, to, co powiedział unWind ... jest niezdefiniowane, może zrobić wszystko. .. w tym JCF (Jump and Catch Fire)
Brian Postow
32
Jestem ciekawy: dlaczego kompilatory nie ostrzegają o konstrukcjach takich jak „u = u ++ + ++ u;” jeśli wynik jest niezdefiniowany?
Naucz się OpenGL ES
5
(i++)nadal ocenia na 1, niezależnie od nawiasów
Drew McGowen
2
Cokolwiek i = (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 zachowanie i = (i++);, wciąż jest to zły kod. Po prostu napiszi++;
Keith Thompson

Odpowiedzi:

566

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

u = (u++);

to podręcznikowy przykład niezdefiniowanego zachowania (patrz wpis Wikipedii o punktach sekwencyjnych ).

rozwijać
źródło
8
@PiX: Rzeczy są niezdefiniowane z wielu możliwych przyczyn. Należą do nich: nie ma wyraźnego „właściwego wyniku”, różne architektury maszyn zdecydowanie faworyzują różne wyniki, istniejąca praktyka nie jest spójna lub wykracza poza zakres normy (np. Jakie nazwy plików są prawidłowe).
Richard
Aby wprowadzić wszystkich w błąd, niektóre takie przykłady są teraz dobrze zdefiniowane w C11, np i = ++i + 1;.
MM
2
Czytając Standard i opublikowane uzasadnienie, jasne jest, dlaczego istnieje koncepcja UB. Standard nigdy nie miał na celu pełnego opisu wszystkiego, co implementacja C musi zrobić, aby była odpowiednia do określonego celu (patrz omówienie zasady „jednego programu”), ale zamiast tego opiera się na osądach implementatorów i chęci stworzenia użytecznych implementacji wysokiej jakości. Wdrożenie wysokiej jakości odpowiednie do programowania systemów niskiego poziomu będzie musiało zdefiniować zachowanie działań, które nie byłyby potrzebne w wysokiej klasy aplikacjach do crunchingu. Zamiast komplikować Standard ...
supercat,
3
... wchodząc w najbardziej szczegółowe informacje na temat tego, które przypadki narożne są zdefiniowane, a które nie są zdefiniowane, autorzy Standardu uznali, że osoby wdrażające powinny być lepiej przygotowane do oceny, jakie rodzaje zachowań będą potrzebne przez rodzaje programów, które mają wspierać . Hiper-modernistyczne kompilatory udają, że pewne działania UB miały sugerować, że żaden program jakościowy nie powinien ich potrzebować, ale Standard i uzasadnienie są niezgodne z tak rzekomym zamiarem.
supercat
1
@jrh: Napisałem tę odpowiedź, zanim zdałem sobie sprawę z tego, jak bezradna stała się filozofia hiper-modernistyczna. To, co mnie denerwuje, to przejście od „Nie musimy oficjalnie rozpoznawać tego zachowania, ponieważ platformy, na których jest ono potrzebne, i tak może je obsługiwać” do „Możemy usunąć to zachowanie bez zapewnienia użytecznej zamiany, ponieważ nigdy nie zostało rozpoznane, a tym samym żadnego kodu potrzebujący został zepsuty ". Wiele zachowań powinno już dawno zostać zaniechanych na rzecz zastąpień, które były pod każdym względem lepsze , ale wymagałyby uznania ich zasadności.
supercat
78

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:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(Przypuszczam, że instrukcja 0x00000014 była pewnego rodzaju optymalizacją kompilatora?)

badp
źródło
jak mogę uzyskać kod maszynowy? Używam Dev C ++ i
bawiłem się z
5
@ronnieaka gcc evil.c -c -o evil.bini gdb evil.bindisassemble evillub cokolwiek to jest Windows odpowiedniki :)
badp
21
Ta odpowiedź tak naprawdę nie dotyczy pytania Why are these constructs undefined behavior?.
Shafik Yaghmour
9
Nawiasem mówiąc, łatwiej będzie skompilować do asemblera (z gcc -S evil.c), co jest tutaj wszystkim, czego potrzeba. Montaż, a następnie demontaż to tylko ronda.
Kat
50
Dla przypomnienia, jeśli z jakiegokolwiek powodu zastanawiasz się, co robi dany konstrukt - a zwłaszcza jeśli istnieje podejrzenie, że może to być nieokreślone zachowanie - odwieczna rada „po prostu wypróbuj go przy pomocy kompilatora i zobacz” to potencjalnie dość niebezpieczne. W najlepszym przypadku dowiesz się, co robi w tej wersji twojego kompilatora w dzisiejszych okolicznościach . Będzie nie wiele nauczyć, jeśli coś o tym, co to jest gwarantowana zrobić. Ogólnie rzecz biorąc, „po prostu wypróbuj go przy użyciu kompilatora” prowadzi do nieeksportowalnych programów, które działają tylko z kompilatorem.
Steve Summit
64

Myślę, że odpowiednimi częściami normy C99 są 6.5 Wyrażenia, §2

Pomiędzy poprzednim i następnym punktem sekwencji obiekt ma zmodyfikowaną wartość przechowywaną co najwyżej raz na podstawie oceny wyrażenia. Ponadto wcześniejszą wartość należy odczytać tylko w celu ustalenia wartości, która ma być przechowywana.

i 6.5.16 Operatorzy przydziałów, §4:

Kolejność oceny operandów jest nieokreślona. W przypadku próby zmodyfikowania wyniku operatora przypisania lub uzyskania dostępu do niego po następnym punkcie sekwencji zachowanie jest niezdefiniowane.

Christoph
źródło
2
Czy powyższe oznaczałoby, że „i = i = 5;” byłoby zachowaniem niezdefiniowanym?
supercat
1
@supercat, o ile mi wiadomo, i=i=5jest także niezdefiniowanym zachowaniem
dhein
2
@Zaibis: Zasadę, którą lubię stosować w większości miejsc, obowiązuje zasada, że ​​teoretycznie platforma wieloprocesorowa może implementować coś takiego A=B=5;jak „Blokada zapisu A; Blokada zapisu B; Przechowywanie 5 do A; Przechowywanie 5 do B; Odblokowywanie B ; Odblokuj A; ”oraz wyrażenie takie C=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;a C=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ątek I=I=5;...
supercat
1
... a kompilator nie zauważył, że oba zapisy były w tej samej lokalizacji (jeśli jedna lub obie wartości zawierają wskaźniki, co może być trudne do ustalenia), wygenerowany kod może zostać zakleszczony. Nie sądzę, aby jakiekolwiek rzeczywiste implementacje implementowały takie blokowanie jako część ich normalnego zachowania, ale byłoby to dopuszczalne zgodnie ze standardem, a jeśli sprzęt mógłby tanio implementować takie zachowania, może to być przydatne. Na dzisiejszym sprzęcie takie zachowanie byłoby zbyt drogie, aby wprowadzić je jako domyślne, ale to nie znaczy, że zawsze tak będzie.
supercat
1
@ superupat, ale czy sama reguła dostępu do punktu sekwencji z c99 nie wystarczy, aby zadeklarować to jako niezdefiniowane zachowanie? Więc nie ma znaczenia, co technicznie mógłby wdrożyć sprzęt?
dhein
55

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)

Biorąc pod uwagę dowolne dwie oceny Ai B, jeżeli Ajest to sekwencjonowane wcześniej B, wykonanie Apoprzedza wykonanie B.

Bez konsekwencji:

Jeśli Anie jest sekwencjonowane przed lub po B, to Ai nie Bsą sekwencjonowane.

Oceny mogą być jedną z dwóch rzeczy:

  • obliczenia wartości , które opracowują wynik wyrażenia; i
  • efekty uboczne , które są modyfikacjami obiektów.

Punkt sekwencji:

Obecność punktu sekwencyjnego między oceną wyrażeń Ai Bimplikuje, że każde obliczenie wartości i związany z nią efekt ubocznyA jest sekwencjonowane przed każdym obliczeniem wartości i związanym efektem ubocznymB .

Teraz dochodzę do pytania, dla takich wyrażeń jak

int i = 1;
i = i++;

standard mówi, że:

6.5 Wyrażenia:

Jeśli efekt uboczny w skalarnej przedmiotu jest unsequenced względem albo innego efektu ubocznego na tym samym obiekcie skalarną lub obliczania wartości z wykorzystaniem wartości skalarnej samego obiektu zachowanie jest niezdefiniowany . [...]

Dlatego powyższe wyrażenie wywołuje UB, ponieważ dwa skutki uboczne na tym samym obiekcie nie isą względem siebie konsekwentne. Oznacza to, że nie jest ustalane, czy efekt uboczny przez przypisanie do izostanie 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 ilewej stronie przypisania be, ila po prawej stronie przypisania (w wyrażeniu i++) być ir, wtedy wyrażenie będzie jak

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Ważną kwestią dotyczącą ++operatora Postfix jest to, że:

tylko dlatego, że ++następuje po zmiennej, nie oznacza, że ​​przyrost następuje późno . Przyrost może nastąpić tak długo, jak kompilator lubi, pod warunkiem, że kompilator zapewni, że zostanie użyta oryginalna wartość .

Oznacza to, że wyrażenie il = ir++może być ocenione jako

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

lub

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

co daje dwa różne wyniki 1i 2które zależy od sekwencji efektów ubocznych według przypisania, ++a zatem wywołuje UB.

haccks
źródło
52

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 c996.5 paragraf 3 mówi ( podkreślenie moje ):

Grupowanie operatorów i operandów jest wskazywane przez składnię.74) Z wyjątkiem przypadków określonych później (dla operatorów call-function (), &&, ||,?: I przecinków), kolejność oceny podwyrażeń i kolejność w które działania niepożądane występują, są nieokreślone.

Więc kiedy mamy taką linię:

i = i++ + ++i;

Nie wiemy, czy i++czy ++inajpierw 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, uitp ..) więcej niż raz pomiędzy punktami sekwencji . Z projektu standardowego 6.5ustępu 2 ( wyróżnienie moje ):

Pomiędzy poprzednim i następnym punktem sekwencji obiekt ma zmodyfikowaną wartość przechowywaną co najwyżej raz na podstawie oceny wyrażenia. Ponadto wcześniejszą wartość należy odczytać tylko w celu ustalenia wartości, która ma być przechowywana .

przytacza następujące przykłady kodu jako niezdefiniowane:

i = ++i + 1;
a[i++] = i; 

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:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

Nieokreślone zachowanie jest zdefiniowane w projekcie standardu c99 w sekcji 3.4.4jako:

użycie nieokreślonej wartości lub inne zachowanie, w przypadku gdy niniejsza Norma Międzynarodowa zapewnia dwie lub więcej możliwości i nie nakłada żadnych dalszych wymagań, na które w żadnym wypadku została wybrana

a niezdefiniowane zachowanie jest zdefiniowane w sekcji 3.4.3jako:

zachowanie, po zastosowaniu niezbywalnej lub błędnej konstrukcji programu lub błędnych danych, dla których niniejszy standard międzynarodowy nie nakłada żadnych wymagań

i zauważa, że:

Możliwe niezdefiniowane zachowanie obejmuje od całkowitego zignorowania sytuacji z nieprzewidzianymi rezultatami, zachowania podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z wydaniem komunikatu diagnostycznego lub bez niego), aż do zakończenia tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego).

Shafik Yaghmour
źródło
33

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:

i = i + 1

C ma oczywiście przydatny skrót:

i++

Oznacza to: „dodaj 1 do i i przypisz wynik z powrotem do i”. Więc jeśli zbudujemy kombinację tych dwóch, pisząc

i = i++

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 .

Steve Summit
źródło
1
Dość paskudne haczyka w odniesieniu do niezdefiniowanej zachowanie jest, że podczas gdy stosowane pewności o 99,9% kompilatory użyć *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 ( elseklauzula jest konieczna, aby pozwolić kompilator zoptymalizuje to, ifczego wymagałyby niektóre nowsze kompilatory).
supercat
@supercat Teraz uważam, że każdy kompilator, który jest wystarczająco „inteligentny”, aby przeprowadzić tego rodzaju optymalizację, musi być również wystarczająco inteligentny, aby zerknąć na assertinstrukcje, aby programista mógł poprzedzić dany wiersz prostym assert(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.)
Steve Summit
25

Często pytanie to jest połączone jako duplikat pytań związanych z podobnym kodem

printf("%d %d\n", i, i++);

lub

printf("%d %d\n", ++i, i++);

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:

x = i++ + i++;

W następującym oświadczeniu:

printf("%d %d\n", ++i, i++);

kolejność oceny argumentów na printf()to nieokreślone . Oznacza to, wyrażenia i++i ++imogą być oceniane w dowolnej kolejności. Standard C11 zawiera kilka odpowiednich opisów na ten temat:

Załącznik J, nieokreślone zachowania

Kolejność, w której desygnator funkcji, argumenty i podwyrażenia w argumentach są oceniane w wywołaniu funkcji (6.5.2.2).

3.4.4, nieokreślone zachowanie

Użycie nieokreślonej wartości lub inne zachowanie, w przypadku gdy niniejszy standard międzynarodowy zapewnia dwie lub więcej możliwości i nie nakłada żadnych dalszych wymagań, w odniesieniu do których zostałby wybrany w żadnym przypadku.

PRZYKŁAD Przykładem nieokreślonego zachowania jest kolejność, w jakiej są oceniane argumenty funkcji.

Zachowanie nieokreślona sama w sobie nie jest problemem. Rozważ ten przykład:

printf("%d %d\n", ++x, y++);

To również ma nieokreślone zachowanie, ponieważ kolejność oceny ++xi y++jest nieokreślona. Ale jest to całkowicie legalne i prawidłowe oświadczenie. W tym oświadczeniu nie ma niezdefiniowanego zachowania. Ponieważ modyfikacje ( ++xi y++) są wykonywane dla różnych obiektów.

Co renderuje następujące oświadczenie

printf("%d %d\n", ++i, i++);

ponieważ nieokreślonym zachowaniem jest fakt, że te dwa wyrażenia modyfikują ten sam obiekt ibez 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:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

Operator przecinka ocenia operandy od lewej do prawej i zwraca tylko wartość ostatniego operandu. Więc j = (++i, i++);, ++iprzyrosty ido 6i i++plony stara wartość i( 6), która jest przypisana j. Następnie istaje się z 7powodu przyrostu.

Więc jeśli przecinek w wywołaniu funkcji miałby być przecinkiem, to wtedy

printf("%d %d\n", ++i, i++);

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.

PP
źródło
Ta sekwencja 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?
kavadias
@kavadias Ta instrukcja printf obejmuje niezdefiniowane zachowanie z tego samego powodu, co wyjaśniono powyżej. Piszesz bi cw 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-pointktóra może pomóc w ich znalezieniu.
PP
23

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:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

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 izwię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ęp i, 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 zmienna ijest używana w obu miejscach, ale jeśli procedura akceptuje odwołania do dwóch wskaźników pi q, i używa (*p)oraz (*q)w powyższym wyrażeniu (zamiast używaćidwa 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, jak pi dla q.

supercat
źródło
16

Podczas gdy składnia wyrażeń podoba a = a++lub a++ + a++jest prawną, zachowanie tych konstruktów jest niezdefiniowany , ponieważ będą w standardzie C nie jest przestrzegane. C99 6.5p2 :

  1. Pomiędzy poprzednim i następnym punktem sekwencji obiekt ma zmodyfikowaną wartość przechowywaną co najwyżej raz na podstawie oceny wyrażenia. [72] Ponadto wcześniejszą wartość odczytuje się tylko w celu ustalenia wartości, która ma zostać zapisana [73]

W przypisie 73 wyjaśniono to dalej

  1. Ten akapit wyświetla niezdefiniowane wyrażenia, takie jak

    i = ++i + 1;
    a[i++] = i;

    jednocześnie pozwalając

    i = i + 1;
    a[i] = i;

Różne punkty sekwencji są wymienione w załączniku C do C11 (i C99 ):

  1. Poniżej przedstawiono punkty sekwencji opisane w 5.1.2.3:

    • Pomiędzy ocenami desygnatora funkcji a rzeczywistymi argumentami w wywołaniu funkcji i rzeczywistym wywołaniem. (6.5.2.2).
    • Między ocenami pierwszego i drugiego argumentu następujących operatorów: logiczne AND && (6.5.13); logiczne OR || (6.5.14); przecinek (6.5.17).
    • Między ocenami pierwszego argumentu warunkowego? : operator i cokolwiek z drugiego i trzeciego argumentu jest oceniane (6.5.15).
    • Koniec pełnego deklaratora: deklaratory (6.7.6);
    • Między oceną pełnego wyrażenia a następnym pełnym wyrażeniem do oceny. Oto pełne wyrażenia: inicjator, który nie jest częścią literału złożonego (6.7.9); wyrażenie w wyrażeniu wyrażeniowym (6.8.3); kontrolne wyrażenie instrukcji wyboru (if lub switch) (6.8.4); kontrolujące wyrażenie instrukcji while lub do (6.8.5); każde (opcjonalne) wyrażenie instrukcji for (6.8.5.3); (opcjonalne) wyrażenie w instrukcji return (6.8.6.4).
    • Bezpośrednio przed powrotem funkcji biblioteki (7.1.4).
    • Po działaniach związanych z każdym sformatowanym specyfikatorem konwersji funkcji wejścia / wyjścia (7.21.6, 7.29.2).
    • Bezpośrednio przed i bezpośrednio po każdym wywołaniu funkcji porównawczej, a także między każdym wywołaniem funkcji porównawczej a każdym ruchem obiektów przekazywanych jako argumenty do tego wywołania (7.22.5).

Brzmienie tego samego akapitu w C11 jest następujące:

  1. Jeśli efekt uboczny na obiekcie skalarnym nie ma wpływu na inny efekt uboczny na ten sam obiekt skalarny lub obliczenia wartości z wykorzystaniem wartości tego samego obiektu skalarnego, zachowanie jest niezdefiniowane. Jeśli istnieje wiele dopuszczalnych kolejności podwyrażeń wyrażenia, zachowanie jest niezdefiniowane, jeśli taki niepowodowany efekt uboczny występuje w którymkolwiek z porządków. 84)

Możesz wykryć takie błędy w programie, na przykład używając najnowszej wersji GCC z -Walli -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:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function main’:
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

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

j = (i ++, ++ i);

jest dobrze zdefiniowany i zwiększy się io 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 pisania

i += 2;
j = i;

Jednak ,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 funkcji

int i = 0;
printf("%d %d\n", i++, ++i, i);

ma nieokreśloną problem , ponieważ nie ma sensu sekwencji między oceną i++i ++iw jej argumenty , zaś wartość ijest modyfikowane w dwa, zarówno przez i++i ++ipomiędzy poprzednim i następnym punkcie sekwencji.

Antti Haapala
źródło
14

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 = i++;
i = i++ + ++i;

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.

while(*src++ = *dst++);

Powyżej jest powszechną praktyką kodowania podczas kopiowania / analizy ciągów.

Nikhil Vidhani
źródło
Oczywiście nie dotyczy to różnych zmiennych w ramach jednego wyrażenia. Gdyby tak się stało, byłby to kompletny błąd projektowy! Wszystko, czego potrzebujesz w drugim przykładzie, to przyrost między zakończeniem instrukcji a początkiem następnego, co jest gwarantowane właśnie dzięki koncepcji punktów sekwencji w centrum tego wszystkiego.
underscore_d
11

W /programming/29505280/incrementing-array-index-in-c ktoś zapytał o zdanie takie jak:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

który drukuje 7 ... OP oczekiwał, że wydrukuje 6.

Te ++iprzyrosty 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 ++iwykonany, wówczas wartości k[]zostały odczytane, to ostatnia ++iwtedy k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

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ś).

TomOnTime
źródło
5

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.

Pomiędzy poprzednim i następnym punktem sekwencji obiekt ma zmodyfikowaną wartość przechowywaną co najwyżej raz na podstawie oceny wyrażenia. Ponadto wcześniejszą wartość należy odczytać tylko w celu ustalenia wartości, która ma być przechowywana.

Punkty sekwencji w wyrażeniu jak i=i++są przed i=i po i++.

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 wpisowi expression-statementw 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.

i=i++

można interpretować jako

tmp = i
i=i+1
i = tmp

lub jako

tmp = i
i = tmp
i=i+1

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 .

Alinsoar
źródło
Jak ta odpowiedź dodała nowe do istniejących odpowiedzi? Wyjaśnienia i=i++są bardzo podobne do tej odpowiedzi .
haccks
@ Haccks Nie przeczytałem innych odpowiedzi. Chciałem wyjaśnić w swoim własnym języku, czego nauczyłem się ze wspomnianego dokumentu z oficjalnej strony ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar
5

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 ++xróż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ść xnajpierw 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 ++xnastę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 xi zwrócić starą wartość x; (2) ++xczęść próbuje dodać 1 do x, zapisać nową wartość xi 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:

  1. 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?
  2. 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 xi 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.

Steve Summit
źródło
3

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:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }

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

W GCC C ++ pierwszeństwo operatorów kontroluje kolejność, w jakiej oceniane są poszczególne operatory

równoważny kod w zdefiniowanym zachowaniu C ++, jak GCC rozumie:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Następnie przechodzimy do Visual Studio . Visual Studio 2015, otrzymujesz:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

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:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

zgodnie z dokumentacją Visual Studio w Precedence and Order of Evaluation :

Gdy kilku operatorów pojawia się razem, mają oni równe pierwszeństwo i są oceniane według ich powiązania. Operatory w tabeli opisano w rozdziałach rozpoczynających się od operatorów Postfix.

Mohamed El-Nakib
źródło
1
Zredagowałem pytanie, aby dodać UB do oceny argumentów funkcji, ponieważ pytanie to jest często używane do tego celu. (Ostatni przykład)
Antti Haapala
1
Pytanie dotyczy także c teraz , a nie C ++
Antti Haapala