Dlaczego te konstrukcje używają niezdefiniowanego zachowania przed-i po-inkrementacji?

#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 %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   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?
}
Author: jww, 2009-06-04

14 answers

C ma pojęcie niezdefiniowanego zachowania, tzn. niektóre konstrukcje językowe są poprawne składniowo, ale nie można przewidzieć zachowania podczas uruchamiania kodu.

Z tego co wiem, standard nie mówi wprost Dlaczego istnieje pojęcie niezdefiniowanego zachowania. Moim zdaniem jest to po prostu dlatego, że projektanci języka chcieli mieć pewną swobodę w semantyce, zamiast wymagać, aby wszystkie implementacje obsługiwały całkowite przepełnienie w dokładnie taki sam sposób, co byłoby bardzo prawdopodobnie nakładają poważne koszty wydajności, po prostu zostawili zachowanie niezdefiniowane, więc jeśli napiszesz kod, który powoduje przepełnienie liczby całkowitej, wszystko może się zdarzyć.

Więc, mając to na uwadze, dlaczego są to "problemy"? Język wyraźnie mówi, że pewne rzeczy prowadzą do niezdefiniowanego zachowania . Nie ma problemu, nie ma" powinien " zaangażowany. Jeśli nieokreślone zachowanie zmienia się, gdy jedna z zaangażowanych zmiennych jest zadeklarowana volatile, to niczego nie dowodzi ani nie zmienia. On undefined ; nie można rozumować o zachowaniu.

Twój najciekawszy przykład, ten z

u = (u++);

Jest przykładem niezdefiniowanego zachowania w książce tekstowej (zobacz wpis w Wikipedii na sequence points ).

 524
Author: unwind,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-16 09:38:33

Po prostu skompiluj i zdemontuj swoją linię kodu, jeśli jesteś tak skłonny wiedzieć, jak dokładnie dostajesz to, co dostajesz.

To jest to, co dostaję na mojej maszynie, razem 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.

(I... Załóżmy, że instrukcja 0x00000014 była jakimś rodzajem optymalizacji kompilatora?)

 73
Author: badp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-01 23:21:50

Myślę, że odpowiednie części standardu C99 to wyrażenia 6.5, §2

Pomiędzy poprzednim i następnym punktem sekwencji obiekt ma swoją wartość zapisaną modyfikowane co najwyżej raz przez ocenę wyrażenia. Ponadto wcześniejsza wartość należy odczytywać tylko w celu określenia wartości, która ma być przechowywana.

I 6.5.16 operatory przypisania, §4:

Kolejność oceny operandów jest nieokreślona. W przypadku próby modyfikacji na wyniku operatora przyporządkowania lub uzyskania do niego dostępu po kolejnym punkcie sekwencji, zachowanie jest nieokreślone.

 55
Author: Christoph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-04 09:42:43

Zachowanie nie może być wyjaśnione, ponieważ wywołuje zarówno nieokreślone zachowanie, jak i nieokreślone zachowanie, więc nie możemy wprowadzać żadnych ogólnych prognoz na temat tego kodu, chociaż jeśli przeczytasz pracę Olve Maudal, taką jak Deep C i nieokreślone i niezdefiniowane czasami możesz zgadywać w bardzo specyficzne przypadki z konkretnym kompilatorem i środowiskiem, ale proszę nie robić tego w pobliżu produkcji.

Więc idąc dalej do nieokreślonego zachowania, w szkic C99 standard sekcja 6.5 paragraf 3 says ():

Grupowanie operatorów i operandów jest wskazane przez składnię.74) Z wyjątkiem określonych później (dla funkcji-wywołanie (), &&, ||, ?:, i operatory przecinkowe), kolejność oceny podwyrażeń i kolejność, w jakiej następują skutki uboczne, są nieokreślone.

Więc kiedy mamy linię jak to:

i = i++ + ++i;

Nie wiemy, czy i++ lub ++i zostaną ocenione jako pierwsze. Ma to na celu przede wszystkim zapewnienie kompilatorowi lepszych opcji optymalizacji.

Mamy również undefined behavior ponieważ program modyfikuje zmienne(i, u, itd..) więcej niż jeden raz pomiędzy punktami sekwencji . From draft standard section 6.5 paragraph 2( / align = "left" / ):

