Jakie są wszystkie typowe niezdefiniowane zachowania, o których powinien wiedzieć programista C ++? [Zamknięte]


201

Jakie są wszystkie typowe niezdefiniowane zachowania, o których powinien wiedzieć programista C ++?

Powiedz:

a[i] = i++;


3
Jesteś pewny. To wygląda dobrze zdefiniowane.
— Martin York,

17
6.2.2. Zamówienie ewaluacyjne [expr. Ocena] w języku programowania C ++ tak. Nie mam żadnych innych odniesień
— yesraaj

4
Ma rację .. właśnie spojrzał na 6.2.2 w języku programowania C ++ i mówi, że v [i] = i ++ jest niezdefiniowany
— dancavallaro,

4
Wyobrażam sobie, ponieważ program kompilujący wykonuje i ++ przed lub po obliczeniu lokalizacji pamięci v [i]. jasne, zawsze będę tam przydzielony. ale może pisać do v [i] lub v [i + 1] w zależności od kolejności operacji.
— Evan Teran

2
Wszystko, co mówi język programowania C ++ to: „Kolejność operacji podwyrażeń w wyrażeniu jest niezdefiniowana. W szczególności nie można założyć, że wyrażenie jest oceniane od lewej do prawej”.
— dancavallaro,

Odpowiedzi:


233

Wskaźnik

  • Dereferencje NULLwskaźnika
  • Dereferencje wskaźnika zwróconego przez „nowy” przydział wielkości zero
  • Używanie wskaźników do obiektów, których okres użytkowania dobiegł końca (na przykład układanie obiektów przydzielonych lub usuwanie obiektów)
  • Dereferencje wskaźnika, który nie został jeszcze zdecydowanie zainicjowany
  • Wykonywanie arytmetyki wskaźnika, która daje wynik poza granicami (powyżej lub poniżej) tablicy.
  • Dereferencje wskaźnika w miejscu poza końcem tablicy.
  • Konwertowanie wskaźników na obiekty niezgodnych typów
  • Używanie memcpydo kopiowania nakładających się buforów .

Przepełnienie bufora

  • Odczyt lub zapis do obiektu lub tablicy z przesunięciem ujemnym lub większym niż rozmiar tego obiektu (przepełnienie stosu / sterty)

Przepełnienia liczb całkowitych

  • Przepełnienie ze znakiem całkowitym
  • Ocena wyrażenia, które nie jest zdefiniowane matematycznie
  • Wartości przesunięcia w lewo o wartość ujemną (przesunięcia w prawo o wartości ujemne są zdefiniowane w realizacji)
  • Przesunięcie wartości o wartość większą lub równą liczbie bitów w liczbie (np. int64_t i = 1; i <<= 72Jest niezdefiniowana)

Rodzaje, obsada i Const

  • Rzucanie wartości liczbowej na wartość, która nie może być reprezentowana przez typ docelowy (bezpośrednio lub przez static_cast)
  • Korzystanie ze zmiennej automatycznej przed jej definitywnym przypisaniem (np. int i; i++; cout << i;)
  • Wykorzystanie wartości dowolnego obiektu typu innego niż volatilelub sig_atomic_tprzy odbiorze sygnału
  • Próba modyfikacji literału łańcuchowego lub dowolnego innego obiektu const w trakcie jego życia
  • Łączenie wąskiego z szerokim dosłownym ciągiem podczas wstępnego przetwarzania

Funkcja i szablon

  • Nie zwraca wartości z funkcji zwracającej wartość (bezpośrednio lub przez odpływ z bloku try)
  • Wiele różnych definicji tego samego obiektu (klasa, szablon, wyliczenie, funkcja wstawiana, funkcja członka statycznego itp.)
  • Nieskończona rekurencja w tworzeniu szablonów
  • Wywołanie funkcji przy użyciu różnych parametrów lub powiązanie z parametrami i powiązaniem, które funkcja jest zdefiniowana jako używanie.

OOP

  • Kaskadowe niszczenie obiektów o statycznym czasie przechowywania
  • Wynik przypisania do częściowo nakładających się obiektów
  • Rekurencyjne ponowne wprowadzanie funkcji podczas inicjalizacji jej obiektów statycznych
  • Wywoływanie funkcji wirtualnych do czysto wirtualnych funkcji obiektu z jego konstruktora lub destruktora
  • Odnosząc się do niestatycznych elementów obiektów, które nie zostały zbudowane lub zostały już zniszczone

