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 :

Fuite de mémoire


Sujet :

C++

  1. #1
    Membre à l'essai
    Homme Profil pro
    Étudiant
    Inscrit en
    Avril 2019
    Messages
    4
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 29
    Localisation : France, Nord (Nord Pas de Calais)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Avril 2019
    Messages : 4
    Par défaut Fuite de mémoire
    Bonjour à tous !

    Je suis étudiant en mathématiques et novice en C++. J'ai pris connaissance, sur un site d'auto formation bien connu, de l'existence de la "fuite de mémoire", et voici mon tourment:

    Je crée une classe "matrice" (dans le cadre de mes cours, construction de la classe imposée) et je souhaite faire une fonction qui ajoute une ligne à la matrice, à la fin.

    Voici les champs privés de la classe matrice:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    int size1;
    int size2;
    vecteur* mrows;
    (j'ai créé une classe vecteur aussi, je sais STL fonctionne très bien mais pas le choix... Cette classe marche très bien)

    Voici la fonction désirée :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    matrice matrice :: ajout_ligne() {
        vecteur zero(size2);
        vecteur* stock;
        stock = new vecteur [size1];
        for (int i=0; i<size1; i++) stock[i] = mrows[i];
        size1 = size1 + 1;
        mrows = new vecteur [size1];
        for(int i=0; i<size1-1; i++) mrows[i] = stock[i];
        mrows[size1-1] = zero;
        return *this;
    }
    Est-ce qu'il y a ici une fuite de mémoire ? Je pense que oui parce que je ne supprime pas l'ancienne valeur du pointeur mrows ainsi la case mémoire utilisée est toujours occupée. Est-ce vrai ce que je dis ?
    Si oui, rajouter la ligne delete[] mrows; juste avant mrows = new vecteur [size1]; réglerait le problème ?

    Pour être franc je ne suis pas rassuré et j'aimerai que cela s'éclaircisse pour moi.

    Merci beaucoup d'avance de prêter attention à ma demande !!

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

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

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 391
    Par défaut
    Je dirais surtout qu'il y a deux fuites de mémoire: une sur stock et une sur mrows.

    Normalement, pour être exception-safe, tu devrais utiliser des std::unique_ptr<> de partout, ainsi que la fonction std::swap() pour échanger entre la variable membre et la variable temporaire (en gros, t'inspirer de l'idiome copy-and-swap).
    À ne pas utiliser unique_ptr, tu fais du C++ "à l'ancienne", pas du C++ moderne. Ce qui suggère que la formation proposée par ce site est obsolète.

    Edit: Ensuite, côté performance, je ne vois pas l'intérêt d'avoir une fonction dédiée pour rajouter une ligne si à chaque appel elle réalloue tout le tableau de lignes; une fonction plus générique redimensionner_matrice() me semblerait plus appropriée.
    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.

  3. #3
    Membre à l'essai
    Homme Profil pro
    Étudiant
    Inscrit en
    Avril 2019
    Messages
    4
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 29
    Localisation : France, Nord (Nord Pas de Calais)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Avril 2019
    Messages : 4
    Par défaut
    Merci pour votre réponse !
    Donc est-ce que rajouter un delete[] stock et delete[] mrows supprimerait la fuite de mémoire?

  4. #4
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 145
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 37
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 145
    Billets dans le blog
    4
    Par défaut
    C'est normal de retourner *this et d'en créer une copie ?
    Je suppose que mrows est une variable membre (préfixe m), dont pourquoi voudrais-tu la delete ?
    Pourtant je vois size1 et size2 ? Donc pas de préfixe m pour les variables membres ?
    stock est bien une variable locale cette fois ?
    Tu réalloues sur mrows sans le libérer avant ? Donc son allocation précédente est maintenant perdue et une fuite mémoire ?
    (j'ai créé une classe vecteur aussi, je sais STL fonctionne très bien mais pas le choix... Cette classe marche très bien)
    De toute évidence elle ne marche pas très bien.

    Si en plus tu manipules de la mémoire, je suppose que tu as correctement implémenté constructeur de copie, mouvement, assignation et assignation par mouvement ? Ou ces fonctions ont été correctement supprimées ?
    Et le destructeur remplit son rôle proprement ?
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  5. #5
    Membre à l'essai
    Homme Profil pro
    Étudiant
    Inscrit en
    Avril 2019
    Messages
    4
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 29
    Localisation : France, Nord (Nord Pas de Calais)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Avril 2019
    Messages : 4
    Par défaut
    Voici mes modifications :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    matrice matrice :: ajout_ligne() {
        vecteur zero(size2); // Création d'un vecteur de size2 0.
        vecteur* stock;
        stock = new vecteur [size1];
        for (int i=0; i<size1; i++) stock[i] = mrows[i];
        size1 = size1 + 1;
        delete[] mrows; // Pour moi, ça permet de libérer la valeur du pointeur mrows
        mrows = new vecteur [size1];
        for(int i=0; i<size1-1; i++) mrows[i] = stock[i];
        mrows[size1-1] = zero;
        delete[] stock; //On libère le pointeur stock
        return *this; //Je retourne la matrice modifiée. 
    }
    Les champs privés de la classe matrice sont :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    int size1; //nombre de lignes
    int size2; //nombres de colonnes
    vecteur* mrows;
    En fait, ma prof m'a appris qu'un pointeur était un "tableau". Ainsi ici, mrows est un tableau de vecteur (où vecteur est la classe que j'ai créé avant la classe matrice évidemment).
    Je ne vois pas l'intérêt d'implémenter constructeur de copie (fait), mouvement (qu'est-ce?), assignation et assignation par mouvement (??).
    (Les champs privés de la classe vecteur sont size (nombre d'éléments) et double [] p ("tableau de double") )

  6. #6
    Invité
    Invité(e)
    Par défaut
    Bonjour,

    Ce serait nettement plus simple si tu utilisais un std::vector. Ton ajout de ligne se résumerait à appeler sa fonction membre push_back().

    Sinon, tu effectues des copies inutiles. Ce que tu fais à la fin pour mrows, c'est ce que tu pourrais faire directement avec stock :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    vecteur *stock = new vecteur [size1 + 1];
    for(int i=0; i<size1; i++)
        stock[i] = mrows[i];
    stock[size1] = zero;
    puis après avoir désalloué mrows, il te suffirait de le faire pointer à la même adresse :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    delete [] mrows;
    mrows = stock;
    ++size1;
    Mais niveau fuite, je te laisse imaginer si un seul de tes new (ici ou dans les vecteurs) lance une exception…

    Citation Envoyé par PierreJssp Voir le message
    Je ne vois pas l'intérêt d'implémenter constructeur de copie (fait), mouvement (qu'est-ce?), assignation et assignation par mouvement (??).
    L'intérêt est que si tu ne les implémente pas, ce que va implicitement implémenter ton compilateur sera faux. Il ne fera que copier les adresses et cela donnera lieu à des fuites mémoire, des double libérations, ou à des accès à de la mémoire déjà libérée.
    Par exemple pour les vecteurs où tu les as sûrement aussi oubliés, lorsque tu fais stock[i] = mrows[i]; (qui utilise l'opérateur d'affectation par copie) : la zone mémoire pointée par stock[i].p sera perdue (fuite) et les deux pointeurs stock[i].p et mrows[i].p vont pointer sur la même zone mémoire. Cette même zone mémoire que tu libéreras avec delete [] mrows;.

    À noter que si tu utilises std::vector, tu n'as pas à t'embêter à en coder un seul, ou à te soucier des fuites. Cf. The rule of three/five/zero et Comment gérer proprement des allocations/désallocations de ressources ? Le RAII !

  7. #7
    Membre éclairé
    Homme Profil pro
    Ingénieur validation
    Inscrit en
    Août 2018
    Messages
    41
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 46
    Localisation : France, Côtes d'Armor (Bretagne)

    Informations professionnelles :
    Activité : Ingénieur validation
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2018
    Messages : 41
    Par défaut
    Bonjour
    La question de Bousk est importante :
    C'est normal de retourner *this et d'en créer une copie ?
    Si tu écris par exemple :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    matrice M1(3, 3);
       // ... opérations sur M1
    matrice M2 = M1.ajout_ligne();
    Tu ajoutes une ligne à M1 et recopies toute la matrice dans M2.
    1) c'est assez curieux de créer une nouvelle matrice alors que tu as modifié la matrice d'origine
    2) Le constructeur par copie et l'opérateur d'affectation par défaut de matrice se contentent de recopier bêtement tous les champs de matrice.
    Dans le cas de mrows, tu te retrouves avec M2.mrows et M1.mrows qui pointent sur la même adresse.
    Quand tu fais un delete sur l'une, tu invalides l'autre.

    De la même façon, tu affectes des vecteurs par l'opérateur d'affectation alors qu'ils contiennent des tableaux de doubles. Là aussi, risques de fuites de mémoire et de tentatives d'effacer plusieurs fois la même zone mémoire.

    Avant de s'occuper de matrice, il faut déjà s'assurer que vecteur est correctement implémenté.

  8. #8
    Membre à l'essai
    Homme Profil pro
    Étudiant
    Inscrit en
    Avril 2019
    Messages
    4
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 29
    Localisation : France, Nord (Nord Pas de Calais)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Avril 2019
    Messages : 4
    Par défaut
    Bonjour,
    Merci à tous pour vos réponse!

  9. #9
    Expert éminent
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 633
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 53
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 633
    Par défaut
    Salut,
    Citation Envoyé par PierreJssp Voir le message
    Voici mes modifications :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    matrice matrice :: ajout_ligne() {
        vecteur zero(size2); // Création d'un vecteur de size2 0.
        vecteur* stock;
        stock = new vecteur [size1];
        for (int i=0; i<size1; i++) stock[i] = mrows[i];
        size1 = size1 + 1;
        delete[] mrows; // Pour moi, ça permet de libérer la valeur du pointeur mrows
        mrows = new vecteur [size1];
        for(int i=0; i<size1-1; i++) mrows[i] = stock[i];
        mrows[size1-1] = zero;
        delete[] stock; //On libère le pointeur stock
        return *this; //Je retourne la matrice modifiée. 
    }
    Les champs privés de la classe matrice sont :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    int size1; //nombre de lignes
    int size2; //nombres de colonnes
    vecteur* mrows;
    Déjà, renomme ces champs privés, en m_lines et m_cols, par exemple, pour que tu ne soit jamais tenté d'utiliser size1 comme étant le nombre de colonnes et size2 comme étant le nombre de lignes.

    En effet, le nom que tu donnes à "quelque chose" (qu'il s'agisse d'une classe, d'une donnée ou d'une fonction) est le seul élément qui te permettra de l'identifier correctement et sans risque d'erreur. Plus ce nom sera précis, moins tu risqueras de faire d'erreur (et, surtout: moins ce nom sera précis, plus tu risqueras d'en faire ).
    Citation Envoyé par Eragon(2006)
    Blisingr, signifie le feu.

    C'est le feu. Le nom, c'est la chose. Connait le nom, et tu maîtrise la chose
    Pour le reste, je te propose de jouer à nous rendre aussi bête qu'un ordinateur, pour que l'on arrive à se faire une idée de ce que fait ton code
    Intéressons nous à une partie de ta fonction, seulement, celle qui correspond à
    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
        stock = new vecteur [size1];  // on alloue suffisamment de mémoire pour pouvoir représenter size1 vecteurs
                                      // j'espère que vecteur expose un constructeur par défaut, ainsi qu'un opérateur d'affectation =
                                      // Q: que vaut size1 ?
        for (int i=0; i<size1; i++) stock[i] = mrows[i];  // on copie chaque élément de mrows dans stock
        size1 = size1 + 1;            // on augmente la valeur de size1
                                      // rappel : size1 correspond au nombre de lignes
        delete[] mrows;               // Pour moi, ça permet de libérer la valeur du pointeur mrows
                                      // Oui, si mrows a été alloué avec new[] (*).
        mrows = new vecteur [size1]; ;  // on alloue suffisamment de mémoire pour pouvoir représenter size1 vecteurs
                                      // j'espère que vecteur expose un constructeur par défaut, ainsi qu'un opérateur d'affectation =
                                      // Q: que vaut size1 ?
                                      // Q: Et si new[] lance une exceptoin... Que se passe-t-il? (**)
        for(int i=0; i<size1-1; i++) mrows[i] = stock[i];  // on copie chaque élément de stock dans mrows
        mrows[size1-1] = zero;   // on assigne une valeur par défaut à la valeur inconnue pour l'instant
        delete[] stock; //On libère le pointeur stock
    Heu, dis moi, tu n'a pas l'impression de faire les choses deux fois

    Parce que, là ce que tu fais, c'est:
    1. allouer assez de mémoire pour représenter size1 éléments
    2. copier les size1 éléments de mrows vers stock
    3. libérer la mémoire allouée à stock
    4. incrémenter la taille
    5. allouer de la mémoire pour size1 + 1 (par rapport à la taille d'origine) éléments à mrows
    6. copier les size1 éléments (par rapport à la taille d'origine) de ... stock vers mrows
    7. libérer la mémoire allouée à stock

    Je reviendrais un peu plus tard sur cette logique

    (*) Il faut faire très attention lorsque l'on utilise new / new[] et delete / delete[], car la mémoire allouée par new doit être libérée par delete, et que celle allouée par new[] doit l'être par delete[].

    Tenter de libérer de la mémoire allouée par new avec delete[], ou tenter de libérer de la mémoire allouée par new[] avec delete représente ce que l'on appelle un comportement indéfini : tout peut arriver. Dans le meilleur des cas, ton programme plantera, dans le pire, cela mettra en branle une connexion sur le serveur sécurisé de l'OTAN, et provoquera la lancée d'un missile sur l'URSS

    (**)C++ est un langage à exceptions. new et new[] sont beaucoup plus complexes que le simple malloc du C, parce
    1. ils appellent automatiquement le constructeur par défaut du type indiqué (à condition qu'il existe)
    2. si, pour une raison ou une autre (et elles sont nombreuses), l'allocation échoue, ils lancent une exception de type std::bad_alloc
    3. le constructeur du type utilisé pour l'allocation dynamique peut lui aussi lancer une exception quelconque

    Or, la loi de l'emmerdement maximum nous dit que,
    si quelque chose peut mal se passer, ce n'est qu'une question de temps pour que cela se passe effectivement mal.
    Le simple fait que new (ou new[]) implique donc que tu dois d'office faire "comme si" les choses allaient effectivement mal se passer (autrement, tu auras forcément un bug à un moment ou à un autre), et donc, comme si une exception allait d'office être lancée lors de chaque appel à new/new[].

    Or, lorsqu'une exception est lancée, on quitte littéralement la fonction en cours d'exécution pour remonter dans la pile d'appels à la recherche d'un catch capable de la traiter. Si le catch est trouvé (considérons que ce soit le cas, car c'est la situation dans laquelle les choses tourneront mal ), que va-t-il se passer, selon toi

    En fait, ma prof m'a appris qu'un pointeur était un "tableau".
    Elle ne semble pas savoir de quoi elle parle

    Un pointeur, c'est, d'abord et avant tout, une valeur numérique entière qui correspond à l'adresse mémoire à laquelle on va trouver "un élément" du type indiqué.

    Il est vrai qu'un pointeur peut éventuellement prendre l'adresse du premier élément d'un ensemble d'éléments (de type identique) contigus en mémoire, et qu'il peut dés lors être considéré comme l'identifiant qui permet de représenter un tableau.

    Cependant, cette description est beaucoup trop restrictive, et, si tu ne connait qu'elle, tu la laisse devenir l'arbre qui te cache la forêt.

    La meilleure preuve en est la logique que cette approche t'a obligé à mettre en place, qui n'est pas fausse à proprement parler (pour autant que tu aies correctement défini ta classe vecteur, dont j'aimerais avoir un apercu, car je crains fort que ce ne soit pas le cas ) mais qui est loin d'être efficace.

    En effet, si tu considère le pointeur pour ce qu'il est (une donnée numérique entière, généralement non signée, correspondant à l'adresse mémoire à laquelle on devrait trouver un "élément du type indiqué"), la logique peut devenir "aussi simple" que
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    stock = new vecteur[size1+1]; // on alloue directement "une taille suffisante"
                                  // pour pouvoir représenter le nouvel élément en plus des autres
    for(size_t i = 0; i<size1; ++i){
        stock[i]= mrows[i]; // on copie le contenu de mrows dans stock
    }
    std::swap(stock, mrows); // on inverse les valeurs de stock et de mrows,
                                            // ce qui fait que, après, stock pointe sur l'adresse qui était représentée par mrows et inversement
                                            // que stock pointe sur l'adresse qui était représentée pas stock
    /* NOTA: on pourrait aussi le faire manuellement sous une forme proche de
    vecteur * temp = stock;
    stock = mrows;
    mrows = temp;
    */
    delete[]  stock; // on libere la mémoire qui correspond à stock (et qui était celle allouée à mrows avant l'inversion)
    Avoue que cela rend les choses beaucoup plus simples, et beaucoup plus rapides, non

    Enfin, je ne résiste pas au besoin impérieux de faire une dernière remarque:

    Ainsi ici, mrows est un tableau de vecteur (où vecteur est la classe que j'ai créé avant la classe matrice évidemment).
    Comme je te l'ai dit, j'aimerais beaucoup voir cette classe, car, tu prétend qu'elle fonctionne correctement, mais je suis comme Saint Tomas : je ne crois que ce que je vois

    Je ne vois pas l'intérêt d'implémenter constructeur de copie (fait), mouvement (qu'est-ce?), assignation et assignation par mouvement (??).
    Tu confirme par toi-même que ta classe vecteur n'a pas correctement été créée

    James Coplien nous a fait remarqué dans la fin des années 1980 qu'une classe qui a sémantique de valeur (comme c'est sans doute le cas de ta classe vecteur) dispose de quatre fonctionnalités "de base":
    1. la capacité d'être "construite par défaut" (comprends: sans avoir besoin de paramètres lors de la construction) ==> le constructeur par défaut (ne prenant pas de paramètre : vecteur())
    2. la capacité d'être copiée à partir d'une instance existante ==> le constructeur de copie (vecteur(vecteur const &))
    3. la capacité de se voir assigner la valeur d'une instance existante ==> opérateur d'affectation (vecteur & operator = (vecteur const & ))
    4. la capacilté d'être correctement détruite lorsqu'elle cesse d'exister ==> destructeur (~vecteur()

    C'est ce que l'on appelle couramment la "forme canonique orthodoxe de Coplien"

    Si on ne lui donne pas de raison de ne pas le faire, le compilateur va automatiquement créer ces quatre fonctions. Mais le constructeur de copie et l'opérateur d'affectation seront "simplistes", car il va se contenter d'y copier "membre à membre" la valeur de chaque membre de l'instance d'origine.

    Pour une classe (ou une structure, c'est pareil en C++, en dehors de l'accessibilité appliquée par défaut) comme
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    class Point{
    private:
        int x;
        int y;
    };
    cela ne posera aucun problème.

    Mais rappelle toi ce que je t'ai dit au sujet des pointeurs: ce n'est qu'une valeur numérique entière correspondant à l'adresse mémoire à laquelle nous devrions trouver un élément du type indiqué.

    Si nous avons une classe vecteur proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class vecteur{
    public:
        /* laissons le compilateur générer le constructeur par copie et l'opérateur d'affectation
         * par contre, pour que ce soit correct, définissons le destructeur
        ~vecteur(){
            delete[] ptr;
        }
        /* d'autres fonctions (add, remove, et toutes les autres) viennent ici */
    private:
        int size;
        int * ptr{nullptr};
    };
    Le constructeur de copie et l'opérateur d'affectation vont travailler ben... comme ils en ont l'habitude: il vont copier les différents membres "tels quels". Pour size, ce n'est pas un problème, mais, pour ptr, ils vont juste ... copier l'adresse représenté par le membre ptr de l'instance d'origine dans la copie.

    Si bien que, quand tu vas utiliser ta classe dans un code peut être aussi simple que
    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
    int main(){
        vecteur v1;
        /* il est rempli ici */
        if(condition){
            vecteur v2 = v1; //OUCH!!!! v2.ptr représente la même adresse que v1.ptr
            /* on manipule v2 ici
             * note que tous les changement appliqués à v2.ptr seront répercutés sur v1.ptr, sauf 
             * ceux qui ont trais à la gestion de la mémoire
             */
        } /* CRAACK: v2 est détruit ici, 
           * son destructeur est automatiquement appelé
           * il appelle automatiquement delete[] sur v2.ptr... Et libère la mémoire allouée à v1.ptr
           */
        /* on continue à travailler (sur autre chose que sur v1 */
    } /* BOUM: v1 est détruit ici
       * son destructeur est automatiquement appelé
       * il appelle automatiquement delete[] sur v1.ptr mais...
       * cette adresse mémoire a déjà été libérée !!! ==>erreur de segmentation 
       */
    Tout cela parce que le compilateur aura fait ... exactement ce qu'on lui a dit de faire : il a trouvé, dans l'instance d'origine, une valeur numérique, il a utilisé cette valeur numérique dans la copie.

    Le fait que cette valeur numérique ait été un pointeur ... pointant vers une zone mémoire allouée de manière dynamique n'y a rien changé. Il n'avait absolument aucune raison de travailler autrement.

    Tout cela nous a mené à décréter la règle dite "des trois grands":
    Citation Envoyé par big three rule
    Si, pour une raison ou une autre, tu en viens à définir toi-même le comportement de n'importe quel fonctionnalité parmi
    • le constructeur de copie
    • l'opérateur d'affectation
    • le destructeur

    (étant entendu que tu voudras le plus souvent définir un comportement spécifique pour le destructeur) tu dois alors impérativement définir un comportement spécifique pour les trois fonctionnalités.
    Comme tu as définis un comportement particulier pour le destructeur, tu es donc face à l'obligation de définir un comportement particulier pour le constructeur de copie et l'opérateur d'affectation.

    Dans le cadre de notre classe vecteur, nous utiliserons généralement l'idiome copy and swap pour l'affectation, ce qui nous donnerait quelque chose comme
    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
    class vecteur{
    public:
        /* le constructeur par défaut */
        vecteur():size{0}, ptr{nullptr}{
        }
        /* le constructeur de copie */
        vecteur(vecteur const & orig):size{orig.size}{
            if(size!=0){ 
                ptr = new int[size];
                for(int i = 0;i<size;++i)
                    ptr[i]=orig.ptr[i];
            }else{
                ptr = nullptr;
            }
        }
        /* l'opérateur d'affectation */
        vecteur & operator = (vecteur const & orig){
            /* on crée une copie de l'original */
           vecteur cpy{orig};
           /* on inverse les donnée de l'instance courante avec celles de la copie */
          swap(cpy);
        } // la copie est détruite, son destructeur est appelé,
          // la mémoire qui était avant allouée à l'élément courant est maintenant correctement libérée
    private:
        void swap(vecteur & v){
            std::swap(v.size, size);
            std::swap(v.ptr, ptr);
        }
        int size;
        int * ptr;
    };
    De cette manière, chaque instance de vecteur disposera d'un espace mémoire qui lui est propre et suffisant que pour pouvoir représenter le nombre d'éléments qu'il est sensé contenir; et, le cas échéant, les éléments qu'il contiendra pourront être identiques (tout en étant différents, malgré tout) à ceux d'une autre instance de vecteur.

    Tout cela, c'est très bien!!! Mais, il faut savoir que la la copie (et donc l'affectation) sont des processus qui prennent ... énormément de temps dans un cas comme notre classe vecteur Il y a deux raison à cela:
    1. l'allocation dynamique de la mémoire est un processus qui doit faire ce que l'on appelle un "appel système": c'est le système d'exploitation qui va décider si, oui ou non, il décidera de fournir "un peu plus de mémoire" à notre application. Et rien que cela prend déjà beaucoup de temps
    2. Ensuite, une fois que l'on dispose d'un espace mémoire suffisant, il faut encore ... copier l'ensemble des éléments contenu par l'instance d'origine. Et ca, ca prend un temps proportionnel au nombre d'éléments qu'elle contient



    les gens se sont donc rendus compte que, parfois, ces quatre fonctionnalités n'étaient pas suffisantes, parce que cela signifiait systématiquement créer une copie pour pouvoir détruire l'instance d'origine dans certains cas (sans compter que certaines classes dites ayant "sémantique d'entité" ne peuvent pas être copiées), alors que ce que l'on veut parfois, c'est pouvoir créer une copie afin ... d'abandonner l'instance d'origine.

    Depuis 2011 (cela fait quand même déjà huit ans, il serait bon que les profs commencent à en parler ), le langage fournit donc ce que l'on appelle la "sémantique de déplacement".

    Je ne vais pas trop rentrer dans les détails, mais, l'idée, de cette sémantique de déplacement est de "vampiriser" littéralement l'instance d'origine, de manière à pouvoir fournir toutes les ressources (qui ont déjà été allouée à cette instance d'origine) à la copie de l'instance.

    Bien sur, cela implique que l'instance d'origine ne peut plus être utilisée par la suite, mais cela permet de travailler beaucoup plus vite.

    La sémantique de déplacement est arrivée avec deux nouvelles fonctionnalités, auxquelles John Coplien n'avait pas pensé (parce que l'idée même du déplacement, de la "vampirisation" d'une instance n'était pas encore apparue), à savoir:
    • un constructeur de copie par déplacement (vecteur(vecteur &&)) et
    • un opérateur d'affectation par déplacement (vecteur & operator = (vecteur && ))

    Et, bien sur, cela a nécessité de modifier la loi des trois grands pour en faire ... la loi des cinq grand:
    Citation Envoyé par big five rule
    Si, pour une raison ou une autre, tu en viens à définir toi-même le comportement de n'importe quel fonctionnalité parmi
    • le constructeur de copie
    • l'opérateur d'affectation
    • le constructuer de copie par déplacement
    • l'opérateur d'affection par déplacement
    • le destructeur

    tu dois alors impérativement définir un comportement spécifique pour les cinq fonctionnalités.
    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

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

Discussions similaires

  1. fuite de mémoire ?
    Par salseropom dans le forum C
    Réponses: 2
    Dernier message: 12/01/2006, 16h19
  2. Réponses: 1
    Dernier message: 02/12/2005, 14h18
  3. fuite de mémoire
    Par mamag dans le forum MFC
    Réponses: 17
    Dernier message: 19/08/2005, 10h42
  4. Fuite de mémoire en utilisant le template list
    Par schtroumpf_farceur dans le forum Langage
    Réponses: 9
    Dernier message: 18/07/2005, 20h44
  5. Réponses: 8
    Dernier message: 17/10/2002, 12h52

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