Il est né ?
Vous êtes ici : xgarreau.org >> aide >> devel >> cpp : Le c++, c + classe (g++ - partie III)
Version imprimable

Le c++, c + classe (g++ - partie III)

Dans ce numéro, nous attaquons enfin la programmation orientée objet ;-). Je tenterai de vous convaincre qu'il y a des avantages à structurer vos programmes à l'aide de classes. Nous verrons qu'un objet est une instance de classe, aborderons la notion d'encapsulation et tenterons de mettre ça en application dans de petits exemples. Pour cet article, une connaissance moyenne du C est souhaitable, notamment des structures mais il devrait être compréhensible par des débutants quand même.



Qu'apporte la classe ?

Autrement dit, pourquoi aller s'embêter avec la programmation orientée objet ? Il est vrai que le fait de structurer ses données en objets peut sembler inutile. Toutefois, sur des projets construits avec attention, il est important de définir des interfaces entre les différents "composants" du logiciel. La raison la plus évidente pour celà est qu'un projet d'envergure est souvent rédigé par plusieurs personnes. Il importe donc que l'interface de votre partie (ou "module") soit la plus claire possible pour les autres. La POO (Programmation Orientée Objet) permet de limiter les erreurs d'interactions entre modules en ne laissant apparaître que ce qui est nécessaire à ceux à qui celà va servir. Il existe bien sûr d'autres raisons comme la facilité accrue avec laquelle on peut reprendre, après une longue période d'abandon, un code objet.

Il est bien sûr possible de programmer comme un goret en C++. Il est possible de ne pas structurer ses programmes mais cela est beaucoup facile de programmer proprement en C++ qu'en C. J'entends par programmer proprement diviser ses programmes en entités logiques dont chacune ne connaît de l'autre que ce qui lui est utile. Il se trouve que la POO et le concept de classe ont été justement créés dans ce but.

Structures

Comme je considère l'apprentissage par la pratique comme le plus efficace, j'arrête de parler et aborde rapidement les exemples.

Nous verrons comment définir et utiliser une classe après avoir vu les structures. Vous savez probablement tous définir une structure en C. En voici une en C++ définissant une personne, utilisée ensuite dans le main :

/* ex_3_1.c++ */
#include <string>
#include <iostream>
using namespace std;

struct personne {
	int age;
	int sexe;
	string nom;
	string prenom;
};

int main () {
	personne pers;
	
	pers.prenom="Xavier";
	pers.sexe=0;
	pers.age=25;
	pers.nom="Garreau";

	cout << pers.nom << " " << pers.prenom << endl;
	cout << " *  " << pers.age << " ans" << endl;
	cout << " *  " << ((pers.sexe) ? "Femme" : "Homme") << endl;

        personne *ppers = &pers;
        ppers->age++;
   
	cout << pers.nom << " " << pers.prenom << endl;
	cout << " *  " << pers.age << " ans" << endl;
	cout << " *  " << ((pers.sexe) ? "Femme" : "Homme") << endl;

	return 0;
}

Vous remarquez qu'une fois la structure personne déclarée, on peut définir une variable pers de type personne et en manipuler les champs age, sexe, nom et prenom. Pour ceux qui pratiquent le C, notez bien que l'on écrit personne pers et non struct personne pers. Vous pouvez également constatez que l'on utilise la notation à point pour accéder aux champs de la structure (pers.nom, pers.age, etc...).

On voit ensuite la déclaration d'un pointeur sur une variable de type personne, ppers. Ce pointeur permet de mettre à jour l'âge de pers (nous veillissons tous...). On constate qu'on accède aux champs des structures pointées grâce à la notation -> qui est équivalente à la notation à point lorsque l'on travaille avec des variables.

Les "champs" sexe, age, etc... sont des propriétés de personne

Après, compilation avec g++ -o ex_3_1 ex_3_1.c++, on obtient l'exécution suivante :

Garreau Xavier
 *  25 ans
 *  Homme
Garreau Xavier
 *  26 ans
 *  Homme

