Cosa succede a una variabile dichiarata, non inizializzata in C? Ha un valore?


139

Se in CI scrivi:

int num;

Prima di assegnare qualcosa a num, è il valore di numindeterminato?


4
Ehm, non è una variabile definita , non dichiarata ? (Mi dispiace se è il mio C ++ che splende attraverso ...)
sbi

6
No. Posso dichiarare una variabile senza definirla: extern int x;tuttavia la definizione implica sempre la dichiarazione. Questo non è vero in C ++, con variabili membro statiche della classe che si possono definire senza dichiarare, poiché la dichiarazione deve essere nella definizione della classe (non dichiarazione!) E la definizione deve essere al di fuori della definizione della classe.
bdonlan,

ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html Sembra definito significa che devi inizializzarlo anche tu.
atp

Risposte:


188

Le variabili statiche (ambito del file e funzione statica) sono inizializzate su zero:

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

Le variabili non statiche (variabili locali) sono indeterminate . Leggendoli prima di assegnare un valore si ottiene un comportamento indefinito.

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}

In pratica, tendono ad avere solo un valore insensato lì inizialmente - alcuni compilatori possono persino inserire valori specifici e fissi per renderlo ovvio quando si guarda in un debugger - ma a rigor di termini, il compilatore è libero di fare qualsiasi cosa dall'arresto anomalo all'evocazione demoni attraverso i tuoi passaggi nasali .

Per quanto riguarda il motivo per cui si tratta di un comportamento indefinito anziché semplicemente "valore indefinito / arbitrario", ci sono un certo numero di architetture CPU che hanno bit di flag aggiuntivi nella loro rappresentazione per vari tipi. Un esempio moderno sarebbe l'Itanium, che ha un bit "Not a Thing" nei suoi registri ; naturalmente, i disegnatori standard C stavano prendendo in considerazione alcune architetture più antiche.

Il tentativo di lavorare con un valore con questi bit di flag impostati può comportare un'eccezione della CPU in un'operazione che non dovrebbe realmente fallire (ad es. Aggiunta di numeri interi o assegnazione a un'altra variabile). E se vai e lasci una variabile non inizializzata, il compilatore potrebbe raccogliere una spazzatura casuale con questi bit di flag impostati, il che significa che toccare quella variabile non inizializzata potrebbe essere mortale.


2
oh no non lo sono. Potrebbero essere, in modalità debug, quando non sei di fronte a un cliente, nei mesi con una R in, se sei fortunato
Martin Beckett,

8
cosa no? l'inizializzazione statica è richiesta dallo standard; vedi ISO / IEC 9899: 1999 6.7.8 # 10
bdonlan

2
il primo esempio va bene per quanto posso dire. Sono meno sul motivo per cui il compilatore potrebbe bloccarsi nel secondo però :)

6
@Stuart: esiste una cosa chiamata "rappresentazione trap", che è fondamentalmente un modello di bit che non denota un valore valido e può causare ad esempio eccezioni hardware in fase di esecuzione. L'unico tipo C per il quale esiste una garanzia che qualsiasi modello di bit sia un valore valido è char; tutti gli altri possono avere rappresentazioni di trappole. In alternativa, poiché l'accesso alla variabile non inizializzata è comunque UB, un compilatore conforme potrebbe semplicemente fare un po 'di controllo e decidere di segnalare il problema.
Pavel Minaev,

5
bdonian è corretto. C è sempre stato specificato in modo abbastanza preciso. Prima di C89 e C99, un documento di dmr specificava tutte queste cose nei primi anni '70. Anche nel sistema embedded più rozzo, ci vuole solo un memset () per fare le cose bene, quindi non ci sono scuse per un ambiente non conforme. Ho citato lo standard nella mia risposta.
DigitalRoss,

57

0 se statico o globale, indeterminato se la classe di archiviazione è automatica

C è sempre stato molto specifico sui valori iniziali degli oggetti. Se globali o static, verranno azzerati. Se autoil valore è indeterminato .

Questo era il caso dei compilatori pre-C89 ed era così specificato da K&R e nel rapporto C originale di DMR.

Questo è stato il caso in C89, vedere la sezione 6.5.7 Inizializzazione .

Se un oggetto con durata di memorizzazione automatica non viene inizializzato esplicitamente, il suo valore è indeterminato. Se un oggetto con durata di archiviazione statica non viene inizializzato esplicitamente, viene inizializzato implicitamente come se a ogni membro con tipo aritmetico fosse assegnato 0 e a ogni membro con tipo di puntatore fosse assegnata una costante puntatore null.

Questo è stato il caso in C99, vedere la sezione 6.7.8 Inizializzazione .

