Accueil > > > CLASSE CANONIQUE : NI FUITE MÉMOIRE, NI TRAP
CLASSE CANONIQUE : NI FUITE MÉMOIRE, NI TRAP
Information sur la source
Description
Cet article est destiné à attirer l'attention sur une erreur FREQUEMMENT rencontrée ... et sa parade
lorsque l'on crée une classe, il faut qu'elle soit utilisable sans danger par les développeurs
Après avoir montré que le compilateur fait tout ce qu'il peut pout rendre les classes canoniques, on explique pourquoi et comment compléter son travail.
De plus, on fournit un moyen (sous forme de jeu de test) de détecter si notre classe est bien canonique
PARTIE I : une classe trop simple, mais moins qu'on le croit
PARTIE II : une classe est naturellement canonique
PARTIE III : une classe DANGEREUSE (à rendre manuellement canonique)
PARTIE IV : une classe CORRECTE du point de vue de la canonicité
PARTIE V : une autre classe CORRECTE du point de vue de la canonicité : la méthode du paresseux ?
RESUME : quand s'inquiéter et que faire ?
Source
- //le code est un peu long, mais peu de choses sont rajoutées à chaque étape
- //il permet de tester chaque étape après copier/collé global et réglage du #define
-
- #define N 1 // a faire varier de 1 à 5 pour voir les différents cas
- /*
- Les classes canoniques : ni fuite ni trap
- remarque préliminaire : cet article fait suite à "La classe vide est elle vide?"
- http://www.cppfrance.com/article.aspx?Val=1080
-
- pour qu'une classe soit utilisable, elle doit respecter certains principes
- Masquage, encapsulation, identité, forte cohésion, ...
- le masquage est le fait de cacher son implémentation, en particulier tous ses champs seront privés
- l'encapsulation est le fait de permettre un accès contrôlé aux champs privés à travers des méthodes publiques (accesseurs et mutateurs)
- etc...
- En C++, il faut de plus veiller à ce que notre classe ne provoque pas de fuite mémoire ni de trap
- Prenons un exemple
-
- PARTIE I : une classe trop simple, mais moins qu'on le croit
- ------------------------------------------------------------
- la classe Personne portera le nom de la personne et permettra la modification et la lecture du nom
- voici la première version :
- */
- #if N == 1
- #include <iostream>
- using namespace std;
-
- class Personne
- {
- private:
- char pNom_[20];//on notera l'allocation NON dynamique de 20 caractères
- public:
- void setNom(char* pNom)
- {
- cout << "changement du nom de la Personne : " << pNom_ << " en " << pNom << endl;
- strcpy(pNom_, pNom);//Attention au dépassement de buffer !!!!
- }
- const char* getNom()
- {
- cout << "Lecture du nom de la Personne : " << pNom_ << endl;
- return pNom_;
- }
- };
-
- int main()
- {
- Personne p1;//Ctor simple
- p1.setNom("Gwendoline");//
- cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
- return 0;
- }//Appel du Destructeur
-
- /*
- Les experts C++ et ceux qui ont lu l'article précédent sur la classe vide peuvent répondre à la question suivante :
- Combien de méthodes de la classe Personne ont été appelées dans le code précédent
- Réponse ... du débutant : 2 (setNom et getNom)
- Réponse ... du connaisseur : 4 (le Constructeur, setNom, getNom et le Destructeur)
-
- On s'aperçoit ici que le compilateur a généré lui même les Constructeur/Destructeur
- Il n'a d'ailleurs pas fait un boulot formidable lors de la construction de notre objet car la zone de nom n'est pas initialisée.
-
- PARTIE II : une classe est naturellement canonique
- -----------------------------------------------
- la classe Personne, encore simple, portera le nom de la personne permettra la lecture du nom et sa modification.
- Elle permettra de tracer les phases de construction et destruction des objets
- Comme on fournit nous même le constructeur, on en profitera pour initialiser le nom de la personne
- On cherchera à régler le problème de l'écrasement de buffer en tronquant le nom !
- */
- #elif N == 2
- #include <iostream>
- using namespace std;
-
- class Personne
- {
- private:
- char pNom_[20];//on notera l'allocation NON dynamique de 20 caractères
- public:
- Personne(char * pNom="inconnu")//on met 'inconnu' dans le nom ... s'il n'est pas fourni
- {
- cout << "Construction de la Personne : " << pNom << endl;
- strncpy(pNom_, pNom, 20-1);//tronqué à 19 caractères utiles + 1 pour '\0'
- pNom_[20-1]=0;
- }
- ~Personne(){cout << "Destruction de la Personne " << pNom_ << endl;};
- void setNom(char* pNom)
- {
- cout << "Changement du nom de la Personne : " << pNom_ << " en " << pNom << endl;
- strncpy(pNom_, pNom, 20-1);//tronqué à 19 caractères
- pNom_[20-1]=0;
- }
- const char* getNom()
- {
- cout << "Lecture du nom de la Personne : " << pNom_ << endl;
- return pNom_;
- }
- };
-
- int main()
- {
- Personne p1("Saluche MoaJaiUnNomTropLong");//Ctor simple avec un nom trop long
- cout << p1.getNom() << endl;//affiche 'Saluche MoaJaiUnNom' (tronqué)
- p1.setNom("Gwendoline");//
- cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
-
- //création d'une personne p2
- Personne p2;
- cout << "nom de p2 = " << p2.getNom() << endl;// affiche 'inconnu'
- //utilisation de l'opérateur d'affectation
- p2 = p1;
- cout << "nom de p2 = " << p2.getNom() << endl;//affiche 'Gwendoline'
-
- //Céation d'une personne p3 à partir d'une personne p1
- //Utilisation du Constructeur de copie
- Personne p3 = p1;
- cout << "nom de p3 = " << p3.getNom() << endl;//affiche 'Gwendoline'
-
- return 0;
- }
- /* Avez vous essayé de suivre l'exécution pas à pas du code dans le débogueur ?
- On remarquera que l'on ne peut pas tracer la construction de p3 !
- et pourtant elle s'effectue bien... et l'objet est même détruit correctement !
- EXPLICATION : le compilateur génère pour nous un constructeur de Copie dont le prototype est
- Personne(const Personne& p)
- On ne peut pas non plus tracer l'affectation lors de 'p2=p1'
- EXPLICATION : le compilateur génère de lui-même un opérateur d'affectation dont le prototype est
- Personne& operator=(const Personne& p)
-
- On peut donner maintenant la définition d'une classe canonique
- C'est une classe qui possède les 4 méthodes vues précédemment... et qui font bien leur travail !!!
- Contructeur / Constructeur de copie / opérateur d'affectation / Destructeur
-
- Nous voyons qu'une classe est naturellement canonique car le compilateur génère lui-même les méthodes qui manquent
- Qu'est ce donc qu'une classe NON canonique, comment peut on en arriver là ?
-
- PARTIE III : une classe DANGEREUSE (à rendre manuellement canonique)
- --------------------------------------------------------------------
- Le problème avec la classe Personne est la limite à 20 (-1) caractères du nom.
- Bien sûr, on peut toujours décider de modifier le 20 en 50 ,
- mais alors quel gâchis pour la pluspart des personnes qui ont un nom compris entre 5 et 10 caractères
-
- La solution est de rendre cette zone dynamique.
- On représentera donc le nom par un simple pointeur, qui pointera vers une zone de taille strictement adaptée au nom de chacun
- Notons que l'on peut mettre un pointeur null dans le cas ou le nom est inconnu
-
- Il s'agit donc de remplacer la déclaration
- char pNom_[2];
- par la déclaration suivante :
- char *pNom;
-
- Est ce bien tout ce qu'il faut faire ?
- Bien sûr que non, il faut maintenant allouer et libérer correctement la mémoire.
- ATTENTION : les allocations/libérations doivent être correctes pendant toutes la durée de vie de l'objet
- ET NON PAS SEULEMENT LORS DE LA CONSTRUCTION ET DE SA DESTRUCTION
- On s'attachera particulièrement à suivre l'exécution de la bande des 4 qui forment une classe canonique
-
- */
- #elif N == 3
- #include <iostream>
- using namespace std;
-
- class Personne
- {
- private:
- char * pNom_;//on notera l'allocation dynamique (à faire lors des constructions d'objets)
- public:
- Personne(char * pNom=0)//on met NULL dans le nom ... s'il n'est pas fourni
- {
- cout << "Construction de la Personne : " ;
- if (pNom)
- {
- cout << pNom << endl;
- pNom_ = new char [strlen(pNom)+1];
- strcpy(pNom_, pNom);//non tronqué ... ajusté
- }
- else
- {
- pNom_ = 0;
- cout <<" <sans nom>"<<endl;
- }
- }
-
- ~Personne()
- {
- cout << "Destruction de la Personne ";
- if (pNom_)
- {
- cout << pNom_ << endl;
- delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
- pNom_ = 0;
- }
- else cout <<" <sans nom>"<<endl;
- };
-
- void setNom(char* pNom)
- {
- cout << "Changement du nom de la Personne : ";
- if (pNom_)
- {
- cout << pNom_ ;
- delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
- }
- else cout <<" <sans nom>";
-
- if (pNom)
- {
- cout << " en " << pNom << endl;
- pNom_ = new char [strlen(pNom)+1];
- strcpy(pNom_, pNom);//non tronqué ... ajusté
- }
- else cout <<" en <sans nom>" << endl;
- }
-
- const char* getNom()
- {
- cout << "Lecture du nom de la Personne : ";
- if (pNom_)
- {
- cout << pNom_ << endl;
- return pNom_;
- }
- else
- {
- cout <<" <sans nom>" << endl;
- return "<sans nom>";
- }
- }
- };
-
- int main()
- {
- Personne p1("Saluche MoaJaiUnNomQuiestOK");//Ctor simple avec un nom très long
- cout << p1.getNom() << endl;//affiche 'Saluche MoaJaiUnNomQuiestOK'
- p1.setNom("Gwendoline");//
- cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
-
- //création d'une personne p2
- Personne p2;
- cout << "nom de p2 = " << p2.getNom() << endl;// affiche 'inconnu'
- //utilisation de l'opérateur d'affectation
- p2 = p1;//A commenter pour éviter le TRAP
- cout << "nom de p2 = " << p2.getNom() << endl;//affiche 'Gwendoline'
-
- //Céation d'une personne p3 à partir d'une personne p1
- //Utilisation du Constructeur de copie
- Personne p3 = p1;//A commenter pour éviter le TRAP
- cout << "nom de p3 = " << p3.getNom() << endl;//affiche 'Gwendoline'//A commenter pour éviter le TRAP
-
- return 0;
- }
- /*
- Exécutez cette version et notez le TRAP provoqué lors de la destruction des objets
- Remarque : Ce trap est apparent avec VisualC++ mais pas avec BorlandC++
- Il est de toutes façons nécessaire de revoir notre code pour rendre la classe canonique
-
- Que se passe t il ?
- Il s'agit tout simplement d'une double libération mémoire
- EXPLICATION :
- prenons le code simple suivant:
- { Personne p1("Bob"), p2;
- p2 = p1; }
- Lorsque p1 est créé, le compilateur alloue uniquement la mémoire correspondant au pointeur
- c'est notre code qui alloue les 4 octets nécessaires pour recevoir 'B' 'o' 'b' '\0'
- Idem pour p2, sauf que 0 octets sont alloués pour le nom (qui n'est pas renseigné)
- Lors de l'exécution de p2=p1, c'est le code généré par le compilateur (operator=) qui fait le boulot
- Il appelle l'opérateur = pour chacun des membres de notre classe (le pointeur donc)
- Comment fait il ? il copie simplement la VALEUR du pointeur pNom_ de la personne p1 sur le pointeur pNom_ de la personne p2
- ... et ce boulot est mal fait car maintenant p1 et p2 référencent la même zone mémoire
- Lors de la destruction de p2, la zone mémoire est détruite et lorsque p1 doit être détruit, on cherche à détruire une zone mémoire qui n'est plus allouée à notre process ... d'où le TRAP !
- Remarquons que des choses bizarres peuvent arriver lors du changement de nom de p1, etc...
- Notre classe n'est donc plus canonique et nous devons ABSOLUMENT définir notre opérateur =
- L'explication est à peu près identique pour le constructeur de copie
-
- Il est intéressant de noter que la suppression des lignes utilisant l'opérateur = et le Ctor de copie
- permet d'éviter le TRAP... c'est à dire de ne pas s'en apercevoir au moment où l'on livre cette classe dans une bibliothèque de production
- Attention donc à bien RAJOUTER ces appels à notre jeu de test AVANT la livraison pour éviter les surprises
-
- On pourrait se dire que la construction par la technique Personne p2 = p1; n'est pas fréquente
- Et qu'il n'est pas très utile de surcharger ce Ctor de copie
- Mais il est aussi utilisé dès que l'on fait des passages de paramètres par valeur (en argument ou en retour de fonction)
-
- On peut aussi supprimer les lignes de delete[] pour éviter les TRAPs mais alors on fait des fuites mémoire
- C'est parfois une option de livraison rapide d'une version béta... qui doit ensuite être corrigée pour la final release.
-
- Voici la classe corrigée
-
- PARTIE IV : une classe CORRECTE du point de vue de la canonicité
- ------------------------------------------------------------------
- */
- #elif N == 4
- #include <iostream>
- using namespace std;
-
- class Personne
- {
- private:
- char * pNom_;//on notera l'allocation dynamique (à faire lors des constructions d'objets)
- public:
- Personne(char * pNom=0)//on met NULL dans le nom ... s'il n'est pas fourni
- {
- cout << "Construction de la Personne : " ;
- if (pNom)
- {
- cout << pNom << endl;
- pNom_ = new char [strlen(pNom)+1];
- strcpy(pNom_, pNom);//non tronqué ... ajusté
- }
- else
- {
- pNom_ = 0;
- cout <<" <sans nom>"<<endl;
- }
- }
-
- //Constructeur de copie
- Personne(const Personne& p )
- {
- cout << "Construction de la Personne : " ;
- if (p.pNom_)
- {
- cout << p.pNom_ << endl;
- pNom_ = new char [strlen(p.pNom_)+1];
- strcpy(pNom_, p.pNom_);//non tronqué ... ajusté
- }
- else
- {
- pNom_ = 0;
- cout <<" <sans nom>"<<endl;
- }
- }
-
-
- ~Personne()
- {
- cout << "Destruction de la Personne ";
- if (pNom_)
- {
- cout << pNom_ << endl;
- delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
- }
- else cout <<" <sans nom>"<<endl;
- };
-
- //opérateur d'affectation
- Personne& operator=(const Personne& p )
- {
- cout << "operateur d'affectation de la Personne : " ;
- //AUTO-AFFECTATION : si les deux objets sont les mêmes ... ne rien faire
- if (this == &p) return (*this);
-
- //destruction de l'ancienne zone de nom (si elle existe)
- if (pNom_)
- {
- cout << pNom_;
- delete[] pNom_;
- pNom_ = 0;
- }
- else
- {
- cout <<" <sans nom>";
- }
- cout << " a partir de " ;
- if (p.pNom_)
- {
- cout << p.pNom_ << endl;
- pNom_ = new char [strlen(p.pNom_)+1];
- strcpy(pNom_, p.pNom_);//non tronqué ... ajusté
- }
- else
- {
- pNom_ = 0;
- cout <<" <sans nom>"<<endl;
- }
- return *this;
- }
-
- void setNom(char* pNom)
- {
- cout << "Changement du nom de la Personne : ";
- if (pNom_)
- {
- cout << pNom_ ;
- delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
- pNom_ = 0;
- }
- else cout <<" <sans nom>";
-
- if (pNom)
- {
- cout << " en " << pNom << endl;
- pNom_ = new char [strlen(pNom)+1];
- strcpy(pNom_, pNom);//non tronqué ... ajusté
- }
- else cout <<" en <sans nom>" << endl;
- }
-
- const char* getNom()
- {
- cout << "Lecture du nom de la Personne : ";
- if (pNom_)
- {
- cout << pNom_ << endl;
- return pNom_;
- }
- else
- {
- cout <<" <sans nom>" << endl;
- return "<sans nom>";
- }
- }
- };
-
- int main()
- {
- Personne p1("Saluche MoaJaiUnNomQuiestOKMaintenant");//Ctor simple avec un nom très long
- cout << p1.getNom() << endl;//affiche 'Saluche MoaJaiUnNomQuiestOKMaintenant'
- p1.setNom("Gwendoline");//
- cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
-
- //création d'une personne p2
- Personne p2;
- cout << "nom de p2 = " << p2.getNom() << endl;// affiche 'inconnu'
- //utilisation de l'opérateur d'affectation
- p2 = p1;
- cout << "nom de p2 = " << p2.getNom() << endl;//affiche 'Gwendoline'
-
- //Céation d'une personne p3 à partir d'une personne p1
- //Utilisation du Constructeur de copie
- Personne p3 = p1;
- cout << "nom de p3 = " << p3.getNom() << endl;//affiche 'Gwendoline'
-
- p1.setNom(0);
- cout << "nom de p1 = " << p1.getNom() << endl;//affiche '<sans nom>'
- return 0;
- }
- /*
- Autre technique utilisable pour règler le problème du TRAP : interdire les copies et affectation
- C'est la parade la plus simple, car il suffit de déclarer (sans implémenter) l'opérateur et le Constructeur ... en private
- Alors, les développeurs utilisateurs de notre classe ne pourront plus
- Voici la classe corrigée ... avec ce contrôle plus directif !
-
- PARTIE V : une autre classe CORRECTE du point de vue de la canonicité
- la méthode du paresseux ?
- ----------------------------------------------------------------------
- */
- #elif N == 5
- #include <iostream>
- using namespace std;
-
- class Personne
- {
- private:
- char * pNom_;
- //Constructeur de copie privé
- Personne(const Personne& p );
- //opérateur d'affectation privé
- Personne& operator=(const Personne& p );
-
- public:
- Personne(char * pNom=0)
- {
- cout << "Construction de la Personne : " ;
- if (pNom)
- {
- cout << pNom << endl;
- pNom_ = new char [strlen(pNom)+1];
- strcpy(pNom_, pNom);
- }
- else
- {
- pNom_ = 0;
- cout <<" <sans nom>"<<endl;
- }
- }
-
-
- ~Personne()
- {
- cout << "Destruction de la Personne ";
- if (pNom_)
- {
- cout << pNom_ << endl;
- delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
- }
- else cout <<" <sans nom>"<<endl;
- };
-
- void setNom(char* pNom)
- {
- cout << "Changement du nom de la Personne : ";
- if (pNom_)
- {
- cout << pNom_ ;
- delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
- pNom_ = 0;
- }
- else cout <<" <sans nom>";
-
- if (pNom)
- {
- cout << " en " << pNom << endl;
- pNom_ = new char [strlen(pNom)+1];
- strcpy(pNom_, pNom);//non tronqué ... ajusté
- }
- else cout <<" en <sans nom>" << endl;
- }
-
- const char* getNom()
- {
- cout << "Lecture du nom de la Personne : ";
- if (pNom_)
- {
- cout << pNom_ << endl;
- return pNom_;
- }
- else
- {
- cout <<" <sans nom>" << endl;
- return "<sans nom>";
- }
- }
- };
-
- int main()
- {
- Personne p1("Saluche MoaJaiUnNomQuiestOKMaintenant");//Ctor simple avec un nom très long
- cout << p1.getNom() << endl;//affiche 'Saluche MoaJaiUnNomQuiestOKMaintenant'
- p1.setNom("Gwendoline");//
- cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
-
- //création d'une personne p2
- Personne p2;
- cout << "nom de p2 = " << p2.getNom() << endl;// affiche 'inconnu'
- //utilisation de l'opérateur d'affectation => IMPOSSIBLE
- p2 = p1;//à commenter pour retirer l'erreur de compil
-
- //Céation d'une personne p3 à partir d'une personne p1
- //Utilisation du Constructeur de copie => IMPOSSIBLE
- Personne p3 = p1;//à commenter pour retirer l'erreur de compil
-
- return 0;
- }
- #endif
-
- /*
- RESUME : quand s'inquiter et que faire ?
- -----------------------------------------
- Il faut se préoccuper de ce problème dès que l'on emploie des pointeurs dans notre classe
- Alors on ajoutera à notre jeu de test (qui appelle toutes les méthodes de la classe)
- les appels aux méthodes invisibles générées par le compilateur
- En particulier :
- le Constructeur par défaut (s'il existe)
- le Destructeur
- le Constructeur de copie
- l'Opérateur d'affectation
-
- Lorsque les emplacements pointés sont gérés par nous mêmes (et non pas directement par le compilateur),
- il faut se poser la question de savoir s'ils ont des contenus partageables ou non
- Dans notre exemple, le nom de chaque personne lui appartient, et il peut en changer sans que le nom
- d'une autre personne en soit affecté. On rendra donc canonique notre classe par l'implémentation
- correcte de l'opérateur d'affectation et du constructeur de copie
- S'il y a partage possible, la solution consiste à développer un système de comptage de références
- dont le but est d'éviter la destruction multiple
- ... mais ceci dépasse largement le cadre de cet article.
- */
//le code est un peu long, mais peu de choses sont rajoutées à chaque étape
//il permet de tester chaque étape après copier/collé global et réglage du #define
#define N 1 // a faire varier de 1 à 5 pour voir les différents cas
/*
Les classes canoniques : ni fuite ni trap
remarque préliminaire : cet article fait suite à "La classe vide est elle vide?"
http://www.cppfrance.com/article.aspx?Val=1080
pour qu'une classe soit utilisable, elle doit respecter certains principes
Masquage, encapsulation, identité, forte cohésion, ...
le masquage est le fait de cacher son implémentation, en particulier tous ses champs seront privés
l'encapsulation est le fait de permettre un accès contrôlé aux champs privés à travers des méthodes publiques (accesseurs et mutateurs)
etc...
En C++, il faut de plus veiller à ce que notre classe ne provoque pas de fuite mémoire ni de trap
Prenons un exemple
PARTIE I : une classe trop simple, mais moins qu'on le croit
------------------------------------------------------------
la classe Personne portera le nom de la personne et permettra la modification et la lecture du nom
voici la première version :
*/
#if N == 1
#include <iostream>
using namespace std;
class Personne
{
private:
char pNom_[20];//on notera l'allocation NON dynamique de 20 caractères
public:
void setNom(char* pNom)
{
cout << "changement du nom de la Personne : " << pNom_ << " en " << pNom << endl;
strcpy(pNom_, pNom);//Attention au dépassement de buffer !!!!
}
const char* getNom()
{
cout << "Lecture du nom de la Personne : " << pNom_ << endl;
return pNom_;
}
};
int main()
{
Personne p1;//Ctor simple
p1.setNom("Gwendoline");//
cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
return 0;
}//Appel du Destructeur
/*
Les experts C++ et ceux qui ont lu l'article précédent sur la classe vide peuvent répondre à la question suivante :
Combien de méthodes de la classe Personne ont été appelées dans le code précédent
Réponse ... du débutant : 2 (setNom et getNom)
Réponse ... du connaisseur : 4 (le Constructeur, setNom, getNom et le Destructeur)
On s'aperçoit ici que le compilateur a généré lui même les Constructeur/Destructeur
Il n'a d'ailleurs pas fait un boulot formidable lors de la construction de notre objet car la zone de nom n'est pas initialisée.
PARTIE II : une classe est naturellement canonique
-----------------------------------------------
la classe Personne, encore simple, portera le nom de la personne permettra la lecture du nom et sa modification.
Elle permettra de tracer les phases de construction et destruction des objets
Comme on fournit nous même le constructeur, on en profitera pour initialiser le nom de la personne
On cherchera à régler le problème de l'écrasement de buffer en tronquant le nom !
*/
#elif N == 2
#include <iostream>
using namespace std;
class Personne
{
private:
char pNom_[20];//on notera l'allocation NON dynamique de 20 caractères
public:
Personne(char * pNom="inconnu")//on met 'inconnu' dans le nom ... s'il n'est pas fourni
{
cout << "Construction de la Personne : " << pNom << endl;
strncpy(pNom_, pNom, 20-1);//tronqué à 19 caractères utiles + 1 pour '\0'
pNom_[20-1]=0;
}
~Personne(){cout << "Destruction de la Personne " << pNom_ << endl;};
void setNom(char* pNom)
{
cout << "Changement du nom de la Personne : " << pNom_ << " en " << pNom << endl;
strncpy(pNom_, pNom, 20-1);//tronqué à 19 caractères
pNom_[20-1]=0;
}
const char* getNom()
{
cout << "Lecture du nom de la Personne : " << pNom_ << endl;
return pNom_;
}
};
int main()
{
Personne p1("Saluche MoaJaiUnNomTropLong");//Ctor simple avec un nom trop long
cout << p1.getNom() << endl;//affiche 'Saluche MoaJaiUnNom' (tronqué)
p1.setNom("Gwendoline");//
cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
//création d'une personne p2
Personne p2;
cout << "nom de p2 = " << p2.getNom() << endl;// affiche 'inconnu'
//utilisation de l'opérateur d'affectation
p2 = p1;
cout << "nom de p2 = " << p2.getNom() << endl;//affiche 'Gwendoline'
//Céation d'une personne p3 à partir d'une personne p1
//Utilisation du Constructeur de copie
Personne p3 = p1;
cout << "nom de p3 = " << p3.getNom() << endl;//affiche 'Gwendoline'
return 0;
}
/* Avez vous essayé de suivre l'exécution pas à pas du code dans le débogueur ?
On remarquera que l'on ne peut pas tracer la construction de p3 !
et pourtant elle s'effectue bien... et l'objet est même détruit correctement !
EXPLICATION : le compilateur génère pour nous un constructeur de Copie dont le prototype est
Personne(const Personne& p)
On ne peut pas non plus tracer l'affectation lors de 'p2=p1'
EXPLICATION : le compilateur génère de lui-même un opérateur d'affectation dont le prototype est
Personne& operator=(const Personne& p)
On peut donner maintenant la définition d'une classe canonique
C'est une classe qui possède les 4 méthodes vues précédemment... et qui font bien leur travail !!!
Contructeur / Constructeur de copie / opérateur d'affectation / Destructeur
Nous voyons qu'une classe est naturellement canonique car le compilateur génère lui-même les méthodes qui manquent
Qu'est ce donc qu'une classe NON canonique, comment peut on en arriver là ?
PARTIE III : une classe DANGEREUSE (à rendre manuellement canonique)
--------------------------------------------------------------------
Le problème avec la classe Personne est la limite à 20 (-1) caractères du nom.
Bien sûr, on peut toujours décider de modifier le 20 en 50 ,
mais alors quel gâchis pour la pluspart des personnes qui ont un nom compris entre 5 et 10 caractères
La solution est de rendre cette zone dynamique.
On représentera donc le nom par un simple pointeur, qui pointera vers une zone de taille strictement adaptée au nom de chacun
Notons que l'on peut mettre un pointeur null dans le cas ou le nom est inconnu
Il s'agit donc de remplacer la déclaration
char pNom_[2];
par la déclaration suivante :
char *pNom;
Est ce bien tout ce qu'il faut faire ?
Bien sûr que non, il faut maintenant allouer et libérer correctement la mémoire.
ATTENTION : les allocations/libérations doivent être correctes pendant toutes la durée de vie de l'objet
ET NON PAS SEULEMENT LORS DE LA CONSTRUCTION ET DE SA DESTRUCTION
On s'attachera particulièrement à suivre l'exécution de la bande des 4 qui forment une classe canonique
*/
#elif N == 3
#include <iostream>
using namespace std;
class Personne
{
private:
char * pNom_;//on notera l'allocation dynamique (à faire lors des constructions d'objets)
public:
Personne(char * pNom=0)//on met NULL dans le nom ... s'il n'est pas fourni
{
cout << "Construction de la Personne : " ;
if (pNom)
{
cout << pNom << endl;
pNom_ = new char [strlen(pNom)+1];
strcpy(pNom_, pNom);//non tronqué ... ajusté
}
else
{
pNom_ = 0;
cout <<" <sans nom>"<<endl;
}
}
~Personne()
{
cout << "Destruction de la Personne ";
if (pNom_)
{
cout << pNom_ << endl;
delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
pNom_ = 0;
}
else cout <<" <sans nom>"<<endl;
};
void setNom(char* pNom)
{
cout << "Changement du nom de la Personne : ";
if (pNom_)
{
cout << pNom_ ;
delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
}
else cout <<" <sans nom>";
if (pNom)
{
cout << " en " << pNom << endl;
pNom_ = new char [strlen(pNom)+1];
strcpy(pNom_, pNom);//non tronqué ... ajusté
}
else cout <<" en <sans nom>" << endl;
}
const char* getNom()
{
cout << "Lecture du nom de la Personne : ";
if (pNom_)
{
cout << pNom_ << endl;
return pNom_;
}
else
{
cout <<" <sans nom>" << endl;
return "<sans nom>";
}
}
};
int main()
{
Personne p1("Saluche MoaJaiUnNomQuiestOK");//Ctor simple avec un nom très long
cout << p1.getNom() << endl;//affiche 'Saluche MoaJaiUnNomQuiestOK'
p1.setNom("Gwendoline");//
cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
//création d'une personne p2
Personne p2;
cout << "nom de p2 = " << p2.getNom() << endl;// affiche 'inconnu'
//utilisation de l'opérateur d'affectation
p2 = p1;//A commenter pour éviter le TRAP
cout << "nom de p2 = " << p2.getNom() << endl;//affiche 'Gwendoline'
//Céation d'une personne p3 à partir d'une personne p1
//Utilisation du Constructeur de copie
Personne p3 = p1;//A commenter pour éviter le TRAP
cout << "nom de p3 = " << p3.getNom() << endl;//affiche 'Gwendoline'//A commenter pour éviter le TRAP
return 0;
}
/*
Exécutez cette version et notez le TRAP provoqué lors de la destruction des objets
Remarque : Ce trap est apparent avec VisualC++ mais pas avec BorlandC++
Il est de toutes façons nécessaire de revoir notre code pour rendre la classe canonique
Que se passe t il ?
Il s'agit tout simplement d'une double libération mémoire
EXPLICATION :
prenons le code simple suivant:
{ Personne p1("Bob"), p2;
p2 = p1; }
Lorsque p1 est créé, le compilateur alloue uniquement la mémoire correspondant au pointeur
c'est notre code qui alloue les 4 octets nécessaires pour recevoir 'B' 'o' 'b' '\0'
Idem pour p2, sauf que 0 octets sont alloués pour le nom (qui n'est pas renseigné)
Lors de l'exécution de p2=p1, c'est le code généré par le compilateur (operator=) qui fait le boulot
Il appelle l'opérateur = pour chacun des membres de notre classe (le pointeur donc)
Comment fait il ? il copie simplement la VALEUR du pointeur pNom_ de la personne p1 sur le pointeur pNom_ de la personne p2
... et ce boulot est mal fait car maintenant p1 et p2 référencent la même zone mémoire
Lors de la destruction de p2, la zone mémoire est détruite et lorsque p1 doit être détruit, on cherche à détruire une zone mémoire qui n'est plus allouée à notre process ... d'où le TRAP !
Remarquons que des choses bizarres peuvent arriver lors du changement de nom de p1, etc...
Notre classe n'est donc plus canonique et nous devons ABSOLUMENT définir notre opérateur =
L'explication est à peu près identique pour le constructeur de copie
Il est intéressant de noter que la suppression des lignes utilisant l'opérateur = et le Ctor de copie
permet d'éviter le TRAP... c'est à dire de ne pas s'en apercevoir au moment où l'on livre cette classe dans une bibliothèque de production
Attention donc à bien RAJOUTER ces appels à notre jeu de test AVANT la livraison pour éviter les surprises
On pourrait se dire que la construction par la technique Personne p2 = p1; n'est pas fréquente
Et qu'il n'est pas très utile de surcharger ce Ctor de copie
Mais il est aussi utilisé dès que l'on fait des passages de paramètres par valeur (en argument ou en retour de fonction)
On peut aussi supprimer les lignes de delete[] pour éviter les TRAPs mais alors on fait des fuites mémoire
C'est parfois une option de livraison rapide d'une version béta... qui doit ensuite être corrigée pour la final release.
Voici la classe corrigée
PARTIE IV : une classe CORRECTE du point de vue de la canonicité
------------------------------------------------------------------
*/
#elif N == 4
#include <iostream>
using namespace std;
class Personne
{
private:
char * pNom_;//on notera l'allocation dynamique (à faire lors des constructions d'objets)
public:
Personne(char * pNom=0)//on met NULL dans le nom ... s'il n'est pas fourni
{
cout << "Construction de la Personne : " ;
if (pNom)
{
cout << pNom << endl;
pNom_ = new char [strlen(pNom)+1];
strcpy(pNom_, pNom);//non tronqué ... ajusté
}
else
{
pNom_ = 0;
cout <<" <sans nom>"<<endl;
}
}
//Constructeur de copie
Personne(const Personne& p )
{
cout << "Construction de la Personne : " ;
if (p.pNom_)
{
cout << p.pNom_ << endl;
pNom_ = new char [strlen(p.pNom_)+1];
strcpy(pNom_, p.pNom_);//non tronqué ... ajusté
}
else
{
pNom_ = 0;
cout <<" <sans nom>"<<endl;
}
}
~Personne()
{
cout << "Destruction de la Personne ";
if (pNom_)
{
cout << pNom_ << endl;
delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
}
else cout <<" <sans nom>"<<endl;
};
//opérateur d'affectation
Personne& operator=(const Personne& p )
{
cout << "operateur d'affectation de la Personne : " ;
//AUTO-AFFECTATION : si les deux objets sont les mêmes ... ne rien faire
if (this == &p) return (*this);
//destruction de l'ancienne zone de nom (si elle existe)
if (pNom_)
{
cout << pNom_;
delete[] pNom_;
pNom_ = 0;
}
else
{
cout <<" <sans nom>";
}
cout << " a partir de " ;
if (p.pNom_)
{
cout << p.pNom_ << endl;
pNom_ = new char [strlen(p.pNom_)+1];
strcpy(pNom_, p.pNom_);//non tronqué ... ajusté
}
else
{
pNom_ = 0;
cout <<" <sans nom>"<<endl;
}
return *this;
}
void setNom(char* pNom)
{
cout << "Changement du nom de la Personne : ";
if (pNom_)
{
cout << pNom_ ;
delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
pNom_ = 0;
}
else cout <<" <sans nom>";
if (pNom)
{
cout << " en " << pNom << endl;
pNom_ = new char [strlen(pNom)+1];
strcpy(pNom_, pNom);//non tronqué ... ajusté
}
else cout <<" en <sans nom>" << endl;
}
const char* getNom()
{
cout << "Lecture du nom de la Personne : ";
if (pNom_)
{
cout << pNom_ << endl;
return pNom_;
}
else
{
cout <<" <sans nom>" << endl;
return "<sans nom>";
}
}
};
int main()
{
Personne p1("Saluche MoaJaiUnNomQuiestOKMaintenant");//Ctor simple avec un nom très long
cout << p1.getNom() << endl;//affiche 'Saluche MoaJaiUnNomQuiestOKMaintenant'
p1.setNom("Gwendoline");//
cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
//création d'une personne p2
Personne p2;
cout << "nom de p2 = " << p2.getNom() << endl;// affiche 'inconnu'
//utilisation de l'opérateur d'affectation
p2 = p1;
cout << "nom de p2 = " << p2.getNom() << endl;//affiche 'Gwendoline'
//Céation d'une personne p3 à partir d'une personne p1
//Utilisation du Constructeur de copie
Personne p3 = p1;
cout << "nom de p3 = " << p3.getNom() << endl;//affiche 'Gwendoline'
p1.setNom(0);
cout << "nom de p1 = " << p1.getNom() << endl;//affiche '<sans nom>'
return 0;
}
/*
Autre technique utilisable pour règler le problème du TRAP : interdire les copies et affectation
C'est la parade la plus simple, car il suffit de déclarer (sans implémenter) l'opérateur et le Constructeur ... en private
Alors, les développeurs utilisateurs de notre classe ne pourront plus
Voici la classe corrigée ... avec ce contrôle plus directif !
PARTIE V : une autre classe CORRECTE du point de vue de la canonicité
la méthode du paresseux ?
----------------------------------------------------------------------
*/
#elif N == 5
#include <iostream>
using namespace std;
class Personne
{
private:
char * pNom_;
//Constructeur de copie privé
Personne(const Personne& p );
//opérateur d'affectation privé
Personne& operator=(const Personne& p );
public:
Personne(char * pNom=0)
{
cout << "Construction de la Personne : " ;
if (pNom)
{
cout << pNom << endl;
pNom_ = new char [strlen(pNom)+1];
strcpy(pNom_, pNom);
}
else
{
pNom_ = 0;
cout <<" <sans nom>"<<endl;
}
}
~Personne()
{
cout << "Destruction de la Personne ";
if (pNom_)
{
cout << pNom_ << endl;
delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
}
else cout <<" <sans nom>"<<endl;
};
void setNom(char* pNom)
{
cout << "Changement du nom de la Personne : ";
if (pNom_)
{
cout << pNom_ ;
delete[] pNom_;//le delete[] est nécessité par l'utilisation du new[]
pNom_ = 0;
}
else cout <<" <sans nom>";
if (pNom)
{
cout << " en " << pNom << endl;
pNom_ = new char [strlen(pNom)+1];
strcpy(pNom_, pNom);//non tronqué ... ajusté
}
else cout <<" en <sans nom>" << endl;
}
const char* getNom()
{
cout << "Lecture du nom de la Personne : ";
if (pNom_)
{
cout << pNom_ << endl;
return pNom_;
}
else
{
cout <<" <sans nom>" << endl;
return "<sans nom>";
}
}
};
int main()
{
Personne p1("Saluche MoaJaiUnNomQuiestOKMaintenant");//Ctor simple avec un nom très long
cout << p1.getNom() << endl;//affiche 'Saluche MoaJaiUnNomQuiestOKMaintenant'
p1.setNom("Gwendoline");//
cout << "nom de p1 = " << p1.getNom() << endl;//affiche 'Gwendoline'
//création d'une personne p2
Personne p2;
cout << "nom de p2 = " << p2.getNom() << endl;// affiche 'inconnu'
//utilisation de l'opérateur d'affectation => IMPOSSIBLE
p2 = p1;//à commenter pour retirer l'erreur de compil
//Céation d'une personne p3 à partir d'une personne p1
//Utilisation du Constructeur de copie => IMPOSSIBLE
Personne p3 = p1;//à commenter pour retirer l'erreur de compil
return 0;
}
#endif
/*
RESUME : quand s'inquiter et que faire ?
-----------------------------------------
Il faut se préoccuper de ce problème dès que l'on emploie des pointeurs dans notre classe
Alors on ajoutera à notre jeu de test (qui appelle toutes les méthodes de la classe)
les appels aux méthodes invisibles générées par le compilateur
En particulier :
le Constructeur par défaut (s'il existe)
le Destructeur
le Constructeur de copie
l'Opérateur d'affectation
Lorsque les emplacements pointés sont gérés par nous mêmes (et non pas directement par le compilateur),
il faut se poser la question de savoir s'ils ont des contenus partageables ou non
Dans notre exemple, le nom de chaque personne lui appartient, et il peut en changer sans que le nom
d'une autre personne en soit affecté. On rendra donc canonique notre classe par l'implémentation
correcte de l'opérateur d'affectation et du constructeur de copie
S'il y a partage possible, la solution consiste à développer un système de comptage de références
dont le but est d'éviter la destruction multiple
... mais ceci dépasse largement le cadre de cet article.
*/
Sources du même auteur
Sources de la même categorie
Commentaires et avis
|
Derniers Blogs
IMAGINE CUP 2012, MAKE A SIGN EN FINALEIMAGINE CUP 2012, MAKE A SIGN EN FINALE par junarnoalg
Voilà qui est fait, la nouvelle est officielle ! L'équipe belge "Make a Sign" va au pays des kangourous défendre son projet dans la catégorie Software Design. http://www.imaginecup.com/CompetitionsContent/Competition/WorldwideFinalists.aspx V...
Cliquez pour lire la suite de l'article par junarnoalg KINECT 1.5 IS OUT !KINECT 1.5 IS OUT ! par Vko
La version 1.5 du Kinect For Microsoft vient tout juste de sortir ! Plein de nouveautés: Tracking de squelette en Near Mode Détection en position assise Détection faciale avec un SDK dédié Documentation et des guideline (enfin) Un out...
Cliquez pour lire la suite de l'article par Vko LES ACTUALITéS DE LA SEMAINE SUR C2I.FR (14 MAI - 20 MAI) LES ACTUALITéS DE LA SEMAINE SUR C2I.FR (14 MAI - 20 MAI) par richardc
Mise à jour des Web API du 14 Mai
Réservez dès maintenant votre journée du 20 juin pour le Windows Azure Dev Camp 2012 à Paris
Mise à jour de Team Foundation Service
MechCommander 2 sur Windows 8
Entity Framework 5 Release Candidate e...
Cliquez pour lire la suite de l'article par richardc REACTIVE EXTENSIONS : CONSOMMER DES SERVICES AVEC RX PARTIE 3, LES PIèGES à éVITERREACTIVE EXTENSIONS : CONSOMMER DES SERVICES AVEC RX PARTIE 3, LES PIèGES à éVITER par Groc
Une mauvaise utilisation de rx lors de l'écriture d'une couche d'accès à des services peut conduire à des cas embarassants avec des erreurs mal gérées, des appels qui ne partent lorsqu'ils le devraient, et même des résultats incorrects . le tout nuis...
Cliquez pour lire la suite de l'article par Groc SHAREPOINT BLOG SITE, PROBLèME D'ARCHIVESSHAREPOINT BLOG SITE, PROBLèME D'ARCHIVES par junarnoalg
Dernièrement, nous avons migré le site
myTIC
vers un nouveau serveur SharePoint 2010. Dans les contenus que nous vouloins récupérer, nous avions un certain nombre de blogs.
Nous avons utilisé les commandes Power...
Cliquez pour lire la suite de l'article par junarnoalg
Forum
MATRICE TEMPLATEMATRICE TEMPLATE par hjr2610
Cliquez pour lire la suite par hjr2610 RE : SAC A DOS RE : SAC A DOS par hadjkaddour
Cliquez pour lire la suite par hadjkaddour
Logiciels
sDEVIS-FACTURES vlPRO (8.1.0.3)SDEVIS-FACTURES VLPRO (8.1.0.3)sDEVIS-FACTURES vlPRO a été mis au point pour les particuliers, créateurs, entrepreneurs, artisa... Cliquez pour télécharger sDEVIS-FACTURES vlPRO 974 Application Server (12.2.4.6)974 APPLICATION SERVER (12.2.4.6)Développez de puissantes applications dans un environnement de 'cloud computing', clusterisé, séc... Cliquez pour télécharger 974 Application Server vPicture (1.4.2.1)VPICTURE (1.4.2.1)Avec vPicture, hébergez vos images facilement et rapidement.
vPicture est un utilitaire simple, ... Cliquez pour télécharger vPicture Easy-Planning (2.2.1.6)EASY-PLANNING (2.2.1.6)Easy-Planning permet de créer des plannings sous la représentation de diagrammes et est adapté au... Cliquez pour télécharger Easy-Planning COM-BACKUP (2.0)COM-BACKUP (2.0)
COM-BACKUP est un logiciel de sauvegarde qui permet de planifier les sauvegardes de vos dossiers ...
Cliquez pour télécharger COM-BACKUP
|