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\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?
}
Odpowiedzi
C ma koncepcję niezdefiniowanego zachowania, tj. Niektóre konstrukcje językowe są poprawne składniowo, ale nie można przewidzieć zachowania podczas wykonywania kodu.
O ile wiem, norma nie mówi wprost, dlaczego istnieje pojęcie niezdefiniowanego zachowania. Moim zdaniem to po prostu dlatego, że projektanci języka chcieli mieć pewną swobodę w semantyce, zamiast np. Wymagać, aby wszystkie implementacje obsługiwały przepełnienie liczb całkowitych w dokładnie ten sam sposób, co najprawdopodobniej spowodowałoby poważne koszty wydajności, po prostu zostawili zachowanie undefined, więc jeśli napiszesz kod powodujący przepełnienie całkowitoliczbowe, wszystko może się zdarzyć.
Mając to na uwadze, dlaczego są to „problemy”? Język jasno mówi, że pewne rzeczy prowadzą do nieokreślonych zachowań . Nie ma problemu, nie ma „należy”. Jeśli niezdefiniowane zachowanie zmienia się po zadeklarowaniu jednej z zaangażowanych zmiennych volatile
, niczego to nie dowodzi ani nie zmienia. To jest nieokreślone ; nie możesz uzasadniać tego zachowania.
Twój najbardziej interesujący przykład, ten z
u = (u++);
to podręcznikowy przykład niezdefiniowanego zachowania (patrz wpis Wikipedii dotyczący punktów sekwencji ).
Po prostu skompiluj i zdemontuj swój wiersz kodu, jeśli tak bardzo chcesz wiedzieć, jak dokładnie otrzymujesz to, co otrzymujesz.
Oto, co otrzymuję na mojej maszynie, 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 jakąś optymalizacją kompilatora?)
Myślę, że odpowiednie części standardu C99 to 6.5 Expressions, §2
Pomiędzy poprzednim a następnym punktem sekwencji obiekt będzie miał swoją przechowywaną wartość zmodyfikowaną co najwyżej raz przez ocenę wyrażenia. Ponadto poprzednia wartość może być tylko do odczytu 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. Jeśli zostanie podjęta próba zmodyfikowania wyniku operatora przypisania lub uzyskania do niego dostępu po następnym punkcie sekwencji, zachowanie jest niezdefiniowane.
Większość odpowiedzi przytacza tutaj standard C, podkreślając, że zachowanie tych konstrukcji jest nieokreślone. Aby zrozumieć, dlaczego zachowanie tych konstrukcji jest nieokreślone , przyjrzyjmy się najpierw tym terminom w świetle standardu C11:
W kolejności: (5.1.2.3)
Biorąc pod uwagę dowolne dwie oceny
A
iB
, jeśliA
jest sekwencjonowane wcześniejB
, wykonanieA
powinno poprzedzać wykonanieB
.
Bez konsekwencji:
Jeśli
A
nie jest sekwencjonowane przed lub poB
, toA
i nieB
są sekwencjonowane.
Oceny mogą być jedną z dwóch rzeczy:
- obliczenia wartości , które obliczają wynik wyrażenia; i
- skutki uboczne , które są modyfikacjami obiektów.
Punkt sekwencji:
Obecność punktu sekwencji między oceną wyrażeń
A
iB
oznacza, że każde obliczenie wartości i związany z nim efekt ubocznyA
jest sekwencjonowane przed każdym obliczeniem wartości i efektem ubocznym związanym zB
.
A teraz przejdźmy do pytania o wyrażenia takie jak
int i = 1;
i = i++;
norma 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 efekty uboczne na tym samym obiekcie nie i
mają kolejności względem siebie. Oznacza to, że nie jest ustalane, czy efekt uboczny przypisania do i
zostanie wykonany przed, czy po efekcie ubocznym przez ++
.
W zależności od tego, czy przypisanie nastąpi przed, czy po inkrementacji, zostaną wygenerowane różne wyniki i tak jest w przypadku niezdefiniowanego zachowania .
Pozwala zmienić nazwę po i
lewej stronie przypisania na il
i po prawej stronie przypisania (w wyrażeniu i++
) być ir
, wtedy wyrażenie będzie podobne
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
++
występuje po zmiennej, nie oznacza, że przyrost następuje późno . Przyrost może nastąpić tak wcześnie, jak kompilator chce, o ile kompilator zapewnia, że używana jest oryginalna wartość .
Oznacza to, że wyrażenie il = ir++
może zostać 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
skutkuje dwoma różnymi wynikami 1
i 2
zależy od sekwencji skutków ubocznych przez przypisanie ++
i dlatego wywołuje UB.
Zachowania nie można naprawdę wyjaśnić, ponieważ wywołuje zarówno nieokreślone zachowanie, jak i niezdefiniowane zachowanie , więc nie możemy zrobić żadnych ogólnych przewidywań dotyczących tego kodu, chociaż jeśli przeczytasz prace Olve Maudal, takie jak Deep C i Unspecified and Undefined, czasami możesz naprawić zgaduje w bardzo konkretnych przypadkach z określonym kompilatorem i środowiskiem, ale nie rób tego nigdzie w pobliżu produkcji.
Przechodząc do nieokreślonego zachowania , w szkicu standardowej sekcji c996.5
paragraf 3 mówi ( moje wyróżnienie ):
Grupowanie operatorów i operandów jest wskazywane przez składnię. 74) Z wyjątkiem przypadków określonych później (dla wywołań funkcji (), &&, ||,?: I operatorów przecinków), kolejność obliczania podwyrażeń i kolejność w jakie skutki uboczne mają miejsce, są nieokreślone.
Więc kiedy mamy taką linię:
i = i++ + ++i;
Nie wiemy, czy i++
czy ++i
najpierw będą oceniane. Ma to głównie na celu zapewnienie kompilatorowi lepszych opcji 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 6.5
paragrafu 2 normy ( moje podkreślenie ):
Pomiędzy poprzednim a następnym punktem sekwencji obiekt będzie miał swoją przechowywaną wartość zmodyfikowaną co najwyżej raz przez ocenę wyrażenia. Ponadto poprzednia wartość jest tylko do odczytu w celu określenia wartości, która ma być przechowywana .
cytuje 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ż raz w tym samym punkcie sekwencji, co zakończy się znakiem ;
w każdym z poniższych przypadków:
i = i++ + ++i;
^ ^ ^
i = (i++);
^ ^
u = u++ + ++u;
^ ^ ^
u = (u++);
^ ^
v = v++ + ++v;
^ ^ ^
Nieokreślone zachowanie jest zdefiniowane w projekcie normy C99 w rozdziale 3.4.4
jako:
stosowanie nieokreślonej wartości lub innego zachowania, w przypadku gdy niniejsza Norma Międzynarodowa zapewnia dwie lub więcej możliwości i nie nakłada żadnych dalszych wymagań, które zostały wybrane w każdym przypadku
a niezdefiniowane zachowanie jest zdefiniowane w sekcji 3.4.3
jako:
zachowanie, w przypadku użycia nieprzenoszalnej lub błędnej konstrukcji programu lub błędnych danych, dla których niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań
i zauważa, że:
Możliwe niezdefiniowane zachowanie obejmuje zarówno całkowite zignorowanie sytuacji z nieprzewidywalnymi skutkami, zachowanie podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z lub bez wydania komunikatu diagnostycznego), jak i przerwanie tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego).
Innym sposobem odpowiedzi na to pytanie, zamiast grzęznąć w tajemniczych szczegółach punktów sekwencji i nieokreślonych zachowań, jest po prostu zapytać, co mają one znaczyć? Co programista próbował zrobić?
Pierwszy fragment, o który pytam i = i++ + ++i
, jest w mojej książce dość ewidentnie szalony. Nikt nigdy nie napisałby tego w prawdziwym programie, nie jest oczywiste, co on robi, nie ma żadnego algorytmu, który ktoś mógłby próbować zakodować, co spowodowałoby tę konkretną wymyśloną sekwencję operacji. A ponieważ nie jest dla mnie i ciebie oczywiste, co ma zrobić, w mojej książce jest dobrze, jeśli kompilator nie może również dowiedzieć się, co powinien zrobić.
Drugi fragment i = i++
jest trochę łatwiejszy do zrozumienia. Ktoś najwyraźniej próbuje zwiększyć i i przypisać wynik z powrotem do i. Ale jest na to kilka sposobów w C. Najbardziej podstawowy sposób dodania 1 do i i przypisania wyniku z powrotem 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 skonstruujemy mieszaninę 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”. Jesteśmy zdezorientowani, więc nie przeszkadza mi to zbytnio, jeśli kompilator też się pomyli.
Realistycznie rzecz biorąc, jedyny raz, kiedy te szalone wyrażenia są zapisywane, to kiedy ludzie używają ich jako sztucznych przykładów tego, jak powinno działać ++. Oczywiście ważne jest, aby zrozumieć, jak działa ++. Ale praktyczną zasadą używania ++ jest: „Jeśli nie jest oczywiste, co oznacza wyrażenie używające ++, nie pisz go”.
Spędzaliśmy niezliczone godziny na comp.lang.c omawiając takie wyrażenia i dlaczego są niezdefiniowane. Dwie z moich dłuższych odpowiedzi, które próbują naprawdę wyjaśnić dlaczego, są zarchiwizowane w sieci:
- Dlaczego standard nie definiuje tego, do czego one służą?
- Czy pierwszeństwo operatorów nie określa kolejności oceny?
Zobacz także pytanie 3.8 i pozostałe pytania w sekcji 3 listy C FAQ .
Często to pytanie jest powiązane jako duplikat pytań związanych z kodem, takim jak
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ż wspomniano, istnieją subtelne różnice w printf()
porównaniu ze stwierdzeniem, takim jak:
x = i++ + i++;
W poniższym oświadczeniu:
printf("%d %d\n", ++i, i++);
kolejność oceny argumentów na printf()
to nieokreślone . Oznacza to, że wyrażenia i++
i ++i
mogą być oceniane w dowolnej kolejności. Norma C11 ma kilka odpowiednich opisów na ten temat:
Załącznik J, nieokreślone zachowania
Kolejność, w jakiej 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 innego zachowania, w przypadku gdy niniejsza Norma Międzynarodowa zapewnia dwie lub więcej możliwości i nie nakłada dalszych wymagań, które zostały wybrane w jakimkolwiek przypadku.
PRZYKŁAD Przykładem nieokreślonego zachowania jest kolejność obliczania argumentów funkcji.
Samo nieokreślone zachowanie NIE jest problemem. Rozważmy ten przykład:
printf("%d %d\n", ++x, y++);
To również ma nieokreślone zachowanie, ponieważ kolejność oceny ++x
i y++
jest nieokreślona. Ale jest to całkowicie legalne i ważne oświadczenie. W tej instrukcji nie ma nieokreślonego zachowania. Ponieważ modyfikacje ( ++x
i y++
) są wykonywane na różnych obiektach.
Co przekłada się na poniższe stwierdzenie
printf("%d %d\n", ++i, i++);
jako nieokreślonej zachowania jest to, że te dwa wyrażenia modyfikacji samego obiektu i
bez pośredniego punktu sekwencji .
Innym szczegółem jest to, że przecinek występujący w wywołaniu printf () jest separatorem , a nie operatorem przecinka .
Jest to ważne rozróżnienie, ponieważ operator przecinka wprowadza punkt sekwencji między oceną ich operandów, co sprawia, że:
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 zwraca tylko wartość ostatniego operandu. Więc j = (++i, i++);
, ++i
przyrosty i
do 6
i i++
plony stara wartość i
( 6
), która jest przypisana j
. Następnie i
dzieje się z 7
powodu post-inkrementacji.
Więc jeśli przecinek w wywołaniu funkcji miałby być operatorem przecinka
printf("%d %d\n", ++i, i++);
nie będzie problemu. Ale wywołuje niezdefiniowane zachowanie, ponieważ przecinek jest tutaj separatorem .
Dla tych, którzy są nowicjuszami w niezdefiniowanym zachowaniu , skorzystaliby na przeczytaniu Co każdy programista C powinien wiedzieć o niezdefiniowanym zachowaniu, aby zrozumieć koncepcję i wiele innych wariantów niezdefiniowanego zachowania w C.
Ten post: Nieokreślone, nieokreślone i zdefiniowane w implementacji zachowanie jest również istotne.
Chociaż jest mało prawdopodobne, aby jakikolwiek kompilator i procesor faktycznie to zrobił, zgodnie ze standardem C kompilator mógłby 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 żaden procesor obsługiwał sprzęt, aby umożliwić sprawne wykonanie takiej czynności, można łatwo wyobrazić sobie sytuacje, w których takie zachowanie ułatwiłoby kod wielowątkowy (np. Gwarantowałoby, że jeśli dwa wątki spróbują wykonać powyższe sekwencja naraz i
zostanie zwiększona o dwa) i nie jest całkowicie niewyobrażalne, że jakiś przyszły procesor może oferować coś takiego.
Gdyby kompilator pisał i++
tak, jak wskazano powyżej (zgodnie ze standardem) i przeplatał powyższe instrukcje podczas oceny całego wyrażenia (również prawnego), i gdyby nie zauważył, że jedna z pozostałych instrukcji się wydarzyła aby uzyskać dostęp i
, kompilator mógłby (i legalnie) wygenerować sekwencję instrukcji, które byłyby zakleszczone. Oczywiście, kompilator prawie na pewno wykryłby 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 byłby zobowiązany do rozpoznania lub uniknięcia zakleszczenia, które wystąpiłoby, gdyby ten sam adres obiektu został przekazany dla obu p
i q
.
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 :
- Pomiędzy poprzednim a następnym punktem sekwencji obiekt będzie miał swoją przechowywaną wartość zmodyfikowaną co najwyżej raz przez ocenę wyrażenia. [72] Ponadto poprzednią wartość należy odczytywać tylko w celu określenia wartości do zapisania [73]
Z przypisem 73 dalej wyjaśniającym
Ten akapit renderuje niezdefiniowane wyrażenia instrukcji, 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 ):
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).
- Pomiędzy wartościami pierwszego i drugiego operandu następujących operatorów: logiczne AND && (6.5.13); logiczne OR || (6.5.14); przecinek, (6.5.17).
- Pomiędzy ocenami pierwszego operandu warunku? : operator i którykolwiek z drugiego i trzeciego argumentu jest obliczany (6.5.15).
- Koniec pełnego deklaratora: deklaratory (6.7.6);
- Pomiędzy oceną pełnego wyrażenia a następnym pełnym wyrażeniem, które ma zostać ocenione. Poniżej przedstawiono 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żenia (6.8.3); wyrażenie sterujące instrukcji wyboru (if lub switch) (6.8.4); kontrolujące wyrażenie instrukcji while lub do (6.8.5); każde z (opcjonalnych) wyrażeń instrukcji for (6.8.5.3); (opcjonalne) wyrażenie w instrukcji return (6.8.6.4).
- Bezpośrednio przed zwróceniem funkcji bibliotecznej (7.1.4).
- Po akcjach skojarzonych 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ównania, a także między każdym wywołaniem funkcji porównania a jakimkolwiek ruchem obiektów przekazanych jako argumenty do tego wywołania (7.22.5).
Brzmienie tego samego akapitu w C11 jest następujące:
- Jeśli efekt uboczny na obiekcie skalarnym nie jest sekwencjonowany względem innego efektu ubocznego tego samego obiektu skalarnego lub obliczenia wartości przy użyciu wartości tego samego obiektu skalarnego, zachowanie jest niezdefiniowane. Jeśli istnieje wiele dopuszczalnych uporządkowań podwyrażeń wyrażenia, zachowanie jest niezdefiniowane, jeśli taki niesekwencjonalny efekt uboczny występuje w którymkolwiek z uporządkowań.
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 kompilacji 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ć, czym jest punkt sekwencji - i co jest punktem sekwencji, a co nim nie jest . Na przykład operator przecinka jest punktem sekwencji, więc
j = (i ++, ++ i);
jest dobrze zdefiniowana i będzie rosła i
o jeden, dając starą wartość, odrzuca tę wartość; następnie przy operatorze przecinka ustal efekty uboczne; a następnie zwiększamy i
o jeden, a wynikowa wartość staje się wartością wyrażenia - tj. 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 ,
lista argumentów w funkcji nie jest operatorem przecinkowym i nie ma punktu sekwencji między wartościami różnych argumentów; zamiast tego ich oceny są bez następstw względem 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 ++i
w jej argumenty , zaś wartość i
jest modyfikowane w dwa, zarówno przez i++
i ++i
pomiędzy poprzednim i następnym punkcie sekwencji.
Standard C mówi, że zmienna powinna być przypisana najwyżej raz między dwoma punktami sekwencji. Na przykład średnik to punkt sekwencji.
Więc każde stwierdzenie w formie:
i = i++;
i = i++ + ++i;
i tak dalej naruszają tę zasadę. Norma mówi również, że zachowanie jest nieokreślone i nieokreślone. Niektóre kompilatory wykrywają je i generują 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ższe jest powszechną praktyką kodowania podczas kopiowania / analizowania ciągów.
W https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c ktoś zapytał o stwierdzenie 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ę wydrukowania 6.
Nie ++i
ma gwarancji, że wszystkie przyrosty zostaną ukończone przed wykonaniem pozostałych obliczeń. 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ści k[]
zostały odczytane, to ostatnia ++i
wtedy k[]
.
num = k[i+1]+k[i+2] + k[i+3];
i += 3
Nowoczesne kompilatory bardzo dobrze to zoptymalizują. W rzeczywistości prawdopodobnie lepszy niż kod, który napisałeś pierwotnie (zakładając, że zadziałał tak, jak miałeś nadzieję).
Twoje pytanie prawdopodobnie nie brzmiało: „Dlaczego te konstrukty są niezdefiniowane w C?”. Twoje pytanie brzmiało prawdopodobnie: „Dlaczego ten kod (przy użyciu ++
) nie dał mi oczekiwanej wartości?”, A ktoś oznaczył Twoje pytanie jako duplikat i wysłał Cię tutaj.
Ta odpowiedź próbuje odpowiedzieć na 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 jak forma prefiksu ++x
różni się od formy postfiksowej x++
. Ale trudno o tych operatorach myśleć, więc aby upewnić się, że rozumiesz, być może napisałeś mały mały program testowy obejmujący coś w rodzaju
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 dane wyjściowe, sugerując, że może ++
robi coś zupełnie innego, a nie to, co myślałeś, że zrobił.
A może patrzysz na trudne do zrozumienia wyrażenie, takie jak
int x = 5;
x = x++ + ++x;
printf("%d\n", x);
Być może ktoś dał ci ten kod jako zagadkę. 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 w tym chodzi? Która odpowiedź jest prawidłowa? (A odpowiedź brzmi, że obaj są albo ż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 jest kompilowany i uruchamiany, wygeneruje unikalny, dobrze zdefiniowany wynik. Ale w przypadku niezdefiniowanego zachowania tak nie jest.
Co sprawia, że wyrażenie jest niezdefiniowane? Czy wyrażenia obejmują ++
i są --
zawsze niezdefiniowane? Oczywiście, że nie: to są przydatne operatory i jeśli używasz ich prawidłowo, są doskonale zdefiniowane.
Wyrażenia, o których mówimy, sprawiają, że są niezdefiniowane, kiedy dzieje się zbyt wiele naraz, kiedy nie jesteśmy pewni, w jakiej kolejności coś się wydarzy, ale kiedy kolejność ma znaczenie dla wyniku, jaki 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, czy przed wywołaniem printf
kompilator oblicza wartość x
first, czy 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 lub od prawej do lewej, lub w jakiejś innej kolejności. Więc nie możemy powiedzieć, czy kompilator zrobi x
, potem ++x
, potem x++
, albo x++
wtedy ++x
następnie x
, czy jakiś inny porządek. Ale kolejność oczywiście ma znaczenie, ponieważ w zależności od kolejności używanej przez kompilator, wyraźnie otrzymamy różne wyniki wydrukowane printf
.
A co z tym szalonym wyrazem twarzy?
x = x++ + ++x;
Problem z tym wyrażeniem polega na tym, że zawiera ono trzy różne próby zmodyfikowania wartości x: (1) x++
część próbuje dodać 1 do x, zapisać nową wartość w x
i zwrócić starą wartość x
; (2) ++x
część próbuje dodać 1 do x, zapisać nową wartość w 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 prób „wygra”? Która z trzech wartości zostanie faktycznie przypisana x
? Ponownie, i być może zaskakująco, w C nie ma reguły, którą można by nam powiedzieć.
Możesz sobie wyobrazić, że pierwszeństwo lub łączność 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 łączność nie determinują 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 próbujemy przypisać nową wartość czemuś w rodzaju x
, pierwszeństwo i skojarzenie nie mówią nam, która z tych prób ma miejsce jako pierwsza, czy ostatnia, czy cokolwiek.
Więc mając całe to tło i wprowadzenie na uboczu, jeśli chcesz mieć pewność, że wszystkie twoje programy są dobrze zdefiniowane, które wyrażenia możesz napisać, a które nie?
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++;
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 niezdefiniowane?
Jak powiedziałem wcześniej, niezdefiniowane wyrażenia to te, w których dzieje się zbyt wiele naraz, w których nie możesz być pewien, w jakiej kolejności coś się dzieje i gdzie kolejność ma znaczenie:
- Jeśli istnieje jedna zmienna, która jest modyfikowana (przypisywana) w dwóch lub więcej różnych miejscach, skąd wiesz, która modyfikacja następuje najpierw?
- Jeśli istnieje zmienna, która jest modyfikowana w jednym miejscu, a jej wartość jest używana w innym miejscu, skąd wiesz, czy używa starej, czy nowej wartości?
Jako przykład # 1 w wyrażeniu
x = x++ + ++x;
istnieją trzy próby zmodyfikowania `x.
Jako przykład # 2 w wyrażeniu
y = x + x++;
oboje używamy wartości x
i modyfikujemy ją.
Oto odpowiedź: upewnij się, że w każdym zapisywanym wyrażeniu 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 w innym miejscu.
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łówna zasada z normy ISO 9899, która ma zastosowanie w tej sytuacji, to 6,5 p2.
Pomiędzy poprzednim a następnym punktem sekwencji obiekt będzie miał swoją przechowywaną wartość zmodyfikowaną co najwyżej raz przez ocenę wyrażenia. Ponadto poprzednia wartość może być tylko do odczytu w celu określenia wartości, która ma być przechowywana.
Sekwencja wskazuje w wyrażeniu, takim jak i=i++
przed i=
i po i++
.
W artykule, który zacytowałem powyżej, jest wyjaśnione, że można wyobrazić sobie, że program składa się z małych prostokątów, z których każde zawiera instrukcje między dwoma kolejnymi punktami sekwencji. Punkty sekwencji są zdefiniowane w załączniku C normy, w przypadku i=i++
gdy istnieją 2 punkty sekwencji, które ograniczają pełną ekspresję. Takie wyrażenie jest syntaktycznie równoważne z zapisem expression-statement
gramatyki w formie Backus-Naur (gramatyka jest zawarta w załączniku A do normy).
Tak więc kolejność instrukcji w pudełku nie ma jasnego 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 interpretacji kodu i=i++
są prawidłowe i ponieważ obie generują różne odpowiedzi, zachowanie jest nieokreślone.
Tak więc punkt sekwencji można zobaczyć na początku i na końcu każdego pudełka, które składa się na program [pudełka są atomami w C], a wewnątrz pudełka 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 niejasności są wpisy ze strony c-faq (również opublikowanej w formie książki ), a mianowicie tutaj i tutaj i tutaj .
Przyczyną jest to, że program działa w sposób niezdefiniowany. 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 innych zgodnie z terminologią C ++ 11).
Jeśli jednak będziesz trzymać się jednego kompilatora, okaże się, że zachowanie jest trwałe, o ile nie dodasz wywołań funkcji lub wskaźników, co uczyniłoby zachowanie bardziej niechlujnym.
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? oblicza 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ą swoje standardy. (Tak, równoważne oprogramowanie w językach Java i C # ma zdefiniowane zachowania). Ocenia każde wyrażenie podrzędne po kolei w instrukcji RHS w kolejności od lewej do prawej; dla każdego wyrażenia podrzędnego: najpierw jest obliczane ++ c (preinkrementacja), następnie wartość c jest używana do operacji, a następnie c ++ po inkrementacji).
zgodnie z GCC C ++: Operators
W GCC C ++ pierwszeństwo operatorów steruje kolejnością, w jakiej oceniane są poszczególne operatory
równoważny kod w zdefiniowanym zachowaniu C ++ w rozumieniu GCC:
#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 Visual Studio, przyjmuje inne podejście, ocenia wszystkie wyrażenia preinkrementacji w pierwszym przebiegu, następnie używa wartości zmiennych w operacjach w drugim przebiegu, przypisuje z RHS do LHS w trzecim przebiegu, a na ostatnim etapie ocenia wszystkie wyrażenia postinkrementacyjne w jednym przebiegu.
Tak więc odpowiednik w zdefiniowanym zachowaniu C ++ w rozumieniu Visual C ++:
#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ą programu Visual Studio w Precedence and Order of Evaluation :
Gdy kilka operatorów występuje razem, mają one równy priorytet i są oceniane zgodnie z ich łącznością. Operatory w tabeli są opisane w sekcjach zaczynających się od Operatory Postfix.