Między poprzednim a następnym punkt sekwencji obiekt ma swoją wartość zapisaną modyfikowane co najwyżej raz przez ocenę wyrażenia. Ponadto poprzednia wartość należy odczytywać tylko w celu określenia wartości, która ma być przechowywana .

Przytacza następujący przykład kodu jako niezdefiniowany:

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

We wszystkich tych przykładach kod próbuje zmodyfikować obiekt więcej niż raz w tym samym punkcie sekwencji, co kończy się ; w każdym z tych przypadki:

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

niesprecyzowane zachowanie jest zdefiniowane w projekcie standardu c99 w sekcji 3.4.4 jako:

Użycie nieokreślonej wartości lub innego zachowania, w którym ten Międzynarodowy Standard zapewnia dwie lub więcej możliwości i nie nakłada żadnych dodatkowych wymogów, na które jest wybrany w żadnym instancja

I nieokreślone zachowanie jest zdefiniowane w sekcji 3.4.3 jako:

Zachowanie, po użyciu nieautoryzowanego lub błędna konstrukcja programu lub błędne dane, dla których ta międzynarodowa norma nie nakłada żadnych wymagań

I zauważa, że:

Możliwe nieokreślone zachowanie waha się od ignorowania sytuacji całkowicie z nieprzewidywalnymi rezultatami, przez zachowanie podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z wydaniem komunikatu diagnostycznego lub bez niego), do zakończenia tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego). wydanie komunikatu diagnostycznego).

 44
Author: Shafik Yaghmour,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:44

Większość odpowiedzi przytoczonych w standardzie C podkreśla, że zachowanie tych konstrukcji jest niezdefiniowane. Aby zrozumieć dlaczego zachowanie tych konstrukcji jest niezdefiniowane , najpierw zrozummy te terminy w świetle standardu C11:

Sequenced: (5.1.2.3)

Biorąc pod uwagę dowolne dwie oceny A i B, Jeśli A jest sekwencjonowane przed B, to wykonanie A poprzedza wykonanie B.

Unsequenced:

Jeśli A nie jest zsekwencjonowana przed ani po B, to A i B nie są zsekwencjonowane.

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

  • obliczenia wartości , które obliczają wynik wyrażenia; oraz
  • efekty uboczne , czyli modyfikacje obiektów.

Punkt Sekwencji:

Obecność punktu sekwencji między oceną wyrażeń A i B zakłada, że każde obliczenie wartości i efekt uboczny związany z A jest sekwencjonowane przed każdym obliczenie wartości i efekt uboczny związany z B.

Teraz przechodząc do pytania, dla wyrażeń takich jak

int i = 1;
i = i++;

Standard mówi, że:

6.5 wyrażenia:

Jeśli efekt uboczny na obiekcie skalarnym jest niezrównoważony względem aby albo inny efekt uboczny na tym samym obiekcie skalarnym lub obliczenie wartości przy użyciu wartości tego samego obiektu skalarnego, zachowanie jest niezdefiniowane. [...]

Dlatego powyższe wyrażenie wywołuje UB, ponieważ dwa efekty uboczne na tym samym obiekcie {[18] } są niezrównoważone względem siebie. Oznacza to, że nie jest sekwencjonowane, czy efekt uboczny przez przypisanie do i zostanie wykonany przed czy po efekcie ubocznym przez ++.
W zależności od niezależnie od tego, czy przypisanie nastąpi przed czy po przyroście, będą generowane różne wyniki i jest to jeden z przypadków undefined behavior.

Pozwala zmienić nazwę i po lewej stronie przypisania be il i po prawej stronie przypisania (w wyrażeniu i++) be ir, wtedy wyrażenie be 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żne jest to, że operator Postfix jest taki, że:

Tylko dlatego, że ++ przychodzi po zmienna nie oznacza, że przyrost następuje późno . Przyrost może nastąpić tak wcześnie, jak kompilator lubi tak długo, jak kompilator zapewnia, że oryginalna wartość jest używana .

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  

Daje dwa różne wyniki 1 i 2, które zależą od kolejności efektów ubocznych przez przypisanie i ++ i stąd wywołuje UB.

 42
Author: haccks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:24

Innym sposobem odpowiedzi na to pytanie, zamiast ugrzęzać w tajemnych szczegółach punktów sekwencji i nieokreślonego zachowania, jest po prostu pytanie, co one mają znaczyć? Co programista próbował zrobić?

