IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C++ Discussion :

Où est créée ma variable, sur le tas ou sur la pile ?


Sujet :

C++

  1. #1
    Membre confirmé
    Homme Profil pro
    Étudiant
    Inscrit en
    Mai 2013
    Messages
    113
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Mai 2013
    Messages : 113
    Par défaut Où est créée ma variable, sur le tas ou sur la pile ?
    Salut à tous,

    je vous sollicite car je me pose une question existentielle .

    Alors voila : Si j'ai une classe A de la sorte :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    class A
    {
         double v;
    }
    et une classe B de la sorte :

    et si maintenant je crée des objets de la sorte :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    int main()
    {
         A a1;
         A *a2;
         B b1;
         B *b2;
         return 0;
    }
    Ma question est la suivante : Dans chaque cas ou sont crée mes données, sur la pile ou sur le tas ?

    Si je ne me trompe a1 ne pose pas de problème, il sera créé sur la pile et v aussi (D'ailleurs quel est l'espace occupé par a1, l'espace du à son membre v ou y a t il une subtilité ?).

    Pour les autres en tout cas (a2, b1 et b2) ce n'est pas très clair pour moi, est ce que quelqu'un pour qui ça l'est pourrait m'expliquer ?

    Merci d'avance

  2. #2
    Expert confirmé
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 503
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 503
    Par défaut
    Changez de bouquin.

    L'utilisation de pointeurs nus est exceptionnelle en C++ Moderne (C++11).

    Pour faire court, tout est dans la pile.

    Pour que quelque chose soit créé dans le tas, il faut faire appel à "new" soit à des fonctions qui le font pour nous.

    L'utilisation de "new" est aussi exceptionnelle que celle des pointeurs nus.

    Un pointeur nu, ce n'est qu'une case mémoire qui à la taille suffisante pour stocker une adresse mémoire (sauf le cas particulier des pointeurs sur méthodes d'instance).

    Donc, avoir un champ "_int64 x" ou un champ "A* a" dans une classe, ça a à la même taille et à peu pré la même tronche.

    Vous n'initialisez aucun de vos pointeurs, ils pointent donc n'importe où dans la mémoire du processus, mais très probablement sur de la mémoire qui n'est pas mappé dans l'espace mémoire du processus (une GPF (General Protection Fault) sous Windows, ou une Segmentation Fault sous Linux, si vous essayez de lire ou d'écrire à cette adresse invalide).

    Ma question est la suivante : Dans chaque cas ou sont crée mes données, sur la pile ou sur le tas ?
    Tous dans la pile.

    D'ailleurs quel est l'espace occupé par a1, l'espace du à son membre v
    sizeof(a1) et sizeof(v), c'est fonction de la plateforme d'exécution cible.

    y a t il une subtilité ?).
    Pour les tailles en mémoire ? Houlà, plein, mais en s'en fout complètement avant de travailler sur l'optimisation du code.

    Pour les autres en tout cas (a2, b1 et b2) ce n'est pas très clair pour moi, est ce que quelqu'un pour qui ça l'est pourrait m'expliquer ?
    a2 et b2 doivent avoir la taille pour stocker une adresse mémoire, donc généralement, 32 ou 64 bits, en fonction de la plateforme cible.

    b1 est un objet complet, il doit donc prendre autant de place que tous ses champs, plus quelques trucs comme la place pour un pointeur de v_table, si la classe a des méthodes virtuelles. Comme b1 ne contient qu'un pointeur nu, b& devrait donc avoir la même taille que a2 et b2.

    Mais la politique de padding et d'alignement peuvent très bien changer la donne, c'est pour cela :
    1 - on utilise sizeof si on a besoin de l'info (mais c'est très peu probable)
    2 - on s'arrange pour que notre code ne soit pas aussi crade pour qu'il faille ce genre de cochonnerie avant de faire de l'optimisation très très poussée.

  3. #3
    Membre Expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    760
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 760
    Par défaut
    Lu,

    S'il n'y a pas d'allocation dynamique alors il n'y a pas d'usage du tas. Donc ici tout est sur la pile.

    Par contre, pour les pointeurs, cela ne veux pas dire qu'ils pointent sur des données en pile. Le pointeur est sur la pile, le contenu pointé dépend de la manière d'avoir l'adresse. Adresse d'un objet sur pile pour A * a2 = &a1, adresse d'un objet sur tas pour A * a2 = new A (et adresse n'importe quoi pour A * a2;).

    L'espace occupé par un objet dépend du padding lié à l'alignement, du contexte d'héritage (classe virtuelle/abstraite ; une vtable est lié) et de la présence ou non de variables dans la classe de base (empty base optimization).

  4. #4
    Expert éminent

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 202
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 202
    Par défaut
    Comme expliqué, ce n'est pas la variable qui est dans le tas. Jamais. Pas même un pointeur.
    Seule une valeur peut s'y trouver. (un objet au sens que donne la norme à ce mot).

    Dans int *p = new int(2);, un bloc mémoire est alloué par new, une valeur y est initialisée (ici un int de valeur 2), et l'adresse de ce bloc mémoire est stocké dans une variable p, qui est justement un pointeur vers int.

    P est la seule variable de cette instruction, et elle est dans la pile.
    Par contre, le int alloué dans le tas doit être libéré.

    C'est à cause de cette difficultée que les pointeurs intelligents sont recommandés.

    L'une des autres difficultée avec les pointeurs, c'est qu'on ne sait pas facilement s'ils contiennent une adresse de mémoire allouée, une adresse de la pile, du vent ou l'adresse d'une mémoire déjà désallouée.
    Dans le premier cas, la désallocation manuelle est impérative, dans les autres, elle est non seulement inutile, mais fausse et dangereuse.

  5. #5
    Membre confirmé
    Homme Profil pro
    Étudiant
    Inscrit en
    Mai 2013
    Messages
    113
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Mai 2013
    Messages : 113
    Par défaut
    Salut et merci pour vos réponses.

    Je suis allé un peu vite, en effet j'avais en tête des déclarations de pointeurs de la sorte A * a2 = new A et B * b2 = new B.

    Donc si je comprends bien avec l'utilisation de new, tout ce qui découle de cette déclaration sera déclaré sur le tas ?

    Donc pour a2, a2->v est sur le tas.
    Et donc pour b2, que la classe A ait un constructeur faisant de l'allocation dynamique ou pas, b2->a->v sera toujours sur le tas, c'est bien ça ?

  6. #6
    Expert éminent

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 202
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 202
    Par défaut
    Pour le code dans une phrase, c'est un secret. Il faut utiliser la balise [c] (ou [codeinline], sa version longue)

    Tu te poses la question un peu de travers.

    Supposons le programme suivant: (pauvre C++ )
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    struct Bidule {
        int a;
        Bidule(int a) : a(a) {}
    };
     
    int main(int argc, char**argv) {
    	Bidule bidule(2);
    	Bidule & ref = bidule;
     
    	Bidule * pointeur = & bidule;
    	Bidule * allocation = new Bidule(3);
     
    	Bidule & douleur = *pointeur;
    	Bidule & chatiment = *allocation;
     
    	//MARQUEUR 1
     
    	Bidule * danger = allocation;
    	Bidule & explosif = *danger;
     
    	//MARQUEUR 2
     
    	Bidule & monstre = *(allocation+2);
    	Bidule * neant;
     
    	//MARQUEUR 3
     
    	return 0;
    }
    On a déclaré 8 variables, toutes définies dans la pile, parce qu'elles sont locales à la fonction.
    Tu remarqueras que argc et argv sont aussi des variables (elles aussi dans la pile).

    A la sortie de la fonction, toutes ces cette variables seront détruites.
    Pour les pointeurs et références, il ne se passera rien. Du tout. Les variables seront uniquement et mécaniquement rendues à la mémoire libre.
    comme si c'était des int.
    Pour les classes, par contre, le destructeur sera appelé (ici, rien, mais ca ne change pas le problème)

    Regardons en détails:

    bidule est une variable normale, du type Bidule, construite sagement.
    ref est une référence sur bidule, ce qui ne pose pas de soucis.

    Viennent alors deux pointeurs:
    pointeur reçoit l'adresse de bidule
    Par contre, allocation est initialisée avec la valeur de l'expression new Bidule(3).
    cette expression crée un Bidule dans le tas, et retourne son adresse.

    A ce point, pointeur ne doit surtout pas être libéré, mais allocation si.

    ensuite viennent les soucis, et la plupart de ces lignes ne compilent même pas.

    douleur serait une référence sur ce que désigne pointeur. En l'occurence c'est la variable bidule. Coup de pot.
    (par contre, le compilateur refuse de compiler ca)

    chatiment est dans la même idée, mais cette fois, avec allocation.
    La, ca pourrait faire mal, car c'est une référence sur une valeur dans le tas, si toute fois le compilateur l'avait accepté.

    Et viennent les gros ennuis.
    danger est un pointeur défini avec la valeur de allocation. Il contient donc lui aussi l'adresse du Bidule créé dans le tas.
    Quant à explosif, c'est une référence sur le pointé de danger c'est à dire le Bidule alloué.

    [c]néant[c] est un pointeur non initialisé. Il est dangereux pour la suite du code, parce que rien n'interdit d'écrit *neant, ce qui provoquera en général, mais pas systématiquement, une erreur de segmentation.

    Que peut-il se passer?

    En MARQUEUR1, si on écrit delete(pointeur); c'est profondément faux, et le programme doit planter par une "double free or corrupted memory", puisque un delete sur de la mémoire de pile.

    Si on écrit delete(allocation); c'est légitime, puisque c'est une allocation par new. Par contre, ca laisse chatiment référer de la mémoire libre.
    C'est ce qu'on appelle une "dangling référence".


    En MARQUEUR2, c'est plus rigolo, tu peux tenter un delete(danger), et ca marchera, si tu n'as pas déjà libéré allocation.
    Le soucis, c'est que l'un des deux pointeurs continuera de pointer sur ce bloc mémoire, dorénavant libre.

    C'est pour cette raison qu'on a créé les shared_ptr (qui comptent combien d'entre eux pointent sur chaque bloc).
    Pire, explosif désignera aussi de la mémoire libre.

    Enfin, en MARQUEUR3, monstre désignerait n'importe quoi, c'est un débordement mémoire.


    Maintenant, il y a plus grave.


    Les arguments d'une fonction peuvent être des références, constantes (const& ou modifiantes (&).
    Voici une illustration des possibilités:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int fonction(int const& i) {
    	return i+1;
    }
    int modifiante(int & i) {
    	return ++i;
    }
    int main() {
    	int entier = 1;
     
    	cout << modifiante(entier) << fonction(entier) << endl; (affiche "2 3")
    	cout << fonction(1) << endl; //compile (affiche "2")
    	cout << fonction(fonction(1)) << endl; //compile aussi (affiche "3")
     
    	cout << modifiante(1) << endl; //ne compile pas
    }
    Comme tu le vois, il est possible de prendre une référence constante sur une temporaire, mais pas une référence modifiante.



    Sur le même principe, j'introduit des fonctions dans l'exemple.
    Au lieu de créer des références explicitement, j'utiliserai ces fonctions pour le faire.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    struct Bidule {
        int a;
        Bidule(int a) : a(a) {}
    };
     
    int fonction(Bidule const& b) {
    	return b.a;
    }
    int modifiante(Bidule & b) {
    	return b.a++;
    }
     
    int tite_maline(Bidule * b) {
    	return b->a++;
    }
     
     
    int main(int argc, char**argv) {
    	Bidule bidule(2);
    	Bidule & ref = bidule;
     
    	Bidule * pointeur = & bidule;
    	Bidule * allocation = new Bidule(3);
     
    	//Bidule & douleur = *pointeur;
    	cout << "douleur  : " << fonction(*pointeur  ) << ' ' << modifiante(*pointeur  ) << endl;
    	cout << "chatiment: " << fonction(*allocation) << ' ' << modifiante(*allocation) << endl;
     
    	Bidule * danger = allocation;
    	cout << "explosif  : " << fonction(*danger) << ' ' << modifiante(*danger) << endl;
     
    	cout << "monstre  : " << fonction(*(allocation+1)) << ' ' << modifiante(*(allocation+1)) << endl;
     
    	return 0;
    }
    Aucun de ces appels à modifiante ne compile, pour ce que j'en sais.
    Et c'est tant mieux.

    Par contre, tous les appels à fonctions sont valides.
    Mais, l'appel à fonction() de "monstre" est faux, et ne va pas forcément planter, parce que la mémoire désignée ne sera probablement pas interdite d'accès. C'est juste une faute gravissime de l'utilisateur de la fonction.

    Passé ce point sur les dangers, voyons un peu le détail de qui est où.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    struct Bidule {
        int a;
    	int *p;
        Bidule(int valeur) : a(valeur), p(&a) {}
    	Bidule(int *p) : a(*p), p(p) {}
     
    	~Bidule() {cout << "aie" <<endl;}
     
    	//comme tu le vois, p n'est pas possédé par Bidule. C'est à l'utilisateur de s'occuper d'un éventuel delete.
    };
     
    int main {
    	{
    		Bidule premier(2);
     
    		premier.a = 3; //premier.a est une partie de premier, qui est défini dans la pile
    		cout << *(premier.p) << endl; //affiche 3, car premier.p est l'adresse du a de premier.
    		/* Ce p est une partie de premier. il est dans la pile, comme premier.
    		   ce 3 est aussi dans la pile, car c'est le a de premier.
    		*/
    	} //ici, premier est relaché, puisqu'il sort de la pile.
     
    	//ca affiche donc "aie"
     
    	{
    		int entier = 2;
    		Bidule deuxieme(&entier);
     
    		deuxieme.a = 3; //deuxieme.a est une partie de deuxieme, qui est défini dans la pile
    		cout << *(deuxieme.p) << endl; //affiche 2, car deuxieme.p est l'adresse de entier.
    		/* Ce p est une partie de deuxieme. il est dans la pile, comme deuxieme.
    		   ce 3 est aussi dans la pile, car c'est entier.
    		*/
    	} //ici, entier et deuxieme sont relachés
    	//ca affiche donc "aie"
     
    	{
    		Bidule troisieme(new int(2));//très très vilain
    		// ce 2 est dans le tas.
     
    		*(troisieme.p) = 3;
    		cout << troisieme.a << endl; //affiche 2, car troisieme.a a été initialisé avec une copie de ce que pointait p lors de la construction.
     
    		troisieme.a = 4;
    		cout << *(troisieme.p) << endl; //affiche 3, car troisieme.p est l'adresse du "new int", pas de troisieme.a.
     
     
    		delete troisieme.p;//très très sale, mais nécessaire.
    	} // ici, seul troisieme est relaché. C'est pour ca qu'il a fallu faire le delete
    	//ca affiche donc "aie"
     
    	{
    		Bidule *quatrieme = new Bidule(new int(2));
    		//voici deux blocs mémoire dans le tas: l'entier et le Bidule
     
    		int* pointeur = quatrieme->p;
    		//pointeur est dans la pile. il contient l'adresse contenu dans le p du bidule pointé par quatrieme.
     
    		*pointeur = 7; //*pointeur est donc dans le tas.
     
    		delete quatrieme; //ce qui affiche aie
    		delete pointeur;//parce que j'ai pris la précaution de récupérer quatrieme->p dans pointeur
    	} //ici, quatrieme et pointeur disparaissent
    	//ca n'affiche pas "aie", quatrieme n'était qu'un pointeur;
     
    	return 0;//parce que malgré tout, main se termine bien.
    }
    Pour résumer:
    new est l'unique moyen de créer un bout de tas.
    Les parties d'un objet, qu'il soit d'une classe, d'une structure (ou d'une union) sont incluse dans l'objet.
    Je n'ai pas parlé de l'héritage, mais quand deux classes A et B sont définies, avec B dérivant A, toute objet de type B contient un A (comme si c'était son premier champ).

    Si une valeur est dans le tas, toutes ses parties aussi.

    Par contre si une classe contient un pointeur, c'est le pointeur (donc l'adresse du pointé) qui est dans l'objet. Ce qui se trouve à l'adresse en question ne dépend pas de l'objet.

    Remarque, dans une union, toutes les parties commencent au même endroit, mais sont bien dans l'objet, qui se trouvent faire la même taille que la plus grande partie.
    ainsi, dans union {char c; long l;} truc;, truc est de la taille de truc.l, et tu as &truc == &truc.c et &truc == &truc.lPS: pour compiler mes exemples, il faut rajouter:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    #include <iostream>
    using namespace std;

  7. #7
    Membre confirmé
    Homme Profil pro
    Étudiant
    Inscrit en
    Mai 2013
    Messages
    113
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Mai 2013
    Messages : 113
    Par défaut
    Merci pour ce cours très intéressant sur les pointeurs

    Et merci pour tous ces exemples concrets.

    Je pense avoir mieux compris le fonctionnement, corrigez moi si je me trompe :

    -> Une variable (qui peut être un pointeur) est toujours stockée dans la pile.
    -> C'est la valeur de la variable (celle pointé par le pointeur dans la cas d'un pointeur) qui peu être stocké soit dans la pile soit dans le tas.
    -> Pour stocker une valeur dans le tas il faut allouer la mémoire dynamiquement avec new.
    -> Quand un objet est dans le tas, toutes ses parties sont dans le tas donc :
    - Toutes les variables et leurs valeurs sont dans le tas.
    - Toutes les pointeurs sont dans le tas mais les valeurs sur lesquelles ils pointent peuvent être dans la pile. Pour que les valeurs sur lesquelles ils pointent soient dans le tas il faut que la mémoire sur laquelle ils pointent soit allouer dynamique à l'aide de new.


    PS : merci pour les balises [c] ou [codeinline]

  8. #8
    Expert éminent

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 202
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 202
    Par défaut
    En résumé, c'est ça.

    Même si pour être tout à fait précis, les variables globales ne sont ni dans la pile, ni dans le tas, mais dans le segment des globales, (généralement l'équivalent du fond de la pile).
    Je n'en ai pas parlé, volontairement, vu que tu n'as jamais besoin d'une globale.

    En termes techniques, le tas correspond à la "dynamic storage duration", tandis que la pile est la "automatique storage duration".
    tas et pile sont des segments de mémoire, au même titre que d'autres, comme celui contenant le code ou celui contenant les constantes de chaines (raison pour laquelle "truc" est une valeur de type char const*).
    A vrai dire, je ne connais pas la liste complete des segments, ni même si cette liste est définie par la norme, mais une chose est sûre, certains sont uniquement lisible, voire même interdit d'accès.


    Pour plus de détails, je te conseille de lire les explications dans storage class and linkage specifiers sur cpprefence.com.
    ainsi que object, lifetime et references

  9. #9
    Membre confirmé
    Homme Profil pro
    Étudiant
    Inscrit en
    Mai 2013
    Messages
    113
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Mai 2013
    Messages : 113
    Par défaut
    J'irais faire un tour sur cpprefence.com.

    Merci pour toutes ces explications leternel, ça m'a bien aidé !


+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Réponses: 1
    Dernier message: 28/03/2007, 19h20
  2. Est t il possible d'avoir un menu sur un clic droit ???
    Par almisuifre dans le forum C++Builder
    Réponses: 6
    Dernier message: 21/12/2004, 11h21
  3. Réponses: 7
    Dernier message: 08/03/2004, 15h30
  4. Réponses: 5
    Dernier message: 20/11/2003, 16h36

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo