Quali sono i comportamenti comuni non definiti/non specificati per C in cui ti imbatti?[Chiuso]

StackOverflow https://stackoverflow.com/questions/98340

  •  01-07-2019
  •  | 
  •  

Domanda

Un esempio di comportamento non specificato nel linguaggio C è l'ordine di valutazione degli argomenti di una funzione.Potrebbe essere da sinistra a destra o da destra a sinistra, semplicemente non lo sai.Ciò influenzerebbe il modo foo(c++, c) O foo(++c, c) viene valutato.

Quale altro comportamento non specificato c'è che può sorprendere il programmatore ignaro?

È stato utile?

Soluzione

Una domanda da giurista linguistico.Ok.

La mia top3 personale:

  1. violando la rigida regola dell'aliasing
  2. violando la rigida regola dell'aliasing
  3. violando la rigida regola dell'aliasing

    :-)

Modificare Ecco un piccolo esempio che sbaglia due volte:

(presupporre int a 32 bit e little endian)

float funky_float_abs (float a)
{
  unsigned int temp = *(unsigned int *)&a;
  temp &= 0x7fffffff;
  return *(float *)&temp;
}

Quel codice tenta di ottenere il valore assoluto di un float manipolando il bit di segno direttamente nella rappresentazione di un float.

Tuttavia, il risultato della creazione di un puntatore a un oggetto eseguendo il cast da un tipo a un altro non è valido C.Il compilatore può presupporre che i puntatori a tipi diversi non puntino alla stessa porzione di memoria.Questo è vero per tutti i tipi di puntatori eccetto void* e char* (il segno non ha importanza).

Nel caso sopra lo faccio due volte.Una volta per ottenere un int-alias per float a e una volta per riconvertire il valore in float.

Esistono tre modi validi per fare lo stesso.

Usa un puntatore char o void durante il cast.Questi sono sempre alias di qualsiasi cosa, quindi sono sicuri.

float funky_float_abs (float a)
{
  float temp_float = a;
  // valid, because it's a char pointer. These are special.
  unsigned char * temp = (unsigned char *)&temp_float;
  temp[3] &= 0x7f;
  return temp_float;
}

Usa la memoria.Memcpy accetta puntatori void, quindi forzerà anche l'aliasing.

float funky_float_abs (float a)
{
  int i;
  float result;
  memcpy (&i, &a, sizeof (int));
  i &= 0x7fffffff;
  memcpy (&result, &i, sizeof (int));
  return result;
}

Il terzo modo valido:utilizzare i sindacati.Questo è esplicitamente non indefinito dal C99:

float funky_float_abs (float a)
{
  union 
  {
     unsigned int i;
     float f;
  } cast_helper;

  cast_helper.f = a;
  cast_helper.i &= 0x7fffffff;
  return cast_helper.f;
}

Altri suggerimenti

Il mio comportamento indefinito preferito è che se un file sorgente non vuoto non termina con una nuova riga, il comportamento è indefinito.

Sospetto che sia vero, tuttavia, che nessun compilatore che vedrò mai ha trattato un file sorgente in modo diverso a seconda che sia terminato o meno a capo, oltre a emettere un avviso.Quindi non è proprio qualcosa che sorprenderà i programmatori ignari, a parte il fatto che potrebbero essere sorpresi dall'avvertimento.

Quindi, per problemi di portabilità autentici (che per lo più dipendono dall'implementazione piuttosto che non specificati o indefiniti, ma penso che rientri nello spirito della domanda):

  • char non è necessariamente (non) firmato.
  • int può avere qualsiasi dimensione, compresa tra 16 bit.
  • i float non sono necessariamente formattati IEEE o conformi.
  • i tipi interi non sono necessariamente complemento a due e l'overflow aritmetico dei numeri interi causa un comportamento indefinito (l'hardware moderno non si bloccherà, ma alcune ottimizzazioni del compilatore si tradurranno in un comportamento diverso dal wraparound anche se questo è ciò che fa l'hardware.Per esempio if (x+1 < x) può essere ottimizzato come sempre false quando x ha il tipo firmato:Vedere -fstrict-overflow opzione nel GCC).
  • "/", "." e ".." in un #include non ha un significato definito e può essere trattato in modo diverso da diversi compilatori (questo varia effettivamente, e se va storto ti rovinerà la giornata).

Quelli davvero seri che possono sorprendere anche sulla piattaforma su cui hai sviluppato, perché il comportamento è solo parzialmente indefinito/non specificato:

  • Threading POSIX e modello di memoria ANSI.L'accesso simultaneo alla memoria non è così ben definito come pensano i principianti.volatile non fa quello che pensano i principianti.L'ordine degli accessi alla memoria non è così ben definito come pensano i principianti.Accessi Potere essere spostato oltre le barriere della memoria in determinate direzioni.La coerenza della cache della memoria non è richiesta.

  • Il codice di profilazione non è così semplice come pensi.Se il ciclo di test non ha alcun effetto, il compilatore può rimuoverlo parzialmente o completamente.inline non ha effetti definiti.

E, come penso che Nils abbia menzionato di sfuggita:

  • VIOLANDO LE RIGOROSE REGOLE DI ALIASING.

Dividere qualcosa per un puntatore a qualcosa.Semplicemente non verrà compilato per qualche motivo...:-)