PIERWSZY fragment, o który zapytałem, i = i++ + ++i, jest całkiem szalony w mojej książce. Nikt nigdy nie napisze tego w prawdziwym programie, nie jest oczywiste co robi, nie ma możliwego algorytmu, który ktoś mógłby próbować kodować, który miałby w wyniku tej konkretnej wymyślonej sekwencji operacji. A ponieważ nie jest oczywiste dla Ciebie i dla mnie, co to ma robić, jest w porządku w mojej książce, jeśli kompilator nie może dowiedzieć się, co to ma robić.

Drugi fragment, i = i++, jest nieco łatwiejszy do zrozumienia. Ktoś wyraźnie próbuje zwiększyć i i przypisać wynik z powrotem do i. ale istnieje kilka sposobów, aby to zrobić w C. najbardziej podstawowy sposób, aby dodać 1 do i i przypisać wynik z powrotem do i, jest taki sam w prawie każdym języku programowania:

i = i + 1

C, Oczywiście, ma poręczny Skrót:

i++

Oznacza to "dodaj 1 do i i przypisz wynik z powrotem do i". Tak więc, jeśli skonstruujemy zbiór tych dwóch, pisząc

i = i++

Tak naprawdę mówimy: "dodaj 1 do i i przypisz wynik z powrotem do i i przypisz wynik z powrotem do i". My jesteśmy zdezorientowani, więc nie przeszkadza mi to zbytnio, jeśli kompilator też się pomyli.

[5]}realistycznie, jedyny raz, gdy te szalone wyrażenia get written są wtedy, gdy ludzie używają ich jako sztucznych przykładów jak ++ ma działać. I oczywiście ważne jest, aby zrozumieć, jak działa++. Ale jedna praktyczna zasada używania ++ brzmi: "Jeśli nie jest oczywiste, co oznacza wyrażenie za pomocą++, nie pisz tego." Spędzaliśmy niezliczone godziny na komp.lang.c omawiając wyrażenia takie jak te i Dlaczego są niezdefiniowane. Dwie z moich dłuższych odpowiedzi, które starają się naprawdę wyjaśnić dlaczego, są zarchiwizowane na www:
 29
Author: Steve Summit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-26 21:27:39

Chociaż jest mało prawdopodobne, aby jakiekolwiek kompilatory i procesory faktycznie to zrobiły, byłoby legalne, zgodnie ze standardem C, aby kompilator zaimplementował "i++" z sekwencją:

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 jakiekolwiek procesory wspierały sprzęt pozwalający na wydajne wykonywanie takich rzeczy, można łatwo wyobrazić sobie sytuacje, w których takie zachowanie ułatwiłoby wielowątkowy kod (np. gwarantowałoby to, że jeśli dwa wątki spróbują wykonać powyższą sekwencję jednocześnie, i zostanie zwiększona o dwa) i nie jest całkowicie nie do pomyślenia, że jakiś przyszły procesor może zapewnić funkcję czegoś takiego.

Gdyby kompilator napisał i++ Jak wskazano powyżej (zgodnie ze standardem) i przeplatał powyższe instrukcje podczas oceny ogólnego wyrażenia (również legalne), a gdyby nie zauważył, że jedna z innych instrukcji zdarzyła się uzyskać dostęp i, byłoby możliwe (i legalne), aby kompilator mógł wygenerować sekwencję instrukcji, która byłaby zablokowana. Aby być pewnym, kompilator prawie na pewno wykryje problem w przypadku, gdy ta sama zmienna i jest używana w obu miejscach, ale jeśli procedura akceptuje odwołania do dwóch wskaźników p i q i używa (*p) i (*q) w powyższym wyrażeniu (zamiast używać i dwa razy), kompilator nie będzie musiał rozpoznawać ani unikać blokady, która miałaby miejsce, gdyby adres tego samego obiektu został przekazany dla obu p i q. q.

 22
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-23 20:06:36

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

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

Lub

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

Lub podobnych wariantów.

Podczas gdy jest to również nieokreślone zachowanie jak już wspomniano, istnieją subtelne różnice, gdy printf() jest zaangażowany w porównaniu do stwierdzenia, takiego jak:

   x = i++ + i++;

W następującym oświadczeniu:

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

The kolejność oceny argumentów w printf() jest nieokreślony. Oznacza to, że wyrażenia i++ i ++i mogą być oceniane W dowolnej kolejności. C11 standard ma kilka istotnych opisów na ten temat:

Załącznik J, nieokreślone zachowania