Plik źródłowy i wstępne przetwarzanie

  • Niepusty plik źródłowy, który nie kończy się znakiem nowej linii lub kończy się odwrotnym ukośnikiem (wcześniej niż C ++ 11)
  • Ukośnik odwrotny, po którym następuje znak, który nie jest częścią określonych kodów specjalnych w stałej znaku lub łańcucha (jest to zdefiniowane w C ++ 11).
  • Przekroczenie limitów implementacji (liczba zagnieżdżonych bloków, liczba funkcji w programie, dostępne miejsce na stosie ...)
  • Wartości numeryczne preprocesora, które nie mogą być reprezentowane przez long int
  • Dyrektywa w sprawie przetwarzania wstępnego po lewej stronie definicji makra podobnej do funkcji
  • Dynamiczne generowanie zdefiniowanego tokena w #ifwyrażeniu

Do sklasyfikowania

  • Wywołanie wyjścia podczas niszczenia programu o statycznym czasie przechowywania

Hm ... NaN (x / 0) i Infinity (0/0) zostały objęte IEE 754, jeśli C ++ został zaprojektowany później, dlaczego zapisuje x / 0 jako niezdefiniowany?
— new123456

Re: „Ukośnik odwrotny, po którym następuje znak, który nie jest częścią określonych kodów specjalnych w stałej znaku lub ciągu”. To jest UB w C89 (§3.1.3.4) i C ++ 03 (który zawiera C89), ale nie w C99. C99 mówi, że „wynik nie jest tokenem i wymagana jest diagnostyka” (§6.4.4.4). Prawdopodobnie C ++ 0x (który zawiera C89) będzie taki sam.
— Adam Rosenfield,

1
Standard C99 zawiera listę niezdefiniowanych zachowań w dodatku J.2. Dostosowanie tej listy do C ++ wymagałoby trochę pracy. Trzeba będzie zmienić odniesienia do poprawnych klauzul C ++ zamiast klauzul C99, usunąć wszystko, co nieistotne, a także sprawdzić, czy wszystkie te rzeczy są naprawdę niezdefiniowane zarówno w C ++, jak i C. Ale to daje początek.
— Steve Jessop,

1
@ new123456 - nie wszystkie jednostki zmiennoprzecinkowe są kompatybilne z IEE754. Gdyby C ++ wymagało zgodności z IEE754, kompilatory musiałyby przetestować i obsłużyć przypadek, w którym RHS wynosi zero, poprzez jawne sprawdzenie. Poprzez niezdefiniowanie tego zachowania kompilator może uniknąć tego narzutu, mówiąc „jeśli użyjesz FPU innego niż IEE754, nie uzyskasz zachowania IEEE754 FPU”.
— SecurityMatt

1
„Ocena wyrażenia, którego wynik nie mieści się w zakresie odpowiednich typów” .... Przepełnienie liczb całkowitych jest dobrze zdefiniowane dla NIEZALEŻNYCH typów całkowych, tylko tych niepodpisanych.
— nacitar sevaht

31

Kolejność oceny parametrów funkcji jest nieokreślonym zachowaniem . (Nie spowoduje to awarii programu, wybuchu ani zamawiania pizzy ... w przeciwieństwie do niezdefiniowanego zachowania ).

Jedynym wymaganiem jest to, że wszystkie parametry muszą zostać w pełni ocenione przed wywołaniem funkcji.


To:

// The simple obvious one.
callFunc(getA(),getB());

Może być równoważne z tym:

int a = getA();
int b = getB();
callFunc(a,b);

Albo to:

int b = getB();
int a = getA();
callFunc(a,b);

Może być albo; to zależy od kompilatora. Wynik może mieć znaczenie, w zależności od skutków ubocznych.


23
Zamówienie jest nieokreślone, nie jest niezdefiniowane.
— Rob Kennedy,

1
Nienawidzę tego :) Straciłem cały dzień pracy, kiedy wyśledziłem jeden z tych przypadków ... w każdym razie nauczyłem się mojej lekcji i na szczęście nie upadłem ponownie
— Robert Gould