Se un oggetto con durata di memorizzazione automatica non viene inizializzato esplicitamente, il suo valore è indeterminato. Se un oggetto con durata di archiviazione statica non viene inizializzato esplicitamente, allora:
- se ha un tipo di puntatore, viene inizializzato su un puntatore nullo;
- se ha un tipo aritmetico, viene inizializzato su zero (positivo o senza segno);
- se si tratta di un aggregato, ogni membro viene inizializzato (ricorsivamente) secondo queste regole;
- se si tratta di un sindacato, il primo membro nominato viene inizializzato (in modo ricorsivo) secondo queste regole.

Quanto al significato esattamente indeterminato , non sono sicuro per C89, C99 dice:

3.17.2
valore indeterminato

o un valore non specificato o una rappresentazione trap

Ma a prescindere da ciò che dicono gli standard, nella vita reale, ogni pagina dello stack inizia effettivamente come zero, ma quando il tuo programma esamina autoi valori di qualsiasi classe di archiviazione, vede tutto ciò che è stato lasciato indietro dal tuo programma quando ha usato per l'ultima volta quegli indirizzi dello stack. Se si allocano molti autoarray, alla fine verranno visualizzati in modo ordinato con zero.

Potresti chiederti, perché è così? Una diversa risposta SO si occupa di quella domanda, vedere: https://stackoverflow.com/a/2091505/140740


3
indeterminato di solito (abituato a?) significa che può fare qualsiasi cosa. Può essere zero, può essere il valore che c'era, può mandare in crash il programma, può far sì che il computer produca pancake ai mirtilli dallo slot del CD. non hai assolutamente garanzie. Potrebbe causare la distruzione del pianeta. Almeno per quanto riguarda le specifiche ... chiunque abbia realizzato un compilatore che effettivamente ha fatto qualcosa del genere sarebbe fortemente disapprovato da B-)
Brian Postow,

Nella bozza C11 N1570, la definizione di indeterminate valuepuò essere trovata in 3.19.2.
user3528438,

È così che dipende sempre dal compilatore o dal sistema operativo che quale valore imposta per la variabile statica? Ad esempio, se qualcuno scrive un sistema operativo o un mio compilatore e se imposta anche il valore iniziale per impostazione predefinita per la statica come indeterminato, è possibile?
Aditya Singh,

1
@AdityaSingh, il sistema operativo può semplificare il compilatore, ma alla fine è responsabilità primaria del compilatore eseguire il catalogo esistente al mondo di codice C e una responsabilità secondaria per soddisfare gli standard. Sarebbe certamente possibile farlo diversamente, ma perché? Inoltre, è difficile rendere indeterminati i dati statici, perché il sistema operativo vorrà davvero azzerare prima le pagine per motivi di sicurezza. (Le variabili automatiche sono solo superficialmente imprevedibili perché il tuo programma di solito ha usato quegli indirizzi di stack in un punto precedente.)
DigitalRoss

@BrianPostow No, non è corretto. Vedi stackoverflow.com/a/40674888/584518 . L'uso di un valore indeterminato provoca un comportamento non specificato , non un comportamento indefinito, ad eccezione del caso delle rappresentazioni di trap.
Lundin,

12

Dipende dalla durata di archiviazione della variabile. Una variabile con durata di memorizzazione statica viene sempre inizializzata implicitamente con zero.

Come per le variabili automatiche (locali), una variabile non inizializzata ha un valore indeterminato . Il valore indeterminato, tra le altre cose, significa che qualsiasi "valore" che potresti "vedere" in quella variabile non è solo imprevedibile, non è nemmeno garantito che sia stabile . Ad esempio, in pratica (ovvero ignorando l'UB per un secondo) questo codice

int num;
int a = num;
int b = num;

non garantisce che le variabili ae briceveranno valori identici. È interessante notare che questo non è un concetto teorico pedante, ciò accade prontamente in pratica come conseguenza dell'ottimizzazione.

Quindi, in generale, la risposta popolare che "è inizializzata con qualsiasi immondizia in memoria" non è nemmeno lontanamente corretta. Non inizializzata comportamento di variabile è diversa da quella di una variabile inizializzata di immondizia.


Non riesco a capire (anche io molto bene ci riesco ) il motivo per cui questo ha molto meno upvotes di quello da DigitalRoss solo un minuto dopo: D
Antti Haapala

7

Esempio di Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1

Abbastanza standard, diamo un'occhiata a un'implementazione :-)

Variabile locale

Standard: comportamento indefinito.

Implementazione: il programma alloca lo spazio dello stack e non sposta mai nulla a quell'indirizzo, quindi viene utilizzato tutto ciò che c'era in precedenza.

#include <stdio.h>
int main() {
    int i;
    printf("%d\n", i);
}