Kolejność, w jakiej wyznacznik 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ślona wartość, lub inne zachowanie, gdzie to Międzynarodowy Standard zapewnia dwie lub więcej możliwości i nakłada Brak dalszych wymagań, które są wybierane w każdym przypadku.

Przykład przykładem nieokreślonego zachowania jest kolejność, w której argumenty do funkcji są oceniane.

nieokreślone zachowanie samo w sobie nie jest problemem. Rozważ ten przykład:

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

To też ma nieokreślone zachowanie ponieważ kolejność ocena ++x i y++ jest nieokreślona. Ale to całkowicie legalne i ważne oświadczenie. Nie ma niezdefiniowanego zachowania w tym stwierdzeniu. Ponieważ modyfikacje (++x i y++) są wykonywane dla odrębnych obiektów .

Co daje następujące stwierdzenie

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

Jako nieokreślone zachowanie jest fakt, że te dwa wyrażenia modyfikująten sam obiekt i bez ingerencji Sekwencja punkt.


Innym szczegółem jest to, że przecinek zaangażowany w wywołanie printf() jest separatorem , a nie operator przecinka.

Jest to ważne rozróżnienie, ponieważ operator przecinka wprowadza punkt sekwencji między oceną ich operandów, co czyni 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 swoje operandy od lewej do prawej i otrzymuje tylko wartość ostatniego operanda. Więc w j = (++i, i++);, ++i przyrosty i do 6 i i++ dają starą wartość i (6) który jest przypisany do j. Następnie i staje się 7 z powodu post-inkrementacji.

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

printf("%d %d\n", ++i, i++);
Nie będzie problemu. Ale wywołuje nieokreślone zachowanie ponieważ przecinek tutaj jest separator .

Dla tych, którzy są nowi w undefined behaviour byłoby korzystne odczytanie tego, co każdy programista C powinien wiedzieć o niezdefiniowanym zachowaniu , aby zrozumieć pojęcie i wiele innych wariantów niezdefiniowanego zachowania w C. {28]}

Ten post: nieokreślone, nieokreślone i zdefiniowane w implementacji zachowanie jest również istotne.

 18
Author: P.P.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:55:11

Standard C mówi, że zmienna powinna być przypisana najwyżej raz między dwoma punktami sekwencji. Na przykład dwukropek jest punktem sekwencji.
Więc każda wypowiedź postaci:

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

I tak dalej. Standard mówi również, że zachowanie jest niezdefiniowane i nieokreślone. Niektóre Kompilatory wykrywają je i wytwarzają pewne wyniki, ale nie jest to zgodne ze standardem.

Jednak dwie różne zmienne mogą być inkrementowane pomiędzy dwoma sekwencjami punktów.

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

Powyższe jest powszechną praktyką kodowania podczas kopiowania / analizowania łańcuchów.

 13
Author: Nikhil Vidhani,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-11 12:36:41

Podczas gdy składnia wyrażeń takich jak a = a++ lub a++ + a++ jest legalna, zachowanie tych konstrukcji jest niezdefiniowane , ponieważ shall W C standard nie jest przestrzegany. C99 6.5p2:

  1. pomiędzy poprzednim i następnym punktem sekwencji obiekt musi mieć zapisaną wartość zmodyfikowaną co najwyżej raz przez ocenę wyrażenia. [72] ponadto w celu określenia wartości do być przechowywane[73]

Z przypisem 73 dodatkowo wyjaśniającym, że

  1. Ten akapit renderuje niezdefiniowane wyrażenia instrukcji, takie jak

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

    Pozwalając

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

Poszczególne punkty sekwencji są wymienione w załączniku C C11 (i C99):

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

    • pomiędzy oceny wyznacznika funkcji i rzeczywistych argumentów w wywołaniu funkcji i rzeczywistym wywołaniu. (6.5.2.2).
    • pomiędzy ocenami pierwszego i drugiego operandu następujących operatorów: logiczny i && (6.5.13); logiczny lub || (6.5.14); przecinek, (6.5.17).
    • pomiędzy ocenami pierwszego operanda warunkowego ? : operator i którykolwiek z drugiego i trzeciego operandów jest oceniany (6.5.15).
    • koniec pełnego zgłaszającego: zgłaszający (6.7.6);
    • pomiędzy oceną pełnego wyrażenia a następnym pełnym wyrażeniem do oceny. Poniżej znajdują się pełne wyrażenia: inicjalizator, który nie jest częścią złożonej instrukcji (6.7.9); wyrażenie w instrukcji expression (6.8.3); wyrażenie sterujące instrukcji selection (if lub switch) (6.8.4); wyrażenie sterujące instrukcji while lub do (6.8.5); każde z (opcjonalnych) wyrażeń instrukcji for (6.8.5.3); (opcjonalnych) wyrażenie w instrukcji selection (if lub switch) Instrukcja 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 pomiędzy dowolnym wywołaniem funkcji porównawczej a dowolnym ruchem obiektów przekazywanych jako argumenty do tego wywołania (7.22.5).

