Quali sono tutti i comuni comportamenti indefiniti che un programmatore C ++ dovrebbe conoscere? [chiuso]


201

Quali sono tutti i comuni comportamenti indefiniti che un programmatore C ++ dovrebbe conoscere?

Dì, come:

a[i] = i++;


3
Sei sicuro. Sembra ben definito.
Martin York,

17
6.2.2 Ordine di valutazione [valutazione expr.] Nel linguaggio di programmazione C ++ lo dico. Non ho altri riferimenti
yesraaj

4
Ha ragione .. ho appena visto 6.2.2 in Il linguaggio di programmazione C ++ e dice che v [i] = i ++ non è definito
dancavallaro,

4
Immagino perché il comiler fa eseguire i ++ prima o dopo il calcolo della posizione della memoria di v [i]. certo, sarò sempre assegnato lì. ma potrebbe scrivere su v [i] o v [i + 1] a seconda dell'ordine delle operazioni ..
Evan Teran,

2
Tutto ciò che dice il linguaggio di programmazione C ++ è "L'ordine delle operazioni delle sottoespressioni all'interno di un'espressione non è definito. In particolare, non si può presumere che l'espressione sia valutata da sinistra a destra".
dancavallaro,

Risposte:


233

pointer

  • Dereferenziare un NULLpuntatore
  • Dereferenziare un puntatore restituito da una "nuova" allocazione di dimensione zero
  • Uso dei puntatori agli oggetti la cui durata è terminata (ad esempio, impilare oggetti allocati o oggetti eliminati)
  • Dereferenziare un puntatore che non è stato ancora definitivamente inizializzato
  • Esecuzione dell'aritmetica del puntatore che produce un risultato al di fuori dei limiti (sopra o sotto) di un array.
  • Dereferenziare il puntatore in una posizione oltre la fine di un array.
  • Conversione di puntatori in oggetti di tipi incompatibili
  • Utilizzo memcpyper copiare buffer sovrapposti .

Overflow del buffer

  • Leggere o scrivere su un oggetto o un array con un offset negativo o oltre la dimensione di tale oggetto (overflow stack / heap)

Overflow di numeri interi

  • Overflow di numeri interi firmati
  • Valutare un'espressione che non è definita matematicamente
  • Valori di spostamento a sinistra di un importo negativo (gli spostamenti a destra di importi negativi sono definiti dall'implementazione)
  • Spostamento di valori di un valore maggiore o uguale al numero di bit nel numero (ad es. Non int64_t i = 1; i <<= 72definito)

Tipi, Cast e Cost

  • Trasmissione di un valore numerico in un valore che non può essere rappresentato dal tipo di destinazione (direttamente o tramite static_cast)
  • Utilizzo di una variabile automatica prima che sia stata definitivamente assegnata (ad es. int i; i++; cout << i;)
  • Utilizzo del valore di qualsiasi oggetto di tipo diverso volatileo sig_atomic_talla ricezione di un segnale
  • Tentativo di modificare una stringa letterale o qualsiasi altro oggetto const durante la sua vita
  • Concatenare uno stretto con una stringa larga letterale durante la preelaborazione

Funzione e modello

  • Non restituire un valore da una funzione di restituzione del valore (direttamente o scorrendo da un blocco try)
  • Più definizioni diverse per la stessa entità (classe, modello, enumerazione, funzione incorporata, funzione di membro statico, ecc.)
  • Ricorsione infinita nell'istanza di modelli
  • Chiamare una funzione utilizzando parametri o collegamenti diversi ai parametri e al collegamento che la funzione viene definita come utilizzo.

OOP

  • Distruzioni in cascata di oggetti con durata dell'archiviazione statica
  • Il risultato dell'assegnazione ad oggetti parzialmente sovrapposti
  • Reinserire ricorsivamente una funzione durante l'inizializzazione dei suoi oggetti statici
  • Effettuare chiamate di funzione virtuale a funzioni virtuali pure di un oggetto dal suo costruttore o distruttore
  • Riferendosi a membri non statici di oggetti che non sono stati costruiti o che sono già stati distrutti

File di origine e preelaborazione

  • Un file di origine non vuoto che non termina con una nuova riga o termina con una barra rovesciata (prima di C ++ 11)
  • Una barra rovesciata seguita da un carattere che non fa parte dei codici di escape specificati in una costante di carattere o stringa (questo è definito dall'implementazione in C ++ 11).
  • Superamento dei limiti di implementazione (numero di blocchi nidificati, numero di funzioni in un programma, spazio stack disponibile ...)
  • Valori numerici del preprocessore che non possono essere rappresentati da a long int
  • Direttiva di preelaborazione sul lato sinistro di una definizione macro simile a una funzione
  • Generazione dinamica del token definito in #ifun'espressione

Essere classificato

  • Chiamata di uscita durante la distruzione di un programma con durata dell'archiviazione statica

Hm ... NaN (x / 0) e Infinity (0/0) sono stati coperti da IEE 754, se C ++ è stato progettato in seguito, perché registra x / 0 come indefinito?
nuovo123456,

Ri: "Una barra rovesciata seguita da un carattere che non fa parte dei codici di escape specificati in un carattere o una costante di stringa". Questo è UB in C89 (§3.1.3.4) e C ++ 03 (che incorpora C89), ma non in C99. C99 afferma che "il risultato non è un token ed è necessaria una diagnostica" (§6.4.4.4). Presumibilmente C ++ 0x (che incorpora C89) sarà lo stesso.
Adam Rosenfield,

1
Lo standard C99 ha un elenco di comportamenti indefiniti nell'appendice J.2. Ci vorrebbe del lavoro per adattare questo elenco al C ++. Dovresti cambiare i riferimenti alle clausole C ++ corrette piuttosto che alle clausole C99, rimuovere qualsiasi cosa irrilevante e anche verificare se tutte quelle cose sono davvero indefinite in C ++ e C. Ma fornisce un inizio.
Steve Jessop,

1
@ new123456 - non tutte le unità a virgola mobile sono compatibili IEE754. Se C ++ richiedesse la conformità IEE754, i compilatori dovrebbero testare e gestire il caso in cui RHS è zero tramite un controllo esplicito. Rendendo indefinito il comportamento, il compilatore può evitare tale sovraccarico dicendo "se si utilizza una FPU non IEE754, non si otterrà il comportamento della FPU IEEE754".
SecurityMatt

1
"Valutazione di un'espressione il cui risultato non rientra nell'intervallo dei tipi corrispondenti" .... L'overflow di numeri interi è ben definito per i tipi integrali UNSIGNED, ma non quelli firmati.
nacitar sevaht

31

L'ordine in cui vengono valutati i parametri della funzione è un comportamento non specificato . (Questo non farà schiantare, esplodere o ordinare la pizza del tuo programma ... diversamente dal comportamento indefinito .)

L'unico requisito è che tutti i parametri devono essere valutati completamente prima di chiamare la funzione.


Questo:

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

Può essere equivalente a questo:

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

O questo:

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

Può essere uno dei due; dipende dal compilatore. Il risultato può essere importante, a seconda degli effetti collaterali.


23
L'ordine non è specificato, non è definito.
Rob Kennedy,

1
Odio questo :) Ho perso una giornata di lavoro dopo aver rintracciato uno di questi casi ... comunque ho imparato la mia lezione e non sono caduto di nuovo fortunatamente
Robert Gould il

2
@Rob: vorrei discutere con te del cambiamento di significato qui, ma so che il comitato per gli standard è molto esigente sulla definizione esatta di queste due parole. Quindi lo cambierò e basta :-)
Martin York il

2
Sono stato fortunato su questo. Mi è stato morso quando ero al college e avevo un professore che ha dato un'occhiata e mi ha detto il mio problema in circa 5 secondi. Non dire quanto altrimenti avrei sprecato il debug altrimenti.
Bill the Lizard,

27

Il compilatore è libero di riordinare le parti di valutazione di un'espressione (supponendo che il significato sia invariato).

Dalla domanda originale:

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;

Doppio bloccaggio controllato. E un semplice errore da fare.

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.

cosa si intende per punto sequenza?
yesraaj,


1
Ooh ... è brutto, soprattutto da quando ho visto la struttura esatta raccomandata in Java
Tom,

Si noti che alcuni compilatori definiscono il comportamento in questa situazione. In VC ++ 2005+, ad esempio, se a è volatile, i barier di memoria necessari sono impostati per impedire il riordino delle istruzioni in modo che il doppio controllo funzioni.
Eclipse,

Martin York: <i> // (c) è garantito dopo (a) e (b) </i> È vero? Certamente in quel particolare esempio l'unico scenario in cui potrebbe importare sarebbe se 'i' fosse una variabile volatile mappata su un registro hardware e un [i] (vecchio valore di 'i') fosse aliasato su di esso, ma c'è qualche garantire che l'incremento avverrà prima di un punto di sequenza?
supercat

5

Il mio preferito è "Ricorsione infinita nell'istanza di modelli" perché credo che sia l'unico in cui il comportamento indefinito si verifica al momento della compilazione.


Fatto prima, ma non vedo come sia indefinito. È abbastanza ovvio che stai facendo una ricorsione infinita nel ripensamento.
Robert Gould,

Il problema è che il compilatore non può esaminare il tuo codice e decidere con precisione se subirà una ricorsione infinita o meno. È un'istanza del problema di arresto. Vedi: stackoverflow.com/questions/235984/...
Daniel Earwicker

Sì, è sicuramente un problema decisivo
Robert Gould il

ha causato il crash del mio sistema a causa dello scambio causato da memoria insufficiente.
Johannes Schaub - litb

