La vie d'un objet se résume à trois épisodes fondamentaux : sa création, son utilisation puis sa destruction. La création et la destruction font appel à des méthodes particulières : respectivement les constructeurs et le destructeur.
Plan du chapitre :
Liste des figures
Liste des programmes
Liste des tableaux
La création d'objets repose sur des méthodes spéciales nommées constructeurs. Celles-ci ont pour but de réaliser la partie « instanciation » de la création d'un objet, c'est à dire, le positionnement de la valeur initiale des variables d'instance.
Les constructeurs sont faciles à repérer : ils portent le même nom que la classe.
Par exemple, si l'on reprend la classe Point, son constructeur est facile à repérer :
Point::Point(int absc=0, int ordo=0);
Cette méthode n'a qu'un seul but : initialiser les attributs abscisse_ et ordonnee_ du nouvel objet créé et tenir à jour le nombre d'instances disponibles dans le système. Notez au passage qu'elle utilise la possibilité offerte par le C++ de proposer des valeurs par défaut aux arguments des fonctions qui s'étend tout naturellement aux méthodes. Examinons son code de plus près :
Point::Point(int absc=0, int ordo=0) { abscisse_=absc; ordonnee_=ordo; NbPoints_++; }
Programme 2.1 Le constructeur de la classe Point première mouture
Ce code peut néanmoins être utilisé en utilisant une liste d'initialisation comme dans l'exemple suivant :
Point::Point(int absc=0, int ordo=0) : abscisse_(absc), ordonnee_(ordo) { NbPoints_++; }
Programme 2.2 constructeur de Point utilisant une liste d'initialisation
Vous notez ici que le corps de la méthode est limité à l'incrémentation du compteur vide . La liste d'initialisation, où sont initialisés abscisse_ et ordonnee_ commence après le signe ":"
La syntaxe de cette liste est :
: attribut(valeur_initialisation) {, attribut(valeur_initialisation)}n
Ainsi, si l'on se réfère au code précédent, les attributs abscisse_ et ordonnée sont respectivement initialisés avec les valeurs des paramètres absc et ordo. N'allez pas croire que vous pouvez uniquement utiliser des paramètres directs dans ce genre de construction, valeur_initialisation peut être n'importe quelle expression C++ valide comprenant des opérateurs, des appels de fonctions ou de méthodes sur tout autre objet que celui en cours de création. Par exemple, si nous avions eu un attribut codant la distance du point à l'origine et nommé distance_, l'initialisation aurait pu être :
Point::Point(int x=0, int y=0) : abscisse_(x), ordonnee_(y), distance_(sqrt(x*x)+(y*y)) { NbPoints_++; };
Programme 2.3 Utilisation d'une expression complexe dans la liste d'initialisation
Pour les attributs atomiques, vous avez le choix entre l'utilisation d'affectations dans le corps du constructeur et la liste d'initialisation. Il est même possible de mixer les deux !
class Point { // Code inutile ici Point(int x=0, int y=0) : abscisse_(x) { ordonnee_ = y; NbPoints_++; };
Programme 2.4 Utilisation mixte de liste d'initialisation et affectations dans le corps du constructeur
Certains attributs ne peuvent pas être initialisés dans le corps du constructeur et devront donc obligatoirement l'être dans la liste d'initialisation : il s'agit des attributs qui sont eux mêmes des objets ou des références sur objets dans le cadre des relations d'aggrégation et d'association. Nous y reviendrons plus tard lorsque nous traiterons de l'agrégation.
Deux petites choses à retenir impérativement concernant les listes d'initialisation :
class Chose { // Code supprime private: int a; double b; };
Alors, le code :
class Chose { public: Chose (double valB, double valA) : b(valB), a (valA) { // code initialisation } };
est illégal car vous essayez d'initialiser b avant a, alors que a est déclaré avant b. Certains compilateurs effectuent d'eux mêmes la correction en emettant un message d'avertissement. Certains autres se contentent de générer du code faux sans avertissement. Il faut donc dans tous les cas faire attention à respecter l'ordre de déclaration !!! Le code correct, dans ce cas, est le suivant :
class Chose { public: Chose (double valB, double valA) : a (valA), b(valB) { // code initialisation } };
La création d'objets (opération d'instanciation) se fait en appelant le constructeur. La syntaxe diffère selon la classe d'allocation de l'objet.
Il y a trois classes d'allocation :
La création d'une instance automatique ou statique est simple et répond à la syntaxe suivante :
Dans le cas d'une instance dynamique, il faut commencer par déclarer un pointeur, puis appeler new suivi du nom du constructeur avec ses paramètres. Ce qui peut se faire sur une ou plusieurs lignes
(parametres d'un constructeur)
Point P1; // Objet statique int main(int, char **) { Point p1; // Utilise le constructeur Point::Point(int, int) // avec les valeurs par defaut des arguments Point p2(10,20); // Crée un nouveau point avec x=10 et y=20 Point *p3; // Déclaration d'un pointeur sur un objet de type Point p3=new Point(20,30); // Instanciation de l'objet dynamique avec arguments explicites p4=new Point; // instanciation de l'objet dynamique avec arguments par défaut delete p3; // On n'oublie pas de rendre la mémoire ! delete p4; // Idem => voir section suivante ! return 0; }
Programme 2.5 Exemple d'instantion d'objets simples, statiques, automatiques ou dynamiques
La syntaxe d'appel d'une méthode se différencie selon 2 modalités : le type de la méthode (instance ou méthode) et, dans le cas d'une méthode d'instance, la classe d'allocation de l'objet cible.
Le code se différencie selon que vous utilisez un objet d'allocation dynamique ou non. Pour les objets à classe d'allocation statique ou automatique, la syntaxe d'appel est la suivante :
Par exemple, appliquons les opérations suivantes à l'objet de classe Point nommé p1 tell qu'il a été instancié au paragraphe précédent :
nous obtenons le code suivant :
cout << p1.x() << endl; p1.deplacerDe(10,20); cout << p1.y();
Programme 2.6 Exemple d'appels de méthodes d'instance sur des objets non dynamiques
Pour un objet dynamique, ce n'est guère plus compliqué. Il fout juste changer le "." par une flèche "->". Ainsi, si l'on reprend l'exemple précédent avec l'objet p4 alloué dynamiquement, l'on obtient :
cout << p4->x() << endl; p4->deplacerDe(10,20); cout << p4->y();
Programme 2.7 Exemple d'appels de méthodes d'instance sur des objets dynamiques
Les méthodes de classe ne sont pas invoquées à travers un objet mais bel et bien à l'aide du nom de la classe. La syntaxe générale est la suivante :
Ainsi, si vous souhaitez afficher le nombre d'objets graphiques dans votre système en invoquant la méthode NbPoints, vous devrez utiliser :
cout << "Nombre de points présents : " << Point::NbPoints() << endl;
Vous aurez remarqué que je n'ai pas parlé d'accès aux attributs. Tout d'abord, soyons clairs : aucun attribut ne doit être visible de l'extérieur. Donc si vous accédez à un attribut ca ne peut être que dans 2 conditions bien particulières :
Quoiqu'il en soit, la syntaxe est la même. On accède à un attribut de la même manière qu'e l'on accède aux méthodes en notation pointée pour les instances statiques ou avec une flèche pour les instances dynamiques.
Les règles suivantes s'appliquent aux objets constants :
Il est dorénavant important de se consacrer aux objets constants car les règles d'appels de méthodes sur eux sont spéciales.
Les règles suivantes s'appliquent aux objets constants :
Vous allez maintenant me demander ce qu'est une méthode constante ... et bien c'est tout simplement une méthode qui ne modifie aucun des attributs des objets. Elle est donc tout à fait applicable sur un objet constant ! D'où la règle importante suivante :
Il est impératif de déclarer constante toute méthode ne modifiant pas l'état des objets auxquels elle s'applique
Si vous appliquez scrupuleusement ce principe, vous pourrez toujours passer en référence constante les objets que vous ne souhaitez pas modifier. C'est une habitude de programmation à prendre dès le début !
Quoi ? vous me demandez maintenant comment l'on rend une méthode constante ? vous êtes insatiables ! allez, je suis bon prince, je vous réponds : il suffit de faire suivre son prototype du mot clef const, que ce soit lors de la déclaration ou lors de la définition. Ainsi le code devient :
class Chose { // Code omis void metConstDep(int i, double d) const; void metConstInl(int j, const char *z) const { // Tout plein de code vachement utile } void metNonConst(double h) { // Code non moins utile } }; void Chose::metConstDep(int i, double d) const { // Code vachement important lui aussi }
Programme 2.8 déclaration et définition de méthodes constantes
Utilisons maintenant ce beau code !
int main(int, char **) { const Chose OBJET_CONSTANT(params d'initialisation); // Objet constant ! OBJET_CONSTANT.metConstDep(1,2.5); // Ok, methode constante sur objet constant OBJET_CONSTANT.metNonConst(35.5); // NON ! metNonConst est une méthode non constante // qui ne peut en aucun cas être appliquée sur un objet constant OBJET_CONSTANT.metConstInk(3,"coucou"); // Ok, methode constante sur objet constant return 0; }
Programme 2.9 Appel de méthodes sur objets contants
Très important : deux méthodes peuvent ne de différencier que par leur caractère constant ! nous reviendrons la dessus lors de la surcharge des opérateurs.
Nous avons vu que la construction d'un objet s'effectue à l'aide d'une méthode spéciale : le constructeur. La mort des objets s'accompagne de l'appel d'une autre méthode spéciale : le destructeur. Notons immédiatement que si un constructeur peut être surchargé, le desctructeur lui ne peut pas l'être. en effet, son appel étant automatique, le système ne saurait pas quel destructeur appeler.
La syntaxe du destructeur est simple. Si les constructeurs de la classe Classe s'appellent Classe::Classe, le destructeur lui est Classe::~Classe. On reconnait ici le symbole ~ associe à l'opérateur binaire NON ! A l'instar du constructeur, le destructeur ne renvoie rien ; en outre, il ne prend pas de paramètre.
Le but du destructeur est de procéder à toutes les opérations de nettoyage nécessaires à la destruction correcte d'un objet. Par exemple, si le constructeur ou une autre méthode réalise des allocations dynamiques, le destructeur veillera à rendre la mémoire afin d'éviter que celle-ci ne soit perdue. Autre cas intéressant, un objet utilise des ressources systèmes, par exemple des outils graphiques Windows, avant que l'objet ne soit détruit, le destructeur rendra la ressource système.
Il est important de noter que vous n'avez pas besoin de spécifier un destructeur si votre classe ne nécessite pas de nettoyage spécifique. Si vous ne spécifiez pas explicitement un destructeur, un destructeur par défaut est créé automatiquement par le compilateur. Ce dernier est très simple : il ne fait rien !
Le processus de destruction se passe en deux fois :
Par exemple, examinons le cas d'un objet possédant 2 attributs d'instance : un entier et un pointeur vers un tableau d'entiers, l'entier spécifiant en fait la taille du tableau. Au cours de la construction de l'objet, nous allouons un tableau. Lors de la destruction, il faudra le détruire.
class Tableau { public: Tableau(int laTailleDuTableau=5) : taille_(laTailleDuTableau) { if (_taille) tab_=new int [taille_]; else tab_=0; } ~Tableau(void) { delete [] tab_; } private: int taille_; int *tab_; };
Figure 2.1 Construction et Destruction d'un objet effectuant une
allocation dynamique
Lorsqu'un objet a les classes d'allocation statique (variable globale) ou automatique (variable locale dans une fonction/méthode), il est détruit automatiquement dès qu'il sort de portée, c'est à dire :
Il en est tout autre pour une variable dynamique, c'est à dire un objet créé dynamiquement à l'aide de new ou de new []. En effet, tout objet instancié dynamiquement doit être explicitement détruit :
Remarque importante : il n'est pas nécessaire de spécifier la taille du tableau lors de sa destruction avec delete []. Seuls quelques compilateurs antiques et démodés l'exigent encore.
L'utilisation de tableaux nécessite un constructeur par défaut. C'est à dire un constructeur pouvant ne prendre aucun paramètre lors de son invocation. Un tel constructeur peut :
Par exemple, le constructeur Point::Point(int absc=0, int ordo=0) est un constructeur par défaut !
Et pourquoi avons nous besoin d'un constructeur par défaut allez vous me demander ? et bien c'est lié à la syntaxe de construction des tableaux, par exemple, pour un vecteur à une dimension :
Comme vous pouvez le constater, il n'y a pas de place pour des paramètres de construction. La construction doit donc se faire sans paramètre, d'où la nécessité d'un constructeur par défaut.
Que faire si l'on doit créer un tableau d'objets sans constructeur par défaut ? La solution est un peu alambiquée :
A ce sujet, il ne faut pas confondre certaines notions. A partir d'une même classe T, on peut avoir :
Type de tableau | Construction des objets | Destruction | Remarques |
---|---|---|---|
Tableau statique d'instances statiques |
T tableau[TAILLE]; |
Automatique | Constructeur par défaut obligatoire. Tout est détruit automatiquement |
Tableau statique d'instances dynamiques |
T *tableau[TAILLE]; |
for (int i=0;i<TAILLE;i++) |
Le constructeur étant appelé par l'utilisateur,
n'importe lequel fait l'affaire. Attention, chaque objet doit être détruit individuellement |
Tableau dynamique d'instances statiques |
T *tableau; |
delete [] tableau; |
Constructeur par défaut obligatoire Seul le tableau doit être détruit explicitement |
Tableau dynamique d'instances dynamiques |
typedef T* PT; |
for (int i=0;i<TAILLE;i++) delete *tableau[i]; delete [] tableau; |
Les éléments doivent être détruits individuellement, avant de détruire le tableau |