Sformułowanie ten sam paragraf w C11 to:

  1. jeśli efekt uboczny na obiekcie skalarnym jest niezrównoważony względem innego efektu ubocznego na tym samym obiekcie skalarnym lub obliczenia wartości przy użyciu wartości tego samego obiektu skalarnego, zachowanie jest niezdefiniowane. Jeśli istnieje wiele dozwolonych porządków podwyrażeń wyrażenia, zachowanie jest niezdefiniowane, jeśli taki niezrównoważony 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 -Wall i -Werror, a następnie GCC odmówi kompilacji Twojego programu. Poniżej znajduje się wyjście 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 sekwencji -- i co to jest punkt sekwencji, a co nie jest. Na przykład operator przecinka jest punktem sekwencji, więc

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

Jest dobrze zdefiniowana i będzie inkrementować i o jeden, dając starą wartość, odrzuca tę wartość; następnie w operatorze przecinek rozlicza efekty uboczne; a następnie inkrementuje i o jeden, a wynikowa wartość staje się wartością wyrażenia - tzn. jest to tylko wymyślony sposób zapisu j = (i += 2), który jest po raz kolejny "sprytnym" sposobem zapisu{[32]]}

i += 2;
j = i;

Jednak , w listach argumentów funkcji jest, a nie operatorem przecinka i nie ma punktu sekwencji między w przeciwieństwie do innych argumentów, ich oceny nie są wyrównane względem siebie; tak więc wywołanie funkcji

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

Ma niezdefiniowane zachowanie, ponieważ Nie ma punktu sekwencji między wartościami i++ i ++i w argumentach funkcji, a wartość i jest zatem modyfikowana dwukrotnie, zarówno przez i++, jak i ++i, pomiędzy poprzednim i następnym punktem sekwencji.

 10
Author: Antti Haapala,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-07 12:49:14

W https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c ktoś zapytał o wypowiedź typu:

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 spodziewał się, że wydrukuje 6.

Przyrosty ++i nie gwarantują ukończenia wszystkich przed resztą obliczeń. W rzeczywistości różne Kompilatory uzyskają tutaj różne wyniki. W podanym przykładzie pierwsze 2 ++i wykonane, następnie odczytane zostały wartości k[], a następnie ostatnie ++i k[].

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

Nowoczesne Kompilatory bardzo dobrze to zoptymalizują. W rzeczywistości, prawdopodobnie lepiej niż kod, który pierwotnie napisałeś(zakładając, że zadziałał tak, jak miałeś nadzieję).

 9
Author: TomOnTime,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:30

Dobre wyjaśnienie tego, co dzieje się w tego rodzaju obliczeniach, znajduje się w dokumencie n1188 z 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 musi mieć zapisaną wartość zmodyfikowaną co najwyżej raz przez ocenę wyrażenia. Ponadto wcześniejszą wartość odczytuje się tylko do określ wartość, która ma być przechowywana.

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

W artykule, który zacytowałem powyżej, wyjaśniono, że można dowiedzieć się, że program jest utworzony przez małe pudełka, każde pudełko zawierające instrukcje między 2 kolejnymi punktami sekwencji. Punkty sekwencji są zdefiniowane w załączniku C do normy, w przypadku i=i++ istnieją 2 punkty sekwencji, które wyznaczają pełne wyrażenie. Taki wyrażenie jest syntaktycznie równoważne z wpisem expression-statement w formie Backus-Naur gramatyki(gramatyka jest podana w załączniku A do normy).

Więc kolejność instrukcji wewnątrz pudełka nie ma wyraźnego porządku.

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 do interpretacji kodu i=i++ są poprawne i ponieważ obie generują różne odpowiedzi, zachowanie jest niezdefiniowane.

Więc punkt sekwencji można zobaczyć przez początek i koniec każdego pudełka składającego się na program [pudełka są jednostkami atomowymi w C] i wewnątrz pudełka kolejność instrukcji nie jest określona we wszystkich przypadkach. Zmieniając tę kolejność można czasami zmienić wynik.

EDIT:

Innym dobrym źródłem do wyjaśnienia takich dwuznaczności są wpisy z C-faq site (również opublikowane jako książka) , a mianowicie tutaj i tutaj i tutaj .

 5
Author: alinsoar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-24 12:15:14

Powodem jest to, że program uruchamia nieokreślone zachowanie. Problem leży w kolejności Oceny, ponieważ nie ma wymaganych punktów sekwencji zgodnie ze standardem C++98 (żadne operacje nie są sekwencjonowane przed lub po innej zgodnie z terminologią C++11).

Jeśli jednak trzymasz się jednego kompilatora, zachowanie będzie trwałe, o ile nie dodasz wywołań funkcji lub wskaźników, co sprawi, że zachowanie będzie bardziej bałaganiarskie.

  • 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 pod wyrażenia w kolejności od lewej do prawej dla prawej strony (RHS) , a następnie przypisuje wartość do lewej strony (LHS) . Tak właśnie zachowują się Java i C# i definiują swoje standardy. (Tak, równoważne oprogramowanie w Javie i C# ma zdefiniowane zachowania). Ocenia każde wyrażenie podrzędne jeden po drugim w instrukcji RHS w kolejności od lewej do prawej; dla każdego sub wyrażenie: najpierw zostanie obliczone ++C (pre-increment), następnie do operacji zostanie użyta wartość c, następnie post increment c++).

Zgodnie z GCC C++: operatory

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

Równoważny kod w zdefiniowanym 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 udajemy się do Visual Studio. Visual Studio 2015, ty get:

#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 visual studio, przyjmuje inne podejście, ocenia wszystkie wyrażenia pre-increment w pierwszym przejściu, następnie używa wartości zmiennych w operacjach w drugim przejściu, przypisuje z RHS do LHS w trzecim przejściu, a następnie w końcu ocenia wszystkie wyrażenia post-increment w jednym przejściu.

Więc odpowiednik w zdefiniowanym zachowaniu C++ jak 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 pod adresem pierwszeństwo i kolejność Ocena :

Gdzie kilka operatorów występuje razem, mają one jednakowy priorytet i są oceniane zgodnie z ich asocjacją. Operatory w tabeli są opisane w sekcjach zaczynających się od operatorów Postfixa.

 3
Author: Muhammad Annaqeeb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-11 01:17:46

Twoje pytanie prawdopodobnie nie brzmiało: "dlaczego te konstrukcje są niezdefiniowanymi zachowaniami w C?". Twoje pytanie brzmiało prawdopodobnie: "dlaczego ten kod (używając ++) nie dał mi oczekiwanej wartości?", a ktoś oznaczył twoje pytanie jako duplikat i wysłał cię tutaj.

odpowiedź próbuje odpowiedzieć na to pytanie: Dlaczego Twój kod nie dał ci odpowiedzi, której oczekiwałeś i jak możesz nauczyć się rozpoznawać (i unikać) wyrażenia, które nie będą działać zgodnie z oczekiwaniami.

Zakładam słyszałeś już podstawową definicję operatorów C ++ i -- oraz czym forma przedrostkowa ++x różni się od formy postfixowej x++. Ale trudno jest myśleć o tych operatorach, więc aby upewnić się, że rozumiesz, być może napisałeś mały program testowy obejmujący coś w rodzaju {38]}

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 inaczej, wcale nie tak, jak myślałeś.

A może patrzysz na trudne do zrozumienia wyrażenie, takie jak]}
int x = 5;
x = x++ + ++x;
printf("%d\n", x);
Może ktoś dał ci ten kod jako układankę. Ten kod również nie ma sensu, zwłaszcza jeśli go uruchomisz - a jeśli skompilujesz i uruchomisz go pod dwoma różnymi kompilatorami, prawdopodobnie otrzymasz dwie różne odpowiedzi! O co chodzi? Która odpowiedź jest prawidłowa? (A odpowiedź jest taka, że obie są, albo żadna z nich są.)