Dans l'exemple précédent on affiche deux fois le contenu de la structure, il est donc logique de penser faire un fonction. En C, on définirait vraisemblablement une fonction personne_affiche() qui prendrait en paramètre une variable de type personne et en modifierai les champs. On ajouterait éventuellement un pointeur vers cette fonction dans la structure. C'est ainsi que fonctionne gtk+ par exemple. En C++, on peut déclarer cette fonction directement dans la structure, comme montré dans l'exemple ci dessous :

/* ex_3_2.c++ */
#include <string>
#include <iostream>
using namespace std;

struct personne {
	int age;
	int sexe;
	string nom;
	string prenom;
	void affiche();
	void vieillit() { age++; }
};

void personne::affiche() {
	cout << nom << " " << prenom << endl;
	cout << " *  " << age << " ans" << endl;
	cout << " *  " << ((sexe) ? "Femme" : "Homme") << endl;
}


int main () {
	personne persX;
	personne persG;

	persX.prenom="Xavier";
	persX.sexe=0;
	persX.age=25;
	persX.nom="Garreau";
	persG.prenom="Guillaume";
	persG.sexe=0;
	persG.age=21;
	persG.nom="Garreau";

	persX.affiche();
	persG.affiche();

	persG.vieillit();

	persX.affiche();
	persG.affiche();

	return 0;
}

Les fonctions affiche et vieillit sont toutes deux déclarées dans la structure personne. vieillit est définie dans la structure alors qu'affiche est, elle, définie en dehors. Elle est donc préfixée par personne::, qui spécifie qu'il s'agit d'une déclaration de fonction "appartenant" à la structure personne. C'est pour celà que l'on peut directement utiliser les champs de la structure dans le corps de la fonction comme s'il s'agissait de variables normales.

On accède aux méthodes de la même façon qu'aux propriétés, en utilisant la notation à point ou ->.

On note que, malgré qu'il n'y ait pas de transmission de paramètre comme en C, ce sont les propriétés de la variable pour laquelle on appelle les fonctions qui sont utilisées et que cela n'agit pas sur les propriétés des autres variables de type personne. En effet, le fait de vieillir persG n'a aucun effet sur persX alors que l'on utilise dans le corps de la fonction vieillit la notation age sans précision supplémentaire. C'est lors de l'appel que la personne sur laquelle on agit est déterminée.

:: est l'opérateur de résolution de portée. Il sert à expliciter à quelle structure, classe ou espace de nom appartient ce qui le suit. C'est grâce à lui que l'on peut définir la méthode affiche en dehors de la structure personne.

affiche et vieillit sont des méthodes de la structure personne.

Les propriétés et les méthodes sont des membres.

Ce qui est pénible avec les objets c'est le volume de code nécessaire pour les gérer. Il faut les initialiser lors de leur création et "faire le ménage" lors de leur disparition. En fait, une solution existe. Une méthode spéciale, le contructeur, est exécutée lors de la création d'un objet. Pour écrire un constructeur, il suffit de définir une méthode portant le même nom que la structure à laquelle elle appartient, dans notre cas, personne(). Une autre méthode, le destructeur, est exécutée lors de la disparition de l'objet (fin de portée, fin de programme, destruction explicite). Le destructeur a le même nom que le constructeur mais préfixé par un ~. Le contenu de ces méthodes est libre.

/* ex_3_3.c++ */
#include <cstdlib>
#include <string>
#include <iostream>
using namespace std;

enum sexe_t {
	SEXE_HOMME=0,
	SEXE_FEMME,
	SEXE_SURPRISE
};

struct personne {
	int age;
	sexe_t sexe;
	string nom;
	string prenom;
	void vieillit() { age++; }
	personne(string p="Anonyme", string n="X", int a=0, sexe_t s=SEXE_SURPRISE);
	~personne();
};

personne::personne(string p, string n, int a, sexe_t s) {
	nom=n;
	prenom=p;
	age=a;
	sexe=(s<2) ? s : (sexe_t)(2*((double)rand()/RAND_MAX));
	cout << "Création d'un" << (sexe ? "e femme" : " homme");
	cout << " : " << prenom << " " << nom << endl;
	cout << " *  " << age << " ans" << endl;
}