result = x/*y;

Il mio preferito è questo:

// what does this do?
x = x++;

Per rispondere ad alcuni commenti, si tratta di un comportamento indefinito secondo lo standard.Vedendo questo, al compilatore è consentito fare qualsiasi cosa, inclusa la formattazione del disco rigido.Vedi ad esempio questo commento qui.Il punto non è che si possa vedere che esiste una possibile ragionevole aspettativa di qualche comportamento.A causa dello standard C++ e del modo in cui vengono definiti i punti della sequenza, questa riga di codice ha in realtà un comportamento indefinito.

Ad esempio, se avessimo x = 1 prima della riga sopra, quale sarebbe il risultato valido dopo?Qualcuno ha commentato che dovrebbe esserlo

x viene incrementato di 1

quindi dovremmo vedere x == 2 dopo.Tuttavia questo non è vero, troverai alcuni compilatori che hanno x == 1 in seguito, o forse anche x == 3.Dovresti osservare attentamente l'assembly generato per capire il motivo per cui ciò potrebbe accadere, ma le differenze sono dovute al problema di fondo.Essenzialmente, penso che ciò sia dovuto al fatto che al compilatore è consentito valutare le due istruzioni di assegnazione nell'ordine che preferisce, quindi potrebbe fare il x++ prima, o il x = Primo.

Un altro problema che ho riscontrato (che è definito, ma sicuramente inaspettato).

char è malvagio.

  • firmato o non firmato a seconda di ciò che sente il compilatore
  • non imposto come 8 bit

Non riesco a contare il numero di volte in cui ho corretto gli identificatori di formato printf in modo che corrispondessero al loro argomento. Qualsiasi mancata corrispondenza è un comportamento indefinito.

  • No, non devi superare un int (O long) A %x - UN unsigned int è obbligatorio
  • No, non devi superare un unsigned int A %d - UN int è obbligatorio
  • No, non devi superare un size_t A %u O %d - utilizzo %zu
  • No, non devi stampare un puntatore con %d O %x - utilizzo %p e lancia a void *

Un compilatore non deve dirti che stai chiamando una funzione con il numero sbagliato di parametri/tipi di parametri errati se il prototipo della funzione non è disponibile.

Ho visto molti programmatori relativamente inesperti morsi da costanti multicarattere.

Questo:

"x"

è una stringa letterale (che è di tipo char[2] e decade a char* nella maggior parte dei contesti).

Questo:

'x'

è una costante di carattere ordinaria (che, per ragioni storiche, è di tipo int).

Questo:

'xy'

è anch'essa una costante di carattere perfettamente legale, ma il suo valore (che è ancora di tipo int) è definito dall'implementazione.È una caratteristica del linguaggio quasi inutile che serve principalmente a creare confusione.

Gli sviluppatori di clang ne hanno pubblicati alcuni ottimi esempi qualche tempo fa, in un post che ogni programmatore C dovrebbe leggere.Alcuni interessanti non menzionati prima:

  • Overflow di numeri interi con segno: no, non è corretto racchiudere una variabile con segno oltre il suo massimo.
  • Dereferenziare un puntatore NULL: sì, questo non è definito e potrebbe essere ignorato, vedere la parte 2 del collegamento.

Gli EE qui hanno appena scoperto che a>>-2 è un po' complicato.

Ho annuito e ho detto loro che non era naturale.

Assicurati di inizializzare sempre le tue variabili prima di usarle!Quando avevo appena iniziato con C, questo mi causò una serie di mal di testa.

Utilizzando le versioni macro di funzioni come "max" o "isupper".Le macro valutano i loro argomenti due volte, quindi otterrai effetti collaterali inaspettati quando chiami max(++i, j) o isupper(*p++)

Quanto sopra è per lo standard C.In C++ questi problemi sono in gran parte scomparsi.La funzione max è ora una funzione basata su modelli.

dimenticando di aggiungere static float foo(); nel file di intestazione, solo per ottenere eccezioni in virgola mobile che vengono lanciate quando restituirebbe 0.0f;

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top