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 :

Ambiguité à lever : constructeurs et destructeurs


Sujet :

C++

  1. #1
    Membre averti
    Avatar de wafiwafi
    Profil pro
    Inscrit en
    Décembre 2008
    Messages
    500
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2008
    Messages : 500
    Points : 328
    Points
    328
    Par défaut Ambiguité à lever : constructeurs et destructeurs
    En C++, quand on crée un objet en appelant un constructeur et sans faire appel à l'allocation dynamique de mémoire, le compilateur réserve d'ors et déjà une partie de la mémoire avant l'exécution.
    On sait également que si on crée un objet avec une allocation dynamique par new par exemple, on a intérêt à le supprimer par delete.
    Voila quelques dires que j'aimerais que vous critiquiez :

    1- un objet créé de façon statique (sans faire appel à l'allocation dynamique de mémoire) est détruit automatiquement selon sa portée. Ne serait il pas intéressant de le détruire avant la fin de sa portée si on en a plus besoin?

    2- Lors de l'allocation dynamique de la mémoire, on crée l'objet avec new par exemple. Quand on veut détruire cet objet, on fait appel au destructeur.
    Si l'objet a des attributs qui pointent vers d'autres objets , on est obligé de déclarer un destructeur différent de celui appelé par défaut, dans le but de supprimer également les objets pointés.

    3- si l'objet ne cache pas de pointeurs à travers ses attributs, alors on a le choix entre le créer en appelant le constructeur par défaut ou par new pour une allocation dynamique.

    4- Quand l'objet cache des pointeurs vers d'autres objets , on est obligé de le créer avec new pour appeler le destructeur (non par défaut) permettant encore une fois de supprimer les objets pointés.

    5- Comparons avec Java. On ne travaille qu'avec des références. Toutes les créations se font avec des allocations dynamiques de la mémoire.

    Merci à vous
    L'immortalité existe, elle s'appelle connaissance

  2. #2
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Salut,

    Déjà, il faut comprendre que le type de gestion de la mémoire (allocation / destruction dynamique de la mémoire ou non) est un concept tout à fait transversal à celui de constructeur / destructeur.

    En effet, la seule différence qu'il existe (en dehors du fait que l'on travaille avec un pointeur vers un objet au lieu de travailler directement avec l'objet) tient au fait que tu prend la responsabilité de la durée de vie de l'objet pointé par le pointeur lorsque tu écris un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Type * ptr = new Type; voir Type * ptr = new Type(arg1, arg2)
    alors que la durée de vie est gérée "automatiquement" lorsque tu écris un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Type obj; //voir Type obj(arg1, arg2)
    Dans les deux cas, le constructeur de l'objet correspondant est appelé, simplement, lorsque tu utilise l'allocation dynamique, il y a "quelque chose de plus" qui est effectué avant l'appel au constructeur : l'allocation dynamique de la mémoire.

    Il en va de même lors de la destruction de l'objet:

    Si tu as assigné à un pointeur l'adresse mémoire à laquelle trouver un objet avec new, tu as pris la responsabilité de sa durée de vie, et tu dois donc le détruire avec delete.

    si tu as "simplement" déclaré une variable sans avoir recours à new, elle est détruite "automatiquement" lorsque tu atteints l'accolade fermante de la portée dans laquelle la variable est déclarée.

    Qu'il s'agisse de destruction "automatique" ou de la destruction utilisant delete, le destructeur est automatiquement appelé, simplement, lorsque c'est effectué avec delete, il y a un "petit plus" qui est effectué après l'appel au constructeur: la libération de la mémoire qui était allouée à l'objet.

    Ceci étant dit
    1- un objet créé de façon statique (sans faire appel à l'allocation dynamique de mémoire) est détruit automatiquement selon sa portée. Ne serait il pas intéressant de le détruire avant la fin de sa portée si on en a plus besoin?
    Il peut arriver que tu n'aie plus besoin d'une variable alors que tu es encore loin d'atteindre la fin de la portée dans laquelle elle est déclarée.

    Mais il faut avouer que, de manière générale, il reste malgré tout *assez* rare de se trouver dans une situation ou la libération de la mémoire nécessaire à la représentation d'un objet soit urgente au point de faire en sorte qu'elle apparaisse quelques instructions plus tôt.

    Quoi qu'il en soit, il te reste toujours "l'astuce" qui consiste, tout simplement, à créer une portée "artificielle" pour la variable pour laquelle tu veux assurer une destruction "prématurée".

    En effet, le code suivant est tout à fait valide:
    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
    void foo()
    {
        int a;
        int b;
        /* on travaille avec a et b */
        {    
            /* c'est une portée "artificielle" dans le sens où l'on a 
             * généralement l'habitude de croiser l'accolade ouvrante en 
             * début de fonction, lors de tests ou de boucles...
             */    
            Type temp;
            /* on travaille avec temp, mais on peut utiliser a et b */
        } // temp est détruit
        //  temp.truc() KO : temp n'existe plus
        /* on peut encore utiliser a et b */
    } // a et b sont détruits
    2- Lors de l'allocation dynamique de la mémoire, on crée l'objet avec new par exemple.
    Non, pas par exemple...

    La seule alternative (hormis new[] pour créer un tableau dynamique d'objets) serait d'utiliser malloc, mais c'est déconseillé avec moulte insistance...
    Quand on veut détruire cet objet, on fait appel au destructeur.
    Non, on fait appel à delete (voire à delete[] si c'est un tableau dynamique d'objet) qui fait, entre autres choses, appel au destructeur de l'objet
    Si l'objet a des attributs qui pointent vers d'autres objets , on est obligé de déclarer un destructeur différent de celui appelé par défaut, dans le but de supprimer également les objets pointés.
    Non:

    Une classe ne peut jamais avoir qu'un seul destructeur, et c'est celui qui sera toujours utilisé, que l'objet soit détruit à coup de delete ou de manière automatique.

    Par contre, on peut citer trois situations particulières:

    1. Le destructeur implémenté par défaut par le compilateur nous suffit
    2. Le destructeur doit détruire des objets créés dynamiquement
    3. Le destructeur doit pouvoir être appelé et adapter son comportement à des types qui héritent du type courent
    Dans le premier cas, on peut, carrément, ne pas le déclarer (et, forcément, ne pas l'implémenter): le compilateur fera tout pour nous

    Dans le second cas, nous devons le déclarer et l'implémenter de manière a invoquer delete (ou delete[]) sur les membres qui ont été créé dynamiquement

    Dans le trosième cas, nous devons le déclarer virtuel avec le mot clé virtual et l'implémenter, même s'il ne fait rien. Il appellera (sans que l'on n'écrive quoi que ce soit) automatiquement le destructeur de chacun des membres de la classe (dans l'ordre inverse de leur déclaration)

    3- si l'objet ne cache pas de pointeurs à travers ses attributs, alors on a le choix entre le créer en appelant le constructeur par défaut ou par new pour une allocation dynamique.
    Encore une fois, tu confond le concept de constructeur et celui d'allocation dynamique...

    Le terme "constructeur par défaut" prend deux significations possibles, mais qui ne changent pas grand chose à l'histoire:
    1. Le fait que ce soit le constructeur (ne prenant aucun argument) automatiquement déclaré et implémenté par le compilateur si nous ne déclarons pas nous même un autre constructeur
    2. Le fait que le constructeur ne prend aucun argument

    Rien ne t'empêche de déclarer plusieurs constructeurs pour autant que le nombre et ou le type des arguments qu'ils nécessitent soit différents.

    ainsi une classe proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MaClass
    {
        public:
            MaClass(); // constructeur "par défaut" (ne prenant aucun argument)
            MaClass(int ); // constructeur prenant un int en argument
            MaClass(std::string const &); // constructeur prenant une chaine
                              // de caractères en argument
            /*...*/
    };
    est il tout à fait valide, et tu peux parfaitement invoquer le constructeur "qui correspond le mieux" à tes besoins:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
    int main()
    {
        MaClass obj1; //appelle le constructeur par défaut
        int lentier=10;
        MaClass obj2(lentier); 
        std::string str("Salut toi");
        MaClass obj3(str);
        /*...*/
        return 0;
    }
    Et nous pouvons parfaitement utiliser les trois constructeurs avec l'allocation dynamique de la mémoire (la seule exception étant en cas de création d'un tableau dynamique, car nous utilisons d'office le constructeur par défaut)...

    Chacune des lignes de code suivantes est donc valide:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int main()
    {
        MaClass *ptr1 = new MaClass;
        int lentier = 12;
        MaClass *ptr2 = new MaClass(lentier);
        std::string str("toi");
        MaClass *ptr3 = new MaClass(str); 
        /* seule exception : les tableaux... on ne peut appeler que le contructeur
         * par défaut... qui DOIT exister 
         */
        MaClass *tab=new MaClass[10];
        /*...*/
        return 0;
    }
    NOTA: N'oublie pas que chaque new ou new[] doit être mis en relation avec un delete ou delete[]

    4- Quand l'objet cache des pointeurs vers d'autres objets , on est obligé de le créer avec new pour appeler le destructeur (non par défaut) permettant encore une fois de supprimer les objets pointés.
    Absolument pas...

    Le principe suivi pour le constructeur est de fournir un objet directement utilisable, car ses membres sont initialisés de manière cohérente (avec allocation dynamique de la mémoire en cas de besoin) et s'appelle le RAII pour Ressources Acquisition Is Initialization (l'acquisition des ressources vaut initilialisation, si tu préfères la langue de Voltère )

    Le principe suivi par le destructeur est exactement l'inverse, et, bien que le terme n'existe pas, nous pourrions dire qu'il s'agit du RRID (Ressource Release Is Destruction).

    Ainsi, avec une class proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MaClass
    {
        public:
            MaClass():size_(10),ptr_(new int[10]){}
            ~MaClass(){delete ptr_;}
        private:
            size_t size_;
            int * ptr_;
    };
    tu peux tout aussi bien travailler en ayant recours à l'allocation dynamique qu'en gestion automatique de la mémoire:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void fooAutomatique()
    {
        MaClass obj; //création automatique
        /*...*/
    } // destruction et appel de ~MaClass() automatiques
    void barDynamique()
    {
        MaClass *ptr = new MaClass;
        /*...*/
        /* destruction "manuelle" obligatoire (pouvant survenir dans 
         * une autre fonction) qui fait, entre autres choses, appel à ~MaClass()
         */
       delete ptr;
    }
    5- Comparons avec Java. On ne travaille qu'avec des références. Toutes les créations se font avec des allocations dynamiques de la mémoire.
    Oui, parce que le comportement de new est différent...

    Il effectue bien une allocation dynamique de la mémoire afin de pouvoir fournir un pointeur vers l'objet créé au garbage collector...

    De cette manière, lorsqu'il n'existe plus de référence vers l'objet, le garbage collector est en mesure de récupérer l'adresse mémoire à laquelle se trouve l'objet en vue de sa destruction "dans les règles de l'art"

    En C++, tu n'a pas de garbage collector par défaut (même s'il est possible d'en créer un, ce qui nécessite, entre autres, de redéfinir le fonctionnement de new et de delete), et tu travailles, par défaut, sur des objets, et non sur des références.

    Il y a, dans une autre section, une grosse centaine de pages de troll afin de déterminer quel langage est le meilleur entre C++ et java...

    Je ne veux pas relancer le débat ici, mais, il faut savoir que la "philosophie" des deux langages est totalement différente, et que cela a, forcément, impliqué des choix différents vis à vis de ce que permet (ou non) le langage.

    Java a pris la décision de "bêtifier" le programmeur à l'extrême en le déchargeant d'une série de responsabilités telles que la gestion de la mémoire (qui est déléguée au garbage collector), le choix de la virtualité des fonctions (qui sont d'office virtuelles) ou la gestion des problèmes éventuellement liés à l'héritage multiple (qui est interdit),... Et d'autres choses encore...

    Si tout passe par des références en java, c'est, simplement, pour éviter les copies incessantes d'objets... mais cela a un cout car... il faut, pour permettre au garbage collector de faire son travail, que chaque objet dispose de son compteur de référence... qui doit être "maintenu à jour" en permanence...

    Et si on passe par new pour déclarer un nouvel objet en java, c'est pour deux raisons:
    • Du fait qu'il est destiné à être géré par le garbage collector, il doit "survivre" à la sortie de la portée dans laquelle il est déclaré et
    • il faut disposer d'un mécanisme permettant d'enregistrer l'objet auprès du garbage collector

    De son coté, C++ a pris les décisions
    1. de laisser une liberté quasi absolue au programmeur, y compris celle de risquer de se "tirer une balle dans le pied".
    2. de rester autant que faire se peut compatible avec C
    Si, par défaut (comprend: en dehors de toute indication contraire), les argument sont passé par valeur en C++, c'est simplement... parce que c'est ainsi qu'ils sont passés en C...

    Comme il n'y a pas de garbage collector, il ne sert à rien d'imposer l'allocation dynamique de la mémoire pour les objets, ce qui, de plus, permet encore une fois la compatibilité avec C...

    Il ne sert non plus à rien d'avoir un compteur de référence dans les différents objets, vu que, par défaut, il n'y a aucun mécanisme qui pourrait en profiter...

    Enfin, si, à un moment donné, le programmeur souhaite ne pas "payer le prix" de la copie de l'objet (ce qui est le cas par défaut), il reste toujours libre de le passer par référence, éventuellement constante.

    Mais cela reste du domaine de la responsabilité du programmeur

    Cette différence de mentalité du langage fait qu'il est à la limite déconseillé de vouloir faire le parallèle entre java et C++, et que l'apprentissage de java en venant de C++ est, finalement, bien plus facile (car le parallèle est plus cohérent) que l'inverse...
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  3. #3
    Membre averti
    Avatar de wafiwafi
    Profil pro
    Inscrit en
    Décembre 2008
    Messages
    500
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2008
    Messages : 500
    Points : 328
    Points
    328
    Par défaut
    J'ai non seulement saisis, mais mes idées sont merveilleusement en place grâce à cette mise au point bien ciblée. Je tiens à t'en remercier indéfiniment.
    Il me manquait le petit plus qui me permet de temps en temps descendre dans la soute.
    Bien à toi
    L'immortalité existe, elle s'appelle connaissance

  4. #4
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 074
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

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

    Informations forums :
    Inscription : Février 2005
    Messages : 5 074
    Points : 12 120
    Points
    12 120
    Par défaut
    Les implémentations "standard" du garbage collector JAVA n'utilisent pas de compteur de référence mais des parcours de graphes générationnelles.

    Il ne faut pas "bêtifier" ni les implémentations JAVA, ni les programmeurs JAVA.

    Le garbage collector est plus un faux ami qu'une panacée.

  5. #5
    Expert éminent sénior
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 369
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 369
    Points : 41 519
    Points
    41 519
    Par défaut
    Citation Envoyé par wafiwafi Voir le message
    En C++, quand on crée un objet en appelant un constructeur et sans faire appel à l'allocation dynamique de mémoire, le compilateur réserve d'ors et déjà une partie de la mémoire avant l'exécution.
    Attention: C'est le cas d'une variable globale, mais pour une variable locale, la mémoire est réservée sur la pile en début de bloc (ou de fonction, selon le compilo)...
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

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

Discussions similaires

  1. Réponses: 9
    Dernier message: 13/11/2007, 13h46
  2. Réponses: 2
    Dernier message: 18/04/2007, 13h34
  3. Réponses: 18
    Dernier message: 08/12/2006, 02h30
  4. Réponses: 4
    Dernier message: 21/09/2006, 12h45
  5. Réponses: 24
    Dernier message: 10/06/2005, 10h11

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