8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
1/24
NOTION DE CLASSE
Le premier des paradigmes de la programmation objet est l'encapsulation. C'est la possibilité de nemontrer de l'objet que ce qui est nécessaire à son utilisation. D'où,
simplification de l'utilisation des objetsmeilleure robustesse du programme
simplification de la maintenance
Elle a pour conséquence de
rapprocher les données et leur traitement : c'est l'objet qui sait le mieux comment gérer unedemande ,masquer l'implémentation.
Sont fournis à l'utilisateur :
des mécanismes de construction ( destruction ) d'objets,
des méthodes d'accès et de modification des données encapsulées.
Une classe est la description d une famille d'objets ayant même structure et même comportement.
Une classe regroupe un ensemble d'attributs ou membres répartis en
un ensemble de données,un ensemble de fonctions appelées méthodes.
Un objet est élément de la classe. C'est une instance de la classe. Il est obtenu par instanciation.
La classe permet de produire autant d'exemplaires d'objets que nécessaire.Les valeurs des données membres peuvent différer d'une instance à l'autre ( sauf pour desdonnées statiques de classe ).Les méthodes sont les mêmes pour toutes les instances d'une classe. On distingue entre méthodesd'objet ( d'instance ) et méthodes de classe.
La déclaration d'une classe donne la nature des membres ( type; signature ), et les droits d'accès :public, protected, private ( défaut ).
La définition d'une classe fournit la définition des méthodes.
L'encapsulation se réalise en donnant à l'utilisateur
un fichier en-tête contenant la déclaration de la classe,un module objet contenant la version compilée du fichier contenant la définition de la classe.
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
2/24
Quelques explications s'imposent :
La déclaration de la classe commence par le mot clef class et est encadrée par une paired'accolades. L'accolade finale doit impérativement être suivie d'un point virgule. Pour l'instant, il vous suffit de savoir que les membres déclarés après le mot clef public formentl'interface de la classe alors que ceux suivant private sont invisibles de l'utilisateur.Les membres précédés du mot clef static sont partagés par tous les objets de la classe. De chacunil n'existe qu'un seul exemplaire par classe, quel que soit le nombre d'objets de la classe.L'ordre de déclaration des méthodes et des attributs est laissé au libre arbitre du programmeur.Toutefois, il est d'usage de déclarer les attributs à la fin car la partie la plus importante d'uneclasse du point de vue de l'utilisateur est son interface. En conséquence, il est important de placer les méthodes au début.
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
3/24
La déclaration des attributs est semblable à la déclaration d'une variable alors que celle d'uneméthode ressemble à s'y méprendre au prototype d'une fonction ! Néanmoins, il ne faut pasoublier que chaque méthode possède un argument caché : l'objet sur lequel elle est invoquée.Lors de l'implémentation des méthodes, il est nécessaire de préfixer le nom de la méthodeimplémentée du nom de la classe suivi de '::'.Par exemple, l'implémentation de la méthode deplacerVers de la classe Point se fait enspécifiant Point::deplacerVersLe constructeur est une méthode particulière qui porte le même nom que la classe et dont le butest d'initialiser les attributs lors de la création d'un objet.Les méthodes x et y sont déclarées constantes à l'aide du mot clef const. Cela signifie que leur code n'affecte en aucune manière la valeur des attributs de l'objet cible.
Les mots clefs public et private sont des modificateurs d'accès. Leur portée s'étend jusqu'au prochain modificateur. Le modificateur par défaut est private. Les membres déclarés private nesont visibles que par les méthodes de la classe elle même. En revanche, tout membre déclaré publicaura une visibilité universelle. Le respect du principe d'encapsulation impose donc que :
Tout attribut d'instance ou de classe sera déclaré private (abstraction de données),
Toute méthode non nécessaire à l'utilisateur sera déclarée private,Toute méthode que vous souhaitez rendre disponible à l'utilisateur, et qui définit donc l'interfacede la classe est à déclarer public.
Les membres de classe sont déclarés avec le mot clef static. Contrairement aux modificateurs d'accès,static n'a d'effet que sur une seule ligne de déclaration.
Particularité du C++ : la déclaration d'une donnée membre static ne lui affecte pas d'adresse, il faudradéfinir cette donnée membre par ailleurs dans le fichier de définitions. C'est précisément le rôle de ladéfinition int Point::NbPoints_=0 qui définit la donnée membre de classe NbPoints et luiaffecte la valeur initiale 0.
Rappelons que si les méthodes d'instance peuvent très bien utiliser les attributs de classe en plus desattributs d'instance, les méthodes de classe elles ne peuvent qu'utiliser les attributs de classe. En effet,sur les attributs de quelle instance devrait on appliquer les actions ?
A l'exception de classes fortement reliées, il est conseillé de placer une seule classe par fichier source.En fait, une classe a même besoin de 2 fichiers sources : un fichier header ( .h) où l'on place ladéclaration de la classe et un fichier de définition des méthodes et des variables de classe ( .cpp).
Afin d'éviter les inclusions redondantes, on place des balises de compilation avec des #ifndef comme
dans l'exemple suivant :
#ifndef NOM_DE_LA_CLASSE_H#define NOM_DE_LA_CLASSE_H
// Placer ici les inclusions et les déclarations externes nécessaires
// Placer ici la déclaration de la classe
class NomDeLaClasse
{ .... ....}; // Ne pas oublier ce ";"
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
4/24
// Sauver sous le nom : nom_de_la_classe.h#endif
Le fichier de définition sera :
#include "nom_de_la_classe.h"
// autres inclusions nécessaires// Définitions des variables de classe
// Définitions des méthodes// Sauver sous le nom : nom_de_la_classe.cpp
Vous aurez remarqué un opérateur particulier "::" appelé « opérateur de résolution de portée ». Il sert à
désigner à quelle classe appartient une méthode ou un attribut.Cet opérateur est nécessaire à la définition des méthodes car le langage C++ ne vous oblige pas àrespecter la règle de séparation de l'implémentation dans différents fichiers. Aussi, la définition dechaque méthode doit être précédée du nom de la classe à laquelle elle se rattache et de l'opérateur derésolution de portée.Par exemple, supposons que dans le même fichier, vous vouliez implémenter la méthode met1 de laclasse A ainsi que la méthode met2 de la classe B, alors, vous auriez à spécifier :
typeRetour A::met1(paramètres){ // code d'implémentation}
typeRetour B::met2(paramètres){ // code d'implémentation}
Pointeur this Nous allons à présent voir comment les fonctions membres, qui appartiennent à la classe, peuventaccéder aux données d'un objet, qui est une instance de cette classe. Ceci est indispensable pour biencomprendre les paragraphes suivants.
À chaque appel d'une fonction membre, le compilateur passe en paramètre un pointeur sur les donnéesde l'objet implicitement. Ce paramètre est le premier paramètre de la fonction. Ce mécanisme estcomplètement invisible au programmeur, et nous ne nous attarderons pas dessus.
En revanche, il faut savoir que le pointeur sur l'objet est accessible à l'intérieur de la fonction membre.
Il porte le nom « this ». Par conséquent, *this représente l'objet lui-même. Nous verrons uneutilisation de this dans le chapitre 4 (redéfinition des opérateurs).
this est un pointeur constant, c'est à dire qu'on ne peut pas le modifier (il est donc impossible de fairedes opérations arithmétiques dessus). Ceci est tout à fait normal, puisque le faire reviendrait à sortir de
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
5/24
l'objet en cours (celui pour lequel la méthode en cours d'exécution travaille).
Il est possible de transformer ce pointeur constant en un pointeur constant sur des données constantes pour chaque fonction membre. Le pointeur ne peut toujours pas être modifié, et les données de l'objetne peuvent pas être modifiées non plus. L'objet est donc considéré par la fonction membre concernéecomme un objet constant. Ceci revient à dire que la fonction membre s'interdit la modification desdonnées de l'objet. On parvient à ce résultat en ajoutant le mot-clé const à la suite de l'en-tête de lafonction membre. Par exemple :
class Entier { int i;
public : int lit(void) const;};
int Entier::lit(void) const{ return i;}
Dans la fonction membre lit, il est impossible de modifier l'objet. On ne peut donc accéder qu'enlecture seule à i.
Il est à noter qu'une méthode qui n'est pas déclarée comme étant const modifie à priori les données del'objet sur lequel elle travaille. Si elle est appelée sur un objet déclaré const, une erreur de compilationse produit donc. Ce comportement est normal. Si la méthode incriminée ne modifie pas réellementl'objet, on devra donc toujours la déclarer const pour pouvoir laisser le choix de déclarer const ou nonun objet.
Note: Le mot-clé const n'intervient pas dans la signature des fonctions en général lorsqu'ils'applique aux paramètres (tout paramètre déclaré const perd sa qualification dans lasignature). En revanche, il intervient dans la signature d'une fonction membre quand ils'applique à cette fonction (ou, plus précisément, à l'objet pointé par this). Il est donc possible de déclarer deux fonctions membres acceptant les mêmes paramètres, dont uneseule est const. Lors de l'appel, la détermination de la fonction à utiliser dépendra de lanature de l'objet sur lequel elle doit s'appliquer. Si l'objet est const, la méthode appelée seracelle qui est const.
Les méthodes inline
Il existe 2 manières de spécifier le code des méthodes. La première consiste à séparer la déclaration dela méthode de son implémentation. C'est celle que nous avons illustrée dans l'exemple précédent. Cesystème présente néanmoins un inconvénient :
"Utiliser un appel de méthode pour récupérer la valeur d'un attribut c'est une perte de tempslamentable"
Les méthodes inline ont été inventées pour cela ! La principale caractéristique des méthodes inlineest leur faculté à déveloper leur code en lieu et place d'un appel à la manière d'une macro. Voussaisissez tout de suite l'avantage : vous gagnez le temps nécessaire à l'appel d'une fonction, ainsi l'accèsà un attribut ne coûte plus rien !
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
6/24
Il y a néanmoins un inconvénient, comme tout appel est remplacé par le développement du code de laméthode, il en résulte un accroissement de la taille du code cible. Aussi, ces méthodes doivent êtrelimitées à quelques instructions sous peine d'accroître quasi indéfininiment la taille de l'exécutable. Enoutre, certains compilateurs refusent de mettre en ligne les méthodes qui contiennent des boucles.
Comment une méthode devient elle inline ? il existe 2 manières de le faire :
1. Décrire l'implémentation de la méthode au niveau de sa déclaration. C'est la manière la plus
simple mais elle présente un sérieux défaut : ne pas séparer l'implémentation de la déclaration cequi est contraire au principe de dissimulation.2. Par opposition aux méthodes décrites dans la déclaration d'une classe, on appelle méthode
déportée une méthode dont le code n'est pas transcrit dans la déclaration de sa classe mais endehors. Notons qu'il est néanmoins possible de faire développer inline une méthode déportée. Ilfaut alors préfixer sa déclaration ainsi que sa définition du mot clef inline. En outre, il faut donner son implémentation dans le fichier de déclaration à la suite de la déclaration de la classe. En effet,si vous souhaitez que le compilateur puisse développer le code de la méthode sur le lieu de l'appel,il faut qu'il connaisse sa taille !
Par exemple, nous allons réécrire la classe Point en utilisant des méthodes inline. Nous allons mettreinlineles deux méthodes d'accès aux attributs ainsi que le constructeur. Afin d'exploiter toutes les possibilités, les méthodes d'accès aux attributs seront placées inline dans la déclaration alors que leconstructeur sera mis inline externe. En respectant la structure en 2 fichiers, nous obtenons finalement :
#ifndef POINT_H#define POINT_Hclass Point{ public:
inline Point(int absc, int ordo); // Constructeur déclaré inline // le compilateur va rechercher le code plus loin
int x(void) const // Déclaration et définition inline { return abscisse_; }
int y(void) const // Déclaration et définition inline {
return ordonnee_; }
void deplacerDe(int incX, int incY); // Juste la déclaration, méthode NON inline void deplacerVers(int dX, int dY); // Juste la déclaration, méthode NON inline
static int NbPoints(void); private: int abscisse_;
int ordonnee_; static int NbPoints_;
};
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
7/24
// Definition inline deportee du constructeur
inline Point::Point(int absc, int ordo){ abscisse_=absc;
ordonnee_=ordo;}
#endif
#include "Point.h"
// Definition de l'attribut statique NbPoints_ // notez que l'on ne repete pas le mot clef static
// la valeur d'initialisation est OBLIGATOIRE
int Point::NbPoints_=0;
void Point::deplacerDe(int incX, int incY){ abscisse_+=incX; ordonnee_+=incY;
}
void Point::deplacerVers(int dX, int dY){ abscisse_=dX; ordonnee_=dY;}
Cycle de vie des objets
La vie d'un objet se résume à trois épisodes fondamentaux : sa création, son utilisation puis sadestruction. La création et la destruction font appel à des méthodes particulières : respectivement lesconstructeurs et le destructeur.
Création d'objets : les constructeurs
La création d'objets repose sur des méthodes spéciales nommées constructeurs. Celles-ci ont pour butde réaliser la partie « instanciation » de la création d'un objet, c'est à dire, le positionnement de lavaleur 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 :
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
8/24
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 objetcréé 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 quis'é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_++;}
Ce code peut néanmoins être utilisé en utilisant une liste d'initialisation comme dans l'exemplesuivant :
Point::Point(int absc=0, int ordo=0) : abscisse_(absc), ordonnee_(ordo){ NbPoints_++;}
Vous notez ici que le corps de la méthode est limité à l'incrémentation du compteur vide . La listed'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)}
Ainsi, si l'on se réfère au code précédent, les attributs abscisse_ et ordonnee_ sont respectivementinitialisés avec les valeurs des paramètres absc et ordo. N'allez pas croire que vous pouvezuniquement 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 oude 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_++;};
Pour les attributs atomiques, vous avez le choix entre l'utilisation d'affectations dans le corps duconstructeur et la liste d'initialisation. Il est même possible de mixer les deux !
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
9/24
class Point{
// Code inutile ici
Point(int x=0, int y=0) : abscisse_(x){
ordonnee_ = y; NbPoints_++;};
Deux petites choses à retenir impérativement concernant les listes d'initialisation :
1. Les attributs doivent apparaître dans leur ordre de déclaration. Par exemple si la classe déclare :
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 émettant un message d'avertissement.Certains autres se contentent de générer du code faux sans avertissement. Il faut donc dans tousles cas faire attention à respecter l'ordre de déclaration !!! Le code correct, dans ce cas, est lesuivant :
class Chose{ public: Chose (double valB, double valA) : a (valA), b(valB) {
// code initialisation }
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
10/24
};
2. Les initialisations réalisées dans la liste d'initialisation sont effectuées avant les instructions situéesdans le code du corps du constructeur prenez donc gare à ne jamais placer de code nécessitantl'exécution du corps du constructeur au sein de la liste d'initialisation.
Instanciation des objets
La création d'objets (opération d'instanciation) se fait en appelant le constructeur. La syntaxe diffèreselon la classe d'allocation de l'objet.
Les classes d'allocations
Il y a trois classes d'allocation :
Allocation statique
Elle concerne les objets placés en variable globale, c'est à dire à l'écart de toute fonction ouméthode.
On recense également dans cette catégorie les objets explicitement mis en allocation statique dansune méthode ou fonction à l'aide du modificateur d'allocation static. Rappelons que dans cecas, ils gardent leur valeur d'un appel de la fonction / méthode sur l'autre.Créés dès le début du programme, ils sont détruits automatiquement à la fin de l'exécution decelui-ci sans que vous ayez à vous en occuper. Il n'existe qu'un seul cas où ces objets ne sont pasdétruits : l'arrêt du programme par la fonction abort.
Allocation automatique
Tout objet créé sur la pile est dit à allocation automatique. Cela concerne donc les variablestemporaires dans une fonction / méthode ainsi que les objets renvoyés par une fonction / méthodequi sont momentanément stockés sur la pile.
Un objet automatique est créé à chaque lancement de la fonction / méthode à l'intérieur delaquelle il est déclaré et détruit automatiquement sans que vous ayez à vous en soucier dès leretour à l'appelant.
Allocation dynamique
Il s'agit des objets créés sur le tas et auxquels vous ne pouvez accéder qu'au travers d'un pointeur.Ils sont créés par un appel à l'opérateur new et doivent être détruits explicitement par un appel àl'opérateur delete.
Création d'instances simples
La création d'une instance automatique ou statique est simple et répond à la syntaxe suivante :
IdentificateurClasse identificateurObjet(parametres d'un constructeur)
Dans le cas d'une instance dynamique, il faut commencer par déclarer un pointeur, puis appeler newsuivi du nom du constructeur avec ses paramètres. Ce qui peut se faire sur une ou plusieurs lignes
IdentificateurClasse *identificateurPointeurObjet;
identificateurPointeurObjet=new IdentificateurClasse(parametres d'un constructeur)
Point P1; // Objet statique
int main(int, char **){
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
11/24
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;}
Appel de méthodes
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 de classe) et, dans le cas d'une méthode d'instance, la classe d'allocation de l'objet cible.
Appel de méthodes d'instance
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 :
objet.methodeInstance(paramètres);
Par exemple, appliquons les opérations suivantes à l'objet de classe Point nommé p1 tel qu'il a étéinstancié au paragraphe précédent :
1. afficher son abscisse2. le déplacer de 10 unités sur x et 20 unités sur y relativement à sa position actuelle3. afficher son ordonnée
nous obtenons le code suivant :
cout
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
12/24
Ainsi, si vous souhaitez afficher le nombre d'objets graphiques dans votre système en invoquant laméthode NbPoints, vous devrez utiliser :
cout
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
13/24
} void metNonConst(double h) { // Code non moins utile }};
void Chose::metConstDep(int i, double d) const{ // Code vachement important lui aussi}
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;}
Très important : deux méthodes peuvent ne de différencier que par leur caractère constant ! nousreviendrons la dessus lors de la surcharge des opérateurs.
Mort des objets
Nous avons vu que la construction d'un objet s'effectue à l'aide d'une méthode spéciale : leconstructeur. La mort des objets s'accompagne de l'appel d'une autre méthode spéciale : ledestructeur. 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'appellentClasse::Classe , le destructeur lui est Classe::~Classe . A l'instar du constructeur, ledestructeur 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 destructioncorrecte d'un objet.
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
14/24
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 objetutilise des ressources systèmes, par exemple des outils graphiques Windows, avant que l'objet ne soitdé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 nenécessite pas de nettoyage spécifique. Si vous ne spécifiez pas explicitement un destructeur, undestructeur 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 :
1. Appel du destructeur.2. Nettoyage de l'espace mémoire occupé par les données membres de l'objet.
Par exemple, examinons le cas d'un objet possédant 2 attributs d'instance : un entier et un pointeur versun 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_;
};
Lorsqu'un objet a les classes d'allocation statique (variable globale) ou automatique (variable locale dansune fonction/méthode), il est détruit automatiquement dès qu'il sort de portée, c'est à dire :
A la fin du programme pour une variable globale.Dès que la fonction/méthode rend la main à l'appelant pour une variable locale.
Il en est tout autre pour une variable dynamique, c'est à dire un objet créé dynamiquement à l'aide denew ou de new []. En effet, tout objet instancié dynamiquement doit être explicitement détruit :
à un appel de new doit correspondre un appel de delete,un ensemble d'objets créés à l'aide de new [] doit être détruit par delete [].
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
15/24
Remarque importante : il n'est pas nécessaire de spécifier la taille du tableau lors de sa destructionavec delete []. Seuls quelques compilateurs antiques et démodés l'exigent encore.
Création et destruction de tableaux —constructeur par défaut
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 :
Ne prendre effectivement aucun paramètre.Avoir des paramètres qui acceptent tous une valeur par défaut.
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 :
IdentificateurClasse identificateurObjet[tailleTableau];
Comme vous pouvez le constater, il n'y a pas de place pour des paramètres de construction. Laconstruction 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 peualambiquée :
1. On créé un tableau de pointeurs2. On appelle le constructeur souhaité sur chacun des pointeurs
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 statiqued'instances statiques
T tableau[TAILLE]; Automatique
Constructeur par défaut obligatoire.Tout est détruitautomatiquement
Tableau statiqued'instances dynamiques
T *tableau[TAILLE];
for (int i=0; i
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
16/24
for (int i=0;i
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
17/24
...
int main(int, char **){ ObjetGraphique unObjet(...);
affichageObjetGraphique(unObjet); return 0;}
Trois points méritent que l'on s'y attarde :
1. Tout d'abord, du point de vue de l'appelant, rien ne permet de distinguer que l'on a passé l'objet
par référence plutôt que par valeur.2. Typiquement l'opérateur "&" servait à prendre une adresse. Ici, il désigne une référence sur unobjet.
3. A l'intérieur de la fonction, on manipule la référence comme l'on manipulerait un objet.
Manipuler les références
Il faut voir que la manipulation des références est beaucoup sure que celle des pointeurs.
Une référence est toujours initialisée sur une variable, il n'y a pas de référence pointant « dans lanature » ou de référence nulle.Une fois affectée à une variable la référence ne peut plus "bouger". Elle reste liée à cette variable jusqu'à sa disparition.Il n'y a pas d'arithmétique sur les références.
Le fragment de code suivant illustre certaines caractéristiques des références :
int main(int, char **){ int i=5; int j=6;
int &r=i; // r référence sur i int *p; // p pointeur non initialise : correct int *q=&i; // q pointeur sur i int &z; // Erreur ! z est une référence non initialisée
r=10; // Affecte 10 a la variable referencee par r soit i r=j; // Affecte la valeur de j a la variable referencee par r soit i // une erreur frequente est de croire qu'a la suite de cette instruction
// r reference j. r reste liée à i
r++; // effectue i++
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
18/24
return 0;}
Vous allez me dire pourquoi ne pas toujours utiliser le passage par référence afin d'éviter desinvocations inutiles de constructeurs de recopie et de destructeurs ? La réponse est simple : on vatoujours passer les objets par référence mais on va utiliser un garde fou dans le cas où un objet n'est pas destiné à être modifié. Cette précaution s'appelle une référence constante. La syntaxe est lasuivante :
const Type &referenceConstante
Il est très important de noter qu'un objet passé par référence constante dans une fonction ou uneméthode y est considéré comme constant. Vous ne pourrez donc invoquer dessus que des méthodesconstantes. C'est pourquoi il est si important de déclarer toutes les méthodes qui le supportent.
Le clonage d'objets : recopie et affectation
Recopier un objet dans un autre est opération assez fréquente. Deux fonctionnalités y sont dédiées enC++ : le constructeur par recopie et l'opérateur d'affectation.
Le constructeur par recopie
Le constructeur par recopie est très important car il permet d'initialiser un objet par clonage d'un autre.Attention, j'ai bien dit initialiser, ce qui signifie que l'objet est en cours de construction. En particulier, leconstructeur par recopie est invoqué dès lors que l'on passe un objet par valeur à une fonction ou une
méthode.
Syntaxe
La syntaxe du constructeur par recopie d'une classe T est la suivante :
T::T(const T& o);
Il est extrèmement important de passer l'objet recopié par référence sous peine d'entrainer un appelrécursif infini !
Que se passe t'il si vous ne fournissez pas de constructeur de recopie ?
Si vous ne fournissez pas explicitement de constructeur par recopie, le compilateur en génèreautomatiquement un pour vous. Celui-ci effectue une recopie binaire optimisée de votre objet ... ce quiest parfait si celui-ci ne contient que des éléments simples.
En revanche, si votre objet contient des pointeurs, ce sont les valeurs des pointeurs qui vont êtrecopiées et non pas les variables pointées, ce qui dans de nombreux cas, conduira directement à lacatastrophe.
Quand doit on fournir un constructeur de recopie ?
Vous devez fournir un constructeur de recopie dès lors que le clonage d'un objet par recopie binaire brute peut entraîner un disfonctionnement de votre classe, c'est à dire, en particulier :
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
19/24
Utilisation de mémoire dynamique.Utilisation de ressources systèmes (fichiers, sockets, etc ...).
L'opérateur d'affectation
Mise en place
L'opérateur d'affectation et le constructeur de recopie sont très proches dans le sens où ils sont requis
dans les mêmes circonstances et qu'ils effectuent la même opération : cloner un objet dans un autre. Il ya tout de même une différence fondamentale :
L'opérateur d'affectation écrase le contenu d'un objet déjà existant et donc totalement
construit.
Ce qui signifie que dans la majorité des cas, il faudra commencer par "nettoyer" l'objet à la manièred'un constructeur avant d'effectuer l'opération de clonage dessus.
Syntaxe
Le prototype de l'opérateur d'affectation d'une classe T est le suivant :
T & operator=(const T& o);
Exemple :
Initialisation ou Affectation ?
Il est parfois délicat de savoir si l'on a affaire à une affectation ou une initialisation car la syntaxe dusigne "=" peut être trompeuse. Il existe pourtant une règle simple :
Toute opération d'initialisation ou d'affectation dans une déclaration est l'affaire d'un constructeur
Le tableau suivant résume quelques cas qui doivent être lus séquentiellement et où T et U sont desclasses quelconques :
Instruction Description Méthode mise en jeu
T t1; Initialisation par le constructeur par défaut T::T(void);
T t2( params); Initialisation par un constructeur quelconque T::T(liste params);
T t3(t1); Initialisation par le constructeur de recopie T::T(const T&);T t4();
C'est le prototype de la fonction t4 qui ne prend pas de paramètre mais renvoie un objet de type T.
T t5=t1
Initialisation par le constructeur de recopieCette ligne est à remplacer par T t5(t1); qui faitexactement la même chose mais est moins ambigue du point de vue de la syntaxe.
T::T(const T&);
t5=t2 Affectation à l'aide de l'opérateur d'affectation T & T::operator=(const T&);
Forme canonique de Coplien
On dit qu'une classe T est sous forme canonique de Coplien si elle fournit les éléments suivants :
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
20/24
Prototype Fonctionnalité
T::T() Constructeur par défaut
T::T(const T&) Constructeur par recopie
T& T::operator=(const T&) Opérateur d'affectation
T::~T() Destructeur
Si ces éléments sont codés correctement, alors l'utilisation de cette classe vis à vis de la mémoire estsécurisé. Dès qu'une classe utilise de la mémoire dynamique ou des ressources critiques, il estindispensable de la mettre sous forme canonique de Coplien.
exemple : la classe Chaine
Nous allons ici créer une classe permettant des manipulations simples sur les chaînes de caractères etillustrant le bien fondé de la forme canonique de Coplien. Notez bien que cette classe est présentéeuniquement à but pédagogique car la librairie standard du C++ propose la classe string qui lui est biensupérieure.
Une bonne classe chaine de caractères doit proposer un stockage dynamique, c'est à dire pouvants'accroître avec le temps. Nous allons donc avoir besoin d'un pointeur et d'allocation dynamique.
#ifndef CHAINE_H#define CHAINE_H#include class Chaine{ public:
// Constructeur par defaut Chaine(int taille=16): longueur_(0), capacite_(taille) { if (taille) tableau=new char [taille]; else tableau=0; }
// Constructeur prenant un pointeur sur charChaine(const char *pStr)
{ if (pStr) { int taille=strlen(pStr); longueur_=capacite_=taille; if (taille) { tableau = new char [taille+1]; strcpy(tableau,pStr); } else
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
21/24
tableau=0;}
else { longueur_=0; capacite_=0; tableau=0;
} }
// Constructeur de recopie Chaine(const Chaine &uneChaine) : longueur_(uneChaine.longueur_), capacite_(uneChaine.capacite_) { tableau = new char [capacite_+1]; if (longueur_)
strcpy(tableau,uneChaine.tableau); }
~Chaine(void) { delete [] tableau; }
// operateur d'affectation
Chaine &operator=(const Chaine &uneChaine) { if (this != &uneChaine) { delete [] tableau;
longueur_=uneChaine.longueur_;
capacite_=uneChaine.capacite_; tableau = new char [capacite_+1]; if (longueur_) strcpy(tableau,uneChaine.tableau);
} return *this; }
const char operator[](int index) const { return tableau[index]; }
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
22/24
char &operator[](int index){
return tableau[index]; }
private:
int longueur_; int capacite_; char *tableau;};
#endif
Ainsi, les objets de la classe Chaine sont à l'abri des problèmes de mémoire. Notez au passage quel'opérateur d'affectation vérifie que l'on essaye pas d'affecter un objet à lui même. C'est le but del'instruction if (this != &uneChaine) qui compare les adresses de l'objet courant et de celui donton veut lui affecter la valeur.
Vous noterez également que le code de l'opérateur d'affectation et celui du constructeur de recopie sontétonament proches. En fait, l'affectation peut se ramener à une opération de nettoyage (comme dans ledestructeur) suivie d'une recopie. Aussi, nous allons pouvoir factoriser le code dupliqué. Le programmesuivant réécrit le constructeur de recopie, l'opérateur d'affectation et le destructeur à l'aide de deuxméthodes privées que nous appellerons clonage et nettoyage.
#ifndef CHAINE_H#define CHAINE_H#include
class Chaine{ public:
// Constructeur par defaut Chaine(int taille=16): longueur_(0), capacite_(taille) { if (taille) tableau=new char [taille]; else tableau=0; }
// Constructeur prenant un pointeur sur charChaine(const char *pStr)
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
23/24
{ if (pStr) { int taille=strlen(pStr); longueur_=capacite_=taille; if (taille) {
tableau = new char [taille+1]; strcpy(tableau,pStr); } else tableau=0;
} else { longueur_=0; capacite_=0;
tableau=0; } }
// Constructeur de recopie Chaine(const Chaine &uneChaine) { clonage(uneChaine); }
~Chaine()
{ nettoyage(); }
// operateur d'affectation
Chaine &operator=(const Chaine &uneChaine)
{ if (this != &uneChaine) { nettoyage(); clonage(uneChaine); } return *this; }
const char operator[](int index) const { return tableau[index]; }
8/18/2019 C++_CHAPITRE2_MIASI_2014_2015
24/24
char &operator[](int index){
return tableau[index]; } private:
int longueur_;
int capacite_; char *tableau;
void clonage(const Chaine &uneChaine) { longueur_=uneChaine.longueur_; capacite_=uneChaine.capacite_; tableau = new char [capacite_+1]; if (longueur_)
strcpy(tableau,uneChaine.tableau); }
void nettoyage(void) { delete [] tableau; }
};
#endif