1 Introduction au concept de classe
La classe contient la description de ses instances. Par exemple, la classe "voiture" contient la description de ses instances par le fait que son nom est voiture, et que l'on sait quels sont les objets qui sont des voitures. Cette description s'appelle une abstraction comme le mot voiture est une abstraction des objets voitures en français (la description est d'ailleurs le moyen utilisé pour définir une abstraction dans un dictionnaire).
En informatique, on travaille sur des objets "informatique" qui sont les
représentants des objets du monde réel. Ces objets sont présents
dans la mémoire de l'ordinateur. Ils sont donc uniquement constitués
d'informations binaires.
Dans un langage à objets, la description des instances ne se limite
pas à son seul nom, quoique le cas puisse parfois se présenter.
La description se fait également par un ensemble d'attributs et de
méthodes. Les attributs sont destinés à recevoir des
valeurs et les méthodes du code. La description d'un objet se fait
donc par les deux composantes des langages de programmation que sont les données
et les programmes (notons que d'autres formes d'expression de comportement
peuvent également exister dans les objets comme, par exemple, des règles
de systèmes experts).
En C++, la description des instances d'une classe se fait à l'aide
d'attributs comme dans les structures du C et les méthodes sont décrites
par des fonctions C. La définition de la classe en C++ se limite donc
à cette description et à un nom. Ainsi, il n'est pas possible
de connaître l'ensemble des objets appartenant à une classe en
C++. En fait ce besoin se fait rarement ressentir dans les langages de programmation
à objets.
class Fenetre { };
Cet exemple écrit en C++ va provoquer la définition de la classe Fenetre dont les instances seront des fenêtres (soit des représentants de fenêtres du monde réel, soit des fenêtres du bureau électronique, métaphore du monde réel. Nous nous intéressons à ce second cas).
Le corps contient la description des instances. Les attributs (aussi appelés données membres dans la littérature C++) sont définis comme les champs d'une structure, soit par exemple :
class Fenetre { short positionX, positionY; short hauteur, largeur; unsigned char *titre; };Il faut noter qu'il n'est pas possible de donner une valeur par défaut à un attribut directement comme pour une variable. Nous verrons qu'il est possible de le faire par un autre moyen.
Les méthodes (aussi appelées fonctions membres dans la littérature C++) sont définies à l'aide de fonctions C dont le prototype est inclus dans le corps de la définition de la classe, ce qui donne dans notre exemple :
class Fenetre { short positionX, positionY; short hauteur, largeur; unsigned char *titre; void deplace(short nouveauX,short nouveauY); void masque(void); };La définition du code de la fonction C se fait alors séparément dans le source de la façon suivante :
void Fenetre :: deplace(short nouveauX, short nouveauY) { positionX = nouveauX; positionY = nouveauY; /* autres opérations */ }La fonction masque pourra ensuite être définie de façon similaire.
Dans la définition du code d'une méthode, il est possible d'accéder aux attributs définis dans la classe en le désignant par leur seul nom. Ainsi dans la méthode deplace, l'accès à l'attribut positionX se fait en donnant uniquement son nom.
Notons qu'une méthode est liée à sa classe. Elle n'aura aucun rapport avec une fonction globale du C qui aurait le même nom qu'elle.
Il est aussi également possible d'intégrer le code de la fonction dans la définition même de la classe, même si ce style d'écriture est largement déconseillé. Ceci peut donner par exemple pour la méthode MetTitre :
class Fenetre { short positionX, positionY; short hauteur, largeur; unsigned char *titre; void deplace(short nouveauX,short nouveauY); void masque(void); void MetTitre(unsigned char *NouveauTitre) {titre=NouveauTitre; /*opération graphique */} };
Fenetre MaFenetre;
MaFenetre désigne alors une instance de la classe Fenetre. La mémoire allouée pour stocker les attributs de MaFenetre se trouve alors dans l'espace des variables locales ou globales.
L'accès aux attributs et aux méthodes se fait à l'aide du point comme pour les structures. Cependant, pour pouvoir accéder aux attributs et aux méthodes d'une instance, il faut que ceux-ci soient visibles, c'est à dire définis publiques dans la classe. Nous reviendrons sur la visibilité des attributs et des méthodes lorsque nous étudierons plus en détail l'encapsulation. La définition publique des attributs se fait de la façon suivante :
class Fenetre { public: short positionX, positionY; short hauteur, largeur; unsigned char *titre; void deplace(short nouveauX,short nouveauY); void masque(void); void MetTitre(unsigned char *NouveauTitre) {titre=NouveauTitre; /*opération graphique */} };Les opérations suivantes sont alors possibles :
MaFenetre.hauteur = 12; strcpy(MaFenetre.titre,"\pSans-Titre");
MaFenetre.deplace(12,12);
La dernière ligne est un appel de méthode. Elle provoque l'appel de la fonction deplace qui va modifier la valeur des attributs positionX et positionY, puis exécuter éventuellement d'autres opérations. Ainsi attributs et méthodes permettent de modifier la description des instances.
Il est aussi possible de créer une instance (on dit également instancier et instanciation pour désigner la création) dynamiquement. Pour cela, il faut déclarer explicitement un pointeur sur la classe, comme par exemple :
Fenetre *MaFenetre;
L'instanciation se fait alors à l'aide de l'opérateur new comme suit :
MaFenetre = new Fenetre;
L'accès aux attributs et aux méthodes se fait alors à l'aide de l'opérateur "->" :
MaFenetre->hauteur = 12; strcpy(MaFenetre->titre,"\pSans-Titre"); MaFenetre->deplace(12,12);
On dit que MaFenetre est une référence vers une instance qui n'a pas de nom spécifique. En effet, elle peut contenir une référence vers une instance puis vers une autre instance. Si on écrit :
MaFenetre = new Fenetre; MaFenetre = new Fenetre;
Alors MaFenetre est une référence vers la deuxième instance anonyme créée. La première est alors non référencée. En C++, la mémoire qu'elle occupe est perdue car à la différence de langages à objets plus évolués comme BETA ou Eiffel, C++ ne sait pas supprimer automatiquement une instance non référencée. Il est donc nécessaire de détruire explicitement les instances à l'aide de l'opérateur delete.
delete MaFenetre;
La dernière ligne a pour conséquence de détruire l'instance
référencée par MaFenetre. La variable MaFenetre devient
une référence dans une zone de la mémoire où il
n'y pas plus d'instance (dangling reference en anglais). L'utilisation de
l'opérateur -> n'a alors plus de sens et pourrait provoquer de sérieux
plantages !
4 Les fonctions constantes
Une fonction constante est définie par le mot réservé const qui est inséré après la déclaration et la définition de la fonction. Par exemple, la fonction ObtientTitre dans la classe Fenetre est une fonction constante :
class Fenetre { public: unsigned char *titre; unsigned char *ObtientTitre(void) const; }; unsigned char * Fenetre::ObtientTitre(void) const { return titre; }Une fonction constante ne peut appeler des fonctions autres que des fonctions constantes.
Par exemple, on veut appeler une fonction qui stocke des références dans un tableau. Cette fonction stocke des objets de la classe Fenetre et est implantée dans la classe FenetreListe.
class FenetreListe { public: Fenetre fenetres[10]; short Indice; void InitIndice(void); void AjouteFenetre(Fenetre unefenetre); }; void FenetreListe::InitIndice(void) { Indice=0; } void FenetreListe::AjouteFenetre(Fenetre unefenetre) { fenetres[Indice] = unefenetre; Indice++; } FenetreListe laFenetreListe; laFenetreListe.InitIndice();Si on crée une méthode AjouteDansListe dans la classe Fenetre :
class Fenetre { public: { void AjouteDansListe(void); }; void Fenetre::AjouteDansListe(void) { laFenetreListe.AjouteFenetre(this); }Dans la méthode AjouteDansListe, il y a un appel de la méthode AjouteFenetre de l'objet laFenetreListe. this est passé comme paramètre pour désigner une référence vers l'objet qui a reçu l'appel de la méthode AjouteDansListe.
L'encapsulation
L'encapsulation est une technique très importante dans les langages à objets. Elle consiste à masquer l'accès à certains attributs et méthodes des instances d'une classe.
L'encapsulation se fait en C++ à l'aide des mots-clés public, protected et private. Ces mots-clés permettent de définir la visibilité des attributs et des méthodes.
La visibilité définie par public est la visibilité totale. N'importe quelle instance pourra accéder aux attributs dont la visibilité est définie par le mot-clé public.
La visibilité définie par private est la visibilité privée. Seules les instances de cette classe pourront accéder aux attributs dont la visibilité est définie par le mot-clé private. La visibilité par défaut est celle de private.
Le mot-clé protected est équivalent à public dans la classe et à private dans les sous-classes de la classe. La notion de sous-classe sera étudiée dans le prochain article.
Soit l'exemple de classe suivant :
class Fenetre { private: short xpos,ypos; public: void move(short newxpos,short newypos); };Les attributs xpos et ypos sont privés. On ne peut pas y accéder depuis une méthode d'une autre classe.
Les classes et fonctions amies
Une déclaration de classes ou de fonctions amies commence par le mot-clé friend. Elle doit être faite à l'intérieur d'une classe. Par convention de style, on regroupe les déclarations de classes ou de fonctions après l'entête de la classe.
L'exemple ci-dessous montre trois exemples de déclarations d'amis : une fonction globale, une fonction d'une classe B, la classe B.
class A { friend void f(sort a); friend void B::g(void); friend class B; };Les fonctions globales amies peuvent servir pour résoudre certains cas où la modélisation par objets n'est pas simple. Par exemple, si on définit la classe matrice, la multiplication de deux matrices peut se faire soit par un envoi de message à la première matrice, soit à la seconde matrice, mais ce n'est pas très intuitif. Ce qui est plus intuitif est de réaliser une fonction globale amie qui va réaliser le produit et qui aura donc besoin d'accéder aux attributs de la classe matrice.
Soit la classe matrice :
class Matrice { friend Matrice multiplication(Matrice a,Matrice b); private: float elements[taille][taille]; }; Matrice multiplication(Matrice a,Matrice b) { // Il est possible d'accéder à l'attribut elements de la classe Matrice. }
Constructeurs et destructeurs
Par exemple, pour la classe Fenetre, il est possible de donner la position et la taille initiale de la fenêtre :
class Fenetre { private: short posx,posy,largeur,hauteur; public: Fenetre(short initposx,short initposy,short initlargeur,short inithauteur); }; Fenetre::Fenetre(short initposx,short initposy,short initlargeur,short inithauteur) { posx = initposx; posy = initposy; largeur = initlargeur; hauteur = inithauteur; }
Lors de l'allocation d'une instance de la classe Fenetre, il convient de donner ces valeurs initiales :
nouvFenetre = new Fenetre(10,10,100,150);
La syntaxe de l'héritage simple est la suivante :
class nom_de_classe_1 : [public | private | protected] nom_de_classe_2
Une des options public, private ou protected peut être choisie sans
que ce soit obligatoire. Par défaut, quand aucune option n'est choisie,
private est implicitement employée. Nous reviendrons par la suite sur
la sémantique de ces options.
Si nous employons cette syntaxe, nous signifions que la classe_1 hérite
de la classe_2 l'ensemble de ses attributs et méthodes. Ainsi une instance
de la classe_1 contiendra tous les attributs de la classe_2 et pourra répondre
aux appels des méthodes de la classe_2.
Dans l'exemple suivant, la classe Developpeur hérite de la classe Personne.
En effet, un développeur de logiciels est (encore !) une personne.
Sa description comporte donc les attributs d'une personne comme le nom, le
prénom, l'âge. Et donc, plutôt que de reporter explicitement
ces caractéristiques, nous allons faire hériter la classe des
développeurs de la classe des personnes. Ainsi un développeur
possédera les attributs des personnes, ce qui est tout à fait
correct puisqu'il est une personne.
Nous pouvons donc écrire :
class Personne { public: char nom[20],prenom[20]; long date_de_naissance; // nombre de secondes depuis le 1/1/1904, par exemple long get_age(void){return Time()-date_de_naissance;} };
La classe des développeurs, sous-classe de la classe Personne, s'écrit alors de la façon suivante :
class Developpeur : Personne { public: long salaire; short vitesse_ecriture; short rentabilite(void) {return vitesse_ecriture/salaire;} };
main() { Developpeur* Arnaud; Arnaud = new Developpeur; }
main() { Developpeur* Arnaud; Arnaud = new Developpeur; strcpy(Arnaud->nom,"Dupont"); // Erreur accès interdit }
Nous avons vu qu'il est possible de spécifier public ou private devant la définition des attributs et méthodes pour les rendre accessible ou non en dehors du corps des méthodes de la classe. Cependant, ces deux techniques d'encapsulation ne sont pas suffisantes pour prendre en compte les problèmes de l'héritage. En effet, quel est l'effet de ses mots-clé dans le corps des méthodes d'une sous-classe ?
Si un attribut a la qualité de public, il est effectivement visible dans les méthodes des sous-classes au même titre qu'il est visible à tout endroit du programme. Si un attribut a la qualité de private, il n'est pas visible dans les méthodes des sous-classes au même titre qu'il n'est pas visible à tout endroit du programme. Il serait cependant intéressant de pouvoir masquer un attribut à l'ensemble du programme sans le masquer dans les méthodes des sous-classes. Cette dernière possibilité existe en C++ avec l'emploi du mot-clé protected. Celui-ci s'emploie de la même façon que les mots-clé private et public.
Par exemple, dans la classe Developpeur, on veut masquer le salaire sauf dans les sous-classes :
class Developpeur : Personne { protected: long salaire; public: short vitesse_ecriture; short rentabilite(void) {return vitesse_ecriture/salaire;} };
Nous allons maintenant étudier ces différents cas d'héritage.
Cas où la super-classe est héritée de façon privée (mot-clé private ou pas de mot-clé)
Dans ce cas, tous les attributs hérités prennent la qualité de privé dans la sous-classe. Ainsi l'attribut "nom" devient privé dans la sous classe Developpeur et il n'est pas possible d'y accéder hors d'une méthode de la sous-classe. Dans l'exemple ci-dessus, les attributs hérités de la classe Personne dans la classe Developpeur sont privés. Ils ne sont donc pas visibles à l'extérieur du corps des méthodes des classes Personne et Developpeur pour une instance de la classe Developpeur.
Cas où la super-classe est héritée de façon publique (mot-clé public)
Dans ce cas, tous les attributs hérités conservent leur qualité dans la sous-classe. Ainsi l'attribut "nom" reste public dans la sous-classe Developpeur. Un attribut défini comme protégé comme salaire dans la classe Developpeur sera également protégé dans une sous-classe héritée de façon publique de Developpeur.
Cas où la super-classe est héritée de façon protégée (mot-clé protected)
Dans ce cas, les attributs hérités ayant la qualité public prennent la qualité de protected dans la sous-classe, les attributs hérités ayant une autre qualité la conservent. Ainsi, si on écrit l'exemple ci-dessus de la façon suivante :
class Developpeur : protected Personne { protected: long salaire; public: short vitesse_ecriture; short rentabilite(void) {return vitesse_ecriture/salaire;} };
Dans les sous-classes, la redéfinition de la méthode doit se faire en respectant le même nombre de paramètres, le même type pour chacun des paramètres et le même type pour le retour de la fonction. Il convient aussi de noter que le mot-clé virtual ne doit pas être nécessairement réutilisé dans la sous-classe, mais qu'il est préférable de le faire pour des raisons de clarté.
Par exemple, on définit une sous-classe de la classe des développeurs, qui sont les développeurs en langage objet pour lesquels la rentabilité est définie par le ratio competence/salaire. Pour réaliser ceci, il faut revoir la définition de la classe Developpeur pour que la méthode rentabilite soit définie comme une méthode virtuelle :
class Developpeur : public Personne { protected: long salaire; public: short vitesse_ecriture; virtual short rentabilite(void) {return vitesse_ecriture/salaire;} }; class Developpeur_objet : public Developpeur { public: short competence; virtual short rentabilite(void) {return competence /salaire;} };Si on définit une instance de la classe Developpeur_objet, la fonction rentabilite appelée sera celle définie dans la classe Developpeur_objet. Soit, par exemple :
Patrick = new Developpeur_objet; Patrick->competence = 100; Patrick->salaire = 50; rentabilitePatrick = Patrick->rentabilite; // rentabilitePatrick vaut alors 2, la fonction rentabilite définie dans la classe Developpeur_objet a été choisie.Par contre, si Patrick était une instance de la classe Developpeur, c'est la méthode définie dans cette dernière classe qui aurait été choisie.
Par exemple, dans l'exemple précédent l'instance pointé par le pointeur Patrick est une instance indirecte de la classe Developpeur. Ainsi l'affectation possible est possible :
Developpeur* ptrDev; ptrDev = Patrick; rentabilitePatrick = ptrDev->rentabilite; // rentabilitePatrick vaut alors 2, la fonction rentabilite définie dans la classe Developpeur_objet a été choisie.
ptrDev->competence = 100;
Car competence n'est pas un attribut defini dans la classe Developpeur.
Classes abstraites
Une classe abstraite est définie comme une classe classique mais contenant une ou plusieurs méthodes virtuelles dont le corps n'est pas défini et dont le prototype est suivi par "=0".
Par exemple, nous voulons réaliser un éditeur d'objets graphiques. Nous allons d'abord définir une classe abstraite Objet_graphique :
class Objet_graphique { public: Rect enclosing_rect; virtual void draw(void)=0; void erase(void) {EraseRect(&enclosing_rect);} };
Objet_graphique est une classe abstraite car la méthode draw n'est pas définie. Nous allons maintenant définir deux sous-classes de Objet_graphique, qui sont Ovale_graphique et Rectangle_graphique :
class Ovale_graphique : public Objet_graphique { public: virtual void draw(void) {FrameOval(&enclosing_rect);} }; class Rectangle_graphique : public Objet_graphique { public: virtual void draw(void) {FrameRect(&enclosing_rect);} }; Soit le programme suivant : main () { Objet_graphique* liste_objets[3]; short i; liste_objets[0] = new Ovale_graphique; liste_objets[1] = new Rect_graphique; liste_objets[2] = new Ovale_graphique; /* on fixe ici les coordonnées des rectangles de trois instances qui ont été créées dans les trois lignes précédentes */ /* affichage des objets graphiques */ for (i=0;i<3;i++) liste_objets[i]->draw(); //appel de la fonction draw en fonction de la classe d'instanciation }
En C++, il est également possible d'utiliser l'héritage multiple. Ce dernier offre la possibilité d'hériter les attributs et méthodes de plusieurs super-classes. La syntaxe est proche de celle de l'héritage simple. Chaque super-classe est séparée par une virgule de la précédente. Ce qui donne la syntaxe suivante : class nom_de_classe_1 : [public | private | protected] super_classe {, [public | private | protected] super_classe}
Pour chaque super-classe, on peut définir si elle est héritée en mode privé, public ou protégé. Ceci donnera la qualité correspondante aux attributs hérités de cette classe.
Le fonctionnement est le même que pour l'héritage simple. Nous l'illustrons sur un exemple :
Nous créons une classe Etudiant, en supposant qu'un étudiant touche un salaire (une bourse, par exemple, est, dans certains cas, un salaire).
class Etudiant : public Personne { protected: short salaire; public: char etudes_suivies[30]; };
class EtuDev : public Etudiant, public Developpeur { public: long temps_etudiant,temps_dev; // temps par semaine comme etudiant et comme développeur long revenu_complet(void); };
Il est alors possible de définir des intances d'EtuDev, qui héritent des propriétés des classes Etudiant et Developpeur.
Nous allons maintenant définir le corps de la fonction revenu_complet qui doit donner l'ensemble des revenus, en tant qu'étudiant et en tant que développeur. Le problème que nous rencontrons est que nous avons défini l'attribut salaire dans les deux super-classes et que nous avons besoin de les distinguer. Pour cela, nous utilisons la notation préfixée devant un nom d'attribut ou de méthode qui est nom_de_classe::nom_attribut ou nom_de_classe::nom_methode.
Le corps de la méthode revenu_complet est donc :
long EtuDev::revenu_complet(void) { return etudiant::salaire + developpeur::salaire; }Un autre problème va se poser. Dans ce que nous avons fait, les attributs de la classe Personne ont été hérités deux fois dans la classe EtuDev. De ce fait, les deux expressions suivantes désignent deux attributs nom d'une instance de la classe EduDev :
Etudiant::nom et Developpeur::nom
Pour remédier à ce problème, il convient d'hériter la classe Personne avec le mot-clé virtual dans les classes Developpeur et Etudiant, ce qui va donner :
class Developpeur : virtual public Personne { }; class Etudiant : virtual public Personne { };
Si les classes Developpeur et Etudiant héritent de la classe Personne
avec le mot-clé virtual, alors la classe EtuDev n'hérite qu'un
seul attribut "nom" provenant de la classe Personne. Si une des deux classes,
ou les deux classes n'héritent pas avec le mot-clé virtual,
la classe EtuDev possède deux attributs nom.
Constructeurs/destructeurs et héritage
Nous donnons d'abord un bref rappel sur les constructeurs et les destructeurs. Un constructeur est une méthode de même nom que la classe qui ne renvoie rien. Il est appelé lors de l'instanciation de l'objet par New ou si l'objet est défini statiquement, au moment de l'allocation de l'objet dans la pile (si c'est une variable locale d'une procédure, au moment de l'exécution de cette procédure).
Un destructeur est une méthode du nom de la classe précédé de "~" qui ne renvoie rien. Il est appelé lors de la destruction de l'objet par Delete ou si l'objet est défini statiquement, au moment de la désallocation de l'objet dans la pile (si c'est une variable locale d'une procédure, au moment de la fin de l'exécution de cette procédure). Un destructeur ne peut pas avoir de paramètres et ne peut donc pas être surchargé (c'est à qu'il ne peut pas y avoir plusieurs destructeurs différents distingués selon le type des paramètres).
Pour l'héritage, chaque constructeur d'une sous-classe va appeler le constructeur de la super-classe en faisant suivre le prototype du constructeur (dans la partie définition du corps de la méthode) d'un appel au constructeur de la super-classe.
La syntaxe est la suivante :
nom_du_constructeur(paramètres_formels): constructeur_super_classe(paramètres_effectifs)
L'appel au constructeur de la super-classe se fait comme un appel classique de méthodes pour ce qui concerne le calcul des paramètres. En cas d'héritage multiple, chaque constructeur de chaque super-classe doit être appelé, en séparant chaque appel d'une virgule.
L'appel au constructeur de la super-classe se fait avant l'exécution du corps du constructeur de la classe. Pour les destructeurs, l'appel au destructeur de la super-classe est implicite et se fait après l'exécution du corps du destructeur de la classe. Notons aussi que l'ordre d'appel des destructeurs en cas d'héritage multiple est toujours l'inverse de celui des constructeurs.
En cas de classe virtuelle héritée une seule fois dans une sous classe unique mais par plusieurs chemins différents, les constructeurs des classes placées sur ces chemins et qui sont appelés par le constructeur de la sous-classe unique, verront leur appel au constructeur de la super-classe virtuelle annulé. Sinon la classe virtuelle aurait été construite plusieurs fois. Dans ce cas, c'est au constructeur de la sous classe unique d'appeler explicitement le constructeur de la super-classe virtuelle. On retrouve la même situation pour les destructeurs, où le destructeur de la sous-classe va appeler directement et implicitement celui de la super-classe virtuelle.
Par exemple, soit la classe abstraite Objet_graphique, modifiée pour accepter les coordonnées de l'objet dans son constructeur et pour construire une région correspondant au rectangle délimitant le bord graphique de l'objet :
class Objet_graphique { private: RgnHandle enclosing_rgn; public: Rect enclosing_rect; virtual void draw(void)=0; void erase(void) {EraseRect(&enclosing_rect);} Objet_graphique(short left,short right, short bottom, short top); ~Objet_graphique(void) {DisposeRgn(enclosing_rgn);} }; Objet_graphique::Objet_graphique(short left,short right, short bottom, short top) { enclosing_rect.left = left; enclosing_rect.right = right; enclosing_rect.bottom = bottom; enclosing_rect.top = top; enclosing_rgn = NewRgn(); RectRgn(enclosing_rgn,&enclosing_rect); }Certes, Objet_graphique est une classe abstraite qui ne peut donc pas servir à créer des instances. Mais les constructeurs et destructeurs ainsi définis vont servir dans les sous-classes d'Objet_graphique :
class Ovale_graphique : public Objet_graphique { public: virtual void draw(void) {FrameOval(&enclosing_rect);} Ovale_graphique(Rect initRect) : Objet_graphique(&initRect) {erase();} }; class Rectangle_graphique : public Objet_graphique { public: virtual void draw(void) {FrameRect(&enclosing_rect);} Rectangle_graphique(Rect initRect) : Objet_graphique(&initRect) {erase();} };
Si, ensuite, on crée une instance de Rectangle_graphique, et qu'après on la détruit, l'exécution donnera les évènements suivants :
Rectangle_graphique* RectGraph; RectGraph = new Rectangle_graphique(100,100,200,200); /* appel du constructeur d'Objet_graphique puis de erase */ delete RectGraph; /* appel du destructeur d'Objet_graphique */
La définition de la classe est la suivante :
const taillemax = 100; typedef short Boolean ; class Pile { private: short Elements[taillemax]; short indiceSommet; public: Pile(void) {indiceSommet=0;} void empile(short NouvSommet); short depile(void); Boolean vide(void); }; void Pile::empile(short NouvSommet) { if (indiceSommet < taillemax) { Elements[indiceSommet] = NouvSommet; indiceSommet++; } } short Pile::depile(void) { indiceSommet--; return Elements[indiceSommet]; } Boolean Pile::vide(void) { return indiceSommet = 0; }La définition de cette classe et des méthodes associées est très simple et très pratique à utiliser :
Pile *mapile; short valeurSommet; mapile = new Pile(); mapile->empile(5); valeurSommet = mapile->depile();
Pour remédier à ce genre de problèmes, le langage C++
introduit la notion de classes "template". Une classe "template" est une classe
dont la définition est paramétrée par des types de données
(classes ou types de données prédéfinis) ou par des constantes.
Il s'agit donc d'une définition générique qui devra ensuite
être spécifiée en donnant des valeurs aux paramètres
formels à l'aide de paramètres effectifs.
Définition des classes "template"
definition_classe_template ::= templateclass nom_de_classe; nom_de_classe ::= identificateur liste_de_parametres_formels ::= def_param_formel , {def_param_formel} def_param_formel ::= class | type_de_donnees nom_du_type nom_du_type ::= identificateur
const taillemax = 100; typedef short Boolean; templateclass Pile { private: T Elements[taillemax]; short indiceSommet; public: Pile(void) {indiceSommet=0;} void empile(T NouvSommet); T depile(void); Boolean vide(void); }; template void Pile ::empile(T NouvSommet) { if (indiceSommet < taillemax) { Elements[indiceSommet] = NouvSommet; indiceSommet++; } } template T Pile ::depile(void) { indiceSommet--; return Elements[indiceSommet]; } template Boolean Pile ::vide(void) { return indiceSommet = 0; }
Cette fois-ci, nous avons créé une classe générique Pile dont les éléments pourront contenir n'importe quel type de données. Mais avant d'utiliser cette classe "template", il convient de faire quelques commentaires sur la définition de la classe Pile.
La désignation générique de la classe Pile devient
Pile T est un paramètre formel de type "type". Le paramètre effectif
pourra être un type de données quelconque.
Par exemple, on veut une pile de nombres entiers courts, on pourra écrire
:
Si l'on veut maintenant une pile de nombres flottants, on peut écrire
:
Ces deux pointeurs peuvent être initialisés de la façon
suivante :
puis utilisés de la façon suivante :
mapileEntier->empile(3); Ce petit exemple montre l'emploi d'une classe "template". Notons qu'un type
de classe "template" peut être utilisé partout où un type
de classe ordinaire peut être utilisé. Ensuite les instances
de la classe "template" sont utilisées de la même façon
que les instances d'une classe ordinaire.
Par exemple, une fonction qui calcule la somme de deux nombres :
b= add(2,3);
Paramètres de classes "template" dont le type est un type de données
ordinaire
Il est aussi possible que le type des paramètres d'une classe "template"
soit un type de données ordinaire. Par exemple, pour la pile, il est
alors possible de spécifier sa taille maximale comme paramètre,
ce qui donne :
short Point::* pcoord;
short Point::* est le type complet du pointeur pcoord. On peut ensuite affecter
le pointeur pcoord de la façon suivante :
pcoord = &Point::h;
pcoord est alors un pointeur sur le champ h de la classe Point. Il peut
ensuite être utilisé sur une instance de la classe Point (par
exemple unpoint) de la façon suivante :
unpoint.*pcoord // si unpoint a été défini par Point
unpoint
unpoint->*pcoord // si unpoint a été défini par Point*
unpoint
De la même façon, il est possible de définir un pointeur
sur une méthode. Par exemple, si on veut définir un pointeur
sur une méthode qui ne renvoie rien (procédure) et qui prend
deux arguments de type short, on écrira :
void (Point::*pmethode)(short,short);
pmethode peut ensuite être défini comme un pointeur sur une
méthode de la classe Point possédant les mêmes types de
paramètres et renvoyant un résultat de même type. Par
exemple, il est possible d'affecter le pointeur pmethode à la méthode
move :
pmethode = &Point::move;
Puis, ensuite, il est possible d'utiliser ce pointeur pour instance de la
classe Point :
(unpoint.*pmethode)(2,3); // si unpoint a été défini
par Point unpoint
(unpoint->*pmethode)(2,3); // si unpoint a été défini
par Point* unpoint
Les parenthèses à gauche sont nécessaires car, en leur
absence, celles de droite sont prioritaires sur l'opérateur . ou ->.
En cas d'absence, le résultat de la fonction serait alors utilisé
comme pointeur d'un attribut d'une classe. En effet, par exemple, la première
ligne serait interprétée de la façon suivante :
unpoint.*(pmethode(2,3));
et donc le résultat de la fonction pmethode devrait être un
pointeur sur un attribut de la classe Point.
Un exemple de redéfinition de l'opérateur + dans une classe
de chaînes de caractères est :
Un exemple d'utilisation est le suivant :
Nous expliquons maintenant comment surcharger les opérateurs les
plus complexes. Par exemple, soit l'appel suivant :
Si x est un pointeur vers un objet, la méthode dessine de cet objet
est invoquée. Si x est un objet, alors la méthode operator->()
de cet objet est appelée. Si celle-ci renvoie, à nouveau, un
objet, la méthode operator >() lui est appliquée. Si celle-ci
renvoie un pointeur, alors la méthode dessine de l'objet pointé
est invoquée. Pile
Pile
mapileEntier = new Pile
mapileReel->empile(3.5);
Spécialisation d'une classe "template"
Une classe "template" peut être spécialisée pour certaines
valeurs d'un paramètre formel. Pour cela, il faut écrire la classe
et/ou les méthodes que l'on veut spécialiser pour cette valeur
du paramètre, en indiquant la valeur correspondante. Par exemple, on
veut spécialiser la classe Pile
class Pile {
private:
char Elements[taillemax][256];
short indiceSommet;
public:
Pile(void) {indiceSommet=0;}
void empile(char* NouvSommet);
char* depile(void);
Boolean vide(void);
};
void Pile::empile(char* NouvSommet)
{
if (indiceSommet < taillemax)
{
strncpy(Elements[indiceSommet], NouvSommet, 256);
indiceSommet++;
}
}
char* Pile::depile(void)
{
indiceSommet--;
return Elements[indiceSommet];
}
Boolean Pile::vide(void)
{
return indiceSommet = 0;
}
Il est nécessaire de réécrire toutes les méthodes
pour la classe Pile.
Fonctions "template"
De la même façon que pour une classe, il est possible de définir
des fonctions "template". La syntaxe est la même que pour les classes.
Lors de l'appel de la fonction, il faudra spécifier les paramètres
effectifs.
template
Cette fonction peut être instanciée avec tout type de données
possédant l'opérateur "+". Par exemple, si b est un short, on
peut écrire :
Classes "template" et classes/fonctions amies
Si la classe A est, elle-même, une classe "template", elle peut indiquer
que ses classes "template" amies le sont que pour les valeurs de ses paramètres
formels. Par exemple, si la classe A est une classe "template" qui prend un
paramètre "class Type", A peut choisir comme classe amie que la classe
Pile dont les éléments sont de ce type.
template
C'est la seule façon de rendre une classe "template" amie d'une autre
classe "template". Une classe "template" ne peut pas être amie d'une classe
non "template".
typedef short Boolean;
template
On peut alors écrire :
Pile *mapileEntier;
mapileEntier = new Pile();
Pointeurs sur attributs et méthodes d'une classe
En C++, vous pouvez définir un pointeur sur un attribut ou une méthode
d'une classe. Soit la classe Point définie de la façon suivante
:
class Point {
public:
short h;
short v;
void move(short deltah,short deltav);
};
void Point::move(short deltah,short deltav)
{
h += deltah;
v += deltav;
}
La définition d'un point sur un attribut de la classe Point de type short
s'écrit de la façon suivante :
Définition des opérateurs en C++
En C++, il est possible de définir un certain nombre d'opérateurs,
en surchargeant les opérateurs existants. Les opérateurs suivants
peuvent être définis comme fonctions globales ou comme méthodes
de classe :
+ - * / % ^ & |
~ ! , < > <= >=
++ -- << >> == != && ||
+= -= /= %= ^= &= |= *=
<<= >>= ->* new delete
Les opérateurs = [] () -> peuvent être définis comme méthode
de classe, mais pas comme fonctions globales.
Enfin les opérateurs :: .* . ?: ne peuvent être surchargés.
class String {
private:
short len;
char* str;
public:
String() {}
String(char* s);
String operator+(String& b);
};
String ::String(char* s)
{
len = strlen(s);
str = NewPtr(len+1);
strcpy(str,s);
}
String String::operator+(String& b)
{
String result;
result.len = this->len + b.len;
result.str = NewPtr(result.len+1);
strcpy(result.str,this->str);
strcat(result.str,b.str);
return result;
}
String a("abcd");
String b("efgh");
String c;
c = a + b;
/* c contient la chaîne "abcdefgh" */
L'opérateur []
Celui-ci doit renvoyer une valeur qui peut être le résultat d'une
expression mais qui peut également être affectée (partie
gauche d'une instruction d'affectation). De ce fait, le résultat de cet
opérateur doit être une référence vers l'objet qui
est le résultat.
L'opérateur new
L'opérateur delete
L'opérateur d'allocation des instances peut être surchargé
mais il doit prendre void* comme type de retour et, en premier argument, un
paramètre de type size_t. Ce paramètre est automatiquement initialisé
par le compilateur avec la taille en octets de l'instance à créer.
L'opérateur de suppression des instances ne doit rien renvoyer (void)
et doit prendre en premier argument un paramètre de type void* qui est
un pointeur vers l'instance à détruire.
L'opérateur =
Celui-ci permet de réaliser l'affectation d'une instance vers une autre.
Il faut prendre attention à bien recopier les valeurs des attributs,
à détruire leur ancienne valeur si nécessaire (il faut
se méfier de l'affectation d'une instance à elle-même).
Par ailleurs, l'opérateur doit renvoyer une référence vers
l'instance qui a reçu l'affectation.
L'opérateur ->
S'il est invoqué pour une instance d'une classe qui surcharge cet opérateur,
cette surcharge est appelée. Cet opérateur peut soit renvoyer
une instance d'une classe où cet opérateur est défini (cette
redéfinition sera alors invoquée) soit un pointeur vers une instance
auquel cas, l'attribut ou la méthode correspondant à l'instance
référencée sera retourné.
x->dessine();