In C, le parentesi graffe fungono da cornice di stack?


153

Se creo una variabile all'interno di un nuovo set di parentesi graffe, quella variabile è saltata fuori dallo stack sulla parentesi graffa di chiusura o rimane fino alla fine della funzione? Per esempio:

void foo() {
   int c[100];
   {
       int d[200];
   }
   //code that takes a while
   return;
}

Occuperà dmemoria durante la code that takes a whilesezione?


8
Intendi (1) secondo lo Standard, (2) pratica universale tra implementazioni o (3) pratica comune tra implementazioni?
David Thornley,

Risposte:


83

No, le parentesi graffe non fungono da cornice di stack. In C, le parentesi graffe indicano solo un ambito di denominazione, ma nulla viene distrutto né viene espulso dallo stack quando il controllo ne esce.

Come programmatore che scrive codice, puoi spesso pensarlo come se fosse uno stack frame. Gli identificatori dichiarati all'interno delle parentesi graffe sono accessibili solo all'interno delle parentesi graffe, quindi dal punto di vista di un programmatore, è come se fossero spinti nello stack man mano che vengono dichiarati e poi spuntati quando si esce dall'ambito. Tuttavia, i compilatori non devono generare codice che spinga / salti qualsiasi cosa in entrata / uscita (e generalmente non lo fanno).

Si noti inoltre che le variabili locali potrebbero non utilizzare alcuno spazio stack: potrebbero essere conservate nei registri della CPU o in qualche altra posizione di memoria ausiliaria o essere completamente ottimizzate.

Quindi, l' darray, in teoria, potrebbe consumare memoria per l'intera funzione. Tuttavia, il compilatore può ottimizzarlo via o condividere la sua memoria con altre variabili locali le cui vite di utilizzo non si sovrappongono.


9
Non è specifico per l'implementazione?
avakar,

54
In C ++, il distruttore di un oggetto viene chiamato alla fine del suo ambito. Se la memoria viene recuperata è un problema specifico dell'implementazione.
Kristopher Johnson,

8
@ pm100: verranno chiamati i distruttori. Ciò non dice nulla sulla memoria occupata da quegli oggetti.
Donal Fellows

9
Lo standard C specifica che la durata delle variabili automatiche dichiarate nel blocco si estende solo fino al termine dell'esecuzione del blocco. Quindi, in sostanza quelle variabili automatiche non vengono "distrutti" alla fine del blocco.
Caf

3
@KristopherJohnson: se un metodo avesse due blocchi separati, ognuno dei quali dichiarasse un array da 1Kbyte e un terzo blocco che chiamasse un metodo nidificato, un compilatore sarebbe libero di usare la stessa memoria per entrambi gli array e / o di posizionare l'array nella parte più bassa dello stack e spostare il puntatore dello stack sopra di esso chiamando il metodo nidificato. Tale comportamento potrebbe ridurre di 2K la profondità dello stack richiesta per la chiamata di funzione.
supercat

39

