En C, les accolades agissent-elles comme un cadre de pile?


153

Si je crée une variable dans un nouvel ensemble d'accolades, cette variable est-elle sortie de la pile sur l'accolade fermante ou est-elle suspendue jusqu'à la fin de la fonction? Par exemple:

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

Prendront de la dmémoire pendant la code that takes a whilesection?


8
Voulez-vous dire (1) selon la norme, (2) pratique universelle parmi les implémentations, ou (3) pratique courante parmi les implémentations?
David Thornley

Réponses:


83

Non, les accolades n'agissent pas comme un cadre de pile. En C, les accolades désignent uniquement une portée de dénomination, mais rien n'est détruit et rien n'est sorti de la pile lorsque le contrôle en sort.

En tant que programmeur écrivant du code, vous pouvez souvent y penser comme s'il s'agissait d'un cadre de pile. Les identificateurs déclarés dans les accolades ne sont accessibles que dans les accolades, donc du point de vue du programmeur, c'est comme s'ils étaient poussés sur la pile lorsqu'ils sont déclarés, puis apparus lorsque la portée est sortie. Cependant, les compilateurs n'ont pas à générer du code qui pousse / affiche quoi que ce soit à l'entrée / à la sortie (et généralement, ils ne le font pas).

Notez également que les variables locales peuvent ne pas utiliser du tout d'espace de pile: elles peuvent être conservées dans les registres du processeur ou dans un autre emplacement de stockage auxiliaire, ou être entièrement optimisées.

Ainsi, le dtableau, en théorie, pourrait consommer de la mémoire pour toute la fonction. Cependant, le compilateur peut l'optimiser ou partager sa mémoire avec d'autres variables locales dont les durées d'utilisation ne se chevauchent pas.


9
N'est-ce pas spécifique à la mise en œuvre?
avakar

54
En C ++, le destructeur d'un objet est appelé à la fin de sa portée. La récupération de la mémoire est un problème spécifique à l'implémentation.
Kristopher Johnson

8
@ pm100: Les destructeurs seront appelés. Cela ne dit rien sur la mémoire qu'occupaient ces objets.
Donal Fellows

9
Le standard C spécifie que la durée de vie des variables automatiques déclarées dans le bloc ne s'étend que jusqu'à la fin de l'exécution du bloc. Donc , essentiellement les variables automatiques ne se « détruits » à la fin du bloc.
caf

3
@KristopherJohnson: Si une méthode avait deux blocs séparés, dont chacun déclarait un tableau de 1 Ko, et un troisième bloc qui appelait une méthode imbriquée, un compilateur serait libre d'utiliser la même mémoire pour les deux tableaux et / ou de placer le tableau dans la partie la moins profonde de la pile et déplacez le pointeur de pile au-dessus en appelant la méthode imbriquée. Un tel comportement pourrait réduire de 2 Ko la profondeur de pile requise pour l'appel de fonction.
supercat

39

Le temps pendant lequel la variable prend réellement de la mémoire dépend évidemment du compilateur (et de nombreux compilateurs n'ajustent pas le pointeur de pile lorsque des blocs internes sont entrés et sortis dans les fonctions).

Cependant, une question étroitement liée mais peut-être plus intéressante est de savoir si le programme est autorisé à accéder à cet objet interne en dehors de la portée interne (mais dans la fonction contenant), c'est-à-dire:

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

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

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

   return;
}