2
Anche le costanti del preprocessore che non rientrano in un int sono tempo di compilazione.
Giosuè,

5

Assegnare a una costante dopo lo stripping constness usando const_cast<>:

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

5

Oltre al comportamento indefinito , esiste anche il comportamento ugualmente brutto definito dall'implementazione .

Il comportamento indefinito si verifica quando un programma fa qualcosa il cui risultato non è specificato dallo standard.

Il comportamento definito dall'implementazione è un'azione di un programma il cui risultato non è definito dallo standard, ma che l'implementazione è richiesta per documentare. Un esempio è "Letterali di caratteri multibyte", dalla domanda Stack Overflow Esiste un compilatore C che non riesce a compilare questo? .

Il comportamento definito dall'implementazione ti morde solo quando inizi il porting (ma anche l'aggiornamento alla nuova versione del compilatore sta effettuando il porting!)


4

Le variabili possono essere aggiornate solo una volta in un'espressione (tecnicamente una volta tra i punti sequenza).

int i =1;
i = ++i;

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

Infact almeno una volta tra due punti di sequenza.
Prasoon Saurav,

2
@Prasoon: penso che volevi dire: al massimo una volta tra due punti di sequenza. :-)
Nawaz,

3

Una comprensione di base dei vari limiti ambientali. L'elenco completo è nella sezione 5.2.4.1 della specifica C. Eccone alcuni;

  • 127 parametri in una definizione di funzione
  • 127 argomenti in una chiamata di funzione
  • 127 parametri in una macro definizione
  • 127 argomenti in una macro invocazione
  • 4095 caratteri in una riga di origine logica
  • 4095 caratteri in una stringa di caratteri letterale o letterale a stringa larga (dopo la concatenazione)
  • 65535 byte in un oggetto (solo in un ambiente ospitato)
  • 15 livelli di interesse per i file #inclusi
  • 1023 etichette caso per un'istruzione switch (escluse quelle per qualsiasi istruzione switch annidata)

In realtà sono rimasto un po 'sorpreso dal limite di 1023 etichette case per un'istruzione switch, posso prevedere che il superamento per codice generato / lex / parser sia abbastanza semplice.

Se questi limiti vengono superati, si ha un comportamento indefinito (arresti anomali, falle nella sicurezza, ecc ...).

Bene, so che proviene dalla specifica C, ma C ++ condivide questi supporti di base.


9
Se si raggiungono questi limiti, si hanno più problemi che comportamenti indefiniti.
nuovo123456,

Si potrebbe FACILMENTE superare 65535 byte in un oggetto, come un STD :: vector
Demi,

2

Utilizzare memcpyper copiare tra aree di memoria sovrapposte. Per esempio:

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

Il comportamento non è definito secondo lo standard C, che è incluso nello standard C ++ 03.

7.21.2.1 La funzione memcpy

Sinossi

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

Descrizione

2 / La funzione memcpy copia n caratteri dall'oggetto puntato da s2 nell'oggetto puntato da s1. Se la copia avviene tra oggetti che si sovrappongono, il comportamento non è definito. Restituisce 3 La funzione memcpy restituisce il valore di s1.

7.21.2.2 La funzione memmove

Sinossi

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

Descrizione

2 La funzione memmove copia n caratteri dall'oggetto puntato da s2 nell'oggetto puntato da s1. La copia avviene come se gli n caratteri dell'oggetto indicato da s2 vengano prima copiati in un array temporaneo di n caratteri che non si sovrappongono agli oggetti indicati da s1 e s2, quindi gli n caratteri dell'array temporaneo vengono copiati in l'oggetto indicato da s1. ritorna

3 La funzione memmove restituisce il valore di s1.


2

L'unico tipo per il quale C ++ garantisce una dimensione è char. E la dimensione è 1. La dimensione di tutti gli altri tipi dipende dalla piattaforma.


Non è a cosa serve <cstdint>? Definisce tipi come uint16_6 et cetera.
Jasper Bekkers il

Sì, ma la dimensione della maggior parte dei tipi, diciamo a lungo, non è ben definita.
JaredPar il

anche cstdint non fa ancora parte dell'attuale standard c ++. vedi boost / stdint.hpp per una soluzione attualmente portatile.
Evan Teran,

Questo non è un comportamento indefinito. Lo standard afferma che la piattaforma conforme definisce le dimensioni, piuttosto che lo standard che le definisce.
Daniel Earwicker,

1
@JaredPar: è un post complesso con molte discussioni, quindi ho riassunto tutto qui . La linea di fondo è questa: "5. Per rappresentare -2147483647 e +2147483647 in binario, sono necessari 32 bit."
John Dibling,

2

Gli oggetti a livello di spazio dei nomi in unità di compilazione diverse non devono mai dipendere l'uno dall'altro per l'inizializzazione, poiché il loro ordine di inizializzazione non è definito.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.