Qu'est-ce que le déroulement de la pile?


193

Qu'est-ce que le déroulement de la pile? J'ai cherché mais je n'ai pas trouvé de réponse éclairante!


76
S'il ne sait pas ce que c'est, comment pouvez-vous vous attendre à ce qu'il sache qu'ils ne sont pas les mêmes pour C et pour C ++?
dreamlax

@dreamlax: Alors, en quoi le concept de "déroulement de pile" est-il différent en C et C ++?
Destructor

2
@PravasiMeet: C n'a pas de gestion des exceptions, donc le déroulement de la pile est très simple, cependant, en C ++, si une exception est levée ou une fonction se termine, le déroulement de la pile implique la destruction de tous les objets C ++ avec une durée de stockage automatique.
dreamlax

Réponses:


150

Le déroulement de la pile est généralement abordé en relation avec la gestion des exceptions. Voici un exemple:

void func( int x )
{
    char* pleak = new char[1024]; // might be lost => memory leak
    std::string s( "hello world" ); // will be properly destructed

    if ( x ) throw std::runtime_error( "boom" );

    delete [] pleak; // will only get here if x == 0. if x!=0, throw exception
}

int main()
{
    try
    {
        func( 10 );
    }
    catch ( const std::exception& e )
    {
        return 1;
    }

    return 0;
}

Ici, la mémoire allouée pour pleaksera perdue si une exception est levée, tandis que la mémoire allouée à ssera correctement libérée par le std::stringdestructeur dans tous les cas. Les objets alloués sur la pile sont "déroulés" lorsque la portée est quittée (ici, la portée est de la fonction func.) Ceci est fait par le compilateur insérant des appels aux destructeurs de variables automatiques (de pile).

Maintenant, c'est un concept très puissant menant à la technique appelée RAII , qui est l' acquisition de ressources est l'initialisation , qui nous aide à gérer des ressources comme la mémoire, les connexions de base de données, les descripteurs de fichiers ouverts, etc. en C ++.

Cela nous permet maintenant de fournir des garanties de sécurité exceptionnelles .


C'était vraiment instructif! Donc, je comprends ceci: si mon processus plante de manière inattendue lors de la sortie de N'IMPORTE QUEL bloc au moment où la pile a été sautée, il peut arriver que le code après le code du gestionnaire d'exceptions ne soit pas exécuté du tout, et cela peut provoquer des fuites de mémoire, heap corruption etc.
Rajendra Uppal

15
Si le programme "plante" (c'est-à-dire se termine en raison d'une erreur), alors toute fuite de mémoire ou corruption de tas est sans importance puisque la mémoire est libérée à la fin.
Tyler McHenry

1
Exactement. Merci. Je suis juste un peu dyslexique aujourd'hui.
Nikolai Fetissov

11
@TylerMcHenry: La norme ne garantit pas que les ressources ou la mémoire sont libérées à la fin. La plupart des systèmes d'exploitation le font cependant.
Mooing Duck

3
delete [] pleak;n'est atteint que si x == 0.
Jib

71

Tout cela concerne le C ++:

Définition : Lorsque vous créez des objets de manière statique (sur la pile au lieu de les allouer dans la mémoire du tas) et que vous effectuez des appels de fonction, ils sont «empilés».

Lorsqu'une portée (tout ce qui est délimité par {et }) est quittée (en utilisant return XXX;, en atteignant la fin de la portée ou en lançant une exception), tout ce qui se trouve dans cette portée est détruit (les destructeurs sont appelés pour tout). Ce processus de destruction d'objets locaux et d'appel de destructeurs est appelé déroulement de pile.