2
@Rob: Kłóciłbym się z tobą o zmianę znaczenia tutaj, ale wiem, że komitet normalizacyjny jest bardzo wybredny co do dokładnej definicji tych dwóch słów. Więc po prostu to zmienię :-)
— Martin York,

2
Mam szczęście w tej sprawie. Ukąsiłem go, gdy byłem na studiach i miałem profesora, który rzucił na to okiem i powiedział mi o moim problemie za około 5 sekund. Nie mówię, ile czasu zmarnowałbym na debugowanie w przeciwnym razie.
— Bill the Lizard,

27

Kompilator może dowolnie zamawiać części wyrażenia ewaluacyjnego (zakładając, że znaczenie nie ulegnie zmianie).

Z pierwotnego pytania:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

Blokada podwójnie sprawdzona. I jeden łatwy do popełnienia błąd.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.

co oznacza punkt sekwencji?
— yesraaj,


1
Ooh ... to paskudne, szczególnie odkąd widziałem tę dokładną strukturę zalecaną w Javie
— Tom

Zauważ, że niektóre kompilatory definiują zachowanie w tej sytuacji. Na przykład w VC ++ 2005+, jeśli a jest niestabilny, potrzebne są bariery pamięci, aby zapobiec zmianie kolejności instrukcji, dzięki czemu działa podwójne sprawdzanie blokowania.
— Eclipse

Martin York: <i> // (c) ma miejsce po (a) i (b) </i> Czy tak jest? Wprawdzie w tym konkretnym przykładzie jedynym scenariuszem, w którym mogłoby mieć znaczenie, byłoby, gdyby „i” było zmienną zmienną zamapowaną na rejestr sprzętowy, a [i] (stara wartość „i”) została do niej przypisana, ale czy istnieje jakikolwiek zagwarantować, że przyrost nastąpi przed punktem sekwencyjnym?
— supercat

5

Moim ulubionym jest „nieskończona rekurencja w tworzeniu instancji szablonów”, ponieważ uważam, że jest to jedyny przypadek, w którym niezdefiniowane zachowanie występuje w czasie kompilacji.


Zrobiłem to wcześniej, ale nie rozumiem, jak to jest niezdefiniowane. To całkiem oczywiste, że dokonujesz nieskończonej rekurencji po namyśle.
— Robert Gould

Problem polega na tym, że kompilator nie może zbadać twojego kodu i zdecydować, czy będzie cierpieć z powodu nieskończonej rekurencji, czy nie. Jest to przykład problemu z zatrzymaniem. Zobacz: stackoverflow.com/questions/235984/...
— Daniel Earwicker

Tak, to zdecydowanie problem z zatrzymaniem
— Robert Gould,

spowodował awarię mojego systemu z powodu wymiany spowodowanej zbyt małą pamięcią.
— Johannes Schaub - litb

2
Stałe preprocesora, które nie pasują do int, to także czas kompilacji.
— Joshua,

5

Przypisywanie do stałej po constrozebraniu za pomocą const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined

5

Oprócz nieokreślonego zachowania istnieje również równie nieprzyjemne zachowanie zdefiniowane w implementacji .

Niezdefiniowane zachowanie występuje, gdy program robi coś, czego wynik nie jest określony przez standard.

Zachowanie zdefiniowane w implementacji jest działaniem programu, którego wynik nie jest zdefiniowany w standardzie, ale wdrożenie musi udokumentować. Przykładem są „Wielobajtowe literały znaków” z pytania Przepełnienie stosu Czy istnieje kompilator C, który tego nie skompilował? .

Zachowanie zdefiniowane w implementacji gryzie cię dopiero po rozpoczęciu przenoszenia (ale uaktualnianie do nowej wersji kompilatora również się przenosi!)


4

Zmienne mogą być aktualizowane tylko raz w wyrażeniu (technicznie raz między punktami sekwencji).

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.

Dotrzyj przynajmniej raz między dwoma punktami sekwencji.
— Prasoon Saurav

2
@Prasoon: Myślę, że miałeś na myśli: najwyżej raz między dwoma punktami sekwencji. :-)
— Nawaz

3