personne::~personne() {
	cout << "Destruction de ";
	cout << prenom << " " << nom << endl;
	cout << " *  " << age << " ans" << endl;
}

int main () {
	srand(time(NULL));
	personne persX("Xavier", "Garreau", 25, SEXE_HOMME);
	personne persG("Guillaume", "Garreau", 21, SEXE_HOMME);
	
	persG.vieillit();

	return 0;
}

Outre l'énumération sexe_t, ce programme ajoute à la structure personne un constructeur et un destructeur. Ces derniers se chargent d'initialiser les objets personne et de prévenir de leur disparition. Les exécutions du constructeur et du destructeur sont mises en évidence par l'exécution du programme :

Création d'un homme : Xavier Garreau
 *  25 ans
Création d'un homme : Guillaume Garreau
 *  21 ans
Destruction de Guillaume Garreau
 *  22 ans
Destruction de Xavier Garreau
 *  25 ans

Il est intéressant par exemple de remarquer que les objets sont détruits dans l'ordre inverse de leur création. On peut ajouter au constructeur une liste d'initialisations de membres plutôt que de les faire nous mêmes. On utilise pour cela la notation suivante lors de la définition du constructeur :

constructeur(arguments) : propriete1(valeur1), propriete2(valeur2)

Notre constructeur personne devient donc :