Vous rencontrez les problèmes suivants liés au déroulement de la pile:

  1. éviter les fuites de mémoire (tout ce qui est alloué dynamiquement qui n'est pas géré par un objet local et nettoyé dans le destructeur sera divulgué) - voir RAII référencé par Nikolai, et la documentation pour boost :: scoped_ptr ou cet exemple d'utilisation de boost :: mutex :: scoped_lock .

  2. cohérence du programme: les spécifications C ++ indiquent que vous ne devez jamais lever d'exception avant qu'une exception existante n'ait été gérée. Cela signifie que le processus de déroulement de la pile ne doit jamais lever d'exception (soit utiliser uniquement du code garanti de ne pas lancer de destructeurs, soit tout entourer de destructeurs avec try {et } catch(...) {}).

Si un destructeur lève une exception pendant le déroulement de la pile, vous vous retrouvez au pays d'un comportement indéfini qui pourrait entraîner la fin inattendue de votre programme (comportement le plus courant) ou la fin de l'univers (théoriquement possible mais n'a pas encore été observé dans la pratique).


2
Au contraire. Bien que les gotos ne devraient pas être abusés, ils provoquent un déroulement de la pile dans MSVC (pas dans GCC, donc c'est probablement une extension). setjmp et longjmp le font de manière multiplateforme, avec un peu moins de flexibilité.
Patrick Niedzielski

10
Je viens de tester cela avec gcc et il appelle correctement les destructeurs lorsque vous sortez d'un bloc de code. Voir stackoverflow.com/questions/334780/… - comme mentionné dans ce lien, cela fait également partie de la norme.
Damyan

1
lire Nikolai, jrista et votre réponse dans cet ordre, maintenant ça a du sens!
n611x007

@sashoalm Pensez-vous vraiment qu'il est nécessaire de modifier un article sept ans plus tard?
David Hoelzer

41

Dans un sens général, une pile "dérouler" est à peu près synonyme de la fin d'un appel de fonction et de l'éclatement ultérieur de la pile.

Cependant, spécifiquement dans le cas de C ++, le déroulement de la pile a à voir avec la façon dont C ++ appelle les destructeurs pour les objets alloués depuis le début de tout bloc de code. Les objets créés dans le bloc sont désalloués dans l'ordre inverse de leur allocation.


4
Il n'y a rien de spécial dans les tryblocs. Les objets de pile alloués dans n'importe quel bloc (que ce soit tryou non) sont sujets au déroulement lorsque le bloc se termine.
Chris Jester-Young

Cela fait un moment que je n'ai pas fait beaucoup de codage C ++. J'ai dû creuser cette réponse dans les profondeurs rouillées. ; P
jrista

ne t'inquiète pas. Chacun a «son mal» de temps en temps.
bitc

13

Le déroulement de la pile est un concept principalement C ++, traitant de la façon dont les objets alloués à la pile sont détruits lorsque sa portée est quittée (soit normalement, soit via une exception).

Disons que vous avez ce fragment de code:

void hw() {
    string hello("Hello, ");
    string world("world!\n");
    cout << hello << world;
} // at this point, "world" is destroyed, followed by "hello"

Cela s'applique-t-il à un bloc? Je veux dire s'il n'y a que {// des objets locaux}
Rajendra Uppal

@Rajendra: Oui, un bloc anonyme définit une zone de portée, donc ça compte aussi.
Michael Myers

12

Je ne sais pas si vous avez encore lu ceci, mais l'article de Wikipedia sur la pile d'appels a une explication décente.

Détente:

Le retour de la fonction appelée fera sortir le cadre supérieur de la pile, laissant peut-être une valeur de retour. L'action plus générale de faire sauter une ou plusieurs images de la pile pour reprendre l'exécution ailleurs dans le programme est appelée déroulement de pile et doit être effectuée lorsque des structures de contrôle non locales sont utilisées, telles que celles utilisées pour la gestion des exceptions. Dans ce cas, le cadre de pile d'une fonction contient une ou plusieurs entrées spécifiant des gestionnaires d'exceptions. Lorsqu'une exception est levée, la pile est déroulée jusqu'à ce qu'un gestionnaire soit trouvé, prêt à gérer (intercepter) le type de l'exception levée.

Certaines langues ont d'autres structures de contrôle qui nécessitent un déroulement général. Pascal permet à une instruction goto globale de transférer le contrôle d'une fonction imbriquée vers une fonction externe précédemment invoquée. Cette opération nécessite le déroulement de la pile, en supprimant autant de cadres de pile que nécessaire pour restaurer le contexte approprié pour transférer le contrôle à l'instruction cible dans la fonction externe englobante. De même, C a les fonctions setjmp et longjmp qui agissent comme des gotos non locaux. Common Lisp permet de contrôler ce qui se passe lorsque la pile est déroulée à l'aide de l'opérateur spécial dérouler-protéger.

Lors de l'application d'une continuation, la pile est (logiquement) déroulée puis rembobinée avec la pile de la continuation. Ce n'est pas la seule façon de mettre en œuvre des continuations; par exemple, en utilisant plusieurs piles explicites, l'application d'une continuation peut simplement activer sa pile et enrouler une valeur à passer. Le langage de programmation Scheme permet d'exécuter des thunks arbitraires à des points spécifiés lors du "déroulement" ou du "rembobinage" de la pile de contrôle lorsqu'une continuation est appelée.

Inspection [modifier |


9

J'ai lu un article de blog qui m'a aidé à comprendre.

Qu'est-ce que le déroulement de la pile?

Dans tous les langages qui supportent les fonctions récursives (c'est-à-dire à peu près tout sauf Fortran 77 et Brainf * ck), le runtime du langage garde une pile des fonctions en cours d'exécution. Le déroulement de la pile est un moyen d'inspecter, et éventuellement de modifier, cette pile.

Pourquoi voudriez-vous faire ça?

La réponse peut sembler évidente, mais il existe plusieurs situations liées, mais subtilement différentes, où le déroulement est utile ou nécessaire:

  1. En tant que mécanisme de flux de contrôle d'exécution (exceptions C ++, C longjmp (), etc.).
  2. Dans un débogueur, pour montrer à l'utilisateur la pile.
  3. Dans un profileur, pour prélever un échantillon de la pile.
  4. Depuis le programme lui-même (comme à partir d'un gestionnaire de crash pour afficher la pile).

Ceux-ci ont des exigences légèrement différentes. Certains d'entre eux sont essentiels aux performances, d'autres non. Certains nécessitent la capacité de reconstruire des registres à partir d'une trame externe, d'autres non. Mais nous aborderons tout cela dans une seconde.

Vous pouvez trouver le message complet ici .


7

Tout le monde a parlé de la gestion des exceptions en C ++. Mais, je pense qu'il y a une autre connotation pour le déroulement de la pile et qui est liée au débogage. Un débogueur doit effectuer le déroulement de la pile chaque fois qu'il est censé aller à une image antérieure à l'image actuelle. Cependant, il s'agit d'une sorte de déroulement virtuel car il doit revenir en arrière lorsqu'il revient à l'image actuelle. L'exemple pour cela pourrait être les commandes up / down / bt dans gdb.


5
L'action du débogueur est généralement appelée "Stack Walking" qui analyse simplement la pile. "Stack Unwinding" implique non seulement "Stack Walking", mais aussi l'appel des destructeurs d'objets qui existent sur la pile.
Adisak

@Adisak Je ne savais pas que cela s'appelait aussi "la marche de pile". J'ai toujours vu le "déroulement de la pile" dans le contexte de tous les articles du débogueur et même à l'intérieur du code gdb. J'ai trouvé le "déroulement de la pile" plus approprié car il ne s'agit pas seulement de regarder dans les informations de pile pour chaque fonction, mais implique le déroulement des informations de trame (cf CFI en nain). Ceci est traité dans l'ordre une fonction par une.
bbv

Je suppose que le "stack walking" est rendu plus célèbre par Windows. En outre, j'ai trouvé comme exemple code.google.com/p/google-breakpad/wiki/StackWalking en dehors de la doc de la norme naine elle-même utilise le terme déroulant à quelques reprises. Bien que d'accord, c'est un déroulement virtuel. De plus, la question semble demander toutes les significations possibles que «déroulement de pile» peut suggérer.
bbv

7

OMI, le diagramme ci-dessous dans cet article explique magnifiquement l'effet du déroulement de la pile sur l'itinéraire de l'instruction suivante (à exécuter une fois qu'une exception est levée qui n'est pas interceptée):

entrez la description de l'image ici

Dans la photo:

  • Le premier est une exécution d'appel normale (sans exception lancée).
  • En bas lorsqu'une exception est levée.

Dans le second cas, lorsqu'une exception se produit, la pile d'appels de fonction est recherchée linéairement pour le gestionnaire d'exceptions. La recherche se termine à la fonction avec le gestionnaire d'exceptions, c'est- main()à- dire avec le try-catchbloc englobant , mais pas avant de supprimer toutes les entrées avant celle-ci de la pile des appels de fonction.


Les diagrammes sont bons mais l'explication est un peu déroutante, à savoir. ... avec un bloc try-catch englobant, mais pas avant de supprimer toutes les entrées avant lui de la pile des appels de fonction ...
Atul

3

Le runtime C ++ détruit toutes les variables automatiques créées entre throw et catch. Dans cet exemple simple ci-dessous, les lancers f1 () et les captures main (), entre les objets de type B et A sont créés sur la pile dans cet ordre. Lorsque f1 () lance, les destructeurs de B et A sont appelés.

#include <iostream>
using namespace std;

class A
{
    public:
       ~A() { cout << "A's dtor" << endl; }
};

class B
{
    public:
       ~B() { cout << "B's dtor" << endl; }
};

void f1()
{
    B b;
    throw (100);
}

void f()
{
    A a;
    f1();
}

int main()
{
    try
    {
        f();
    }
    catch (int num)
    {
        cout << "Caught exception: " << num << endl;
    }

    return 0;
}

La sortie de ce programme sera

B's dtor
A's dtor

C'est parce que la pile d'appels du programme lorsque f1 () lance ressemble à

f1()
f()
main()

Ainsi, quand f1 () est sautée, la variable automatique b est détruite, puis quand f () est sautée, la variable automatique a est détruite.

J'espère que cela aide, bon codage!


2

Lorsqu'une exception est levée et que le contrôle passe d'un bloc try à un gestionnaire, le runtime C ++ appelle des destructeurs pour tous les objets automatiques construits depuis le début du bloc try. Ce processus est appelé déroulement de la pile. Les objets automatiques sont détruits dans l'ordre inverse de leur construction. (Les objets automatiques sont des objets locaux qui ont été déclarés auto ou register, ou non déclarés static ou extern. Un objet automatique x est supprimé chaque fois que le programme quitte le bloc dans lequel x est déclaré.)

Si une exception est levée lors de la construction d'un objet constitué de sous-objets ou d'éléments de tableau, les destructeurs ne sont appelés que pour les sous-objets ou éléments de tableau construits avec succès avant que l'exception ne soit levée. Un destructeur pour un objet statique local ne sera appelé que si l'objet a été construit avec succès.


Vous devriez fournir un lien vers l'article d'origine où vous avez copié cette réponse à partir de: Base de connaissances IBM
Déroulement de la

0

Dans la pile Java, le déroulement ou le déroulement n'est pas très important (avec garbage collector). Dans de nombreux articles sur la gestion des exceptions, j'ai vu ce concept (déroulement de pile), en particulier ces rédacteurs traitent de la gestion des exceptions en C ou C ++. avec des try catchblocs, nous ne devons pas oublier: pile libre de tous les objets après les blocs locaux .

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.