(En d'autres termes: le compilateur est-il autorisé à désallouer d, même si en pratique la plupart ne le font pas?).

La réponse est que le compilateur est autorisé à désallouer det à accéder p[0]là où le commentaire indique un comportement indéfini (le programme n'est pas autorisé à accéder à l'objet interne en dehors de la portée interne). La partie pertinente de la norme C est 6.2.4p5:

Pour un tel objet [qui a une durée de stockage automatique] qui n'a pas de type de tableau de longueur variable, sa durée de vie s'étend de l'entrée dans le bloc auquel il est associé jusqu'à la fin de l'exécution de ce bloc . (La saisie d'un bloc fermé ou l'appel d'une fonction suspend, mais ne termine pas, l'exécution du bloc actuel.) Si le bloc est saisi de manière récursive, une nouvelle instance de l'objet est créée à chaque fois. La valeur initiale de l'objet est indéterminée. Si une initialisation est spécifiée pour l'objet, elle est effectuée à chaque fois que la déclaration est atteinte lors de l'exécution du bloc; sinon, la valeur devient indéterminée à chaque fois que la déclaration est atteinte.


En tant que personne qui apprend comment la portée et la mémoire fonctionnent en C et C ++ après des années d'utilisation de langages de niveau supérieur, je trouve cette réponse plus précise et utile que celle acceptée.
Chris

20

Votre question n’est pas assez claire pour qu’on y réponde sans ambiguïté.

D'une part, les compilateurs ne font normalement aucune allocation-désallocation de mémoire locale pour les étendues de blocs imbriqués. La mémoire locale est normalement allouée une seule fois à l'entrée de la fonction et libérée à la sortie de la fonction.

En revanche, lorsque la durée de vie d'un objet local se termine, la mémoire occupée par cet objet peut être réutilisée ultérieurement pour un autre objet local. Par exemple, dans ce code

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

les deux tableaux occuperont généralement la même zone de mémoire, ce qui signifie que la quantité totale de stockage local nécessaire à la fonction fooest ce qui est nécessaire pour le plus grand des deux tableaux, pas pour les deux en même temps.

dC'est à vous de décider si ce dernier se qualifie comme continuant à occuper la mémoire jusqu'à la fin de la fonction dans le contexte de votre question.


6

Cela dépend de la mise en œuvre. J'ai écrit un petit programme pour tester ce que fait gcc 4.3.4, et il alloue tout l'espace de la pile à la fois au début de la fonction. Vous pouvez examiner l'assembly produit par gcc en utilisant l'indicateur -S.


3

Non, d [] ne sera pas sur la pile pour le reste de la routine. Mais alloca () est différent.

Edit: Kristopher Johnson (et Simon et Daniel) ont raison , et ma réponse initiale était fausse . Avec gcc 4.3.4 sur CYGWIN, le code:

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

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

donne:

_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

Vis et apprend! Et un test rapide semble montrer qu'AndreyT a également raison sur les allocations multiples.

Ajouté beaucoup plus tard : Le test ci-dessus montre que la documentation gcc n'est pas tout à fait correcte. Pendant des années, il a dit (italiques ajoutés):

"L'espace pour un tableau de longueur variable est libéré dès que la portée du nom du tableau se termine ."


La compilation avec l'optimisation désactivée ne vous montre pas nécessairement ce que vous obtiendrez dans un code optimisé. Dans ce cas, le comportement est le même (allouer au début de la fonction, et uniquement gratuit en quittant la fonction): godbolt.org/g/M112AQ . Mais gcc non-cygwin n'appelle pas de allocafonction. Je suis vraiment surpris que cygwin gcc fasse cela. Ce n'est même pas un tableau de longueur variable, donc IDK pourquoi vous en parlez.
Peter Cordes

2

Ils pourraient. Peut-être pas. La réponse dont je pense que vous avez vraiment besoin est: ne supposez jamais rien. Les compilateurs modernes font toutes sortes d'architecture et de magie spécifique à l'implémentation. Écrivez votre code simplement et lisiblement aux humains et laissez le compilateur faire le bon travail. Si vous essayez de coder autour du compilateur, vous demandez des problèmes - et les problèmes que vous rencontrez habituellement dans ces situations sont généralement horriblement subtils et difficiles à diagnostiquer.


1

Votre variable dn'est généralement pas sautée de la pile. Les accolades bouclées ne désignent pas un cadre de pile. Sinon, vous ne pourriez pas faire quelque chose comme ceci:

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

Si les accolades provoquaient un véritable push / pop de pile (comme le ferait un appel de fonction), alors le code ci-dessus ne se compilerait pas car le code à l'intérieur des accolades ne serait pas en mesure d'accéder à la variable varqui vit en dehors des accolades (tout comme un sous- ne peut pas accéder directement aux variables de la fonction appelante). Nous savons que ce n’est pas le cas.

Les accolades bouclées sont simplement utilisées pour la portée. Le compilateur traitera tout accès à la variable "interne" depuis l'extérieur des accolades englobantes comme invalide, et il pourra réutiliser cette mémoire pour autre chose (cela dépend de l'implémentation). Cependant, il ne peut pas être détaché de la pile avant le retour de la fonction englobante.

