Jeżeli mam:
unsigned int x;
x -= x;
jasne jest, że po tym wyrażeniu x
powinno być zero, ale gdziekolwiek spojrzę, mówią, że zachowanie tego kodu jest nieokreślone, a nie tylko wartość x
(aż do odejmowania).
Dwa pytania:
Czy zachowanie tego kodu jest rzeczywiście nieokreślone?
(Np. Czy kod może ulec awarii [lub gorzej] w zgodnym systemie?)Jeśli tak, dlaczego C mówi, że zachowanie jest nieokreślone, skoro jest całkowicie jasne, że
x
powinno to wynosić zero?tj. jaka jest korzyść wynikająca z braku zdefiniowania tutaj zachowania?
Oczywiście kompilator mógłby po prostu użyć dowolnej wartości śmieciowej, którą uznałby za „przydatną” wewnątrz zmiennej i działałby zgodnie z przeznaczeniem… co jest złego w tym podejściu?
c
undefined-behavior
initialization
user541686
źródło
źródło
x -= x
. Powstaje pytanie, dlaczego w ogóle dostęp do niezainicjowanych wartości to UB.Odpowiedzi:
Tak, to zachowanie jest nieokreślone, ale z innych powodów niż większość ludzi jest świadoma.
Po pierwsze, użycie wartości zjednostkowanej nie jest samo w sobie niezdefiniowanym zachowaniem, ale wartość jest po prostu nieokreślona. Dostęp do tego jest wtedy UB, jeśli wartość jest reprezentacją pułapki dla typu. Typy bez znaku rzadko mają reprezentacje pułapek, więc po tej stronie będziesz stosunkowo bezpieczny.
To, co sprawia, że zachowanie jest niezdefiniowane, to dodatkowa właściwość twojej zmiennej, a mianowicie to, że „mogła być zadeklarowana z
register
”, czyli jej adres nigdy nie jest brany. Takie zmienne są traktowane specjalnie, ponieważ istnieją architektury, które mają rzeczywiste rejestry procesora, które mają rodzaj dodatkowego stanu, który jest „niezainicjowany” i który nie odpowiada wartości w domenie typu.Edycja: Odpowiednia fraza normy to 6.3.2.1p2:
Aby było jaśniej, poniższy kod jest legalny w każdych okolicznościach:
unsigned char a, b; memcpy(&a, &b, 1); a -= a;
a
ib
, więc ich wartość jest po prostu nieokreślona.unsigned char
nigdy nie ma reprezentacji pułapki, że nieokreślona wartość jest po prostu nieokreślona, każda wartośćunsigned char
może się zdarzyć.a
musi mieć wartość0
.Edit2:
a
ib
mają nieokreślone wartości:źródło
unsigned
pewnością można przedstawić pułapki. Czy możesz wskazać część normy, która tak mówi? W §6.2.6.2 / 1 widzę, co następuje: „W przypadku typów całkowitych bez znaku innych niż znak bez znaku , bity reprezentacji obiektu powinny być podzielone na dwie grupy: bity wartości i bity wypełniające (nie musi być żadnej z tych ostatnich). ... to powinno być znane jako reprezentacja wartości. Wartości jakichkolwiek bitów wypełniających są nieokreślone. ⁴⁴⁾ "z komentarzem:" ⁴⁴⁾ Niektóre kombinacje bitów wypełniających mogą generować reprezentacje pułapek ".unsigned char
, ale ta odpowiedź jest używanaunsigned char
. Uwaga: ściśle zgodny program może obliczyćsizeof(unsigned) * CHAR_BIT
i określić, na podstawieUINT_MAX
tego, że określone implementacje nie mogą mieć reprezentacji pułapekunsigned
. Po tym, jak program określi to, może przystąpić do wykonania dokładnie tego, co robi ta odpowiedźunsigned char
.memcpy
to rozproszenie, tj. Twój przykład nie miałby zastosowania, gdyby został zastąpiony przez*&a = *&b;
.unsigned char
a zatemmemcpy
pomaga,*&
jest mniej jasny. Zgłoszę, gdy to się uspokoi.Standard C daje kompilatorom dużą swobodę w przeprowadzaniu optymalizacji. Konsekwencje tych optymalizacji mogą być zaskakujące, jeśli przyjmie się naiwny model programów, w których niezainicjowana pamięć jest ustawiona na jakiś losowy wzorzec bitowy, a wszystkie operacje są wykonywane w kolejności, w jakiej zostały zapisane.
Uwaga: poniższe przykłady są poprawne tylko dlatego,
x
że jego adres nigdy nie został zajęty, więc jest „podobny do rejestru”. Byłyby również ważne, gdyby typx
miał reprezentacje pułapki; rzadko ma to miejsce w przypadku typów bez znaku (wymaga to „marnowania” co najmniej jednego bitu pamięci i musi być udokumentowane) i niemożliwe w przypadkuunsigned char
. Gdybyx
miał typ ze znakiem, to implementacja mogłaby zdefiniować wzór bitowy, który nie jest liczbą między - (2 n-1 -1) a 2 n-1 -1 jako reprezentację pułapki. Zobacz odpowiedź Jensa Gustedta .Kompilatory próbują przypisać rejestry do zmiennych, ponieważ rejestry są szybsze niż pamięć. Ponieważ program może wykorzystywać więcej zmiennych niż procesor posiada rejestry, kompilatory dokonują alokacji rejestrów, co prowadzi do różnych zmiennych wykorzystujących ten sam rejestr w różnym czasie. Rozważ fragment programu
unsigned x, y, z; /* 0 */ y = 0; /* 1 */ z = 4; /* 2 */ x = - x; /* 3 */ y = y + z; /* 4 */ x = y + 1; /* 5 */
Kiedy wiersz 3 jest oceniany,
x
nie jest jeszcze zainicjowany, dlatego (uzasadnia kompilator) wiersz 3 musi być jakimś przypadkiem, który nie może się zdarzyć z powodu innych warunków, których kompilator nie był wystarczająco inteligentny, aby dowiedzieć się. Ponieważz
nie jest używany po linii 4 ix
nie jest używany przed linią 5, ten sam rejestr może być używany dla obu zmiennych. Tak więc ten mały program jest skompilowany do następujących operacji na rejestrach:r1 = 0; r0 = 4; r0 = - r0; r1 += r0; r0 = r1;
Końcowa wartość
x
to końcowa wartośćr0
, a końcowa wartośćy
to końcowa wartośćr1
. Te wartości to x = -3 i y = -4, a nie 5 i 4, jak by się stało, gdybyx
został poprawnie zainicjowany.Aby uzyskać bardziej rozbudowany przykład, rozważ następujący fragment kodu:
unsigned i, x; for (i = 0; i < 10; i++) { x = (condition() ? some_value() : -x); }
Załóżmy, że kompilator wykryje, że
condition
nie ma to żadnego efektu ubocznego. Ponieważcondition
nie modyfikujex
, kompilator wie, że pierwszy przebieg pętli nie może uzyskać dostępu,x
ponieważ nie został jeszcze zainicjowany. Dlatego pierwsze wykonanie treści pętli jest równoważnex = some_value()
, nie ma potrzeby testowania warunku. Kompilator może skompilować ten kod tak, jakbyś to napisałunsigned i, x; i = 0; /* if some_value() uses i */ x = some_value(); for (i = 1; i < 10; i++) { x = (condition() ? some_value() : -x); }
Sposób, w jaki można to modelować w kompilatorze, polega na rozważeniu, że każda wartość zależna od
x
ma jakąkolwiek wartość jest wygodna, o ile niex
jest zainicjowana. Ponieważ zachowanie, gdy niezainicjowana zmienna jest niezdefiniowana, a nie zmienna ma jedynie nieokreśloną wartość, kompilator nie musi śledzić żadnych specjalnych matematycznych relacji między wartościami, które są wygodne. Dlatego kompilator może przeanalizować powyższy kod w następujący sposób:x
jest inicjowany do czasu-x
oceny.-x
ma niezdefiniowane zachowanie, więc jego wartość jest taka, jaka jest-wygodna.condition ? value : value
condition; value
W konfrontacji z kodem w twoim pytaniu, ten sam kompilator analizuje, że kiedy
x = - x
jest oceniany, wartość-x
jest cokolwiek-jest-wygodne. Dzięki temu można zoptymalizować przypisanie.Nie szukałem przykładu kompilatora, który zachowuje się tak, jak opisano powyżej, ale jest to rodzaj optymalizacji, który dobre kompilatory próbują wykonać. Nie zdziwiłbym się, gdyby takiego spotkałem. Oto mniej prawdopodobny przykład kompilatora, z którym program ulega awarii. (Może to nie być takie nieprawdopodobne, jeśli kompilujesz swój program w jakimś zaawansowanym trybie debugowania).
Ten hipotetyczny kompilator mapuje każdą zmienną na innej stronie pamięci i ustawia atrybuty strony w taki sposób, że odczyt z niezainicjowanej zmiennej powoduje pułapkę procesora, która wywołuje debugger. Każde przypisanie do zmiennej najpierw upewnia się, że jej strona pamięci jest odwzorowana normalnie. Ten kompilator nie próbuje wykonywać żadnej zaawansowanej optymalizacji - działa w trybie debugowania, mającym na celu łatwe lokalizowanie błędów, takich jak niezainicjowane zmienne. Gdy
x = - x
jest oceniany, prawa strona powoduje pułapkę i uruchamia debuger.źródło
x
ma niezainicjowaną wartość, ale zachowanie przy dostępie byłoby być zdefiniowane, jeśli x nie ma zachowania podobnego do rejestru.x
, to wszystkie operacje na nim można pominąć, niezależnie od tego, czy jego wartość została zdefiniowana, czy nie. Gdyby kod następujący np.if (volatile1) x=volatile2; ... x = (x+volatile3) & 255;
Byłby równie zadowolony z dowolnej wartości 0-255, którax
mogłaby zawierać w przypadku,volatile1
gdy przyniosłaby zero, pomyślałbym, że implementacja, która pozwoli programiście pominąć niepotrzebny zapis,x
powinna być uznana za wyższą jakość niż taka, która zachowywałby się ...Tak, program może ulec awarii. Mogą na przykład istnieć reprezentacje pułapek (określone wzorce bitów, których nie można obsłużyć), które mogą spowodować przerwanie procesora, które nieobsłużone może spowodować awarię programu.
(To wyjaśnienie ma zastosowanie tylko na platformach, na których
unsigned int
można przedstawić pułapki, co jest rzadkością w rzeczywistych systemach; szczegółowe informacje i odniesienia do alternatywnych i być może bardziej powszechnych przyczyn, które prowadzą do aktualnego brzmienia normy, można znaleźć w komentarzach).źródło
(Ta odpowiedź dotyczy C 1999. Dla C 2011, patrz odpowiedź Jensa Gustedta.)
Standard C nie mówi, że użycie wartości obiektu automatycznego czasu trwania przechowywania, który nie jest zainicjowany, jest niezdefiniowanym zachowaniem. Norma C 1999 mówi, w 6.7.8 10, „Jeśli obiekt, który ma automatyczny czas przechowywania, nie jest jawnie zainicjowany, jego wartość jest nieokreślona”. (W tym akapicie opisano, w jaki sposób inicjowane są obiekty statyczne, więc jedynymi niezainicjowanymi obiektami, o które się martwimy, są obiekty automatyczne).
3.17.2 definiuje „nieokreśloną wartość” jako „nieokreśloną wartość lub reprezentację pułapki”. 3.17.3 definiuje „nieokreśloną wartość” jako „ważną wartość odpowiedniego typu, jeśli niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań dotyczących wyboru wartości w jakimkolwiek przypadku”.
Tak więc, jeśli niezainicjowany
unsigned int x
ma nieokreśloną wartość, tox -= x
musi dać zero. Pozostaje pytanie, czy może to być reprezentacja pułapki. Dostęp do wartości pułapki powoduje niezdefiniowane zachowanie, zgodnie z 6.2.6.1 5.Niektóre typy obiektów mogą mieć reprezentacje pułapek, takie jak sygnalizacyjne NaN liczb zmiennoprzecinkowych. Ale liczby całkowite bez znaku są wyjątkowe. Zgodnie z 6.2.6.2, każdy z N bitów wartości liczby int bez znaku reprezentuje potęgę 2, a każda kombinacja bitów wartości reprezentuje jedną z wartości od 0 do 2 N -1. Tak więc liczby całkowite bez znaku mogą mieć reprezentacje pułapek tylko z powodu pewnych wartości w ich bitach wypełniających (takich jak bit parzystości).
Jeśli na platformie docelowej bez znaku int nie ma bitów wypełnienia, to niezainicjowany int bez znaku nie może mieć reprezentacji pułapki, a użycie jego wartości nie może spowodować niezdefiniowanego zachowania.
źródło
x
ma reprezentację pułapki, tox -= x
może pułapka, prawda? Mimo to +1 za wskazanie liczb całkowitych bez znaku bez dodatkowych bitów musi mieć określone zachowanie - jest to wyraźnie przeciwieństwo innych odpowiedzi i (zgodnie z cytatem) wydaje się, że jest to to, co sugeruje standard.x
ma reprezentację pułapki,x -= x
może to być pułapka. Nawet zwykłex
użycie jako wartości może spowodować pułapkę. (Bezpieczne jest użyciex
jako lwartości; na zapis do obiektu nie wpłynie reprezentacja pułapki, która się w nim znajduje.)Tak, to nie jest zdefiniowane. Kod może ulec awarii. C mówi, że zachowanie jest nieokreślone, ponieważ nie ma konkretnego powodu, aby robić wyjątek od ogólnej reguły. Zaletą jest ta sama zaleta, co wszystkie inne przypadki niezdefiniowanego zachowania - kompilator nie musi wyprowadzać specjalnego kodu, aby to zadziałało.
Jak myślisz, dlaczego tak się nie dzieje? Dokładnie takie podejście zostało przyjęte. Kompilator nie jest wymagany, aby działał, ale nie jest wymagany do tego, aby się nie udał.
źródło
x
można go zadeklarować jakoregister
, tj. Jego adres nigdy nie jest brany. Nie wiem, czy byłeś tego świadomy (jeśli skutecznie to ukrywałeś), ale poprawna odpowiedź musi o tym wspominać.W przypadku dowolnej zmiennej dowolnego typu, która nie została zainicjowana lub z innych powodów ma nieokreśloną wartość, do kodu odczytującego tę wartość stosuje się następujące zasady:
W przeciwnym razie, jeśli nie ma reprezentacji pułapek, zmienna przyjmuje nieokreśloną wartość. Nie ma gwarancji, że ta nieokreślona wartość jest spójna przy każdym odczycie zmiennej. Jednak gwarantuje się, że nie będzie reprezentacją pułapki, a zatem gwarantuje się, że nie wywoła niezdefiniowanego zachowania [3].
Wartość może być następnie bezpiecznie używana bez powodowania awarii programu, chociaż taki kod nie jest przenośny do systemów z reprezentacjami pułapek.
[1]: C11 6.3.2.1:
[2]: C11 6.2.6.1:
[3] C11:
źródło
stdint.h
zawsze powinno być używane zamiast natywnych typów C. Ponieważstdint.h
wymusza dopełnienie 2 i brak bitów dopełniających. Innymi słowy,stdint.h
typy nie mogą być pełne bzdur.Podczas gdy wiele odpowiedzi koncentruje się na procesorach, które pułapki na dostęp do niezainicjowanych rejestrów, dziwaczne zachowania mogą pojawić się nawet na platformach, które nie mają takich pułapek, przy użyciu kompilatorów, które nie podejmują żadnego szczególnego wysiłku w celu wykorzystania UB. Rozważ kod:
volatile uint32_t a,b; uin16_t moo(uint32_t x, uint16_t y, uint32_t z) { uint16_t temp; if (a) temp = y; else if (b) temp = z; return temp; }
kompilator dla platformy takiej jak ARM, w której wszystkie instrukcje inne niż ładowanie i magazyny działają w 32-bitowych rejestrach, może rozsądnie przetwarzać kod w sposób równoważny z:
volatile uint32_t a,b; // Note: y is known to be 0..65535 // x, y, and z are received in 32-bit registers r0, r1, r2 uin32_t moo(uint32_t x, uint32_t y, uint32_t z) { // Since x is never used past this point, and since the return value // will need to be in r0, a compiler could map temp to r0 uint32_t temp; if (a) temp = y; else if (b) temp = z & 0xFFFF; return temp; }
Jeśli którykolwiek z nietrwałych odczytów dadzą wartość niezerową, r0 zostanie załadowany wartością z zakresu 0 ... 65535. W przeciwnym razie zwróci wszystko, co trzymał, gdy wywołano funkcję (tj. Wartość przekazaną do x), co może nie być wartością z zakresu 0..65535. W standardzie brakuje terminologii opisującej zachowanie wartości typu uint16_t, ale której wartość jest poza zakresem 0..65535, z wyjątkiem stwierdzenia, że każda akcja, która mogłaby spowodować takie zachowanie, wywołuje UB.
źródło
uint16_t
, ta zmienna może czasami odczytywać jako 123, a czasami 6553623). Jeśli wynik zostanie zignorowany ...register
, to może mieć dodatkowe bity, które sprawiają, że zachowanie jest potencjalnie niezdefiniowane. Dokładnie to mówisz, prawda?