Programmation en C++
Programmation en C++
Programmation en C++
Anne 2005-2006
Programmation en C++
1 Bertrand Cottenceau
1. 2.
Introduction du cours de C++ .................................................................................................................................. 3 Le C++ en dehors des classes dobjets ....................................................................................................................... 3 2.1. Les notions du C utilises en C++ ....................................................................................................................... 3 2.2. Les rfrences en C++........................................................................................................................................... 8 2.3. Les paramtres avec valeurs par dfaut ............................................................................................................... 11 2.4. La surcharge de fonctions.................................................................................................................................. 12 3. Introduction la POO et la reprsentation de classes en UML........................................................................... 12 4. Les classes en C++................................................................................................................................................... 15 4.1. Les classes ostream et istream............................................................................................................................. 15 4.2. Mthodes et attributs......................................................................................................................................... 15 4.3. Constructeurs : mthodes d'initialisation d'un objet ......................................................................................... 17 4.4. Diffrence entre slecteurs/accesseurs et modificateurs...................................................................................... 19 4.5. Constructeur de copie, oprateur daffectation ................................................................................................. 19 5. Les oprateurs : fonctions globales ou mthodes d'une classe................................................................................. 21 5.1. La pluralit des oprateurs du C++ .................................................................................................................... 21 5.2. Les oprateurs avec traitement par dfaut ......................................................................................................... 21 5.3. Les oprateurs que lon peut surcharger en C++ ................................................................................................ 21 5.4. Le mcanisme de dfinition des oprateurs en C++........................................................................................... 22 5.5. Exemple : classe de nombres rationnels............................................................................................................. 22 6. L'utilisation de classes paramtres en type (introduction la bibliothque STL) .................................................. 24 6.1. Introduction ...................................................................................................................................................... 24 6.2. Namespace......................................................................................................................................................... 26 6.3. Classe string ................................................................................................................................................. 27 6.4. Conteneurs vector<T> ................................................................................................................................. 28 6.5. Conteneurs list<T> ..................................................................................................................................... 30 7. Ralisation de la composition en C++.................................................................................................................... 31 7.1. Reprsentation UML de la composition ............................................................................................................ 31 7.2. Objets composants comme attributs dune classe.............................................................................................. 32 7.3. Utilisation d'attributs de type tableau d'objets.................................................................................................. 34 7.4. Utilisation d'un objet membre de type vector<T> ...................................................................................... 35 8. Ralisation de la spcialisation en C++................................................................................................................... 35 8.1. Reprsentation de la spcialisation en UML...................................................................................................... 35 8.2. Exemple de ralisation de la spcialisation en C++............................................................................................ 36 8.3. Les conversions de type entre sous-classe et super-classe..................................................................................... 38 8.4. Le polymorphisme et les mthodes virtuelles..................................................................................................... 39 8.5. Les classes abstraites........................................................................................................................................... 41 9. Visibilit des membres dune classe......................................................................................................................... 42 9.1. Mot-cl friend : fonctions ou classes amies................................................................................................... 42 9.2. Membres protgs (mot-cl protected ) et visibilit des membres................................................................. 43 9.3. Navigation dans une hirarchie de classes, phnomnes de masquage............................................................... 45 10. Classes avec donnes en profondeur....................................................................................................................... 46 10.1. Les oprateurs de gestion de mmoire (new et delete)................................................................................... 46 10.2. Ralisation de la composition par gestion dynamique de mmoire (donnes en profondeur)...................... 47 10.3. Ralisation de conteneurs : tableaux, piles, files, listes .................................................................................. 48 11. Les fonctions et les classes paramtres en type....................................................................................................... 52 11.1. Les fonctions paramtres en type................................................................................................................ 52 11.2. Classes paramtres en type........................................................................................................................... 53 12. Les flots entre/sortie, les fichiers ........................................................................................................................... 55 12.1. Les dclarations d'numrations au sein des classes ...................................................................................... 55 12.2. La dclaration d'une classe au sein d'une classe............................................................................................. 55 12.3. La hirarchie des classes de flots.................................................................................................................... 56 12.4. Les fichiers .................................................................................................................................................... 57 12.5. L'accs direct dans les fichiers ....................................................................................................................... 58 13. Les changements de type......................................................................................................................................... 59 13.1. Les constructeurs comme convertisseurs de type........................................................................................... 59 13.2. Conversion dun type classe vers un type primitif ....................................................................................... 61 14. Conclusion et fiches synthtiques........................................................................................................................... 63
Le C++ est un langage typ. Le compilateur C++ vrifie donc la compatibilit des types lors des oprations arithmtiques, des affectations et des passages de paramtres aux fonctions. 2.1.1.
double ,
Les types primitifs du C++ sont les mmes quen C, savoir char, short, int, long, float, les variables non signes unsigned char, unsigned short, unsigned int et les variables pointeur 3
etc. Le nombre doctets assign chacun de ces types peut varier dun
compilateur un autre. Rappel : la taille dune variable (exprime comme multiple de la taille dun char) sobtient laide de loprateur sizeof .
int var; printf("taille de var=%d, taille dun char= %d",sizeof(var),sizeof(char));
2.1.2.
Les pointeurs
Les variables pointeurs sont toujours utilises en C++ notamment pour le passage de tableaux en paramtre dune fonction. Rappelons quun pointeur reprsente un type. Une variable de type T* est destine recevoir ladresse dune variable de type T. Lorsque ptr est une variable pointeur de type T*, loprateur * permet de d-rfrencer ce pointeur : *ptr est la variable de type T dont ladresse est stocke dans la variable ptr . Il est noter que ptr est une variable et *ptr en est une autre. Par ailleurs, loprateur & permet de fournir ladresse dune variable. En synthse on a :
int var=12; int * ptr; // variable de type int dclare et initialise // variable de type pointeur sur int non initialise.
// ne pas excuter *ptr=45 ici, la variable pointeur nest pas initialise !!! ptr=&var ; *ptr=45 ; // ptr initialise avec ladresse de la variable var. // *ptr d-rfrence ptr. A cet instant, *ptr est la variable var.
Rappel : puisque les pointeurs sont typs, le compilateur ralise aussi des vrifications de type impliquant les pointeurs :
int varI=12; int * ptrI=&varI; char varC=12; char* ptrC=&varC; ptrC=&varI ; // erreur de compilation : char * non compatible avec int * ptrI=&varC ; // idem : int * pas compatible avec char *
2.1.3.
En C et en C++, le nom dun tableau correspond ladresse de sa premire case. De plus, on peut toujours utiliser une variable pointeur comme sil sagissait de ladresse de la premire case dun tableau :
int tab[3]={1,2,7}; int * ptrI=tab ; ptrI[2]=3 ; ptrI=&tab[1] ; ptrI[1]=2 ; // // // // // tableau de 3 int initialiss licite, identique ptrI=&tab[0] licite : ptrI[2] quivaut ici tab[2] licite aussi , identique ptrI=tab+1 ptrI[1] quivaut maintenant tab[2]
2.1.4.
Le C++ utilise la mme syntaxe que le langage C pour les diffrentes structures de contrle. Les structures de contrle sont if/else, for et while (ou do while). 4
La structure if(){}else{}
int entier; printf("Saisir un entier:\n"); scanf("%d",&entier); if((entier%2)==0) { printf("Entier pair"); } else { printf("Entier impair"); }
La structure for(
; ;){}
int tab[10]; // rserve un tableau de 10 entiers (non initialiss) int indice; tab[0]=0; // initialise le premier lment 0 for(indice=1;indice<10;indice++) { tab[indice]=tab[indice-1]+indice; }
// tab[indice]=indice + (indice-1)+...
2.1.5.
Le compilateur C++ utilise et vrifie les types pour sassurer que les oprations ralises sont cohrentes et que les paramtres passs une fonction correspondent aux paramtres attendus. Nanmoins, dans certains cas, le compilateur autorise des conversions implicites (conversions que le compilateur peut dduire grce au contexte) ou explicites de types. Il faut noter que certaines conversions implicites gnrent tout de mme des mises en garde (warnings) au moment de la compilation (par exemple lorsque lon compare un entier sign avec un entier non sign). Il y a par exemple conversion implicite quand on affecte la valeur dune variable de type float une variable de type int (ou linverse). Dans cette situation, le compilateur met en place automatiquement une conversion de type. Cette conversion est dite dgradante (perte dinformation) dans le sens float-> int. Lexemple ci-dessous prsente quelques conversions de type.
int varI=513; char varC; float varF=2.5 ; // variable entire sur 32 bits sign // variable entire sur 8 bits sign // variable utilisant la norme flottant IEEE 32 bits
varC=varI; // conversion dgradante varI=varF; // ici, un warning signale possible loss of data printf("%d %d",varC,varI); // affiche 1 (513mod256) et 2(partie entire de 2.5)
Lorsque le programmeur souhaite outrepasser les incompatibilits de type, celui-ci peut indiquer explicitement la conversion raliser. On parle alors de conversion explicite, de transtypage explicite ou encore de cast. Dans ce cas, le programmeur a la responsabilit de sassurer de la validit de lopration.
int varI=513; char * ptrC =(char *) &varI; printf("%d %d",ptrC[0],ptrC[1]);
// conversion int*-> char* demande // affiche les deux octets de poids faible de varI
Lors des conversions explicites (comme ci-dessus), cest au programmeur de vrifier scrupuleusement ce quil fait. Cet aspect du langage donne beaucoup de libert au programmeur et permet de grer des problmes de bas niveau. Mais labus de conversions explicites, notamment pour rgler des warnings rcalcitrants la compilation, peut conduire de grosses erreurs de programmation. Lexemple suivant illustre ce quil ne faut pas faire mme si le compilateur lautorise.
int varI=513; char * ptrC=(char *) &varI; int adresse=(int) ptrC; int * pI=(int *) adresse; *pI=2; printf("%d",varI);
// // // //
cast int * -> char * cast char * -> int (a cest vraiment douteux) cast int -> int * (aussi douteux) miraculeusement, pI contient ici ladresse de varI.
Les conversions de pointeurs sont parfois utiles pour tromper le compilateur. En revanche les conversions de pointeurs en valeurs numriques (ou linverse) sont farfelues. Lexemple prcdent montre que le compilateur accepte tout et nimporte quoi ds quon utilise les conversions de type. Rappelons quelques cast utiles :
T * -> V* : conversion de pointeurs. Les adresses restent intactes car elles ont la mme taille. Mais ces adresses ne dsignent pas des variables de mme type ! A utiliser avec prcaution. float -> int : (dgradant) conserve la partie entire. int -> float : passage au format flottant IEEE char -> int : la variable passe sans altration de 8 bits 32 bits int -> char : dgradant : les 32 bits sont tronqus aux 8 bits de poids faible.
2.1.6.
En C++, on utilise abondamment les fonctions au sein des classes (on parlera de fonctions membres ou de mthodes dune classe). Il est donc ncessaire de rappeler brivement le mode de transmission des paramtres. En C et en C++, les paramtres sont passs une fonction par copie , cest--dire via la pile. Le programme qui utilise une fonction recopie dans la pile les paramtres dappel (encore appels paramtres effectifs). Par consquent, la fonction appele na accs (dans la pile) qu une copie des paramtres effectifs, et non loriginal. Il faut noter que les variables locales une fonction sont galement des emplacements mmoire allous dans la pile (comme les paramtres). En rsum, quil sagisse des paramtres ou des variables locales une fonction, ce sont des variables temporaires qui nexistent que le temps dexcution de la fonction.
void MaFonction(int param) // param ppv pAppel { param++ ; // ici, param est modifi mais pas pAppel }
Vu diffremment, on peut considrer quau dbut de la fonction le paramtre formel (param) est une variable locale initialise laide de la valeur du paramtre dappel, mais quil sagit bien de deux variables distinctes. Ce mode de passage est appel passage par copie ou passage par valeur. Par consquent, lorsque lon souhaite quune fonction modifie une variable, on na pas dautre choix que de lui fournir ladresse de la variable modifier. On appelle souvent cette technique passage de paramtres par adresse.
void MaFonction(int * adrVariable) { (*adrVariable)++; } void main() { int pAppel=23; int * adrAppel=&pAppel; MaFonction(adrAppel) ; printf("pAppel =%d",pAppel); //pAppel vaut ici 24 }
Remarquez bien que cette fois-ci cest ladresse qui est passe par copie, mais on peut dsormais modifier la variable dsigne par ladresse contenue dans adrAppel, peu importe que la variable pointeur adrAppel ne puisse pas tre modifie par la fonction ! 2.1.7. Performances lies au mode de transmission des paramtres
Le mode de transmission des paramtres peut avoir des rpercussions sur la rapidit dexcution. Considrons le programme suivant o un type structur personne est dfini. Une variable de type personne contient 3 champs : deux tableaux de 30 caractres et un entier non sign. Une variable de type personne a une taille de 64 (60 char + 1 unsigned cod sur 32 bits). Rappel : pour les pointeurs sur des variables structures, la notation ptr->nom est quivalente
(*ptr).nom ----------------------------------------------------------------------------------struct personne { char nom[30]; char prenom[30]; unsigned age; }; void Affiche(personne p) { printf(" nom : %s prenom : %s age : %d \n",p.nom,p.prenom,p.age); } void AfficheBis(personne * ptr) { printf(" nom : %s prenom : %s age : %d \n",ptr->nom,ptr->prenom,ptr->age); }
void main(void) { personne AM={"Martin","Arthur",32}; printf("sizeof(AM) = %d\n",sizeof(AM)); // affiche : sizeof(AM)=64 Affiche(AM); AfficheBis(&AM); // excution plus rapide que Affiche(AM) } -----------------------------------------------------------------------------------
La fonction AfficheBis(personne *) est plus rapide que la fonction Affiche(personne) pour un service quivalent. Pourquoi ? Lappel de la fonction Affiche(personne) ncessite une recopie dune variable de 64 octets dans la pile, alors que lappel de la fonction AfficheBis(personne *) ne requiert que la copie dun pointeur de 32 bits (4 octets). Les soucis de performance imposent au programmeur de bien connatre tous ces aspects afin de raliser les meilleurs choix. On verra quen C++ on utilisera le passage de rfrence plutt que le passage dadresse pour amliorer les performances. 2.1.8. Les paramtres constants : garantie d'un code avec moins de bugs
float Division(int num,int den) { if( den=0 ) return num; return (float)num/den ; }
La fonction prcdente contient une erreur de programmation que le compilateur ne peut dtecter. En effet, den=0 est une affectation et non un test dgalit. Le paramtre den prend donc systmatiquement la valeur 0. Comment prvenir de telles erreurs ? On peut utiliser le modificateur const pour les paramtres dentre (paramtres qui ne sont pas censs voluer dans la fonction).
float Division(const int num,const int den) { if(den=0) return num; // error C2166: l-value specifies const object else return (float)num/den ; }
Dans ce cas, le compilateur indique quune variable (ou un objet) constante est gauche dune affectation (l-value). Le compilateur dtecte ainsi trs facilement ce type derreur. Le programmeur qui prend lhabitude de dclarer comme constants les paramtres dentre prvient ce type derreur. 2.2. Les rfrences en C++ Loprateur & ci-dessous correspond la dfinition dune rfrence (o T est un type).
T var; T & ref = variable ; // ref est une rfrence var
Ci-dessus, ref est une rfrence la variable var de type T.1 Cela signifie que les identificateurs var et ref dsignent la mme variable. Manipuler l'un, revient manipuler l'autre. Ds lors, var et ref sont des synonymes, ou des alias . L'initialisation d'une rfrence lors de sa dclaration est obligatoire.
Attention ne pas confondre lemploi de loprateur & pour obtenir ladresse dune variable et pour dclarer une rfrence ! 8
----------------------------------------------------------------------------------#include <iostream.h> void main(void) Rsultats { I = 3 int I=3; refI = 3 int &refI = I; adresse de I : 0065FDF4 adresse de refI : 0065FDF4 printf("I = %d \n",I); Incrment de I (I++) printf("refI = %d \n",refI); refI = 4 printf("adresse de I = %x \n",&I); printf("adresse de refI = %x \n",&refI); printf("Incrment de I (I++)"); I++; printf("refI = %d \n",refI); } -----------------------------------------------------------------------------------
On constate que les variables I et refI ont la mme adresse . Elles dsignent donc bien la mme variable. Puisque I et refI sont des alias, modifier lun revient modifier lautre. 2.2.1. Les rfrences en C++ : un nouveau mode de transmission de paramtres
Lutilisation des rfrences faite prcdemment ne prsente pas dintrt particulier. Lutilisation la plus courante en C++ concerne lchange dinformations avec une fonction. Cette partie du cours est essentielle et doit faire lobjet dune attention toute particulire La notion de rfrence va nous affranchir, dans beaucoup de cas, de la technique de passage par adresse rappele prcdemment.
#include <stdio.h> void f(int &); // noter que le pramtre est une rfrence void main(void) { int pAppel=5; f(pAppel); printf("pAppel = %d \n",pAppel); }
Rsultats
pAppel = 6
void f(int & pFormel) // pFormel est un alias de pAppel { pFormel++; // ici on modifie rellement le paramtre dappel, grce lalias }
Ici le paramtre formel pFormel de la fonction f(int & ) est une rfrence au paramtre dappel. Dans ce cas, manipuler le paramtre formel pFormel revient manipuler le paramtre dappel. Par la suite, on appellera ce mode de transmission passage par rfrence Le passage dargument par rfrence autorise donc la modification du paramtre dappel au sein de la fonction. Ce mode de passage convient donc aux paramtres de sortie et aux paramtres dentre/sortie . De plus, lutilisation dune rfrence est moins lourde que la technique du passage par adresse. En terme de performances, le passage par rfrence est quivalent au passage par adresse. Lors de lappel de la fonction, seule ladresse du paramtre pass par rfrence est transmise la fonction. Le compilateur gre les rfrences grce des pointeurs qui sont cachs au programmeur.
2.2.2.
Le passage par rfrence est aussi performant que le passage par adresse. Il vite ainsi certaines duplications dobjets volumineux (ncessitant beaucoup doctets en mmoire). Du coup, cela expose aussi le programmeur des erreurs de programmation qui se rpercutent sur le programme appelant. Reprenons lexemple dune fonction ayant un paramtre dentre dont la taille en mmoire est importante. Les diffrentes fonctions illustrent les modes de passage de paramtres possibles en C++.
----------------------------------------------------------------------------------struct personne { char nom[30]; char prenom[30]; unsigned age; }; void PassageParValeur(personne p) { /* inconvnient : duplication du paramtre dappel (64 octets ici)*/ printf(" nom : %s prenom : %s age : %d \n",p.nom,p.prenom,p.age); } void PassageParValeurConst(const personne p) { printf(" nom : %s prenom : %s age : %d \n",p.nom,p.prenom,p.age); } void PassageParAdresse(personne * ptr) { /* avantage : plus rapide que par copie inconvnient : les erreurs peuvent se propager au programme appelant */ printf(" nom : %s prenom : %s age : %d \n",ptr->nom,ptr->prenom,ptr->age); } void PassageParAdresseConst(const personne * ptr) { printf(" nom : %s prenom : %s age : %d \n",ptr->nom,ptr->prenom,ptr->age); } void PassageParReference(personne & p) { /* avantage : plus rapide que par copie (aussi bien que par adresse) inconvnient : les erreurs peuvent se propager au programme appelant */ printf(" nom : %s prenom : %s age : %d \n",p.nom,p.prenom,p.age); } void PassageParReferenceConst(const personne & p) { /* mode de transmission privilgi en C++ pour les paramtres dentre */ printf(" nom : %s prenom : %s age : %d \n",p.nom,p.prenom,p.age); } -----------------------------------------------------------------------------------
2.2.3.
On appelle paramtre dentre dune fonction un paramtre dont la valeur est utilise par la fonction pour raliser un traitement mais dont la valeur nest pas modifie par la fonction. On appelle paramtre de sortie un paramtre dont la valeur en entrant dans la fonction est sans importance mais dont la valeur va tre modifie par la fonction. Enfin, certains paramtres peuvent tre la fois dentre et de sortie. En C++, daprs la nature entre-sortie dun paramtre et sa taille, on peut choisir le mode de passage de paramtre le plus adapt. Les paramtres dentre : si peu volumineux (moins de 4 octets : char, int, float, long ) utiliser le passage par valeur, sinon le passage par rfrence constante 10
Les paramtres de sortie et dentre/sortie : passage par rfrence (le passage par adresse est obsolte) Remarque : les tableaux sont toujours passs aux fonctions sous la forme de deux paramtres : ladresse de la premire case puis la taille du tableau. On donne ci-dessous quelques exemples de fonctions simples en C++.
-----------------------------------------------------------------------------------
Fonction Incrementer(E/S param : entier) permettant dajouter 1 au paramtre. Le paramtre est de type entre-sortie (E/S).
void Incrementer(int & param) { param++ ;}
Fonction PlusUn(E param :entier) retournant la valeur du paramtre (entier) dentre augmente de 1. Le paramtre nest plus ici quun paramtre dentre.
int PlusUn(int param){return (param+1); }
Fonction Echanger(E/S a:entier,b :entier) permettant dchanger les valeurs des deux paramtres. Les deux paramtres sont de type E/S.
void Echanger(int & a, int & b){ int c=a ; a=b ; b=c ; }
Fonction Random(S r:entier) gnrant une valeur alatoire. Le paramtre est de type sortie S.
void Random(int & r) { r=rand(); } -----------------------------------------------------------------------------------
2.3.
En C++, des valeurs par dfaut peuvent tre fournies certains paramtres. Dans ce cas, si le paramtre dappel fait dfaut, le paramtre formel de la fonction prend pour valeur la valeur par dfaut .
----------------------------------------------------------------------------------#include <stdio.h> float Addition(float, float =3 ); void main(void) { printf("%f \n",Addition(10,5)); printf("%f \n",Addition(7)); } // 2nd paramtre avec valeur par dfaut
Rsultats
15 10
En cas dabsence dun second paramtre dappel pour la fonction Addition() , le second argument prend pour valeur 3. 11
Note : les arguments ayant des valeurs par dfaut doivent tous figurer en fin de liste des arguments. Voici quelques exemples, bons et mauvais, de prototypes :
float float float float Addition(float=2, float =3); Addition(float=2, float); Addition(float=1, float, float =3); Addition(float, float=5, float =3); // // // // OK NON !!! NON PLUS !!! OK
Remarque : la valeur quun paramtre doit prendre par dfaut doit figurer au niveau du prototype mais pas au niveau de len-tte de la fonction. 2.4. La surcharge de fonctions
Dfinition (Signature) : on appelle signature dune fonction la combinaison de sa classe (si elle est membre dune classe) de son identificateur et de la suite des types de ses paramtres. Les 3 fonctions suivantes ont des signatures diffrentes :
float Addition(float); float Addition(int,float); float Addition(float,float );
Note : le type de la valeur de retour nappartient pas la signature. En C++, il est possible de dfinir plusieurs fonctions avec le mme identificateur (mme nom) pour peu quelles aient des signatures diffrentes. On appelle cela la surcharge de fonction. Lexemple suivant prsente un programme o deux fonctions Addition() sont dfinies. Il faut noter quil sagit bien de fonctions diffrentes et quelles ne ralisent donc pas le mme traitement. Le compilateur ralise lappel de la fonction approprie partir du type des paramtres dappel.
----------------------------------------------------------------------------------#include<stdio.h> int Addition(int, int=4); float Addition(float, float =3);
Rsultats
6.2 17 5.5 18
void main(void) { float fA=2.5; float fB=3.7; int iA=2; int iB=3; printf("%f \n",Addition(fA,fB)); printf("%i \n",Addition(iA,iB)); printf("%f \n",Addition(fA)); printf("%d \n",Addition(iA)); } float Addition(float a, float b){ return a+b; }
int Addition(int a,int b){ const int offset=12; return a+b+offset; } -----------------------------------------------------------------------------------
contexte, pour raliser un logiciel, on fait une analyse fonctionnelle descendante pour mettre en vidence les fonctions principales et les structures de donnes manipules par ces fonctions. On dcoupe le problme initial en sous-problmes, autant de fois qu'il nous parat ncessaire, jusqu' pouvoir rsoudre les sousproblmes par des fonctions de taille raisonnable . En pratique, cette approche prsente de gros dsavantages dans les projets informatiques. Dune part les fonctions crites spcialement pour un projet sont rarement utilisables dans un autre projet. Dautre part, une modification des structures de donnes entrane de multiples points de correction du logiciel. Les logiciels conus avec cette approche voluent difficilement et leur maintenance nest pas aise. Les classes dobjets La Programmation Oriente Objet (POO) cherche s'adapter de manire plus naturelle notre perception de la "ralit". Dans notre quotidien, nous ralisons naturellement une classification de ce qui nous entoure. Nous mettons chaque lment de notre entourage dans une classe d'lments qui ont des caractristiques communes fortes. Un lment dune classe sera appel de manire gnrique objet (mme sil sagit dun tre vivant) par la suite. Les tres vivants, les vgtaux, les minraux, les vhicules, les tlphones, les ordinateurs, les tudiants, les systmes lectroniques, les meubles sont autant dexemples de classes d'objets (on rappelle quobjet prend ici un sens large). Une classe dcrit donc un ensemble dobjets. Les classes ne sont pas ncessairement disjointes. Par exemple, la classe des tres humains est une sous-classe des tres vivants, les hommes/les femmes sont deux sous-classes des tres humains, les animaux une sous-classe des tres vivants, les chiens une sous-classe des animaux etc. Attributs, comportement, tat Dans chacune des classes, les objets sont caractriss par des attributs et des comportements. La valeur des attributs un instant donn caractrise ltat dun objet. Le comportement dun objet peut avoir des consquences sur son tat et inversement, ltat dun objet peut contraindre ses comportements possibles ( un instant donn). Objet de la classe des tlviseurs attributs : taille dcran, marque, tat allum/teint, chane slectionne, volume sonore ... comportement : allumer, teindre, changer de chane, rgler le volume sonore, Objet de la classe des voitures attributs : marque, modle, nergie (essence/diesel), puissance, nombre de portes comportement : mettre moteur en marche, l'arrter, changer de vitesse, Dans la notation UML (Unified Modeling Language), les classes sont reprsentes par des rectangles indiquant quels sont les attributs, leur visibilit (- priv et + public), quelles sont les mthodes accessibles.
Voiture -marque : string -modele : string -energie : string -nombre de portes : int -puissance : int -moteurEnMarche : bool -vitesseEnclenchee : int +MettreMoteurEnMarche() +ArreterMoteur() +ChangerVitesse(in noVitesse : int) +SetClignotant(in OnOffDroiteGauche : int) +Accelerer() +Freiner() Televiseur -tailleEcran : int -marque : string -enMarche : bool -chaineSelectionnee : int -volumeSon : int -reglageCouleur : int +Allumer() +Eteindre() +ChangerChaine(in noChaine : int) +ReglerVolume(in volume : int) +ReglerCouleur(in couleur : int)
Enumrer les attributs et comportements dun objet nest pas toujours chose possible. On imagine mal dans un programme de gestion de comptes bancaires, dcrire un client propritaire dun compte par lensemble de ses caractristiques morphologiques ou psychologiques. Sans doute quon se contentera de le 13
dcrire par son tat civil plus quelques autres renseignements. Toute reprsentation dun objet ou dune classe dobjets ncessitera disoler les attributs et les comportements pertinents pour lutilisation que lon fera de cet objet. Cette dmarche dabstraction est arbitraire et est dpendante du contexte dutilisation de la classe dobjets. En informatique, une classe dobjets va dcrire quels sont les attributs et les comportements retenus pour dcrire un objet au sein dun programme. Encapsulation des donnes Un objet contient des donnes et des mthodes prvues pour accder ces donnes. L'ide de la POO est de ne pouvoir "dialoguer" avec un objet qu' travers l'interface constitue par l'ensemble de ses mthodes. Les mthodes jouent donc le rle d'interface entre les donnes et l'utilisateur de l'objet. Elles filtrent les accs aux donnes. Pour assurer cette encapsulation, des mot-cls dfinissent la visibilit des membres d'une classe. Les relations entre les objets (par extension, entre les classes) Lide de la POO est dutiliser nos aptitudes naturelles classifier des objets et tablir des liens entre eux. Ds lors, un logiciel va tre le rsultat de linteraction dobjets entre eux. Encore faudra-t-il, avant de faire collaborer des objets entre eux, tablir quelles sont les classes dobjets pertinentes , et quelles sont leurs relations. Il est noter que diffrentes relations peuvent apparatre entre des objets ou entre des classes. Deux objets peuvent par exemple schanger des donnes. Par exemple, un objet tlcommande communique avec un objet tlviseur
Televiseur * +Allumer() +Eteindre() +ChangerChaine(in noChaine : int) +ReglerVolume(in volume : int) +ReglerCouleur(in couleur : int) * Telecommande
-envoi commandes
Deux objets peuvent tre en relation du type composant /composite . Un objet voiture est compos dun objet moteur. La notation UML introduit un lien avec un losange du ct du composite.
Voiture +MettreMoteurEnMarche() +ArreterMoteur() 1 Moteur -moteur 1 -energie -puissance +Demarrer() +Arreter()
Enfin, les diffrentes classes peuvent tre en relation hirarchique. On peut dcrire une classe comme reprsentant un sous-ensemble d'une classe plus gnrale. La relation entre une classe et une sousclasse est une relation de type spcialisation/gnralisation. La classe des vlos est une sous-classe des vhicules. La classe des vhicules moteur est une sous-classe des vhicules. Dans ce sens, un vhicule moteur est plus spcifique quun vhicule, ou inversement, un vhicule est plus gnral quun vhicule moteur. La flche part d'une sous-classe (ou classe spcialise) et va vers une classe plus gnrale (ou super-classe).
Vehicule
VehiculeAMoteur
Vlo
Moto
Voiture
14
Un programme en POO contiendra donc lcriture des diffrentes classes dobjets et leur utilisation. Chaque classe sera dfinie par des attributs (accessibles seulement par les mthodes de la classe), et par des fonctions membres (ou mthodes). Enfin, les relations entre les classes devront galement tre implmentes. On verra notamment que la relation de gnralisation peut tre naturellement ralise en C++. Parfois, pour une mme relation, plusieurs ralisations diffrentes peuvent tre implmentes en C++.
Avant daller plus loin, notons quen C++ des classes standard dentre-sortie facilitent la saisie et laffichage dinformations. Les entres sorties standard seffectuent grce aux objets cin (entre standard) et cout (sortie standard). Ces objets sont des instances des classes istream (flot dentre) et ostream (flot de sortie). Ces classes sont dfinies dans le fichier dentte iostream (sans .h). Les objets cin et cout permettent laffichage et la saisie de tous les types de base char, int, float et mme les chanes char * .
--------------------------------------------------------------------------------------#include<iostream> // dfinition des classes ostream et istream using namespace std; // espace de nom utilis pour ces classes (explication ultrieure) void main() { char chaine[10]="Toto"; int age=12; cout << chaine << " a " << age << " ans."; cout << "Saisir un entier : \n"; cin >> age; cout << "Valeur saisie = " << age; } ---------------------------------------------------------------------------------------
4.2.
Mthodes et attributs
Le C++ introduit le mot cl class pour dfinir une classe dobjets. Une classe est un type structur avec des champs de donnes typs (attributs) et des fonctions membres (mthodes). Pour expliquer la syntaxe, le plus simple est encore de donner un exemple. La classe ci-aprs est une classe de points coordonnes entires. La dfinition de la classe est situe dans un fichier dentte (point.h), la dfinition des mthodes dans un fichier source (point.cpp) et enfin lutilisation dans un autre fichier source (main.cpp). Les mots cl public et private indiquent la visibilit des membres. Les membres privs ne sont accessibles que par les objets de la classe. Par convention, les attributs sont toujours accs priv.
--------------------------------------------------------------------------------// point.h fichier dentte contenant la dfinition de la classe #include<iostream> #ifndef __POINT__ #define __POINT__ class Point { public: //partie publique de la classe void SetX(const unsigned x); // les mthodes void SetY(const unsigned y); void SetXY(const unsigned x,const unsigned y);
15
unsigned GetX() const; unsigned GetY() const; float Distance(const Point & p) const; void Translate(const unsigned x,const unsigned y); void Display(std::ostream & flot) const; private: // partie prive (encapsule) unsigned _x; // les attributs unsigned _y; }; // <- ne pas oublier ce point virgule #endif ---------------------------------------------------------------------------------
Le fichier dentte contient la dfinition de la classe. Celle-ci dcrit les donnes membres (attributs) et les mthodes. En fait, bien que ce ne soit pas ncessaire, seuls les prototypes des fonctions membres (mthodes) sont gnralement donns dans la dfinition de la classe.
--------------------------------------------------------------------------------// point.cpp fichier source contenant la dfinition des fonctions membres #include "point.h" //inclusion du fichier dentte de la classe #include<math.h> void Point::SetX(const unsigned int x) { _x=x; } void Point::SetY(const unsigned int y) { _y=y; } void Point::SetXY(const unsigned int x,const unsigned int y) { _x=x; _y=y; } unsigned Point::GetX() const{ return _x; } unsigned Point::GetY() const{ return _y; } void Point::Translate(const unsigned Dx, const unsigned Dy) { _x=_x+Dx; _y=_y+Dy; } float Point::Distance(const Point & p) const { return sqrt((_x-p._x)*(_x-p._x)+(_y-p._y)*(_y-p._y)); } void Point::Display(std::ostream & flot) const { // affichage sur flot de sortie flot << "(" << _x << "," << _y << ")"; } ---------------------------------------------------------------------------------
16
Le fichier dimplmentation des mthodes (ici point.cpp) contient le code des mthodes de la classe. Il faut noter la syntaxe permettant de dfinir une fonction membre. Le nom de la classe propritaire de la mthode prcde le nom de la mthode. Enfin, lutilisation de la classe est faite dans un autre fichier.
--------------------------------------------------------------------------------// main.cpp : fichier dutilisation de la classe point #include "Point.h" // inclusion de la dfinition de la classe Point using namespace std; void main() { Point p1,p2; p1.SetXY(10,10); p2.SetXY(20,20); p1.Display(cout); p2.Display(cout);
// p1 et p2 sont deux objets de la classe Point // donne ltat (10,10) lobjet p1 // donne ltat (20,20) lobjet p2 // affiche p1 sur le flot de sortie // affiche p2 sur le flot de sortie
// p1._x=12 ; est interdit par le compilateur, les donnes sont prives } ---------------------------------------------------------------------------------
Que faut-il remarquer ? - les champs privs ne sont pas accessibles par lutilisateur de la classe - les attributs privs sont nanmoins accessibles aux objets de la classe, cest--dire dans la dfinition des mthodes (regarder le code de la mthode distance) - deux objets de la mme classe ont les mmes attributs mais pas ncessairement le mme tat 4.3. Constructeurs : mthodes d'initialisation d'un objet
Il ne manque pas grand chose pour que la classe Point dfinie prcdemment soit rellement utilisable. Son plus gros dfaut est que ltat dun objet de la classe Point est indfini avant lutilisation de la mthode Point::SetXY() . Le C++ prvoit un mcanisme pour raliser une initialisation correcte des attributs ds la cration dun objet. On peut (on doit) ajouter des mthodes appeles constructeurs. Le rle dun constructeur est de donner une valeur initiale (tat initial) aux attributs. Un constructeur est une fonction membre qui porte comme nom le nom de la classe et qui ne retourne rien. Un constructeur peut avoir zro ou plusieurs arguments, ventuellement avec valeurs par dfaut. Il peut y avoir plusieurs constructeurs dans la mme classe (surcharge) dans la mesure o ils ont des signatures diffrentes. On peut donc complter la classe en ajoutant des constructeurs.
--------------------------------------------------------------------------------// point.h : dfinition de la classe #ifndef __POINT__ #define __POINT__ #include<iostream> class Point { public: // les constructeurs Point(); Point(const unsigned x, const unsigned y);
17
void SetXY(const unsigned x,const unsigned y); le reste est identique private: unsigned _x; unsigned _y; }; #endif ----------------------------------------------------------------------------------------------------------------------------------------------------------------// point.cpp : dfinition des fonctions membres #include "Point.h" #include<math.h> Point::Point() { _x=0; _y=0; } // constructeur sans argument
// constructeur 2 arguments
Il faut bien remarquer, dans lutilisation suivante, la syntaxe permettant dutiliser les constructeurs. Les valeurs des arguments passs au constructeur sont donnes lors de la dclaration de lobjet.
--------------------------------------------------------------------------------// utilisation de la classe Point munie de constructeurs #include "Point.h" using namespace std; void main() { // utilisation du constructeur Point::Point(const unsigned,const unsigned) Point p1(10,20); // p1 est initialis laide du constructeur 2 arguments // ltat de p1 est (10,20) // utilisation du constructeur Point::Point() Point p2; // p2 est initialis avec le constructeur sans argument // ltat de p2 est (0,0) p1.Display(cout); p2.Display(cout); // affiche (10,20) // affiche (0,0)
} ---------------------------------------------------------------------------------
De mme, le C++ prvoit un mcanisme pour raliser des oprations juste avant la mort dun objet (par exemple, la dsallocation de mmoire) . Il sagit du destructeur de la classe. On reviendra sur ce point ultrieurement. 18
4.4.
Parmi les mthodes dune classe, on distingue deux catgories : les mthodes qui modifient ltat de lobjet, mthodes appeles modificateurs, et les mthodes qui ne modifient pas ltat de lobjet mais qui y accdent en lecture. Ces dernires sont appeles accesseurs (celles qui utilisent ltat en lecture) ou slecteurs (celles qui retournent ltat de lobjet). Il est important de dterminer dans quelle catgorie se situe une mthode car son prototype en dpend. Les slecteurs/les accesseurs sont des mthodes dclares constantes (il y a un const aprs la liste des arguments). Les modificateurs ont frquemment un nom tel que Set() et les slecteurs Get(). Pour la classe Point , les modificateurs sont SetX(), SetY(), SetXY(), - les slecteurs sont GetX(), GetY() , les accesseurs sont Display(), Distance().
Translate()
Remarque : ni laffichage ni le calcul de distance naffecte ltat de lobjet sur lequel la mthode sapplique.
class Point { public: // modificateurs void SetX(const unsigned x); void SetY(const unsigned y); void SetXY(const unsigned x,const unsigned y); void Translate(const unsigned x,const unsigned y); // slecteurs unsigned GetX() const ; unsigned GetY() const ; // accesseurs float Distance(const Point & p) const; void Display(std::ostream & flot) const; };
4.5.
Parmi les constructeurs, le constructeur qui a un seul paramtre du mme type que la classe est appel constructeur de copie. Il sert crer un nouvel objet ayant le mme tat quun objet de la mme classe.
--------------------------------------------------------------------------------// point.h : dfinition de la classe #ifndef __POINT__ #define __POINT__ #include<iostream> class Point { public: Point(const Point & p); // prototype du constructeur de copie Point & operator=(const Point & p); // prototype de loprateur = ... }; #endif ---------------------------------------------------------------------------------
19
--------------------------------------------------------------------------------// point.cpp : #include "Point.h" Point::Point(const Point & p) { // recopie de ltat de p dans lobjet courant _x=p._x; _y=p._y; } Point & Point::operator=(const Point & p) { // operateur = pour laffectation de ltat dun Point _x=p._x; _y=p._y; return (*this); // retourne lobjet courant. } ... ---------------------------------------------------------------------------------
Dans le langage C++, laffectation entre variables ou objets de mme type est habituellement ralise par loprateur =. Le langage C++ autorise la programmation des oprateurs agissant sur des types utilisateur (voir section suivante). On peut donc programmer le comportement de loprateur = portant sur des objets dune classe. En C++, un oprateur est simplement trait comme une fonction particulire dont le nom de fonction comporte le mot rserv operator . Loprateur daffectation est donc simplement une mthode de la classe Point.
--------------------------------------------------------------------------------// mise en vidence de lutilisation du constructeur de copie et de loprateur = #include "Point.h" using namespace std; void main() { Point p1(10,20); Point p3; Point p2(p1);
p3=p1;
4.5.1.
Le pointeur this
Dans la dfinition de loprateur =, on voit lutilisation dun membre appel this. Le membre this dun objet est du type pointeur sur un objet de la classe. Pour la classe Point , this est donc un membre du type Point * . Ce membre contient ladresse de lobjet courant. Autrement dit, tout objet peut connatre sa propre adresse . Logiquement, la notation (*this) reprsente lobjet courant, cest--dire lobjet sur lequel la mthode a t appele.
Point & Point::operator=(const Point & p){ _x=p._x; _y=p._y; return (*this); // retourne lobjet courant. }
20
En rsum, loprateur = retourne lobjet sur lequel il sapplique. A quoi cela sert-il ? A garder lhomognit avec le C++ qui autorise les affectations successives telles que ci-dessous
void main(){ Point p1(10,20), p2,p3; p3=p2=p1; /* quivaut p3.operator=(p2.operator=(p1)); */ }
Ci-dessus, p2=p1 quivaut p2.operator=(p1) et cette mthode retourne p2. Cest donc p2 (aprs affectation) qui est affect p3.
5.
Il est possible de surcharger la plupart des oprateurs. Cela signifie quon va pouvoir dcrire quels traitements les oprateurs doivent raliser. Cest notamment le cas pour loprateur = permettant laffectation entre objets dune mme classe (voir section prcdente). Cette surcharge nest toutefois possible que sur les types crs par le programmeur : il nest pas possible de redfinir les oprateurs agissant sur les types lmentaires tels que int, float , etc. 5.1. La pluralit des oprateurs du C++
Selon le nombre doprandes dont loprateur a besoin, loprateur sera qualifi doprateur unaire (1 seul oprande) ou doprateur binaire (2 oprandes). Par exemple, loprateur = est un oprateur binaire, la syntaxe dutilisation de cet oprateur tant Op1 = Op2 . L oprateur ++ en revanche est un oprateur unaire. En effet, il sapplique un seul oprande : Op1++ . Remarque : il y a en fait deux oprateurs ++ . Celui qui pr-incrmente et qui sutilise ++Op1 et celui qui post-incrmente et qui sutilise Op1++ . Idem pour les oprateurs de dcrment -- . Enfin, de mme quavec loprateur ++ o le mme signe peut reprsenter deux oprateurs diffrents (la syntaxe dutilisation permet au compilateur de les distinguer), certains oprateurs peuvent avoir une version unaire et une version binaire. Cest le cas par exemple de loprateur - .
#include<iostream> using namespace std; void main() { int a=4, b=5; cout << (a-b) << endl; cout << -a; }
La syntaxe a-b utilise loprateur binaire (soustraction) tandis que a utilise loprateur unaire (oppos). 5.2. Les oprateurs avec traitement par dfaut
Lorsque lon dfinit de nouveaux types, par exemple des types structurs, certains oprateurs ralisent un traitement par dfaut. Cest le cas de loprateur = et de loprateur & . Le traitement par dfaut de loprateur = Lorsque lon dfinit un nouveau type structur (cest le cas des classes), le traitement ralis par dfaut pour loprateur = est une copie membre membre. Cette caractristique sappliquait dj en langage C sur les types structurs. Le traitement par dfaut de loprateur & Lorsque lon dfinit un nouveau type, loprateur & (oprateur unaire) retourne ladresse de lobjet auquel il sapplique. 5.3. Les oprateurs que lon peut surcharger en C++ Les diffrents oprateurs que lon peut surcharger en C++ sont les suivants : 21
+ * & +=
/ |
++ % &&
-+ -
~ <<
* >>
& <
new >
delete == !=
(cast)
-=
*=
Les oprateurs membres dune classe On verra ci-aprs que la dfinition des oprateurs passe simplement par lcriture de fonctions ayant un nom particulier comportant le mot cl operator suivi du signe de loprateur. En outre, le programmeur aura le choix entre la possibilit de dfinir ces oprateurs comme fonctions membres dune classe ou comme fonctions non membres. Nanmoins (le C++ est un langage truff dexceptions!) certains oprateurs ne peuvent tre dfinis que sils appartiennent une classe. Il sagit alors ncessairement de mthodes dune classe. Cest le cas pour loprateur = vu au chapitre prcdent. Doivent tre imprativement dfinis comme oprateurs membres dune classe les oprateurs suivants :
= ( ) [ ] -> (oprateur d'affectation) (oprateur fonction) (oprateur dindexation)
Remarque : rien noblige a priori le programmeur a faire de loprateur = un oprateur daffectation, mais cest tout de mme recommand. Sans quoi, la classe devient trs difficile comprendre par lutilisateur ! 5.4. Le mcanisme de dfinition des oprateurs en C++
Lorsquun oprateur est utilis sur un type dfini par le programmeur (une classe ou un type structur), lemploi de cet oprateur est quivalent lappel dune fonction particulire qui peut tre hors classe ou membre dune classe (ou d'une structure). Si lon dfinit une nouvelle classe CLS, et deux objets x et y de la classe CLS, alors la syntaxe (1) peut tre quivalente (2) ou (3) pour le compilateur C++ :
(1) (2) (3) x+y; operator+(x,y); x.operator+(y); // oprateur non membre de la classe CLS // oprateur membre de la classe CLS
Autrement dit, les oprateurs sont vus comme des fonctions avec des identifiants particuliers : suivi du signe de loprateur. Dans le cas prcdent, si lon souhaite que loprateur + ralise un traitement particulier sur les variables de type CLS il suffit de dfinir une fonction (hors classe) appele operator+(CLS op1,CLS op2) qui accepte deux arguments de type CLS (ou des rfrences CLS ) ou bien dajouter une mthode CLS::operator+(CLS op2) la classe CLS . On retrouve bien, dans les deux cas, les deux oprandes de l'oprateur +. Dans le premier cas, les deux oprandes sont les paramtres de la fonction. Dans le second cas, le premier oprande est lobjet sur lequel la fonction membre oprateur est appele et le second le paramtre de la fonction membre.
operator
5.5.
Les oprateurs sont dun usage particulirement intressant pour les objets mathmatiques. On fournit ici une bauche dune classe de nombre rationnels mettant en vidence l'intrt des oprateurs. Les objets de la classe Rationnel sont simplement des fractions rationnelles o le numrateur et le dnominateur sont des entiers. On pourra ainsi manipuler des fractions du type 2/3, 27/11 sans erreurs d'arrondis, ou mme des entiers (le dnominateur doit alors tre gal 1). Il faut noter qu'aucun objet de cette classe ne doit avoir de dnominateur nul (on teste donc cette ventualit au niveau du constructeur 22
deux arguments). L'exemple donn ici illustre les deux faons de dfinir un oprateur (fonction membre ou hors classe).
--------------------------------------------------------------------------------rationnel.h : fichier dentte de la classe Rationnel #ifndef __RATIONNEL__ #define __RATIONNEL__ #include<iostream> class Rationnel { public: Rationnel(int num=0, int den=1); Rationnel(const Rationnel & r); int GetNum() const; int GetDen() const; // oprateurs membres de la classe Rationnel & operator=(const Rationnel & r); Rationnel operator+(const Rationnel & r) const; private: int _num; int _den; }; // prototypes des oprateurs hors classe std::ostream & operator<<(std::ostream & flot, const Rationnel & r); Rationnel operator*(const Rationnel & r1, const Rationnel & r2); #endif ----------------------------------------------------------------------------------------------------------------------------------------------------------------// Rationnel.cpp : implmentation de la classe #include "Rationnel.h" // Constructeurs Rationnel::Rationnel(int num,int den){ if(den==0) _den=1; // viter le dnominateur nul if(den<0){ // placer le signe au numrateur _den=-den; _num=-num; } } Rationnel::Rationnel(const Rationnel & r) { _den=r._den; _num=r._num; } // Slecteurs int Rationnel::GetNum() const { int Rationnel::GetDen() const { // constructeur de copie
// oprateur pour laffichage dun rationnel (non membre) std::ostream & operator<<(std::ostream & flot, const Rationnel & r) { flot << r.GetNum() << "/" << r.GetDen(); return flot; }
23
// oprateur daffectation (obligatoirement membre) Rationnel & Rationnel::operator=(const Rationnel & r) { _num=r._num; _den = r._den; return *this; } // oprateur pour la somme de nombres rationnels (membre) Rationnel Rationnel::operator+(const Rationnel & r) const { Rationnel resultat(_num*r._den+r._num*_den,_den*r._den); return resultat; } // oprateur pour le produit de nombres rationnels (hors classe ) Rationnel operator*(const Rationnel & r1,const Rationnel & r2) const { // Attention, cet oprateur n'a pas accs aux membres privs ! Rationnel resultat(r1.GetNum()*r2.GetNum(),r1.GetDen()*r2.GetDen()); return resultat; } ----------------------------------------------------------------------------------------------------------------------------------------------------------------// utilisation de la classe Rationnel #include "Rationnel.h" using namespace std; void main() { Rationnel r1(2,3),r2(4,5); cout << " r1=" << r1; cout << " r2=" << r2 << endl; Rationnel r; r=r1+r2; // equivaut r.operator=(r1.operator+(r2)) cout << "r1+r2=" << r << endl; r=r1*r2; // equivaut r.opertator=(operator*(r1,r2)) cout << "r1*r2=" << r; } ---------------------------------------------------------------------------------
Dans lexemple prcdent il faut accorder une attention particulire aux prototypes. Il faut par exemple remarquer que la somme ne modifie ni lobjet courant (mthode constante) ni lobjet argument (rfrence constante). En outre, loprateur retourne un objet (diffrent de lobjet courant et de lobjet argument) de type rationnel correspondant au rsultat de la somme.
Le C++ introduit la notion de fonctions et de classes paramtres en type . Cela signifie simplement quau sein dune classe ou dune fonction, certains types peuvent tre passs en paramtre. Le mot cl template est introduit en C++ pour pouvoir paramtrer des types. Dans les manuels de C++, on trouvera
24
diffrentes appellations pour le paramtrage de type : on parlera de classes paramtres, de patrons de classes ou de template-classes, voire de modle de classes. Une bibliothque standard de classes paramtres en type, libre dutilisation, est fournie avec la plupart des compilateurs C++. Il sagit de la bibliothque STL (Standard Template Library). Elle regroupe diffrentes classes paramtres et fonctions paramtres. On y retrouve des classes conteneur paramtres et des algorithmes paramtrs (recherche de valeurs dans un conteneur, tris de conteneur ) Notion de conteneur Un conteneur est simplement un objet capable de stocker des donnes avec la possibilit de changer dynamiquement sa taille. Les conteneurs de la STL reprennent les structures de stockage de donnes les plus connues, savoir le conteneur vector : tableau unidimensionnel le conteneur list : liste doublement chane le conteneur stack :pile (structure LIFO) le conteneur queue :file (structure FIFO) le conteneur deque : structure de donnes hybride le conteneur set : ensemble le conteneur map : tableau associatif
En plus de toutes ces structures de donnes, la bibliothque STL fournit une classe string de gestion des chanes de caractres. Le programmeur C++ tire bien des bnfices utiliser les objets string la place des chanes de caractres habituelles (la mmoire est alloue par les objets) et utiliser des objets vector la place des tableaux statiques ou mmes dynamiques. Les primitives daccs, dajout, de suppression diffrent bien videmment entre ces conteneurs. Par exemple, laccs un lment dun objet vector est un accs direct (indpendant de la taille du vector ) alors que laccs un lment dune liste chane est squentiel : on doit parcourir la liste pour retrouver un lment. Par consquent, certaines oprations sont plus efficaces sur certains conteneurs que sur dautres. Ce sont ces caractristiques qui guident le programmeur dans ses choix. Par exemple, on utilisera une liste chane plutt quun tableau si lon ajoute/retire souvent des lments aux extrmits (en dbut ou en fin) puisque ces oprations sont plus efficaces sur une liste. Tous les conteneurs STL sont paramtrs en type. Cela signifie quon peut choisir le type des donnes stockes dans le conteneur. On note vector<T> la classe obtenue partir du conteneur vector pour le type T. Noter que vector<int> est une classe et vector<float> en est une autre.
-------------------------------------------------------------------------------#include<iostream> // entte pour les classe istream et ostream #include<vector> // entte pour le conteneur vector using namespace std; // la bibliothque STL utilise le namespace std
void main(void) { vector<int> tab1(4); // objet de la classe vector<int> vector<float> tab2(2); // objet de la classe vector<float> vector<vector<int> > tab3(2); //objet de la classe vector<vector<int> > tab1[1]=3; tab2[0]=3.7; // les lments stocks dans tab1 sont des entiers // les lments stocks dans tab2 sont des float
25
tab3[0]=tab1; // tab3 stocke des tableaux dentiers (vector<int>) // affichage des lments de tab1 for(unsigned i=0;i<tab1.size();i++) { cout << tab1[i] << " "; } } --------------------------------------------------------------------------------
Ci-dessus tab1 est un objet de la classe vector<int>. La classe vector<int> est une classe de tableaux dont les lments stocks sont des entiers. Lobjet tab1 est donc un tableau dentiers. La taille initiale du tableau est passe au constructeur de lobjet. Lobjet tab1 peut donc stocker 4 entiers, tab2 peut stocker 2 float et tab3 peut stocker 2 objets de la classe vector<int> . On peut donc copier tab1 dans une case du tableau tab3 (tab3 est un tableau de tableaux). Remarquer que laccs une case dun objet vector<T> se fait simplement avec loprateur [ ] , comme sur un tableau classique en C. Mais la classe vector<T> dispose de bien dautres mthodes utilisables pour changer la taille, insrer des lments, supprimer des lments. Par exemple, on voit ci-dessus lutilisation de la mthode vector<T>::size() sur lobjet tab1 . Lobjectif nest pas de dtailler ici toutes les possibilits dutilisation des classes paramtres de la bibliothque STL, mais de prsenter simplement quelques caractristiques des classes string, vector , et list. Ces classes paramtres vont fournir des briques intressantes pour crer des classes plus complexes dans les chapitres suivants. 6.2. Namespace
Avant daller plus loin dans la prsentation de la STL, il semble temps dexpliquer ce que reprsente un namespace (espace de noms). Diffrentes classes ou fonctions peuvent tre regroupes dans un espace de noms particulier pour viter certains conflits didentifiant. Ds lors, pour utiliser une classe dun espace de noms particulier, on doit faire prcder le nom de la classe du nom de lespace de noms. On peut aussi prciser un namespace par dfaut pour le programme via la directive using namespace. Dans lexemple qui suit, il ny a pas de conflit de nom puisque les deux fonctions sont dfinies dans deux espaces de noms distincts (les namespaces NA et NB). On voit aussi que le flot cout dpend du namespace std.
--------------------------------------------------------------------------------#include <iostream> namespace NA{ void MaFonction() { std::cout << "Fonction du namespace NA \n"; } } namespace NB{ void MaFonction() { std::cout << "Fonction du namespace NB \n"; } }
26
Toutes les classes de la STL appartiennent au namespace std. Il convient donc de prciser ce pour lutilisation de ces classes. Ceci explique certaines parties de programme non comprhensibles jusque l.
namespace
Remarque : il est noter que lon ne peut pas utiliser la directive using dentte lors des dfinitions de classes. 6.3. Classe string
namespace
La classe string nest pas une classe paramtre. Elle sert uniquement grer des chanes de caractre de faon un peu plus souple quen langage C. Voici un premier exemple.
--------------------------------------------------------------------------------#include<string> // pour la classe string #include<iostream> using namespace std; void main(void) { string s1("Une chaine"),s2(" Toto"); string s3(s1); string s4; s4=s1+s2; // + : concatnation = : affectation
cout << s3 << endl; // insertion possible dans cout cout << s3[0] << endl;// utilisable comme un tableau : s3[i] de type char if(s1==s3) cout << "Chaines identiques\n"; if(s1<=s4) cout << "s1<=s4 selon l'ordre lexicographique\n"; } ---------------------------------------------------------------------------------
Lexemple suivant illustre les faits suivants : la classe contient - un constructeur sans argument pour crer une chane vide - un constructeur avec un argument de type char * pour assurer la compatibilit avec le C - un constructeur de copie pour crer une chane identique une chane existante - des oprateurs de concatnation : + et += - un oprateur daffectation : = - un oprateur dindexation [ ] de sorte quun objet string se comporte aussi comme un tableau - les oprateurs de test : == != - les oprateurs de comparaison selon lordre lexicographique (lordre des mots dans un dictionnaire) : <= < >= > 27
// possibilit d'obtenir un pointeur compatible const char * const char * ptr=s1.c_str(); } ---------------------------------------------------------------------------------
6.4.
Conteneurs vector<T>
La classe paramtre vector<T> permet de gnrer des objets tableaux pouvant stocker des objets de nimporte quel type. On rappelle que vector<int> est une classe (le paramtre de type vaut ici int ) et vector<float> en est une autre. Par consquent, ces types ne sont pas compatibles. Le premier exemple suivant met en vidence les principales fonctionnalits de ce patron de classes de tableaux.
--------------------------------------------------------------------------------#include<iostream> #include<vector> using namespace std; void main(void) { vector<int> t1(4,-2),t2(5); vector<float> t3(2),t4(4,1.2); t1[0]=5; t4[2]=-2.3; //t1=5,2,2,-2 //t4=1.2,1.2,-2.3
// t1=-2,2,2,-2 // t3=0,0
t2=0,0,0,0,0 t4=1.2,1.2,1.2
t2=t1; // possible car t1 et t2 de la mme classe t3=t4; // possible car t3 et t4 de la mme classe // t1=t4; impossible car t1 et t4 de types diffrents vector<int> t5(t1); // t5=5,2,2,-2 vector<float> t6(t3); // t6=1.2,1.2,-2.3 //vector<float> t7(t1); impossible car t1 nest pas de la classe vector<float> } ---------------------------------------------------------------------------------
28
La classe dispose dun constructeur un argument (la taille initiale du vecteur), dun constructeur deux arguments (taille et valeurs initiales du vecteur). La classe dispose aussi dun constructeur de copie. Attention, les copies ne sont possibles quentre objets de mme classe ! La classe dispose dun oprateur daffectation. L encore, laffectation nest possible quentre objets de mme classe. Enfin, puisque cette classe implmente le fonctionnement dun tableau, loprateur dindexation [ ] donne accs aux lments du tableau (comme pour un tableau classique). Les itrateurs Dans le but dhomogniser les interfaces des classes de la STL, les classes conteneur implmentent la notion ditrateur qui gnralise la notion de pointeur. Un itrateur est simplement un objet qui pointe un emplacement dun conteneur (un peu comme un pointeur). En outre, les oprateurs ++ et - sont surchargs sur les itrateurs pour passer lemplacement suivant ou prcdent du conteneur (quil sagisse dun tableau ou dune liste chane). Loprateur * permet lindirection sur un itrateur (comme sur un pointeur). Grce aux itrateurs, les algorithmes sappliquant aux conteneurs sont moins dpendants de leur structure interne. Les classes conteneurs contiennent des classes membres publiques pour instancier des itrateurs adapts. Il y a deux classes ditrateurs par conteneur : les itrateurs constants et les itrateurs non constants. Les premiers permettent de parcourir un conteneur sans pouvoir en modifier le contenu.
--------------------------------------------------------------------------------void main(void) { vector<int> t1(4); vector<int>::iterator it; // it est un iterateur sur vector<int>
Les deux boucles for prcdentes ralisent le mme traitement, lune grce aux itrateurs, lautre grce loprateur []. Les mthodes begin() et end() retournent des itrateurs. La premire mthode retourne un itrateur sur le premier lment du conteneur. La seconde pointe juste aprs le dernier lment. Lutilisation des conteneurs est donc un peu droutante puisque des itrateurs sont utiliss comme arguments de certaines mthodes dinsertion ou de suppression. Il faut donc avoir une connaissance minimale sur leur utilisation pour pouvoir exploiter la bibliothque STL. Nanmoins, lutilisation des itrateurs est la mme pour les autres conteneurs. En consquence, dans la bibliothque STL, lutilisation dune liste chane nest pas plus complique que celle dun vecteur. Lexemple suivant illustre les capacits dinsertion et de suppression dans un vecteur (voire dans un conteneur diffrent)
--------------------------------------------------------------------------------void main(void) { vector<int> t1(3,2); //t1=2,2,2 cout << "Taille = "<< t1.size() << endl; // Taille=3 t1.push_back(3); t1.resize(6,-2); //t1=2,2,2,3 //t1=2,2,2,3,-2,-2
29
t1.insert(t1.begin(),2,-3); //t1=-3,-3,2,2,2,3,-2,-2 t1.insert(t1.begin()+3,-1); //t1=-3,-3,2,-1,2,2,3,-2,-2 t1.insert(&t1[0],-6); //t1=-6,-3,-3,2,-1,2,2,3,-2,-2 t1.erase(t1.begin()+2,t1.begin()+4); //t1=-6,-3,-1,2,2,3,-2,-2 t1.erase(t1.begin()+3); //t1=-6,-3,-1,2,3,-2,-2 t1.pop_back(); //t1=-6,-3,-1,2,3,-2 } ---------------------------------------------------------------------------------
Ajout/retrait en fin = push_back et pop_back Ajout/retrait en tte = push_front et pop_front Premier/dernier lment : front et back Changement de dimension : resize (nouvelle taille, valeur des cases complmentaires ). Insertion : plusieurs mthodes. insert (itrateur, nombre dajouts, valeur insre) : plusieurs cases insres insert (itrateur, valeur insre) : une seule valeur insre Remarquer quun pointeur sur entier peut tre converti en itrateur sur entier. Suppression : plusieurs mthodes. erase (itrateur debut,iterateur fin) : supprime les cases entre ces itrateurs erase (itrateur) : supprime la case pointe par litrateur 6.5. Conteneurs list<T>
Les autres conteneurs ont de nombreux points communs avec ce quon a dj vu. Cest pourquoi un exemple devrait suffire comprendre lutilisation des listes chanes. Hormis le fait que loprateur [ ] nest videmment pas dfini pour les listes chanes.
--------------------------------------------------------------------------------void main(void) { list<int> l1; list<float> l2,l4; l1.push_front(12); l1.push_back(13); l1.push_front(-3); l1.push_front(7); l2.push_back(2.3); l2.push_back(2.7); l2.push_back(-1.2); // // // // //liste vide d'entiers // liste vide de flottants ajout en tte ajout en fin ajout en fin l1=7,-3,12,13
//l2=2.3,2.7,-1.2
list<int> l3(l1); //l3=7,-3,12,13 cout << "Taille = "<< l3.size() << endl;
30
l4=l2; //l4=2.3,2.7,-1.2 l1.insert(l1.begin(),2,-3); //l1=-3,-3,7,-3,12,13 cout << l1.front() << l1.back() << endl; // -3 13 list<int>::iterator it=l1.begin(); while(it!=l1.end()){ if((*it)==-3) // suppression des noeuds valant -3 { it=l1.erase(it); } else it++; } // affichage de la liste l1 for(it=l1.begin();it!=l1.end();it++) cout << (*it) << " "; } ---------------------------------------------------------------------------------
Dans un premier temps, on va dcrire le reprsentation UML de la composition. On peut dcrire la composition de deux manires : - soit les objets composants apparaissent comme attributs des objets composites - soit une relation de composition (lien avec losange ct composite) lie les classes composant et composite.
Point
+Point(in x : Rationnel, in y : Rationnel) +GetX() : Rationnel +GetY() : Rationnel +SetX(in x : Rationnel) +SetY(in y : Rationnel) 1 2 Rationnel -_numerateur : int -_denominateur : int +Rationnel(in num : int, in den : int) +FormeIrreductible() : Rationnel +GetNum() : int +GetDen() : int +SetNum(in num : int) +SetDen(in den : int) +operator+(in r : Rationnel) : Rationnel +operator-(in r : Rationnel) : Rationnel +operator*(in r : Rationnel) : Rationnel +operator/(in r : Rationnel) : Rationnel -coordonnes
Classe Point : dans la premire partie, un point du plan a t reprsent par des coordonnes entires. De manire un peu diffrente, on peut aussi reprsenter des points coordonnes rationnelles (voir la section sur les oprateurs).
-_x : Rationnel -_y : Rationnel +Point(in x : Rationnel, in y : Rationnel) +GetX() : Rationnel +GetY() : Rationnel +SetX(in x : Rationnel) +SetY(in y : Rationnel)
Un point peut alors tre vu comme la composition de deux coordonnes de type Rationnel (classe Rationnel). 31
En notation UML, les coordonnes sont soit reprsentes comme des attributs de type Rationnel dans la classe Point (reprsentation de gauche) soit par la relation de composition (reprsentation de droite). Dans le deuxime cas, la relation de composition est reprsente par un losange ct composite. Classe Segment : on peut dcrire un segment par ses extrmits (objets de la classe Point ). L encore, on pourra avoir une reprsentation plus ou moins clate selon que lon reprsente la composition comme attribut ou par une association particulire. Les trois reprsentations UML suivantes reprsentent la mme classe Segment dont les extrmits sont des points coordonnes rationnelles.
Segment -_A : Point -_B : Point +GetA() : Point +GetB() : Point +GetDimension() : Rationnel Segment +GetA() : Point +GetB() : Point +GetDimension() : Rationnel 1 2 Segment Point +GetA() : Point +GetB() : Point +GetDimension() : Rationnel 1 2 -extrmitsAB -_x : Rationnel -_y : Rationnel +Point(in x : Rationnel, in y : Rationnel) +GetX() : Rationnel +GetY() : Rationnel +SetX(in x : Rationnel) +SetY(in y : Rationnel) 2 Rationnel 1 -_numerateur : int -_denominateur : int +Rationnel(in num : int, in den : int) +FormeIrreductible() : Rationnel +GetNum() : int +GetDen() : int +SetNum(in num : int) +SetDen(in den : int) +operator+(in r : Rationnel) : Rationnel +operator-(in r : Rationnel) : Rationnel +operator*(in r : Rationnel) : Rationnel +operator/(in r : Rationnel) : Rationnel -coordonnes -extrmitsAB
Point -_x : Rationnel -_y : Rationnel +Point(in x : Rationnel, in y : Rationnel) +GetX() : Rationnel +GetY() : Rationnel +SetX(in x : Rationnel) +SetY(in y : Rationnel)
7.2.
La ralisation la plus naturelle de la composition en C++ est de faire apparatre les objets composants comme des attributs de la classe composite. Sur le plan technique, seule lcriture du constructeur introduit une notation particulire. Prenons lexemple de la classe Point pour laquelle les coordonnes sont de type Rationnel (classe dfinie dans la section 5). Seule la dclaration de la classe composant est utile pour lcriture de la classe composite. On a seulement besoin de connatre les mthodes membre (notamment les constructeurs disponibles) Rappelons tout dabord le fichier dentte de la classe composant puisque celui-ci contient toute linformation ncessaire la rutilisation de la classe Rationnel.
--------------------------------------------------------------------------------// rationnel.h : entte de la classe Rationnel (classe composant) #ifndef _RATIONNEL__ #define _RATIONNEL__ class Rationnel { public: Rationnel(long num=0,long den=1); Rationnel(const Rationnel &); Rationnel & operator=(const Rationnel & ); long GetDen() const; long GetNum() const;
32
Rationnel FormeIrreductible(); void Affiche(std::ostream & ) const; Rationnel Rationnel Rationnel Rationnel operator+(const operator*(const operator-(const operator/(const Rationnel Rationnel Rationnel Rationnel & & & & r) r) r) r) const; const; const; const;
private: long pgcd(long j,long k); long _num; long _den; }; #endif ---------------------------------------------------------------------------------
Ensuite, la classe Point qui est une classe composite se dfinit de la manire suivante :
--------------------------------------------------------------------------------//point.h : entte de la classe point (classe composite) #ifndef _POINTRAT__ #define _POINTRAT__ #include "rationnel.h" // inclusion de lentte de la classe composant class Point { public: Point(); Point(const Rationnel & x, const Rationnel & y); Point(const Point & p); Rationnel GetX() const; Rationnel GetY() const; private: Rationnel _x; // objets membres de type Rationnel Rationnel _y; }; #endif ----------------------------------------------------------------------------------------------------------------------------------------------------------------//point.cpp (implmentation des mthodes de la classe Point) #include "point.h" Point::Point():_x(0,1),_y(0,1) { } Point::Point(const Rationnel & x, const Rationnel & y):_x(x),_y(y) { } Point::Point(const Point &p) :_x(p._x),_y(p._y) { } Rationnel Point::GetX() const { return _x; }
33
Liste dinitialisation (en gras dans limplmentation de la classe Point ) : dans le fichier source de la classe Point , seule lcriture des constructeurs introduit une nouvelle syntaxe. A la suite des arguments dun constructeur, on peut mettre une liste dinitialisation des objets membres composants. Grce cette liste dinitialisation, le compilateur sait quel constructeur de la classe composant doit tre invoqu pour initialiser lobjet membre. Constructeur sans argument de la classe Point : initialisation de _x et _y comme les rationnels 0/1. Indique au compilateur que le constructeur deux arguments de la classe Rationnel doit tre utilis. Constructeur de copie de la classe Point : la liste d'initialisation indique que les attributs _x et _y sont initialiss grce aux attributs p._x et p._y de l'objet Point pass en argument. Autrement dit, c'est le constructeur de copie de la classe Rationnel qui est invoqu. 7.3. Utilisation d'attributs de type tableau d'objets
Une autre ralisation possible de la composition consiste mettre un attribut de type tableau d'objets. Pour la classe Point , on peut voir les coordonnes comme un tableau d'objets Rationnel deux cases. Cette autre ralisation est mise en uvre dans la classe PointTab ci-dessous.
--------------------------------------------------------------------------------// pointtab.h #ifndef _POINTTAB__ #define _POINTTAB__ #include "rationnel.h" class PointTab { public: PointTab(const Rationnel & x, const Rationnel & y); Rationnel GetX() const; Rationnel GetY() const; private: Rationnel _XY[2]; // tableau de deux objets Rationnel }; #endif ----------------------------------------------------------------------------------------------------------------------------------------------------------------//PointTab.cpp #include "PointTab.h" PointTab::PointTab(const Rationnel & x, const Rationnel & y) { _XY[0]=x; _XY[1]=y; } Rationnel PointTab::GetX() const { return _XY[0]; } Rationnel PointTab::GetY() const { return _XY[1];} ---------------------------------------------------------------------------------
Remarque : il est noter quici la liste dinitialisation ne permet pas dinitialiser le tableau dobjets. Aussi, il est ncessaire quun constructeur sans argument existe dans la classe Rationnel .
34
7.4.
Une autre ralisation de la composition (o la multiplicit est suprieure 1) consiste utiliser objet membre de type vector<Rationnel>. Ceci implique assez peu de changements par rapport la solution prcdente. Seule l'initialisation de l'objet membre vector<Rationnel> doit faire l'objet d'une attention particulire.
--------------------------------------------------------------------------------#ifndef _POINTTAB__ #define _POINTTAB__ #include "rationnel.h" #include<vector> class PointTab { public: PointTab(const Rationnel & x, const Rationnel & y); Rationnel GetX() const; Rationnel GetY() const; private: std::vector<Rationnel> _XY; }; #endif ---------------------------------------------------------------------------------
--------------------------------------------------------------------------------//PointTab.cpp #include "PointTab.h" // Attention ! Utiliser la liste d'initialisation pour initialiser le vecteur la taille 2 PointTab::PointTab(const Rationnel & x, const Rationnel & y):_XY(2) { _XY[0]=x; _XY[1]=y; } Rationnel PointTab::GetX() const { return _XY[0]; }
Remarque : une autre ralisation possible de la composition est propose dans la section traitant des classes avec donnes en profondeur.
Le langage UML propose une notation (une flche pointant la super-classe) pour dcrire la relation de spcialisation entre classes. La classe spcialise (ou sous-classe dun point de vue ensembliste) possde les attributs et le comportement de la super-classe. On dit aussi que la sous-classe hrite de la super-classe. En plus du comportement hrit, la sous-classe peut contenir des attributs et des mthodes spcifiques. La spcialisation est reprsente par une flche en langage UML. 35
Exemple : une classe de ds 6 faces peut tre vue comme une spcialisation dune classe de ds n faces : il suffit de fixer le nombre de faces.
PointQn
DeNFaces -_valeur : unsigned int -_nbFaces : unsigned int +DeNFaces(in nbFaces : unsigned int = 6) +Lancer() +GetValeur() : unsigned int +GetNombreDeFaces()() : unsigned int
-_coordonnees : vector<Rationnel> +PointQn(in n : unsigned int) +PointQn(in p : PointQn) +operator=(in p : PointQn) : PointQn +GetCoordonnee(in n : unsigned int) : Rationnel +SetCoordonnee(in n : unsigned int, in x : Rationnel)
De6 +De6()
PointQ2
PointQ3
Exemple : on peut aussi voir un point du plan QxQ (lensemble des couples de nombres rat ionnels), classe note PointQ2 , comme la spcialisation dun point n coordonnes (classe PointQn ) On retrouve dans la bibliographie C++ un certain vocabulaire associ cette notion de spcialisation/gnralisation. Au sujet de la classification ci-dessus, on dira : - la classe De6 est une spcialisation de la classe DeNFaces - la classe De6 drive de la classe DeNFaces - la sous-classe De6 hrite des fonctionnalits de la super-classe DeNFaces . En effet, un objet d'une sous-classe dispose galement de l'interface de la super-classe. - la classe DeNFaces est la super-classe de la classe De6 - la classe DeNFaces est la classe de base de la classe De6 . 8.2. Exemple de ralisation de la spcialisation en C++
Nous donnons l'exemple de la classe drive De6. Nous rappelons d'abord le fichier header (fichier dentte) de la classe DeNFaces car il est ncessaire de connatre l'interface de la super-classe pour crire la classe drive. Ensuite nous donnons la dfinition de la classe drive (la sous-classe) ainsi que l'utilisation de cette classe. Pour cet exemple, la classe drive n'a pas d'attribut supplmentaire. La classe drive se contente de contraindre la valeur du nombre de faces. Un d 6 faces est un d N faces o _nbFaces vaut toujours 6. Pour la classe spcialise, seul le constructeur sans argument est dfini pour initialiser correctement le nombre de faces. Ensuite, toutes les mthodes de la classe DeNFaces peuvent tre utilises sans problme sur la classe spcialise De6 .
--------------------------------------------------------------------------------// DeNFaces.h : entte dune classe de ds N faces #ifndef __CLS_DENFACES__ #define __CLS_DENFACES__ class DeNFaces { public: DeNFaces(unsigned nbFaces=6); unsigned GetNombreDeFaces() const; void Lancer(); unsigned GetValeur() const; private: unsigned _valeur; // valeur du d un instant unsigned _nbFaces; // nombre de faces du d }; #endif ---------------------------------------------------------------------------------
36
L'implmentation de la classe drive est ici trs simple puisque seul un constructeur est dfini. Encore une fois, la liste d'initialisation (revoir la section prcdente) est utilise au niveau des constructeurs.
--------------------------------------------------------------------------------// De6.h : fichier dentte de la classe De6 (drive de DeNFaces) #ifndef __CLS_DE6__ #define __CLS_DE6__ #include "DeNFaces.h" class De6:public DeNFaces // la classe De6 drive de DeNFaces { public: De6(); // constructeur de la classe drive }; #endif ----------------------------------------------------------------------------------------------------------------------------------------------------------------// De6.cpp :implmentation de la classe spcialise #include "De6.h" De6::De6(): DeNFaces(6){ } ----------------------------------------------------------------------------------------------------------------------------------------------------------------// Utilisation.cpp : utilisation de la classe spcialise #include "De6.h" #include<iostream> using namespace std; void main(void) { DeNFaces de1(9); // d 9 faces De6 de2; // d 6 faces cout << de1.GetNombreDeFaces() << "\n"; cout << de2.GetNombreDeFaces() << "\n"; de1.Lancer(); de2.Lancer(); // appel de mthode hrite cout << d1.GetValeur() << endl; cout << d2.GetValeur() << endl; // appel de mthode hrite } ---------------------------------------------------------------------------------
Notons que l'on peut encore poursuivre la hirarchie de classes d'objets ds. On peut par exemple ajouter un attribut de couleur . --------------------------------------------------------------------------------#ifndef __CLS_DE6COUL__ #define __CLS_DE6COUL__ #include "De6.h" class De6Couleur:public De6 { public: De6Couleur(unsigned couleur); unsigned GetCouleur() const; private: unsigned _couleur; }; #endif -------------------------------------------------------------
DeNFaces -_valeur : unsigned int -_nbFaces : unsigned int +DeNFaces(in nbFaces : unsigned int = 6) +Lancer() +GetValeur() : unsigned int +GetNombreDeFaces()() : unsigned int
De6 +De6()
De6Couleur -_couleur : unsigned int +De6Couleur(in couleur : unsigned int) +GetCouleur() : unsigned int
37
--------------------------------------------------------------------------------// De6Couleur.cpp : implmentation de la classe de De6Couleur #include "De6Couleur.h" De6Couleur::De6Couleur(unsigned c): De6(),_couleur(c) { } unsigned De6Couleur::GetCouleur() const { return _couleur; } ---------------------------------------------------------------------------------
8.3.
Une sous-classe reprsente un sous-ensemble d'objets de la classe plus gnrale. Si l'on se rfre la hirarchie de classes de ds prcdente, on comprend facilement qu'un d 6 faces est avant tout un d N faces. Aussi, le type De6 peut tre converti en type DeNFaces . En C++, les conversions de type entre sous-classe et super classe sont lgales. Par exemple, on peut convertir un objet De6 en objet DeNFaces . On peut de la mme manire convertir un objet de la classe De6Couleur en un objet de la classe DeNFaces . Nanmoins, ces conversions sont dgradantes (perte de donnes), comme lorsque l'on convertit un float en int. Naturellement, lorsque lon convertit un d color en d (sans couleur) on perd linformation de couleur. En C++, les conversions sous-classe vers super-classe sont possibles, mais elles perdent la partie dite incrmentale cest--dire la partie spcifique dune classe drive. Lexemple suivant prsente quelques transtypages possibles.
--------------------------------------------------------------------------------#include "De6Couleur.h" #define rouge 5 void main(void) { De6Couleur de6r(rouge); De6 de6; DeNFaces denf(9); ((DeNFaces)de6r) .GetValeur(); // cast De6Couleur -> DeNFaces
Conversion sous-classe * -> super-classe * : les conversions de pointeurs entre sous-classe et super-classe sont galement lgales en C++.
--------------------------------------------------------------------------------#include "De6Couleur.h" #define rouge 5 void main(void) { De6Couleur de6r(rouge); De6 *ptrDe6; DeNFaces *ptrDeNF; ptrDe6=&de6r; ptrDe6->Lancer(); // De6Couleur * -> De6 *
38
ptrDeNF=ptrDe6; ptrDeNF->Lancer();
} ---------------------------------------------------------------------------------
8.4.
Le polymorphisme reprsente le fait qu'une fonction ayant le mme nom puisse tre appele sur des objets de classes diffrentes. Par exemple, le fait qu'on puisse mettre une mthode DessinerDans(fenetre) dans diffrentes classes peut tre vu comme du polymorphisme. En effet, pour la classification faite cidessous, les classes reprsentent des objets graphiques destins tre dessins dans des fentre sous windows. La fonction de dessin est donc prsente dans les diffrentes classes.
En C++, le polymorphisme reprsente aussi la possibilit pour des objets d'une descendance rpondre diffremment lors de l'appel d'une mthode de mme nom. Le polymorphisme est donc li dans ce cas aux hirarchies de classes. Si l'on reprend la classification prcdente et que l'on ajoute une super-classe ObjetGraphique , on voit que tous les objets graphiques ont besoin d'une mthode permettant de dessiner lobjet dans une fentre. De plus, la mthode de dessin doit agir diffremment sur les diffrents objets graphiques : un rectangle ne se dessine pas de la mme manire qu'un cercle. Si la classe de base de la descendance contient une mthode DessinerDans() , en raison des compatibilits de type entre sous-classe et super-classe, on peut donc excuter le programme suivant.
--------------------------------------------------------------------------------#include "Rectangle.h" ObjetGraphique #include "Cercle.h" #include<iostream> +DessinerDans(out fenetre) using namespace std; void main(void) { Fenetre f; // objet fentre graphique ObjetGraphique * tab[3]; tab[0]=new Cercle; tab[1]=new Rectangle; tab[2]=new Polygone tab[0]->DessinerDans(f); tab[1]->DessinerDans(f); tab[2]->DessinerDans(f);
// mthode de la classe ObjetGraphique invoque // car la mthode nest pas polymorphe par dfaut
39
Dans l'exemple prcdent la mthode DessinerDans() peut tre invoque sur tous les objets. Mais, si la mthode DessinerDans() n'est pas dclare virtual dans la classe de base ObjetGraphique, c'est ncessairement la mthode de la classe ObjetGraphique qui va tre invoque, mme si l'objet point est de la classe Rectangle ou Cercle. Par dfaut en C++, un lien statique relie les mthodes aux classes. Autrement dit, par dfaut, les mthodes ne sont pas polymorphes. Il faut prciser quand on souhaite que le polymorphisme sapplique. Remarque : en Java, le polymorphisme sapplique automatiquement. Le C++ laisse la possibilit de mettre en place ou non le polymorphisme (via les mthodes virtuelles) pour des raisons de performance. Car la dfinition de mthodes virtuelles (voir ci-dessous) implmente une table dindirection supplmentaire qui augmente la taille du code et ralentit lexcution. Par consquent en C++, selon le besoin, le programmeur peut privilgier la vitesse quand le polymorphisme nest pas utile. Les mthodes virtuelles. Si l'on dfinit la mthode DessinerDans() comme tant virtuelle (mot cl virtual ) dans la classe de base ObjetGraphique , le programme prcdent va bien invoquer les mthodes DessinerDans() de chacun des objets points. La mthode adapte l'objet est retrouve dynamiquement. Le mot cl virtual indique au compilateur de mettre en place une ligature dynamique de mthodes. Le compilateur cre alors une table dindirection (ceci est transparent pour le programmeur) permettant de retrouver la bonne mthode lexcution. Le choix de la mthode invoque a alors lieu lexcution et non la compilation.
--------------------------------------------------------------------------------#ifndef __OBJETGRAPHIQUE__ #define __OBJETGRAPHIQUE__ #include "Fenetre.h" class ObjetGraphique { public: ObjetGraphique(); virtual void DessinerDans(Fenetre &) const; }; #endif ----------------------------------------------------------------------------------------------------------------------------------------------------------------#ifndef __RECTANGLE__ #define __RECTANGLE__ #include "ObjetGraphique.h" class Rectangle: public ObjetGraphique { public: Rectangle(); virtual void DessinerDans(Fenetre &) const; }; #endif ---------------------------------------------------------------------------------
Remarque : il faut noter que la ligature dynamique (obtenue par le mot cl virtual ) ne concerne que la mthode prcise, et non toutes les mthodes de la classe. En C++, quand on parle de polymorphisme, on pense en priorit ce lien dynamique mis en place en utilisant le mot cl virtual . 40
8.5.
Grce au polymorphisme on peut crire du code gnrique et faire des structures de donnes htrognes. Pour comprendre ces ides, le plus simple est de considrer de nouveau la classification dobjets graphiques faite prcdemment. On souhaite faire un petit programme de dessin o lutilisateur peut dessiner des formes gomtriques simples (ellipses, rectangles.), les slectionner, les supprimer, les dplacer Pour cela, les formes gomtriques doivent tre mmorises dans une structure de donnes. En outre, toutes les formes gomtriques devront pouvoir ragir des mmes demandes (mmes mthodes).
Linterface de la classe de base ObjetGraphique va contenir tout ce que les diffrentes formes gomtriques seront capable dexcuter. Mais la notion dobjet graphique reste abstraite. Que signifie dessiner un objet graphique ? ou mme dplacer un objet graphique ? En fait, la classe ObjetGraphique va tre une classe dite abstraite. Elle contiendra des fonctions virtuelles pures, cest--dire des fonctions membres qui ne seront pas dfinies (en outre on ne saurait pas quoi y mettre) mais pour lesquelles le typage dynamique sappliquera. Les fonctions virtuelles pures nont pas de code et ont un prototype qui se termine par =0. Une classe qui contient au moins une fonction virtuelle pure est dite abstraite. On ne peut alors pas crer dobjet dune telle classe (mais on pourra crer des objets dune classe drive).
--------------------------------------------------------------------------------#ifndef __OBJETGRAPHIQUE__ #define __OBJETGRAPHIQUE__ #include "Fenetre.h" #include "Point.h" class ObjetGraphique // classe abstraite { public: ObjetGraphique(); virtual ~ObjetGraphique(); // destructeur virtuel // fonctions virtual void virtual void virtual void virtuelles pures DessinerDans(Fenetre &) const =0; Deplacer(int DeltaX, int DeltaY) =0 ; Contient(const Point &) const =0 ;
}; #endif ---------------------------------------------------------------------------------
On ne peut pas crer dobjet de la classe ObjetGraphique, mais en revanche, on peut faire un conteneur dobjets graphiques. En effet, on peut faire par exemple un tableau de pointeurs dans lequel on peut placer les adresses des diffrentes formes gres par le programme. Le programme ci-dessous gre une 41
telle structure de donnes contenant des objets htrognes. Ils sont en revanche tous descendants de la classe ObjetGraphique.
void main() { vector<ObjetGraphique *> FormesGraphiques; Fenetre F ; FormesGraphiques.push_back(new Rectangle(10,10,40,55)); FormesGraphiques.push_back(new Ellipse(60,60,140,155)); FormesGraphiques.push_back(new Rectangle(30,30,40,40)); for(int i=0;i<FormesGraphiques.size();i++) FormesGraphiques[i]->DessinerDans(F); for(int i=0;i<FormesGraphiques.size();i++) delete FormesGraphiques[i]; }
De plus, on voit quil est trs simple de redessiner lensemble des objets (code en gras), puisque on envoie le mme message chaque objet, ce message tant interprt diffremment par chacun des objets (grce au typage dynamique). En rsum, une classe abstraite dcrit ce quon attend dune classe (comportement) sans savoir comment cela va tre ralis. En fait le comportement final est programm dans la ou les classes descendant de la classe abstraite.
On peut dclarer, dans une classe CLS, le fait que dautres fonctions ou dautres classes aient accs la partie encapsule de la classe CLS . Cette fonctionnalit est particulirement intressante lorsque lon veut dfinir des oprateurs comme fonctions non membres de la classe. On illustre ceci en reprenant la classe Rationnel vue dans la section sur les oprateurs (section 5) et en dfinissant les oprateurs + et * comme des fonctions non membres de la classe Rationnel mais amis de cette classe. Les membres _den et _num deviennent accessibles ces fonctions non membres.
class Rationnel { // fonctions non membres mais amies de la classe friend Rationnel operator+(const Rationnel & r1,const Rationnel & r2); friend Rationnel operator*(const Rationnel & r1,const Rationnel & r2); ... };
42
#include "Rational.h" #include <iostream> Rationnel operator+(const Rationnel & r1,const Rationnel & r2){ Rationnel local(r1._num*r2._den+r2._num*r1._den,r1._den*r2._den); return local; } Rationnel operator*(const Rationnel & r1,const Rationnel & r2){ Rationnel local(r1._num*r2._num,r1._den*r2._den); return local; }
On peut galement dclarer une classe amie. Dans lexemple suivant, la classe B est dclare amie de la classe A. Cela signifie que toutes les mthodes de la classe B peuvent avoir accs la partie prive dun objet de la classe A.
#include "B.h" class A { public: friend B; };
Remarque : la dclaration damiti permet de contourner lencapsulation des donnes dune classe. Il ne faut donc pas abuser de ce droit. Il est prfrable de chercher viter autant que possible dutiliser lamiti (on peut souvent procder diffremment en ajoutant des accesseurs notamment). 9.2. Membres protgs (mot-cl protected) et visibilit des membres
Un membre (donne ou mthode) dclar protected est accessible par les objets de la classe mais pas par lutilisateur de la classe (comme pour un membre priv). En revanche, la diffrence dun membre private, un membre protected est accessible par un objet dune classe drive. On dclare donc protected un membre que lon souhaite rendre accessible a une classe drive. En rsum, pour une classe (classe Base) et une classe drive (classe Derivee) donne on a :
-------------------------------------#ifndef ___CLS_DERIVEE__ #define ___CLS_DERIVEE__ #include "Base.h" class Derivee : public Base { public: Derivee(); MethodeD(); protected: bool _indProtegeD; void FonctionProtegeeD(); private: int _dPriveeD; void FPriveeD(); }; #endif --------------------------------------
-------------------------------------#ifndef ___CLS_BASE__ #define ___CLS_BASE__ class Base { public: Base(); MethodeB(); protected: bool _indPProtegeB; void FonctionProtegeeB(); private: int _dPriveeB; void FPriveeB(); }; #endif --------------------------------------
43
Tous les membres privs et protgs de la classe Base sont accessibles dans les mthodes (prives ou protges) de la classe Base. Par contre un utilisateur de la classe Base na accs qu la mthode publique de cette classe. De mme, tous les membres privs et protgs de la classe Derivee sont accessibles dans les mthodes (prives ou protges) de la classe Derivee. En outre, du fait de la drivation, les mthodes publiques de la classe Base sont utilisables sur les objets de la classe Derivee.
void main() { Derivee D ; D.MethodeD() ; D.MethodeB() ; }
// mthode hrite
Dans une mthode prive ou protge de le classe Derivee, on peut accder la partie publique de la classe de base (normal) et la partie protge de la classe de base.
void Derivee::FPriveeD() { MethodeB(); MethodeD(); _indPProtegeB=true; // _dPriveeB=3; FonctionProtegeeB(); //FPriveeB(); } // implmentation de la classe Derivee // possible (car publique) // possible (car publique) // possible (car membre protg de Base) // non possible (car membre priv de Base) // possible (car fonction protge de Base) // non possible (car fonction prive de Base)
Trois formes de drivation sont possibles : la drivation publique, prive ou protge. Tout ce qui vient dtre expos ne tient que pour la drivation publique (class Derivee : public Base). Cest le mode de drivation le plus courant. Il existe aussi une drivation prive (class Derivee : private Base) et protge (class Derivee : protected Base). La visibilit des membres de la classe de base dpend du mode de drivation. On rsume ci-dessous la visibilit des membres selon le mode de drivation. Par la suite, FMA signifie Fonctions Membres et Amies La drivation publique (class
Statut dans la classe de base Public Protg Priv
Derivee : public Base )
La drivation prive (class Derivee : private Base) Dans ce mode de drivation, pour un utilisateur de la classe drive, tout ce qui est hrit de la classe de base est encapsul par la drivation. Cest un peu comme si lon dfinissait un membre priv de la classe de base dans la classe drive. Autrement dit, on nhrite pas vraiment des mthodes.
2
44
La drivation protge (class Derivee : protected Base) Dans ce mode de drivation, les membres publics de la classe de base seront considrs comme protgs lors des drivations ultrieures. Tableau de synthse
Classe de base Statut initial Accs FMA Public Protg Priv Oui Oui Oui Drive publique Nouveau Accs statut utilisateur Public Oui Protg Non Priv Non Drive protge Nouveau Accs statut utilisateur Protg Non Protg Non Priv Non Drive prive Nouveau Accs statut utilisateur Priv Non Priv Non Priv Non
Ce tableau est difficile assimiler. Seule la pratique de la programmation permet de retenir ces aspects du langage. Il faut dans un premier temps se concentrer sur la drivation publique qui est le mode de drivation le plus couramment employ. 9.3. Navigation dans une hirarchie de classes, phnomnes de masquage
On sintresse ici ce que va pouvoir exploiter un utilisateur dans le cas dune hirarchie de classes. Considrons les classes partiellement dcrites ci-dessous.
class Base { public: Base(); void Affiche() const; void AfficheB() const; }; class Derivee:public Base { public: Derivee(); void Affiche() const; void AfficheD() const; };
La classe drive comporte, comme la classe de base, une mthode Affiche() . Sur un objet Derivee, la mthode Affiche() appele est ncessairement celle de la classe drive. On dit quelle masque celle de la classe de base. Mais, cette dernire demeure utilisable en prcisant lappel le nom de la classe propritaire. Dailleurs on peut toujours prciser le nom de la classe lors dun appel de mthode. Par exemple, lappel cidessous est tout fait lgal (mais pas trs concis)
void main() { Base B; B.Base::Affiche(); }
// quivaut B.Affiche();
----------------------------------------------------------------------------------------#include "derivee.h" void main() { Derivee D; Base B; B.Affiche(); B.AfficheB(); D.AfficheD(); D.Affiche();
45
D.Base::Affiche(); // appel explicite de la mthode de la classe de base D.Derivee::Affiche() ; // quivalent D.Affiche(); B.Base::AfficheB(); // quivalent B.AfficheB() ; // B.Derivee::AfficheD(); na en revanche pas de sens } -----------------------------------------------------------------------------------------
En C, on demande de la mmoire avec la fonction malloc() et on la libre avec la fonction free(). En C++, l'allocation dynamique de mmoire s'effectue avec les oprateurs new et delete. 10.1.1. Syntaxe La syntaxe d'utilisation de loprateur new est la suivante :
T * ptr; ptr = new T; ptr = new T[n];
// allocation d'un objet (ou une variable) de type T // allocation d'un tableau de n objets de type T
o T est un identificateur de type quelconque (type primitif, type structur ou classe), n est une expression entire quelconque.
La premire syntaxe rserve de la mmoire pour stocker 1 lment ayant le type T, alors que la seconde rserve de la mmoire pour stocker n lments de type T. Dans les deux cas, le rsultat de l'opration est une adresse. Soit l'adresse de l'espace mmoire rserv sous la forme d'un pointeur de type (T *), soit le pointeur NULL si l'espace demand n'a pu tre obtenu. L'utilisation de l'oprateur delete est la suivante :
delete ptr; delete [] ptr;
ptr
La premire syntaxe libre l'espace mmoire d'un objet point par ptr. La seconde syntaxe sert librer l'espace occup par un tableau d'objets. Cette seconde syntaxe assure que tous les destructeurs des objets du tableau sont appels avant que le tableau soit libr. Ceci est important si les objets du tableau ont des donnes en profondeur (voir la suite de ce chapitre). Il est noter quil nest pas ncessaire de rappeler la taille du tableau allou lors de la libration de la mmoire. Remarque : les objets string de la STL ont des donnes en profondeur. Le code suivant conduit des fuites mmoire
string * ptr; ptr = new string[6]; delete ptr;
Remarque : le fonctionnement de l'oprateur delete est indtermin si ptr pointe sur une zone qui n'a pas t alloue dynamiquement par l'oprateur new , ou si la zone mmoire a dj t libre. Il est dconseill de mlanger lutilisation des oprateurs new/delete avec celle des fonctions malloc()/free() . On peut prciser l'utilisation d'un constructeur particulier lors de l'allocation d'un objet (mais pas pour les tableaux allous dynamiquement). Ci dessous, l'oprateur new alloue la mmoire pour stocker un objet de la classe string et cet objet est initialis l'aide du constructeur 1 argument de type char* .
ptr = new string("toto"); /* le constructeur 1 argument de type char* de la classe string ralise l'initialisation de l'objet */
46
10.2. Ralisation de la composition par gestion dynamique de mmoire (donnes en profondeur). Nous avons vu dans la section 7 diffrentes ralisations de la composition. On peut galement grer un objet composant dans la zone de mmoire gre dynamiquement (dans le tas). Dans ce cas, l'objet composant n'est pas rellement dans la classe mais est connu via un pointeur. Les donnes de l'objet composant n'tant plus dans l'objet composite, on dit que les donnes sont en profondeur, autrement dit obtenues indirectement par un pointeur. Si l'on reprend l'exemple de la classe Point qui est compose de 2 objets de type Rationnel, on peut donner une nouvelle ralisation de la composition base sur la gestion dynamique d'objets (mme si la reprsentation UML est la mme)
--------------------------------------------------------------------------------#ifndef _POINT__ Point #define _POINT__ +Point(in x : Rationnel, in y : Rationnel) #include "rationnel.h" class Point { 1 public: 2 -coordonnes Point(const Rationnel & x, const Rationnel & y); Rationnel Point(const Point & p); -_numerateur : int ~Point(); //destructeur -_denominateur : int +Rationnel(in num : int, in den : int) Point & operator=(const Point & p); +FormeIrreductible() : Rationnel Rationnel GetX() const; +GetNum() : int +GetDen() : int Rationnel GetY() const; +SetNum(in num : int) +SetDen(in den : int) private: +operator+(in r : Rationnel) : Rationnel +operator-(in r : Rationnel) : Rationnel Rationnel * _XY; // pointeur sur le composant +operator*(in r : Rationnel) : Rationnel }; +operator/(in r : Rationnel) : Rationnel #endif --------------------------------------------------------------------------------+GetX() : Rationnel +GetY() : Rationnel +SetX(in x : Rationnel) +SetY(in y : Rationnel)
Remarque : pour la premire fois depuis le dbut de ce manuel, le rle du destructeur dune classe va tre illustr. Le destructeur est la mthode de la classe dont le nom est celui de la classe prcd dun tilde (~), cidessous la mthode ~Point . Cette mthode est appele juste avant la disparition de lobjet.
--------------------------------------------------------------------------------//Point.cpp #include "Point.h" #include <process.h> #include <iostream> using namespace std; Point::Point(const Rationnel & x, const Rationnel & y){ _XY=new Rationnel[2]; if(_XY==NULL){ cerr << "echec d'allocation de mmoire."; exit(2); } _XY[0]=x; _XY[1]=y; } // constructeur de copie Point::Point(const Point &p){ _XY=new Rationnel[2]; if(_XY==NULL){ cerr << "echec d'allocation."; exit(2); } _XY[0]=p._XY[0]; _XY[1]=p._XY[1]; }
47
// destructeur : mthode appele automatiquement juste avant la disparition de lobjet Point::~Point() { delete [] _XY; } Point & Point::operator=(const Point &p) { _XY[0]=p._XY[0]; _XY[1]=p._XY[1]; return *this; } Rationnel Point::GetX() const { return _XY[0]; }
Les donnes lies aux coordonnes ne sont pas directement dans l'objet Point mais dans une zone mmoire gre dynamiquement par l'objet. Dans ce cas, le constructeur a pour rle dallouer de la mmoire et le destructeur (mthode ~Point) celui de librer la zone mmoire alloue par le constructeur. Une telle structuration a des consquences. Il faut crire les constructeurs (qui allouent la mmoire), le destructeur (qui libre la mmoire) et l'oprateur d'affectation avec soin. Faute de quoi, des fuites mmoires peuvent avoir lieu. 10.3. Ralisation de conteneurs : tableaux, piles, files, listes ...
Les conteneurs sont des objets destins stocker/restituer des donnes. Les conteneurs se distinguent les uns des autres par la faon dont ils grent et accdent aux donnes. Les conteneurs les plus utiliss sont : - les tableaux : l'accs a une donne est indic et direct. - les listes : l'accs une donne est squentiel - les piles/les files : on peut voir ces conteneurs comme des listes particulires La taille des donnes d'un objet conteneur est susceptible d'voluer durant la vie de l'objet. Un objet conteneur ne peut donc pas prvoir ds la construction quelle sera la quantit de mmoire qui lui sera ncessaire. Un conteneur va donc grer dynamiquement la mmoire qui lui sera ncessaire. Les changements de taille des donnes conduisent des allocations/dsallocations de mmoire. Nous illustrons ceci par une formulation lmentaire d'une classe d'objets tableaux.
--------------------------------------------------------------------------------#ifndef __CLS_TABLEAU__ #define __CLS_TABLEAU__ class Tableau { public: Tableau(unsigned taille=5); Tableau(const Tableau & tab); Tableau & operator=(const Tableau & tab); virtual ~Tableau(); double & operator[](unsigned idx); double operator[](unsigned idx) const; void NouvelleTaille(unsigned taille); unsigned GetTaille() const;
48
unsigned GetCapacite() const; private: double * _tab; unsigned _capacite; unsigned _taille; }; #endif ---------------------------------------------------------------------------------
La mmoire est gre ici selon la technique utilise dans la bibliothque STL pour les vectors . Un objet alloue une zone mmoire d'une certaine capacit (attribut _capacite ). Dans cette zone, il stocke les donnes utiles (qui occupent une certaine _taille). Par consquent, _taille doit toujours tre infrieur ou gal _capacite . La diffrence _capacite-_taille constitue une rserve de mmoire utilisable lors des changements de taille ou des affectations entre objets tableaux.
--------------------------------------------------------------------------------#include "Tableau.h" #include<process.h> #include<iostream> using namespace std; Tableau::Tableau(unsigned taille):_taille(taille), _capacite(2*taille), _tab(new double[2*taille]) { if(_tab==NULL){ cerr << "echec d'allocation"; exit(2); } // les lments du tableau sont initialiss 0 for(unsigned i=0;i<_taille;i++) _tab[i]=0; } Tableau::~Tableau(){ delete [] _tab; } // constructeur de copie Tableau::Tableau(const Tableau & t):_taille(t._taille), _capacite(t._capacite),_tab(new double[t._capacite]) { if(_tab==NULL){ cerr << "echec d'allocation"; exit(2); } for(unsigned i=0;i<_taille;i++) _tab[i]=t._tab[i]; } double & Tableau::operator [](unsigned idx) { if(idx>=GetTaille()){ // verification dindice cerr << "indice incorrect!"; exit(2); } return _tab[idx]; }
49
double Tableau::operator [](unsigned idx) const { if(idx>=GetTaille()) { cerr << "indice incorrect!"; exit(2); } return _tab[idx]; } // oprateur d'affectation Tableau & Tableau::operator=(const Tableau & t) { if(_capacite<t._taille) // rallocation si capacit insuffisante { _capacite=2*t._taille; double * temp=new double[_capacite]; if(_tab==NULL){ cerr << "echec d'allocation"; exit(2); } delete [] _tab; _tab=temp; } _taille=t._taille; for(unsigned i=0;i<t._taille;i++) _tab[i]=t._tab[i]; return *this; } unsigned Tableau::GetTaille() const { return _taille; } unsigned Tableau::GetCapacite() const { return _capacite; } // mthode permettant le changement de taille void Tableau::NouvelleTaille(unsigned taille) { if(taille<=GetTaille()) _taille=taille; else { if(taille>_capacite) { _capacite=2*taille; double * temp=new double[_capacite]; if(_tab==NULL){ cerr << "echec d'allocation"; exit(2); } for(unsigned i=0;i<_taille;i++) temp[i]=_tab[i]; delete [] _tab; _tab=temp; } for(unsigned i=_taille;i<taille;i++) _tab[i]=0; _taille=taille; } } ---------------------------------------------------------------------------------
50
Fonction retournant une rfrence Il faut noter que loprateur dindexation [] retourne une rfrence. Ainsi on peut modifier un lment du tableau via cet oprateur. Dans limplmentation de loprateur simplifie ici
double & Tableau::operator [](unsigned idx){ return _tab[idx]; }
il faut interprter cela comme le fait que la fonction retourne une rfrence _tab[idx] . Dans ce cas, la fonction ne retourne pas une copie de la case, mais la case elle-mme. Deux oprateurs dindexation [] Il faut galement remarquer que la classe dispose de deux oprateurs dindexation. Un oprateur constant qui peut tre appel sur des objets constants (cest--dire des tableaux constants). Celui-ci ne doit pas pouvoir modifier les lments du tableau et renvoie donc le contenu de la case par valeur (retour classique). Lautre oprateur (non constant) quant lui retourne une rfrence. Dailleurs, on ne peut appeler sur un objet constant que les mthodes constantes : ici il sagit de GetTaille(), GetCapacite() et un oprateur []. Lexemple ci-dessous montre comment lon peut utiliser la classe Tableau .
--------------------------------------------------------------------------------#include "Tableau.h" #include<iostream> using namespace std; void main() { Tableau T1(4); T1[0]=2; // modifie la case dindice 0 T1[2]=1.3; cout << T1[0] << endl ; cout << T1.GetTaille() << endl; // ici T1 contient {2,0,1.3,0}, _taille==4 et _capacite==8 T1.NouvelleTaille(10); // ici T1 contient {2,0,1.3,0,0,0,0,0,0,0}, _taille==10 et _capacite==20 const Tableau T2(T1); T2 est un tableau constant, fait par copie de T2 // ici T2 contient {2,0,1.3,0,0,0,0,0,0,0} et son tat nest pas cens voluer // on ne peut appeler sur T2 que des mthodes constantes cout << T2[7] << endl ; cout << T2.GetTaille() << endl; } ---------------------------------------------------------------------------------
Cette classe ressemble, en beaucoup moins volue, la classe vector<double> de la bibliothque STL. Il reste nanmoins prfrable dutiliser la bibliothque STL plutt que de redfinir des conteneurs. Cependant, crire des conteneurs est un bon exercice pour assimiler les techniques dcriture des classes. Il peut tre intressant de dvelopper, titre dexercice, les conteneurs suivants (prsents dans la bibliothque STL): classe Ensemble (permettant de stocker des lments sans doublon possible), classe ListeChainee (classe de listes chanes), classe Pile( structure LIFO), classe Chaine (classe de chanes de caractres). 51
Considrons lexemple classique dune fonction de permutation des valeurs de deux variables entires.
Nous voulons dsormais, dans la mme application, permuter deux valeurs de type double . Grce la surcharge de fonctions, on peut complter le programme de la manire suivante
--------------------------------------------------------------------------------void permute( int & a, int & b) { int c=a; a=b; b=c; } void permute( double & a, double & b) { double c=a; a=b; b=c; } void main() { int x=2,y=3; double u=1.3,v=1.7; permute(x,y); permute(u,v); } ---------------------------------------------------------------------------------
La seule diffrence entre ces deux fonctions concerne les types utiliss. Si lon peut paramtrer les types, on na quune seule dfinition donner. Cest ce que permet la notation template (patron).
--------------------------------------------------------------------------------template<class T> void permute(T & a,T & b) { T c=a; a=b; b=c; } void main() { int x=2,y=3; double u=1.3,v=1.7; permute(x,y); // T=int permute(u,v); // T=double } ---------------------------------------------------------------------------------
La notation template<class T> indique au compilateur que T est un paramtre de type (type primitif, structur ou classe). La fonction permute peut alors tre appele avec deux arguments d'un type quelconque, 52
dans la mesure o les deux arguments sont du mme type (a n'aurait pas de sens de permuter un caractre avec un flottant). Quand un paramtre de type est utilis dans une fonction C++, on parle alors de patron de fonctions , ou de fonction paramtre en type ou encore de fonction gnrique . Un patron de fonctions ne conduit pas directement du code. Par exemple, sil ny avait pas dappel de la fonction permute() , aucun code ne serait gnr (contrairement une fonction classique). Le code des fonctions cres partir du patron nest gnr que sil y a un appel de la fonction pour un type donn. Dans lexemple prcdent, le compilateur cre deux fonctions partir du patron. La premire fonction est cre pour le paramtre de type T=int et la seconde pour le paramtre de type T=double. Le code des fonctions est gnr selon le besoin. Dans un patron de fonctions, on peut aussi faire apparatre des arguments typs normalement. Ces paramtres sont parfois appels paramtres expression. L'exemple suivant reprsente ce que pourrait tre une fonction de tri de tableau paramtre :
template <class T> void tri(T * tab,unsigned taille) { // algorithme du tri d'un tableau de taille lments de type T; // ... // }
Un patron de fonctions dfinit une famille de fonctions ayant toutes le mme algorithme mais pour des types diffrents. Il reste nanmoins possible de dfinir explicitement le code dune fonction qui devrait normalement tre prise en charge par le patron. Dans lexemple suivant, nous ralisons un patron de fonctions calculant le minimum de deux variables de mme type. La fonction min() obtenue partir du patron a du sens pour tous les types lmentaires sauf pour le type char* . Nous avons donc dfini une spcialisation pour le type char * . Cette spcialisation remplace le code que gnrerait le patron.
--------------------------------------------------------------------------------#include<iostream> #include<string.h> using namespace std; //patron de fonctions template <class T> T min(T a,T b){ return a<b ? a : b; } //spcialisation du patron pour le type char * char * min(char * s1,char * s2) { if(strcmp(s1,s2)<0) return s1; else return s2; } void main() { int a=1,b=3; char chaine1[]="coucou",chaine2[]="salut"; cout << min(a,b); //gnre partir du patron cout << min(chaine1,chaine2); //spcialisation } ---------------------------------------------------------------------------------
11.2.
On peut galement introduire des paramtres de type dans la dfinition d'une classe. Dailleurs, les conteneurs STL (voir section 6) sont des classes paramtres en type. Pour prsenter la syntaxe, le plus simple 53
est une fois encore de traiter un exemple. On va rcrire la classe Point de sorte de pouvoir instancier des points dont les coordonnes soient d'un type choisi par l'utilisateur. On rappelle que dans l'utilisation d'une classe paramtre, la valeur du paramtre de type est prcise T > . Par exemple, pour la classe paramtre Point<T> donne ci-aprs, l'utilisation sera la suivante.
--------------------------------------------------------------------------------void main() { Point<int> p1(12,16); // point coordones entires Point<double> p2(1.3,4.6); // point coordonnes rationnelles } ---------------------------------------------------------------------------------
entre <
Point<T>.
--------------------------------------------------------------------------------//fichier point.h #ifndef __POINT_TEMPLATE__ #define __POINT_TEMPLATE__ template<class T> // T est un paramtre de type class Point { public: Point<T>(){ _XY[0]=0; _XY[1]=0;} Point<T>(const T & x,const T & y) { _XY[0]=x; _XY[1]=y; } Point<T>(const Point<T> & p) { _XY[0]=p._XY[0]; _XY[1]=p._XY[1]; } Point<T> & operator=(const Point<T> & p) { _XY[0]=p._XY[0]; _XY[1]=p._XY[1]; return *this; } void SetX(const T & x) { _XY[0]=x; } void SetY(const T & y){ _XY[1]=y; } const T GetX() const { return _XY[0]; } const T GetY() const { return _XY[1]; } private: T _XY[2]; // tableau de 2 lments de type T }; #endif ---------------------------------------------------------------------------------
Remarque : pour les classes paramtres en type, toute l'implmentation des mthodes doit tre disponible quand on utilise la classe paramtre. Aussi, il est habituel que dans ce cas, le corps des mthodes soit fourni 54
dans la dfinition de la classe. Il n'y a donc qu'un fichier header (fichier dntte dextension .h) contenant toute limplmentation des mthodes (comme ci-dessus). On doit aussi remarquer que la dfinition du paramtre de type n'est donne qu'une seule fois avant le dbut de la classe.
est un type, et false/true sont des valeurs pour le type bool . Les numrations sont gres comme des variables de type entier. En C++, on peut dfinir des numrations au sein des classes. On dfinit ainsi une famille de constantes au sein d'une classe (ce qui est une alternative la dclaration de constantes via la directive #define ).
bool class Calendrier { public: enum jour{lundi,mardi,mercredi,jeudi,vendredi,samedi,dimanche}; Calendrier(); //... };
Le type numration, ainsi que ses valeurs possibles, appartiennent la classe Calendrier . C'est donc l'oprateur de rsolution de porte :: qui permet de les rfrencer.
Calendrier::jour j=Calendrier::mardi;
Cette technique est trs couramment utilise, notamment dans la bibliothque STL pour les modes d'ouverture des flots et la dfinition des formats. 12.2. La dclaration d'une classe au sein d'une classe
C'est la technique utilise par les itrateurs dans la bibliothque STL. La classe ci-dessous illustre cette technique qui peut se gnraliser aux patrons de classes.
class vecteur{ public: class iterateur{ public: iterateur(); ... }; vecteur(int taille); iterateur begin() const; int & operator[](int idx); };
55
Ici, la classe iterateur est dfinie dans la classe vecteur. L'utilisation est la suivante.
vecteur v1(3); vecteur::iterateur i=v1.begin();
12.3.
Une vue (trs partielle) de la hirarchie des classes de gestion des flots entre/sortie est la suivante.
Ces modes peuvent tre combins par l'oprateur |, par exemple ios::out|ios::app. Mthodes de la classe ostream : : sortie non formate d'un caractre ostream::write(char *,int n) : sortie de n caractres ostream::operator<<( ) : insertion format e dans le flot pour certains types de base (char , int ...)
ostream::put(char) #include<fstream> #include<iostream> using namespace std; void main() { cout.put('a'); cout.put('\n'); char str[]={'a','b','c',0,'f','g','h'}; cout << str << endl; cout.write(str,7); }
56
A titre d'exemple, le format de sortie peut tre modifi par certaines mthodes de la classe ainsi que par des manipulateurs de flot (ci-dessous, endl est un manipulateur de flot)
void main() { float f1=1.3698; cout << f1 << endl; cout.precision(3); cout << f1 << endl; }
// sortie formate -> 1.3698 // changement de la prcision // sortie formate -> 1.37
lecture d'une suite d'au plus nbCarac caractres. La lecture s'arrte aussi si le caractre dlimiteur (par dfaut '\n') est rencontr. istream::read(char * pStr,int nbCarac ) : lecture de nbCarac caractres. istream::operator>>( ) : lecture formate pour les types de base.
//... inclusion des fichiers ... using namespace std; void main() { char tampon[30]; string str; cin.getline(tampon,30,'\n'); //la lecture s'arrte sur retour chariot cin >> str; // la lecture s'arrte sur les espaces cout << tampon << endl; cout << str << endl; }
12.4.Les fichiers En C++, les fichiers peuvent tre grs par des flots de type fstream (file stream). La classe fstream hrite des mthodes des classes istream et ostream, donc de celles prsentes prcdemment. La classe fstream dispose des mthodes suivantes (ou hrites de ios, istream , ostream )
fstream::fstream() : cre un objet non connect un fichier fstream::fstream(char * nomFichier, ios::open_mode ) : ouvre un fichier fstream::~fstream() : fermeture d'un fichier ouvert fstream::open(char *, ios::open_mode) : ouvre un fichier fstream::close() : ferme le fichier en cours
Quelques quivalences entre les modes d'ouverture des flots et les modes d'ouverture de fichiers par fopen() en C ios::open_mode
ios::out ios::in ios::out|ios::app ios::in|ios::out ios::in|ios::binary ios::out|ios::binary ios::out|ios::app|ios::binary
57
void main() { char tampon[150]; // cration ou remplacement du fichier fstream f("toto.txt",ios::out); // criture dans le fichier f<<"test \t" << 1 << "\t"<< 1.5 << endl; f.close(); // rouverture du fichier en mode ajout f.open("toto.txt",ios::out|ios::app); f<<"ajout \t" << 2 << "\t"<< 2.5 << endl; f.close(); // ouverture en mode lecture f.open("toto.txt",ios::in); while(!f.eof()) { f.getline(tampon,150); cout << tampon << endl; } }
12.5.
La classe ios dfinit pour cela des valeurs ios::beg (dbut du flot), ios::end (fin du flot), ios::cur (position courante). Il est noter que la lecture ou l'criture font voluer la position courante.
void main() { char tampon[150]; // ouverture en criture fstream f("fichier.txt",ios::out); for(int i=0;i<3;i++) f<<"ligne numero "<< i << "\n"; // instant t1 f.seekp(0,ios::beg); //positionne en dbut f<<"premiere ligne \n"; f.close(); // instant t2 // ouverture en lecture f.open("fichier.txt",ios::in); buffer fichier.txt (instant t1) --ligne numero 0---ligne numero 1---ligne numero 2--
buffer fichier.txt (instant t2) premiere ligne ro 0---ligne numero 1---ligne numero 2--
58
//positionne au 3ime du dbut f.seekg(2*sizeof(char),ios::beg); f.getline(tampon,150); cout << tampon <<endl; sortie // dplace de 3 caractres f.seekg(3*sizeof(char),ios::cur); f.getline(tampon,150); cout << tampon <<endl; }
59
Entier operator+(const Entier & e1,const Entier & e2) { cout << "operator+(Entier,Entier)"<<endl; return e1.Get()+e2.Get(); } Entier & Entier::operator =(const Entier & e) { cout << "Entier::operator=(Entier)"<<endl; _data=e._data; return *this; } -----------------------------------------------------------------
de type.
Le programme ci-dessous met en vidence le rle des constructeurs 1 argument pour la conversion
----------------------------------------------------------------#include<iostream> #include "entier.h" using namespace std; void f(Entier e) { cout << "valeur de e = " << e.Get() << endl; } Sortie void main(void) { int varInt=4; float varFloat=2.5; f(varInt); f(varFloat); Entier e(3); e=e+varFloat; // conversion float->Entier e=1+e; // conversion int->Entier } ----------------------------------------------------------------;conversion int->Entier ;conversion float->Entier constructeur Entier::Entier(int) valeur de e = 4 constructeur Entier::Entier(float) valeur de e = 2 constructeur Entier::Entier(int) constructeur Entier::Entier(float) operator+(Entier,Entier) constructeur Entier::Entier(int) Entier::operator=(Entier) constructeur Entier::Entier(int) operator+(Entier,Entier) constructeur Entier::Entier(int) Entier::operator=(Entier)
La rgle suivante sapplique en C++ : lorsquun objet de type classe CLS (ici Entier) est attendu, et quune variable autre de type autreType est fournie, le compilateur cherche si parmi les constructeurs de la classe CLS il en exite un qui accepte un seul argument de type autreType . Si un tel constructeur exite, un objet temporaire est construit avec ce constructeur et est utilis la place de la variable autre. Autrement dit : Les constructeurs un argument dfinissent une rgle de conversion. Par exemple, le constructeur Entier::Entier(float) dfinit la conversion dun type float vers le type Entier. Aussi, chaque fois qu'un type Entier est attendu, et qu'un type float est fourni, le constructeur ralise un objet temporaire pour cette conversion. Ce rle des constructeurs doit tre bien compris car en pratique beaucoup de conversions de type sont ralises tacitement sans que le programmeur en soit tenu inform.
60
On peut donner un autre exemple utilisant la classe string de la STL. Puisqu'il est possible de crer un objet string partir d'une chaine de caractres du C, les lignes suivantes sont compiles sans aucun problme.
string s1("chaine"); s1=s1+"ajout"; // constructeur string::string(char *) // conversion char * -> string avant appel de l'oprateur +
En effet, l o loprateur + attend un objet de type string , on lui passe une variable de type char *. Le compilateur cherche donc sil est possible de convertir la chane "ajout" en un objet de type string . C'est possib le car la classe string dispose d'un constructeur un argument de type char * . La chane "ajout" est donc concatne ici la chaine s1. Grce cel, on peut utiliser des chanes du C la place des objets string. Mot cl explicit : par dfaut, les conversions possibles grce aux constructeurs peuvent tre ralises implicitement (tacitement) par le compilateur (il n'y a pas de warning). Il peut donc y avoir des conversions sans que le programmeur en soit conscient. On peut nanmoins mentionner dans la classe que les constructeurs ne soient utiliss comme convertisseurs que de manire explicite (mot cl explicit ).
class Entier { public: Entier(); explicit Entier(int val); explicit Entier(float val); ... };
// conversion explicite
13.2.
On peut galement dfinir des rgles permettant de convertir un objet dun type classe en un type de base. Il sagit en fait de dfinir le traitement ralis par un oprateur de cast. Dans la classe complexe suivante, l'oprateur double ralise la conversion complexe->double . L o un objet complexe est pass alors qu'un double est attendu, cet oprateur est appel pour raliser la conversion de type.
----------------------------------------------------------------------------------//complexe.h #ifndef __COMPLEXE__ #define __COMPLEXE__ #include<iostream> class complexe { private: double re; // partie relle
61
double im; // partie imaginaire public: complexe(double r=0,double i=0){ // constructeur cout << "complexe(double=0,double=0) \t objet : "<<this << endl; re=r; im=i; } complexe operator+(const complexe & c) const { cout << "complexe::operator+() \t obj:"<<this<<" + obj:" <<&c<<endl; return complexe(re+c.re,im+c.im); } operator double(){ // oprateur de cast cout << "complexe::operator double() \t obj:"<<this<<endl; return re; // la conversion complexe->double retourne la partie relle } friend ostream & operator<<(std::ostream & flot,const complexe & c); }; std::ostream & operator<<(std::ostream & flot, const complexe & c) { flot << c.re; if(c.im){flot << "+"<<c.im<<"i";} return flot; } #endif -----------------------------------------------------------------------------------
Le programme dutilisation suivant illustre les conversions possibles grce cette classe lmentaire. On y retrouve aussi les conversions ralises par le constructeur de la classe complexe . Il faut noter qu'on peut utiliser le constructeur de la classe complexe avec un seul argument de type double, l'autre ayant la valeur 0 par dfaut. Ce constructeur peut donc tre utilis pour raliser les conversions double->complexe.
----------------------------------------------------------------------------------#include "complexe.h" void main() { complexe c1,c2(1,3); //1)-2) c1=2.4; cout << c1 << endl; c2=c1+c2; cout << c2 << endl; //3) //4) //5)-6) //7)
Rsultats complexe(dou ble=0,double=0) complexe(double=0,double=0) complexe(double=0,double=0) 2.4 complexe::operator+() objet : 0x0066FDDC objet : 0x0066FDCC objet : 0x0066FDB4 1) 2) 3) 4) obj :0x0066FDDC + obj :0x0066FDCC 5) objet : 0x0066 FDA4 6) 7) objet : 0x0066FD94 8)
c1=c1+(complexe)3.5; //8)-9)-10) cout << c1 << endl; //11) c1=(double)c1+3.5; cout << c1 << endl; double d; d=c1; cout << d << endl; //12)-13) //14)
complexe::operator+()
obj :0x0066FDDC + obj :0x0066FD94 9) objet : 0x0066FD84 10) 11) obj :0x0066FDDC objet : 0x0066FD74 12) 13) 14) obj :0x0066FDDC 15) 16)
//15) //16)
9.4
62
Les lignes de la sortie cran ont t numrotes : il y a 16 sorties cran. 1) construction de c1 (objet dadresse 0x0066FDDC) 2) construction de c2 (objet dadresse 0x0066FDCC) 3) construction dun objet temporaire, partir de largument 2.4, qui est pass loprateur = . Ladresse de cet objet temporaire est 0x0066FDB4.
Note : puisque loprateur = na pas t explicitement dfini, cest loprateur = par dfaut qui est utilis. Ce dernier fait une copie membre--membre qui convient parfaitement puisque les objets de la classe complexe nont pas de donnes en profondeur.
Affichage du contenu de c1 grce loprateur << 5) Loprateur + de la classe complexe est appel. 6) Un objet temporaire dadresse 0x0066FDA4 est construit pour retourner le rsultat de loprateur car cest un retour par valeur. Cest dailleurs cet objet temporaire qui est utilis comme argument de loprateur =. 7) Affichage de c2 8) Li la conversion explicite. La valeur 3.5 doit tre convertie en un objet complexe. Il y a donc construction dun objet de la classe complexe dadresse 0x0066FD94. 9) Appel de loprateur + de la classe complexe . On remarque que largument de loprateur est bien lobjet dadresse 0x0066FD94 10) Loprateur + retourne un objet par valeur. Il y a construction dun objet dadresse 0x0066FD84. 11) Affichage de c1 12) L encore, il ya une conversion explicite. On souhaite convertir c1 en type double, ce qui fait appel loprateur double de la classe complexe sur lobjet c1 (dont ladresse est 0x0066FDDC) Note : par consquent, cest loprateur + correspondant au type double qui est utilis ici, et non celui du type complexe . 13) Puisque (double)c1+3.5 est de type double , il doit y a voir une conversion implicite double-> complexe avant laffectation. Il y a donc construction dun objet temporaire dadresse 0x0066FD74. 14) Affichage de c1 15) Il y a conversion implicite du type complexe vers le type double. Cette conversion implicite utilise loprateur double de la classe complexe sur lobjet c1. 16) Affichage de c1
4)
Conclusion Ces conversions de type constituent un sujet sensible. Lutilisation de ces moyens de conversions peut conduire des situations o le compilateur ne sait pas quelle conversion mettre en uvre. Cest le cas pour la ligne suivante :
c1=c1+3.5; // c1=c1+(complexe)3.5 ? ou c1=(complexe)((double)c1+3.5) ?
Le compilateur ne sait pas si lon souhaite que 3.5 soit converti en complexe ou bien que c1 soit converti en double . Le compilateur indique alors le message derreur : error c2666 : 2 overloads have similar conversions. Notons que cela ne conduit dailleurs pas au mme rsultat !
Ne pas confondre les deux sens de l'oprateur & : adresse de / dclaration d'une rfrence
int & refI=varI; // dclaration d'une rfrence int * p=&varI; // oprateur qui fournit l'adresse d'une variable
Surcharge de fonctions Plu sieurs fonctions peuvent avoir le mme nom mais pas la mme signature Paramtres avec valeurs par dfaut Valeur attribue au paramtre quand celui-ci fait dfaut
void f(int pI, double d=2.1); void f(char *, int i=4);
64
Dfinition des mthodes dans un fichier spar (fichier d'extension .cpp) [source partielle]
MaClasse::MaClasse() { _val=0; // la donne membre _val est initialise 0; _autre = 2.1; } MaClasse::MaClasse(const MaClasse & obj) { // implementation non fournie ... } int MaClasse::GetVal() const { return _val; // retourne la valeur de la donne membre _val(l'tat de l'objet) } void MaClasse::SetVal(int val) const { _val=val; // affecte une nouvelle valeur l'tat }
A noter un constructeur/un destructeur ne retourne rien (ne pas mettre void non plus) le destructeur n'a pas d'argument un constructeur sert donner un tat cohrent l'objet (penser initialiser toutes les donnes membres) un accesseur est une mthode qui retourne ou donne accs l'tat de l'objet. C'est une mthode constante. inversement, une mthode qui permet la modification de l'tat d'un objet ne doit pas tre constante.
MaClasse objet3(objet1); // utilise le constructeur par copie pour objet3 cout << objet1.GetVal() << endl; objet1.SetVal(12); } // nouvel tat : (objet1._val==12,objet1._autre==2.1)
65
class MaClasse { public: MaClasse(); void Affiche(std::ostream & sortie) const; }; #endif --------------------------------------------------------------------------
Projet C++ : pour pouvoir compiler et diter les liens, les deux fichiers maclasse.cp p et utilisation.cpp doivent tre dans un mme projet. Note : on peut galement faire un fichier librairie partir d'un ou de plusieurs fichiers de sources de dfinition des mthodes d'une classe. Pour utiliser une classe, l'utilisateur n'a plus ensuite qu' crer un projet incluant la librairie ( la place des fichiers sources de dfinition des mthodes).
66
void main() { cout << "Hello world !" << endl; string str1("abc"); string str2("def"); string str3(str1); str1=str1+str2; // construction d'un objet string // str3 est construit par copie de str1 // l'oprateur + ralise une concatnation
cout << str1 << endl; // un objet string s'affiche sur le flot cout cout << "3ime caractre de str1 :" cout << str1[2] << endl; // un objet string s'utilise comme un tableau }
Utilisation d'une classe paramtre en type. La bibliothque STL fournit des classes paramtres en type pour stocker des donnes
vector<Ty> stack<Ty> = classe de tableaux stockant des donnes de type Ty = classe de piles de donnes de type Ty
------------------------------------------------------------------------------------------#include <iostream> #include <vector> // dclaration de la classe paramtre vector<Ty> using namespace std; void main() { vector<int> v1(5,2); vector<int> v2(8); v1[2]=3; v1[4]=2; // utiliser l'espace de nom std aussi pour ces classes
// ici, viter v1[5]=7 car les indices valides vont de 0 4 (car la taille est 5) v1.resize(12); // mthode qui modifie la taille du vecteur // vector<Ty>::size() est l'accesseur de taille
// ici c'est bon, car le vecteur vient d'tre redimensionn // l'affectation entre deux objets vector<int> est possible // v2 est un vecteur de caractres de taille 7
// v3 stocke donc des caractres // objet "pile" pour grer des int // place la valeur 12 sur la pile // objet "pile" pour grer des float // place la valeur float 3.25 sur la pile pileF
/* ce qui ne fonctionne pas (voir chapitre ddi aux classes paramtres) v3=v1; // les classes vector<int> et vector<char> sont diffrentes
*/ } -------------------------------------------------------------------------------------------
67
----------------------------------------------------------------------//fichier composite.h #ifndef __CLS_COMPOSITE__ #define __CLS_COMPOSITE__ #include "composant.h" class Composite { public: Composite(); Composant GetComposant() const; // retourne une copie du composant private: Composant _composant; // objet membre }; #endif -----------------------------------------------------------------------
Il faut souligner le rle de la liste d'initialisation pour les classes composites. Ci-dessus, l'objet composant est initialis par le constructeur 1 argument de type int (seul constructeur disponible pour cette classe).
----------------------------------------------------------------------#include "Composite.h" void main() { Composite c1; Composant c2(c1.GetComposant()); } -----------------------------------------------------------------------
Remarque : pour l'exemple ci-dessus, la classe Composant doit tre dote d'un constructeur de copie valide : d'une part pour que l'accesseur Composite::GetComposant() retourne une copie du composant et que la construction de c2 soit possible dans le programme d'utilisation.
68
L encore, il faut souligner le rle de la liste d'initialisation. Ci-dessus, l'objet spcialis initialise la partie hrite par le constructeur 1 argument de type string. On peut supposer qu'ici la classe gnrale contient un objet membre de type string, mais a importe peu. Ce qui importe c'est de regarder l'interface de la super-classe pour savoir quels sont les constructeurs disponibles. Toutes les mthode publiques de la super-classe sont utilisables sur la sous-classe. Eventuellement, certains phnomnes de masquage ncessitent l'utilisation du nom de la classe et de l'oprateur de porte ::
----------------------------------------------------------#include "specialisee.h" void main() { Specialisee s1; s1.MethodeC(); // la mthode C est accessible pour la sous-classe
s1.MethodeB(); // mthode B de la super-classe s1.MethodeB(std::string("toto")); // mthode B de la sous-classe s1.MethodeA(); // la mthode de la classe spcialise // masque celle de la super-classe // en raison du masquage, il faut prciser // si l'on souhaite // utiliser la mthode de la super-classe.
s1.Generale::MethodeA();
} -----------------------------------------------------------
69
Le polymorphisme en C++
Lorsqu'une hirarchie de classes existe, le C++ autorise certaines conversions de type. D'un point de vue ensembliste, il est normal que les objets d'une sous-classe puissent tre aussi considrs comme des objets de la superclasse. Par exemple, la classe des tres humains tant un sous-ensemble de celle des tres vivants, un tre humain est un tre vivant.
class EtreVivant { public: void Methode(); }; class Animal: public EtreVivant { public: void Methode(); }; class Europeen: public EtreHumain { public: void Methode(); };
EtreVivant +Methode()
Animal +Methode()
EtreHumain +Methode()
Europeen +Methode()
Pour la hirarchie dcrite ci-dessus, certaines conversions de types sont possibles. Par ailleurs, faute de prcision, la ligature des mthodes est statique. Cela signifie que via un pointeur de type EtreHumain *, on n'accde qu'au mthodes de la classe EtreHumain ou des super-classess (par exemple EtreVivant ).
void main()// conversion sous-classe * -> classe * { Europeen E; Animal A; EtreHumain * ptrEH=&E; // Europeen * -> EtreHumain * ptrEH->Methode(); // appel de la mthode de la classe EtreHumain mme si l'objet // point est de type Europeen //mthode galement accessible
ptrEH->EtreVivant::Methode();
EtreVivant * ptrEV = ptrEH; // conversion EtreHumain * -> EtreVivant * ptrEV->Methode(); // appel de la mthode de la classe EtreVivant // Animal * -> EtreVivant *
Ligature dynamique : on peut prciser dans la classe de base si une recherche dynamique de mthode doit tre effectue (mot cl virtual).
class EtreVivant { public: virtual void Methode(); // demande au compilateur de mettre en place un lien dynamique virtual ~EtreVivant(); // le destructeur doit tre virtuel }; void main() { EtreVivant * tableau[4]; tableau[0]=new EtreVivant; tableau[1]=new Animal; tableau[2]=new Europeen; tableau[3]=new EtreHumain; tableau[0]->Methode(); tableau[1]->Methode(); tableau[2]->Methode(); tableau[3]->Methode(); // // // // mthode mthode mthode mthode de de de de la la la la classe classe classe classe EtreVivant Animal Europeen EtreHumain
70
Utilisation du patron :
-----------------------------------------------------------------------------#include "Tableau.h" #include "Rationnel.h" #include "Point.h" void main() { Tableau<int> tab1(3); Tableau<Rationnel> tab2(4); Tableau<Point> tab3(10); tab1.SetElement(2,3); tab2.SetElement(0,Rationnel(1,2)); tab1[0]=4; } ------------------------------------------------------------------------------
71