personne::personne(string p, string n, int a, sexe_t s) :
	prenom(p),
	nom(n),
	age(a),
	sexe(s)
{
	sexe=(s<2) ? s : (sexe_t)(2*((double)rand()/RAND_MAX));
[...]

Classes

Etant donné ce que permettent de faire les structures en C++, on en arrive à se demander pourquoi on a besoin de classes. En effet, on a déjà des objets avec leurs méthodes et leur propriétés sans les utiliser.

La classe et les structures founrissent la notion d'encapsulation que nous allons voir tout de suite et la possibilité d'héritage que nous verrons dans le prochain article. Qu'est ce que l'encapsulation ? Nous allons voir cela en observant notre exemple de personne. Une fois un objet personne créé, on ne devrait pas pouvoir modifier ses nom, prenom, age et sexe directement. Le prénom, le nom et le sexe sont déterminés lors de la "création" d'une personne et son age est incrémenté de 1 tous les ans. Traditionellement, une femme peut éventuellement changer de nom lors de son/ses mariage(s)/divorce(s) mais on ne traite pas ce cas pour l'instant. On dit que les nom, prenom, age et sexe sont des membres privés c'est à dire qu'on ne peut pas y accéder directement sauf si on est la personne à qui elles appartiennent. En revanche, tout le monde peut créer une personne et consulter son nom. On doit donc créer une méthode get_identification permettant de consulter les nom et prénom d'une personne. On dit alors que le constructeur et la méthode get_identification sont publics, c'est à dire que tout le monde peut les utiliser.

On déclare et définit une classe comme une structure mais en remplaçant le mot clé struct par class. En outre, on l'utilise de la même manière. Une structure est une classe dont tous les membres sont publics par défaut.

Les variables et méthodes déclarées immédiatement après class personne { sont privées. Pour commencer à déclarer des membres publics, on doit utiliser l'étiquette public:. On peut ensuite redéclarer des membres privés en utilisant l'étiquette private: puis revenir à des membres publics, etc... . La déclaration d'une classe ressemble donc à ceci :

class nom_classe{
        /* membres privés */
        /* ... */
public:
        /* membres publics */
        /* ... */
private:
        /* membres privés */
        /* ... */
public:
        /* membres publics */
        /* ... */
}

Oui mais alors, comment accéder aux membres privés, pour "vieillir" la personne ou en connaître le nom ? Simple ! Les méthodes d'une classe peuvent librement accéder aux propriétés de la même classe et les modifier et on peut librement accéder aux membres publics d'une classe depuis l'extérieur.

Voyons en application de ce qui vient d'être dit comment déclarer la classe personne :

class personne {
	int age;
	sexe_t sexe;
	string nom;
	string prenom;

public:
	personne(string p="Anonyme", string n="X", int a=0, sexe_t s=SEXE_SURPRISE);
	~personne();

	string get_identification() const { return (prenom + " " + nom); }
	void vieillit() { age++; }
};

Vous voyez mis en application le contenu du paragraphe précédent. Il faut noter que le destructeur doit toujours être public et ne peut être surchargé, sans quoi votre programme ne se compile pas et dans la plupart des cas, un constructeur doit également être public. Nous verrons une exception à cette règle lorsque nous parlerons des singletons. Le constructeur peut, contrairement au destructeur, être surchargé.

Les propriétés age, sexe, nom et prenom sont privées et ne peuvent être modifiées que lors de la construction de l'objet à l'exception de la propriété age, qui peut être modifiée par la méthode vieillit. Cette façon de faire permet d'éviter toute utilisation non adaptée d'un objet. Le concepteur d'une classe peut prévoir l'utilisation qui sera faite de sa classe et restreindre les accès à cette dernière, ce qui permet de construire des interfaces claires entre entités logiques d'une application. On peut notamment obtenir des propriétés en lecture seule en les déclarant privées mais en fournissant une méthode publique en renvoyant la valeur, comme c'est plus ou moins le cas pour la méthode get_identification de notre exemple. Cette méthode est par ailleurs suivie du mot clé const, qui spécifie que son exécution ne modifie pas l'objet pour lequel elle est appelée.

La méthode vieillit permet quant à elle d'être sûr que l'âge ne sera modifié que de la façon que l'on a prévue, c'est à dire incrémenté de 1 à chaque fois. Comme elle modifie une propriété de l'objet pour lequel elle est appelée, elle ne peut pas être déclarée avec le mot clé const.

Pour résumer, voyons le fragment de code suivant, qui utilise la classe personne :

1: personne persGw("Gwenaëlle", "Garreau", 26, SEXE_FEMME);
2: cout << persGw.nom;
3: cout << persGw.get_identification();
4: persGw.vieillit();

La ligne 1 crée la personne, comme dans le cas d'une structure, on parle d'instanciation en programmation orientée objet ce qui est équivalent à dire qu'un objet est une instance de classe.

La ligne 2 génère une erreur lors de la compilation car nom est une propriété privée.

La ligne 3 affiche "Gwenaëlle Garreau"

La ligne 4 incrémente indirectement l'âge de l'objet persGw

delete[] new personne[10];

Derrière ce titre énigmatique (qui sert à libérer la mémoire occuppée par un tableau de 10 objets personne tout de suite après sa création ...) se cache une présentation sommaire des création/destruction d'objets dynamiques, qui est indispensable pour attaquer le clonage de personnes, comme nous aurons l'occasion de le voir ensuite.

La gestion de mémoire "à la C", à base de malloc, free et autres est performante mais un peu fastidieuse. Le C++ met à notre disposition les opérateurs new et delete permettant de simplifier la gestion de la mémoire dynamique (aka "le tas").

Considérons le fragment de code suivant, qui utilise la classe personne et met en évidence l'utilisation des opérateurs new et delete :

int main () {
	srand(time(NULL));
	personne* persX = new personne ("Xavier", "Garreau", 25, HOMME);
	personne* persG = new personne ("Guillaume", "Garreau", 21, HOMME);
	personne* X2 = new personne[2];

	delete persX;
	delete persG;
	delete[] X2;
}

La sortie de ce programme est collée ci-dessous (à quelques différences de sexe près) :

Création d'un homme : Xavier Garreau
 *  25 ans
Création d'un homme : Guillaume Garreau
 *  21 ans
Création d'une femme : Anonyme X
 *  0 ans
Création d'un homme : Anonyme X
 *  0 ans
Destruction de Xavier Garreau
 *  25 ans
Destruction de Guillaume Garreau
 *  21 ans
Destruction de Anonyme X
 *  0 ans
Destruction de Anonyme X
 *  0 ans

Il devrait donc être clair que :

  1. new permet de réserver de la place en mémoire pour un objet et de le créer en appelant son constructeur.
  2. On peut utiliser new pour créer des tableaux d'objets, avec un notation semblable à celle utilisée pour une variable.
  3. new renvoit un pointeur vers le nouvel objet, soit l'adresse de la zone de mémoire allouée.
  4. delete appelle le destructeur de l'objet et libère la mémoire qu'il utilisait.
  5. delete[] est l'équivalent de delete mais s'utilise dans le cas de tableaux.
Il faut bien faire attention à TOUJOURS détruire les objets créés avec new avec delete et ceux créés avec new...[] avec delete[]. La non observation de cette règle ne pardonne pas et conduit à des segmentation fault.

Les habitués du C verrons en new et delete les équivalents évolués de malloc et free mais se demanderont où est passé realloc. La réponse est ailleurs ;-). Un besoin de realloc en C peut très souvent être remplacé par l'utilisation d'un type évolué du C++ comme un map ou un vector que nous aurons l'occasion de présenter dans un article futur. Dans les rares cas ou un realloc est nécessaire, il est possible de l'utiliser puisque le C++ est basé sur le C.

Constructeur de copie

Il existe plusieurs types de constructeurs : le constructeur par défaut qui ne prend pas de paramètres : il sert dans le cas de déclarations de style C comme dans le cas de p0, ci-dessous. Les constructeurs "normaux", qui servent à l'initialisation normale et plus ou moins complète de l'objet, et le constructeur de copie qui sert lors d'une initialisation par affectation telle que pour p2, ci-dessous :

personne p0; // (équivaut à personne p0 ("Anonyme", "X", 0, SEXE_SURPRISE);
personne p1 ("Xavier", "Garreau", 25, SEXE_HOMME);
personne p2 = p1; // équivaut à personne p2 (p1);

On peut considérer celà comme du clonage. Si vous n'avez pas fourni de constructeur de copie, les propriétés de p1 sont copiés dans p2. Ce n'est pas forcément ce que l'on veut ici. Votre clone, lors de sa création est vous à l'identique mais il est agé de 0 ans. On remédie au problème posé par le constructeur de copie par défaut en en définissant un nous mêmes. Le prototype du constructeur de copie pour une classe cl est toujours cl(const cl&) :

class personne {
/* ... */
public: 
        personne(const personne& pers) : 
                prenom(pers.prenom),
                nom(pers.nom),
                age(0),
                sexe(pers.sexe);
}

personne::personne(const personne& p) :
	nom(p.nom),
	prenom(p.prenom),
	age(0),
	sexe(p.sexe)
{
	cout << "Clonage de " << p.prenom << " " << p.nom << " " << endl;
}

On définit un constructeur de copie lorsque le comportement par défaut n'est pas approprié. C'est notamment le cas quand une classe contient des membres pointeurs. La copie par défaut fait que les membres pointeurs des deux objets pointent, après la copie, au même endroit, c'est à dire que la valeur qui est copiée est en fait l'adresse pointée par les pointeurs et non son contenu. La destruction des deux objets (l'original et la copie) entraîne une double libération du même espace mémoire, ce qui est catastrophique.
Un constructeur de copie traitant une classe dont les membres sont des pointeurs doit en général copier les valeurs des membres non pointeurs, affecter de la mémoire pour les pointeurs de l'objet copié et faire une copie de la mémoire pointée par l'original dans la mémoire pointée par la copie.
Il faut garder deux choses en tête à propos du constructeur de copie :
1. C'est lui qui est utilisé dans le cas d'initialisation d'objets en fournissant en argument une référence à un autre objet de la meme classe.
2. C'est également lui qui est utilisé dans la transmission d'arguments par valeur. c'est à dire que lors d'un appel de la fonction ayant pour prototype test (personne p), p est "créé" à l'aide du constructeur de copie. Pour utiliser l'objet et non une copie, il faut écrire test (personne& p).

Afin de s'assurer que des copies ne seront pas utilisées dans des cas imprévus, on peut rendre privé le contructeur de copie en le déclarant dans une section private de la déclaration de la classe. Ainsi une tentative d'utilisation de la fonction de prototype test (personne) déclenchera une erreur à la compilation.

Opérateurs

Il est extrêmement utile de pouvoir utiliser les opérateurs classiques avec les variables que l'on crée. Par exemple, si on veut additionner deux variables entières, il est normal de vouloir taper a=b+c plutôt que a=additione(b,c). Si on veut additioner deux variables complexes, on est obligé (en C) d'en passer par une fonction complex_add() ou quelque chose d'approchant. En C++ non, les opérateurs peuvent être surchargés, au même titre que les fonctions classiques. Un opérateur peut toujours être modélisé par une fonction en C++. Il existe par exemple une fonction double operator+(double, double) qui vous permet d'ajouter deux variables de type double, de même qu'il existe un operator+ pour les complex, qui est un type intégré à la librairie standard, soit dit en passant. On pourrait en citer beaucoup d'autres comme par exemple operator<, operator+=, operator*, ... Les opérateurs sont presque tous surchargeables.

On peut déclarer un opérateur dans une classe ou en dehors. Nous allons voir les deux cas ici. Evidemment le choix de mettre un opérateur dans une classe ou en dehors dépend du développeur. Si cet opérateur doit accéder à un ou des membres privés de la classe, alors, on doit le définir dans la classe. Pour le reste, chacun fait ce qui lui plaît.

Dans notre petit exemple de personnes, nous allons créer notre propre opérateur * pour modéliser le clonage.

personne* personne::operator*(const int& nb) const {
	if (nb<=0) 
		return NULL;

	if (nb==1)
		return new personne(*this);

	return new personne[nb](*this);
}

Cet opérateur permet de donner un résultat à la multiplication d'une personne par un entier. La multiplication de personne par un entier positif non nul nb renvoie un tableau de nb objets personne mais dont l'âge est 0, puisqu'elles sont créées par l'opérateur de copie, privé mais qu'on peut utiliser ici car l'opérateur est un membre de la classe personne.

Si vous essayez d'utiliser cet opérateur, vous vous rendrez compte que pour une personne p, p*1 ne pose pas de problème mais qu'en revanche 1*p provoque une erreur à la compilation. Ce problème est résolu en définissant l'opérateur correspondant en temps que fonction non membre, par exemple :

personne* operator*(const int& nb, const personne& p) {
	return p*nb;
};

Cet opérateur utilise l'opérateur défini dans la classe et permet d'utiliser la multiplication "dans les deux sens". Voici ci-dessous un exemple d'utilisation de cet opérateur :

int main () {
	srand(time(NULL));
	personne persX("Xavier", "Garreau", 25, HOMME);

	personne* clones;
	clones = 2*persX;
	delete[] clones;

	return 0;
}

Ce programme produit la sortie suivante :

Création d'un homme : Xavier Garreau
 *  25 ans
Clonage de Xavier Garreau 
Clonage de Xavier Garreau 
Destruction de Xavier Garreau
 *  0 ans
Destruction de Xavier Garreau
 *  0 ans
Destruction de Xavier Garreau
 *  25 ans

On retrouve bien le double clonage de l'objet personne Xavier Garreau et les trois destructions, deux pour les clones, reconnaissables à leurs 0 an, commandées par l'opérateur delete[] et une pour l'original, à la fin du programme.

Opérateur d'affectation

L'opérateur d'affectation est très semblable au constructeur de copie. Il est utilisé lors d'une affectation, comme son nom l'indique. Son prototype est pour une classe cl : cl& operator=(const cl&). Il doit s'assurer de la libération des membres de l'objet à gauche de l'opérateur et copier les membres de l'objet de droite dans celui de gauche, avec les mêmes précautions que pour le constructeur de copie pour ce qui est de la gestion de la mémoire.

Nous allons créer une classe test ayant pour membre un pointeur sur un objet personne pour illustrer l'utilisation de l'opérateur d'affectation.

[...]
class test {
	personne* p;
public:
	test(const string& prenom) { p = new personne(prenom); }
	~test() { delete p; }
	void cout_identification() { cout << p << " : " << p->get_identification() << endl; }
};

int main () {
	test persTo("toto");	
	test persTi("titi");
	persTo.cout_identification();
	persTi.cout_identification();
	persTi = persTo;
	persTo.cout_identification();
	persTi.cout_identification();

        return 0;
}

L'exécution de ce code donne :

Création d'une femme : toto X
 *  0 ans
Création d'un homme : titi X
 *  0 ans
0x804dcc8 : toto X
0x804dce0 : titi X
0x804dcc8 : toto X
0x804dcc8 : toto X
Destruction de toto X
 *  0 ans
Destruction de totoÀXtitiX`toto Xtoto X Àà @` Àà ¸@¸@ðÐ0
 

Il est évident qu'en utilisant l'opérateur d'affectation par défaut, lors de la destruction des objets, la zone mémoire à l'adresse 0x804dcc8 est libérée deux fois et qu'on essaie de s'en servir après sa première libération. Quand à elle, la zone mémoire située en 0x804dce0 n'est jamais libérée. Il est nécessaire de définir un opérateur d'affectation pour la classe test qui tienne compte du fait que le membre p est un pointeur, ce que nous allons faire maintenant.

On doit déclarer et définir l'opérateur d'affectation pour la classe test.

1: test& test::operator=(const test& t) {
2: 	if (this != &t) {
3: 		delete p;
4: 		p = 1**(t.p);
5: 	}
6: 	return (*this);
7: }

Ligne 2, on évite de rencontrer des problèmes dans le cas ou l'objet à gauche de l'opérateur d'affectation serait le même que celui à droite en comparant this (l'adresse de l'objet courant) à l'adresse du paramètre t.
Ligne 3, on efface l'ancien objet.
Ligne 4, on utilise la définition publique de l'opérateur * avec nb=1 pour effectuer le clonage.
Ligne 6, on renvoit le nouvel objet mis à jour.

Voici l'exemple du dessus après définition du nouvel opérateur :

Création d'un homme : toto X
 *  0 ans
Création d'un homme : titi X
 *  0 ans
0x804df58 : toto X
0x804df70 : titi X
Destruction de titi X
 *  0 ans
Clonage de toto X 
0x804df58 : toto X
0x804df70 : toto X
Destruction de toto X
 *  0 ans
Destruction de toto X
 *  0 ans

On remarque la destruction de titi X avant la création d'un clone de toto X. Les deux adresses libérées sont différentes et ne posent donc pas de problème.

Conclusion

Ne vous inquiétez pas, ce n'est pas fini ! Il reste encore beaucoup à dire sur le C++. Citons par exemple les flux de chaînes, les entrées/sorties sur fichiers, les classes abstraites, les méthodes virtuelles, le polymorphisme, les design patterns et notamment les singletons, gtkmm (Qt a été et est traité actuellement dans linuxmag), les espaces de noms, les exceptions, etc ... Bref, il nous reste de quoi faire ;-). Vous pouvez influencer les sujets abordés et/ou me demander de traiter un thème en particulier. Une fois que nous avons les bases nécessaires, l'ordre des aspects évoqués importe moins qu'en début de série. Comme c'est un point important pour la compréhension globale du langage, nous verrons la prochaine fois comment créer des personnes sachant faire des choses formidables mais étant du coup spécialisées et différentes les unes des autres, tout en ayant un base commune. En bref, le mois prochain, nous aborderons l'héritage.

Vous pouvez retrouver les codes sources utilisés dans les exemples de cet article sur mon site, dans la section C++ qui devrait avoir vu le jour après la parution de cet article.

a+
Xavier GARREAU - http://www.xgarreau.org/ - <xavier@xgarreau.org>

Ingénieur de recherche PRIM'TIME TECHNOLOGY
http://www.prim-time.com/

Président du ROCHELUG
http://lug.larochelle.tuxfamily.org/

Références :


Précédent Index Suivant

a+

Auteur : Xavier GARREAU
Modifié le 10.09.2004

Rechercher :

Google
 
Web www.xgarreau.org