Il tempo durante il quale la variabile sta effettivamente occupando memoria è ovviamente dipendente dal compilatore (e molti compilatori non regolano il puntatore dello stack quando i blocchi interni vengono inseriti ed usciti all'interno delle funzioni).

Tuttavia, una domanda strettamente correlata ma forse più interessante è se al programma è consentito accedere a quell'oggetto interno al di fuori dell'ambito interno (ma all'interno della funzione contenitiva), ovvero:

void foo() {
   int c[100];
   int *p;

   {
       int d[200];
       p = d;
   }

   /* Can I access p[0] here? */

   return;
}

(In altre parole: il compilatore è autorizzato a deallocare d, anche se in pratica la maggior parte non lo fa?).

La risposta è che il compilatore è autorizzato a deallocare de l'accesso in p[0]cui il commento indica un comportamento indefinito (al programma non è consentito accedere all'oggetto interno al di fuori dell'ambito interno). La parte rilevante della norma C è 6.2.4p5:

Per un tale oggetto [uno che ha una durata di memorizzazione automatica] che non ha un tipo di array di lunghezza variabile, la sua durata si estende dall'entrata nel blocco a cui è associato fino a quando l'esecuzione di quel blocco non termina in alcun modo . (L'inserimento di un blocco racchiuso o la chiamata di una funzione sospende, ma non termina, l'esecuzione del blocco corrente.) Se il blocco viene inserito in modo ricorsivo, ogni volta viene creata una nuova istanza dell'oggetto. Il valore iniziale dell'oggetto è indeterminato. Se viene specificata un'inizializzazione per l'oggetto, questa viene eseguita ogni volta che viene raggiunta la dichiarazione nell'esecuzione del blocco; in caso contrario, il valore diventa indeterminato ogni volta che viene raggiunta la dichiarazione.


Mentre qualcuno impara come funzionano l'ambito e la memoria in C e C ++ dopo anni di utilizzo di linguaggi di livello superiore, trovo questa risposta più precisa e utile di quella accettata.
Chris,

20

La tua domanda non è abbastanza chiara per avere una risposta inequivocabile.

Da un lato, i compilatori normalmente non eseguono alcuna allocazione-deallocazione di memoria locale per ambiti di blocco nidificati. La memoria locale viene normalmente allocata una sola volta all'ingresso della funzione e rilasciata all'uscita della funzione.

D'altra parte, quando termina la durata di un oggetto locale, la memoria occupata da quell'oggetto può essere riutilizzata per un altro oggetto locale in un secondo momento. Ad esempio, in questo codice

void foo()
{
  {
    int d[100];
  }
  {
    double e[20];
  }
}

entrambi gli array occupano di solito la stessa area di memoria, il che significa che la quantità totale di memoria locale richiesta dalla funzione fooè tutto ciò che è necessario per il più grande dei due array, non per entrambi contemporaneamente.

Se ddecidi di continuare a occupare la memoria fino alla fine della funzione nel contesto della tua domanda, decidi tu.


6

Dipende dall'implementazione. Ho scritto un breve programma per testare cosa fa gcc 4.3.4 e alloca tutto lo spazio dello stack in una volta all'inizio della funzione. Puoi esaminare l'assemblaggio che gcc produce usando il flag -S.


3

No, d [] non sarà in pila per il resto della routine. Ma alloca () è diverso.

Modifica: Kristopher Johnson (e simon e Daniel) hanno ragione , e la mia risposta iniziale è stata sbagliata . Con gcc 4.3.4.on CYGWIN, il codice:

void foo(int[]);
void bar(void);
void foobar(int); 

void foobar(int flag) {
    if (flag) {
        int big[100000000];
        foo(big);
    }
    bar();
}

dà:

_foobar:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $400000008, %eax
    call    __alloca
    cmpl    $0, 8(%ebp)
    je      L2
    leal    -400000000(%ebp), %eax
    movl    %eax, (%esp)
    call    _foo
L2:
    call    _bar
    leave
    ret

Vivere e imparare! E un test rapido sembra dimostrare che AndreyT ha ragione anche su allocazioni multiple.

Aggiunto molto più tardi : il test sopra mostra che la documentazione di gcc non è del tutto corretta. Per anni ha detto (enfasi aggiunta):

"Lo spazio per un array di lunghezza variabile viene deallocato non appena termina l' ambito del nome dell'array ."


La compilazione con l'ottimizzazione disabilitata non ti mostra necessariamente cosa otterrai nel codice ottimizzato. In questo caso, il comportamento è lo stesso (allocare all'inizio della funzione e libero solo quando si esce dalla funzione): godbolt.org/g/M112AQ . Ma gcc non cygwin non chiama una allocafunzione. Sono davvero sorpreso che Cygwin CCG lo farebbe. Non è nemmeno un array di lunghezza variabile, quindi IDK perché lo tiri fuori.
Peter Cordes,

2

Potrebbero. Potrebbero no. La risposta di cui ho davvero bisogno è: non assumere mai nulla. I compilatori moderni eseguono ogni tipo di architettura e magia specifica per l'implementazione. Scrivi il tuo codice in modo semplice e leggibile agli umani e lascia che il compilatore faccia le cose buone. Se provi a programmare attorno al compilatore stai chiedendo problemi - e il problema che di solito si presenta in queste situazioni è di solito terribilmente sottile e difficile da diagnosticare.


1

La variabile in dgenere non viene eliminata dallo stack. Le parentesi graffe non indicano una cornice di pila. Altrimenti, non saresti in grado di fare qualcosa del genere:

char var = getch();
    {
        char next_var = var + 1;
        use_variable(next_char);
    }

Se le parentesi graffe causassero un vero push / pop di stack (come farebbe una chiamata di funzione), il codice sopra riportato non verrebbe compilato perché il codice all'interno delle parentesi non sarebbe in grado di accedere alla variabile varche vive al di fuori delle parentesi graffe (proprio come un sub- la funzione non può accedere direttamente alle variabili nella funzione chiamante). Sappiamo che non è così.

Le parentesi graffe vengono semplicemente utilizzate per lo scoping. Il compilatore considererà non valido qualsiasi accesso alla variabile "interna" dall'esterno delle parentesi graffe e potrà riutilizzare quella memoria per qualcos'altro (dipende dall'implementazione). Tuttavia, non può essere rimosso dallo stack fino a quando non viene restituita la funzione di chiusura.

Aggiornamento: ecco cosa dicono le specifiche C. Per quanto riguarda gli oggetti con durata di memorizzazione automatica (sezione 6.4.2):

Per un oggetto che non ha un tipo di array di lunghezza variabile, la sua durata si estende dall'entrata nel blocco a cui è associato fino a quando l'esecuzione di quel blocco non termina comunque.

La stessa sezione definisce il termine "vita" come (enfasi mia):

La durata di un oggetto è la parte dell'esecuzione del programma durante la quale è garantito che l'archiviazione ne sia riservata. Un oggetto esiste, ha un indirizzo costante e conserva il suo ultimo valore memorizzato per tutta la sua durata. Se si fa riferimento a un oggetto al di fuori della sua durata, il comportamento non è definito.

La parola chiave qui è, ovviamente, "garantita". Una volta che lasci l'ambito dell'insieme di parentesi graffe, la durata dell'array è finita. L'archiviazione può o meno essere allocata per esso (il compilatore potrebbe riutilizzare lo spazio per qualcos'altro), ma qualsiasi tentativo di accedere all'array invoca comportamenti indefiniti e porta a risultati imprevedibili.

La specifica C non ha nozioni di stack frame. Parla solo di come si comporterà il programma risultante e lascia i dettagli dell'implementazione al compilatore (dopotutto, l'implementazione sembrerebbe piuttosto diversa su una CPU stackless rispetto a una CPU con uno stack hardware). Non c'è nulla nelle specifiche C che impone dove un frame dello stack finirà o non finirà. L'unico vero modo per sapere è compilare il codice sul proprio compilatore / piattaforma particolare ed esaminare l'assembly risultante. Anche l'attuale set di opzioni di ottimizzazione del compilatore avrà un ruolo in questo.

Se si desidera assicurarsi che l'array dnon stia più esaurendo la memoria mentre il codice è in esecuzione, è possibile convertire il codice tra parentesi graffe in una funzione separata o esplicitamente malloce freela memoria invece di utilizzare l'archiviazione automatica.


1
"Se le parentesi graffe causassero uno stack push / pop, il codice sopra riportato non verrebbe compilato perché il codice all'interno delle parentesi graffe non sarebbe in grado di accedere alla variabile var che vive al di fuori delle parentesi graffe" - semplicemente non è vero. Il compilatore può sempre ricordare la distanza dal puntatore stack / frame e usarlo per fare riferimento a variabili esterne. Inoltre, vedere la risposta di Giuseppe per un esempio di parentesi graffe che fanno causa uno stack push / pop.
george

@ george- Il comportamento che descrivi, così come l'esempio di Joseph, dipende dal compilatore e dalla piattaforma che stai utilizzando. Ad esempio, la compilazione dello stesso codice per un target MIPS produce risultati completamente diversi. Stavo parlando puramente dal punto di vista della specifica C (poiché l'OP non specificava un compilatore o un obiettivo). Modificherò la risposta e aggiungerò ulteriori dettagli.
BTA

0

Credo che esca dall'ambito, ma non viene espulso dallo stack fino a quando non viene restituita la funzione. Quindi occuperà ancora memoria nello stack fino al completamento della funzione, ma non accessibile a valle della prima parentesi graffa di chiusura.


3
Nessuna garanzia Una volta chiuso l'ambito, il compilatore non tiene più traccia di quella memoria (o almeno non è necessario per ...) e potrebbe riutilizzarla. Questo è il motivo per cui toccare la memoria precedentemente occupata da una variabile fuori ambito è un comportamento indefinito. Fai attenzione ai demoni nasali e ad avvertimenti simili.
dmckee --- ex gattino moderatore

0

Sono già state fornite molte informazioni sullo standard che indica che è effettivamente specifico per l'implementazione.

Quindi, un esperimento potrebbe essere di interesse. Se proviamo il seguente codice:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
        printf("%p\n", (void*) x);
    }
    {
        int b;
        y = &b;
        printf("%p\n", (void*) y);
    }
}

Usando gcc otteniamo qui due volte lo stesso indirizzo: Coliro

Ma se proviamo il seguente codice:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
    }
    {
        int b;
        y = &b;
    }
    printf("%p\n", (void*) x);
    printf("%p\n", (void*) y);
}

Usando gcc otteniamo qui due indirizzi diversi: Coliro

Quindi, non puoi essere veramente sicuro di quello che sta succedendo.

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.