compilare con:

gcc -O0 -std=c99 a.c

uscite:

0

e si decompila con:

objdump -dr a.out

per:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    $0x10,%rsp
  40053e:       8b 45 fc                mov    -0x4(%rbp),%eax
  400541:       89 c6                   mov    %eax,%esi
  400543:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400548:       b8 00 00 00 00          mov    $0x0,%eax
  40054d:       e8 be fe ff ff          callq  400410 <printf@plt>
  400552:       b8 00 00 00 00          mov    $0x0,%eax
  400557:       c9                      leaveq
  400558:       c3                      retq

Dalla nostra conoscenza delle convenzioni di chiamata x86-64:

  • %rdiè il primo argomento printf, quindi la stringa "%d\n"all'indirizzo0x4005e4

  • %rsiè il secondo argomento printf, quindi i.

    Viene da -0x4(%rbp), che è la prima variabile locale a 4 byte.

    A questo punto, rbpè nella prima pagina dello stack che è stato allocato dal kernel, quindi per capire quel valore dovremmo esaminare il codice del kernel e scoprire a cosa lo imposta.

    TODO il kernel imposta quella memoria su qualcosa prima di riutilizzarla per altri processi quando un processo muore? In caso contrario, il nuovo processo sarebbe in grado di leggere la memoria di altri programmi finiti, perdendo dati. Vedi: I valori non inizializzati sono mai un rischio per la sicurezza?

Possiamo quindi anche giocare con le nostre modifiche allo stack e scrivere cose divertenti come:

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}

Variabile locale in -O3

Analisi di implementazione su: Cosa significa <valore ottimizzato> in gdb?

Variabili globali

Standard: 0

Implementazione: .bsssezione.

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c

compila per:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 05 04 0b 20 00       mov    0x200b04(%rip),%eax        # 601044 <i>
  400540:       89 c6                   mov    %eax,%esi
  400542:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400547:       b8 00 00 00 00          mov    $0x0,%eax
  40054c:       e8 bf fe ff ff          callq  400410 <printf@plt>
  400551:       b8 00 00 00 00          mov    $0x0,%eax
  400556:       5d                      pop    %rbp
  400557:       c3                      retq
  400558:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40055f:       00

# 601044 <i>dice che iè all'indirizzo 0x601044e:

readelf -SW a.out

contiene:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4

che dice 0x601044è proprio nel mezzo della .bsssezione, che inizia da 0x601040ed è lungo 8 byte.

Lo standard ELF garantisce quindi che la sezione denominata .bsssia completamente piena di zeri:

.bssQuesta sezione contiene dati non inizializzati che contribuiscono all'immagine di memoria del programma. Per definizione, il sistema inizializza i dati con zeri all'avvio del programma. La sezione occu- torte senza spazio per i file, come indicato dal tipo di sezione, SHT_NOBITS.

Inoltre, il tipo SHT_NOBITSè efficiente e non occupa spazio sul file eseguibile:

sh_sizeQuesto membro fornisce le dimensioni della sezione in byte. A meno che non sia il tipo di SHT_NOBITSsezione, la sezione occupa sh_size byte nel file. Una sezione di tipo SHT_NOBITSpuò avere dimensioni diverse da zero, ma non occupa spazio nel file.

Quindi spetta al kernel Linux azzerare quella regione di memoria quando si carica il programma in memoria all'avvio.


4

Dipende. Se tale definizione è globale (al di fuori di qualsiasi funzione), numverrà inizializzata a zero. Se è locale (all'interno di una funzione), il suo valore è indeterminato. In teoria, anche il tentativo di leggere il valore ha un comportamento indefinito: C consente la possibilità di bit che non contribuiscono al valore, ma devono essere impostati in modi specifici per ottenere anche risultati definiti dalla lettura della variabile.


1

Poiché i computer hanno una capacità di archiviazione limitata, le variabili automatiche verranno in genere trattenute in elementi di archiviazione (registri o RAM) che sono stati precedentemente utilizzati per altri scopi arbitrari. Se una tale variabile viene utilizzata prima che un valore le sia stato assegnato, tale memoria può contenere qualunque cosa contenesse in precedenza e quindi il contenuto della variabile sarà imprevedibile.

Come ulteriore ruga, molti compilatori possono mantenere variabili nei registri che sono più grandi dei tipi associati. Sebbene sarebbe necessario un compilatore per garantire che qualsiasi valore scritto in una variabile e riletto venga troncato e / o esteso di segno alla sua dimensione corretta, molti compilatori eseguiranno tale troncamento quando le variabili vengono scritte e si aspettano che avrà stato eseguito prima della lettura della variabile. Su tali compilatori, qualcosa del tipo:

uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q; 
  if (mode==1) q=2; 
  if (mode==3) q=4; 
  return q; }

 uint32_t wow(uint32_t mode) {
   return hey(1234567, mode);
 }