Mise à jour: voici ce que la spécification C a à dire. Concernant les objets à durée de stockage automatique (section 6.4.2):

Pour un objet qui n'a pas de type tableau de longueur variable, sa durée de vie s'étend de l'entrée dans le bloc auquel il est associé jusqu'à ce que l'exécution de ce bloc se termine de toute façon.

La même section définit le terme «durée de vie» comme (c'est moi qui souligne):

La durée de vie d'un objet est la partie de l'exécution du programme pendant laquelle il est garanti que le stockage lui est réservé. Un objet existe, a une adresse constante et conserve sa dernière valeur stockée pendant toute sa durée de vie. Si un objet est référencé en dehors de sa durée de vie, le comportement n'est pas défini.

Le mot clé ici est, bien entendu, «garanti». Une fois que vous quittez la portée de l'ensemble interne d'accolades, la durée de vie du tableau est terminée. Le stockage peut ou non lui être encore alloué (votre compilateur peut réutiliser l'espace pour autre chose), mais toute tentative d'accès au tableau invoque un comportement non défini et entraîne des résultats imprévisibles.

La spécification C n'a aucune notion de cadres de pile. Il ne parle que de la façon dont le programme résultant se comportera et laisse les détails de l'implémentation au compilateur (après tout, l'implémentation serait assez différente sur un processeur sans pile que sur un processeur avec une pile matérielle). Il n'y a rien dans la spécification C qui impose où un cadre de pile se terminera ou non. Le seul vrai moyen de le savoir est de compiler le code sur votre compilateur / plateforme particulier et d'examiner l'assembly résultant. L'ensemble actuel d'options d'optimisation de votre compilateur jouera probablement également un rôle à cet égard.

Si vous voulez vous assurer que le tableau dne consomme plus de mémoire pendant l'exécution de votre code, vous pouvez convertir le code entre accolades en une fonction distincte ou explicitement mallocet freela mémoire au lieu d'utiliser le stockage automatique.


1
"Si les accolades provoquaient un push / pop de la pile, alors le code ci-dessus ne serait pas compilé car le code à l'intérieur des accolades ne serait pas en mesure d'accéder à la variable var qui vit en dehors des accolades" - ce n'est tout simplement pas vrai. Le compilateur peut toujours se souvenir de la distance du pointeur de pile / cadre et l'utiliser pour référencer des variables externes. En outre, voir la réponse de Joseph pour un exemple d'accolades qui font la cause d' une poussée de la pile / pop.
george

@ george - Le comportement que vous décrivez, ainsi que l'exemple de Joseph, dépendent du compilateur et de la plate-forme que vous utilisez. Par exemple, la compilation du même code pour une cible MIPS donne des résultats complètement différents. Je parlais uniquement du point de vue de la spécification C (puisque l'OP n'a pas spécifié de compilateur ou de cible). Je modifierai la réponse et ajouterai plus de détails.
bta le

0

Je crois que cela sort du cadre, mais n'est pas sorti de la pile jusqu'à ce que la fonction retourne. Donc, il occupera toujours de la mémoire sur la pile jusqu'à ce que la fonction soit terminée, mais pas accessible en aval de la première accolade de fermeture.


3
Aucune garantie. Une fois la portée fermée, le compilateur ne garde plus trace de cette mémoire (ou du moins n'est pas obligé de ...) et peut bien la réutiliser. C'est pourquoi toucher la mémoire autrefois occupée par une variable hors de portée est un comportement indéfini. Méfiez-vous des démons nasaux et des avertissements similaires.
dmckee --- ex-moderator chaton

0

De nombreuses informations ont déjà été fournies sur la norme, indiquant qu'elle est effectivement spécifique à sa mise en œuvre.

Donc, une expérience pourrait être intéressante. Si nous essayons le code suivant:

#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);
    }
}

En utilisant gcc on obtient ici deux fois la même adresse: Coliro

Mais si nous essayons le code suivant:

#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);
}

En utilisant gcc nous obtenons ici deux adresses différentes: Coliro

Donc, vous ne pouvez pas être vraiment sûr de ce qui se passe.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.