Podstawowe zrozumienie różnych ograniczeń środowiskowych. Pełna lista znajduje się w sekcji 5.2.4.1 specyfikacji C. Tu jest kilka;

  • 127 parametrów w definicji jednej funkcji
  • 127 argumentów w jednym wywołaniu funkcji
  • 127 parametrów w jednej definicji makra
  • 127 argumentów w jednym wywołaniu makra
  • 4095 znaków w logicznej linii źródłowej
  • 4095 znaków w dosłownym ciągu znaków lub w dosłownym ciągu znaków (po konkatenacji)
  • 65535 bajtów w obiekcie (tylko w środowisku hostowanym)
  • 15 poziomów testowania dla plików zawartych w pakiecie
  • 1023 etykiety przypadków dla instrukcji zamiany (z wyłączeniem etykiet dla jakichkolwiek nowych instrukcji zamiany)

Byłem trochę zaskoczony limitem 1023 etykiet przypadków dla instrukcji switch. Mogę zauważyć, że przekroczenie generowanego kodu / lex / parserów jest dość łatwe.

Jeśli limity te zostaną przekroczone, masz niezdefiniowane zachowanie (awarie, wady bezpieczeństwa itp.).

Tak, wiem, że pochodzi ze specyfikacji C, ale C ++ udostępnia te podstawowe funkcje obsługi.


9
Jeśli osiągniesz te limity, masz więcej problemów niż niezdefiniowane zachowanie.
— new123456

Możesz ŁATWO przekroczyć 65535 bajtów w obiekcie, takim jak STD :: vector
— Demi

2

Używanie memcpydo kopiowania między nakładającymi się regionami pamięci. Na przykład:

char a[256] = {};
memcpy(a, a, sizeof(a));

Zachowanie jest niezdefiniowane zgodnie ze standardem C, który jest objęty standardem C ++ 03.

7.21.2.1 Funkcja memcpy

Streszczenie

1 / #include void * memcpy (void * ograniczenia s1, const void * ograniczenia s2, size_t n);

Opis

2 / Funkcja memcpy kopiuje n znaków z obiektu wskazywanego przez s2 do obiektu wskazywanego przez s1. Jeśli kopiowanie odbywa się między nakładającymi się obiektami, zachowanie jest niezdefiniowane. Zwraca 3 Funkcja memcpy zwraca wartość s1.

7.21.2.2 Funkcja memmove

Streszczenie

1 #include void * memmove (void * s1, const void * s2, size_t n);

Opis

2 Funkcja memmove kopiuje n znaków z obiektu wskazywanego przez s2 do obiektu wskazywanego przez s1. Kopiowanie odbywa się tak, jakby n znaków z obiektu wskazanego przez s2 zostało najpierw skopiowanych do tymczasowej tablicy n znaków, która nie zachodzi na obiekty wskazane przez s1 i s2, a następnie n znaków z tablicy tymczasowej jest skopiowanych do obiekt wskazywany przez s1. Zwroty

3 Funkcja memmove zwraca wartość s1.


2

Jedynym typem, dla którego C ++ gwarantuje rozmiar, jest char. Rozmiar to 1. Rozmiar wszystkich innych typów zależy od platformy.


Czy nie po to jest <cstdint>? Definiuje typy, takie jak uint16_6 i tak dalej.
— Jasper Bekkers,

Tak, ale rozmiar większości typów, powiedzmy długi, nie jest dobrze zdefiniowany.
— JaredPar,

także cstdint nie jest jeszcze częścią obecnego standardu c ++. zobacz boost / stdint.hpp, aby znaleźć obecnie przenośne rozwiązanie.
— Evan Teran,

To nie jest niezdefiniowane zachowanie. Norma mówi, że platforma zgodna definiuje rozmiary, a nie norma definiująca je.
— Daniel Earwicker,

1
@JaredPar: To złożony post z wieloma wątkami, więc podsumowałem to tutaj . Najważniejsze jest to: „5. Aby reprezentować -2147483647 i +2147483647 w formacie binarnym, potrzebujesz 32 bitów”.
— John Dibling

2

Obiekty na poziomie przestrzeni nazw w różnych jednostkach kompilacji nigdy nie powinny zależeć od siebie przy inicjalizacji, ponieważ ich kolejność inicjowania jest niezdefiniowana.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.