potrebbe benissimo comportare la wow()memorizzazione dei valori 1234567 nei registri 0 e 1, rispettivamente, e la chiamata foo(). Poiché xnon è necessario all'interno di "pippo" e poiché si suppone che le funzioni inseriscano il loro valore di ritorno nel registro 0, il compilatore può allocare il registro 0 a q. Se modeè 1 o 3, il registro 0 verrà caricato con 2 o 4, rispettivamente, ma se è un altro valore, la funzione potrebbe restituire qualunque cosa fosse nel registro 0 (ovvero il valore 1234567) anche se quel valore non è compreso nell'intervallo di uint16_t.

Per evitare che i compilatori debbano fare un lavoro extra per garantire che le variabili non inizializzate non sembrino mai contenere valori al di fuori del loro dominio ed evitare di dover specificare comportamenti indeterminati con dettagli eccessivi, lo Standard afferma che l'uso di variabili automatiche non inizializzate è Comportamento indefinito. In alcuni casi, le conseguenze di ciò possono essere persino più sorprendenti di un valore al di fuori dell'intervallo del suo tipo. Ad esempio, dato:

void moo(int mode)
{
  if (mode < 5)
    launch_nukes();
  hey(0, mode);      
}

un compilatore potrebbe dedurre che poiché il richiamo moo()con una modalità maggiore di 3 porterà inevitabilmente al programma che richiama comportamenti indefiniti, il compilatore può omettere qualsiasi codice che sarebbe rilevante solo se modeè 4 o maggiore, come il codice che normalmente impedirebbe il lancio di armi nucleari in tali casi. Si noti che né la Standard, né la moderna filosofia del compilatore, si preoccuperebbero del fatto che il valore di ritorno da "hey" sia ignorato - l'atto di provare a restituirlo dà a un compilatore una licenza illimitata per generare codice arbitrario.


0

La risposta di base è sì, non è definita.

Se stai riscontrando un comportamento strano a causa di ciò, potrebbe dipendere da dove viene dichiarato. Se all'interno di una funzione nello stack, il contenuto sarà molto probabilmente diverso ogni volta che viene chiamata la funzione. Se si tratta di un ambito statico o di un modulo, non è definito ma non cambierà.


0

Se la classe di memoria è statica o globale, durante il caricamento, il BSS inizializza la variabile o la posizione della memoria (ML) su 0 a meno che alla variabile non venga inizialmente assegnato un valore. Nel caso di variabili locali non inizializzate, la rappresentazione trap viene assegnata alla posizione di memoria. Quindi, se uno qualsiasi dei tuoi registri contenenti informazioni importanti viene sovrascritto dal compilatore, il programma potrebbe bloccarsi.

ma alcuni compilatori potrebbero avere un meccanismo per evitare tale problema.

Stavo lavorando con la serie nec v850 quando mi sono reso conto che esiste una rappresentazione trap che ha schemi di bit che rappresentano valori indefiniti per i tipi di dati ad eccezione di char. Quando ho preso un carattere non inizializzato ho ottenuto un valore di default pari a zero a causa della rappresentazione trap. Questo potrebbe essere utile per any1 utilizzando necv850es


Il sistema non è conforme se si ottengono rappresentazioni trap durante l'utilizzo di caratteri non firmati. Non sono esplicitamente autorizzati a contenere rappresentazioni di trap, C17 6.2.6.1/5.
Lundin,

-2

Il valore di num sarà un valore di immondizia dalla memoria principale (RAM). è meglio se inizializzi la variabile subito dopo la creazione.


-4

Per quanto mi riguarda, dipende principalmente dal compilatore, ma in generale nella maggior parte dei casi il valore è presunto come 0 dai compilatori.
Ho ottenuto il valore di immondizia in caso di VC ++ mentre TC ha dato il valore come 0. Lo stampo come di seguito

int i;
printf('%d',i);

Se ottieni un valore deterministico come ad esempio il 0tuo compilatore molto probabilmente compie ulteriori passaggi per assicurarti di ottenere quel valore (aggiungendo codice per inizializzare comunque le variabili). Alcuni compilatori lo fanno quando eseguono la compilazione "debug", ma scegliere il valore 0per questi è una cattiva idea poiché nasconderà errori nel codice (cosa più corretta garantirebbe un numero davvero improbabile 0xBAADF00Do qualcosa di simile). Penso che la maggior parte dei compilatori lascerà semplicemente qualsiasi immondizia che capita di occupare la memoria come valore della variabile (cioè, in generale, non è considerata come 0).
stelle il
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.