Jak już słyszałeś, wszystkie te wyrażenia są niezdefiniowane , co oznacza, że język C nie daje gwarancji, co zrobią. Jest to dziwny i zaskakujący wynik, ponieważ prawdopodobnie myślałeś, że każdy program, który możesz napisać, tak długo, jak jest skompilowany i uruchomiony, wygeneruje unikalne, dobrze zdefiniowane wyjście. Ale w przypadku nieokreślonego zachowania tak nie jest.

Co sprawia, że wyrażenie jest nieokreślone? Są wyrażeniami zawierającymi ++ i -- zawsze niezdefiniowany? Oczywiście, że nie: są to użyteczne operatory, a jeśli używasz ich prawidłowo, są doskonale zdefiniowane.

Dla wyrażeń, o których mówimy, to, co czyni je niezdefiniowanymi, jest wtedy, gdy dzieje się za dużo naraz, kiedy nie jesteśmy pewni, w jakiej kolejności rzeczy się wydarzą, 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 napisałem
printf("%d %d %d\n", x, ++x, x++);

Pytanie brzmi, zanim zadzwonisz printf, czy kompilator oblicza wartość x jako pierwszą, czy x++, a może ++x? Ale okazuje się, że nie wiemy. Nie ma reguły w C, która mówi, że argumenty funkcji są oceniane od lewej do prawej, od prawej do lewej lub w innej kolejności. Nie możemy więc powiedzieć, czy kompilator zrobi x najpierw, potem ++x, potem x++, czy x++ potem ++x potem x, czy też w innej kolejności. Ale kolejność wyraźnie ma znaczenie, ponieważ w zależności od jakiej kolejności kompilator używa, wyraźnie otrzymamy różne wyniki wydrukowane przez printf.

A co z tym szalonym wyrażeniem?

x = x++ + ++x;

Problem z tym wyrażeniem polega na tym, że zawiera ono trzy różne próby modyfikacji wartości x: (1) część x++ próbuje dodać 1 do x, zapisać nową wartość w x i zwrócić starą wartość x; (2) część ++x próbuje dodać 1 do x, zapisać nową wartość w x i zwrócić nową wartość x; oraz (3) część x = próbuje przypisać suma pozostałe dwa wracają do X. który z tych trzech prób "wygra"? Która z trzech wartości zostanie faktycznie przypisana do x? Znowu, i być może zaskakujące, nie ma reguły w C, aby nam powiedzieć.

Możesz sobie wyobrazić, że pierwszeństwo, Asocjacja 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 proszę, uwierz mi na słowo, a powiem to jeszcze raz: pierwszeństwo i Asocjacja nie determinują każdego aspektu kolejność oceny wyrażenia w C. w szczególności, jeśli w jednym wyrażeniu jest wiele różnych miejsc, w których staramy się przypisać nową wartość do czegoś takiego jak x, pierwszeństwo i Asocjacja nie mówią nam, która z tych prób dzieje się pierwsza, czy ostatnia, czy cokolwiek innego.


Więc z tym całym tłem i wstępem, jeśli chcesz mieć pewność, że wszystkie Twoje programy są dobrze zdefiniowane, które wyrażenia możesz napisać, a które nie piszesz?

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++);

I ostatnie pytanie brzmi: jak możesz stwierdzić, które wyrażenia są dobrze zdefiniowane, a które nieokreślone?

Jak powiedziałem wcześniej, wyrażenia nieokreślone są tymi, w których dzieje się zbyt wiele na raz, gdzie nie możesz być pewien, w jakim porządku rzeczy się dzieją i gdzie porządek ma znaczenie: {]}

  1. jeśli istnieje jedna zmienna, która jest po modyfikacji (przypisaniu) w dwóch lub więcej różnych miejscach, skąd wiesz, która modyfikacja nastąpi najpierw?
  2. jeśli zmienna jest modyfikowana w jednym miejscu, a jej wartość jest używana w innym miejscu, skąd wiesz, czy używa starej czy nowej wartości?

Jako przykład #1, w wyrażeniu

x = x++ + ++x;

Istnieją trzy próby modyfikacji ' x.

Jako przykład #2, w wyrażeniu

y = x + x++;

Oboje używamy wartość x i zmodyfikować ją.

Więc to jest odpowiedź: upewnij się, że w każdym wyrażeniu, które piszesz, każda zmienna jest modyfikowana co najwyżej raz, a jeśli zmienna jest modyfikowana, nie próbuj również używać wartości tej zmiennej gdzie indziej.

 2
Author: Steve Summit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-16 11:54:35