LE LANGAGE C++ ENAC 1997 A. DANCEL
1 - GENERALITES "L'homme se découvre quand il se mesure avec l'objet." Antoine de Saint-Exupéry, Terre des hommes 1.1 INTRODUCTION Ce cours est conçu pour permettre aux programmeurs maîtrisant le langage C de passer facilement au langage C++. Pour pouvoir suivre avec profit ce cours sur le langage C++, il est donc indispensable de bien connaître le langage C-ANSI, car je ne présente que les aspects nouveaux du langage C++. Le langage C++ a été développé dans les laboratoires «AT&T Bell» au début des années 1980 par le Danois Bjarne Stroustrup (http://www.research.att.com/~bs). Il est une évolution du langage C auquel il apporte : la programmation orientée par les objets. des améliorations notables (sans perte d'efficacité) : contrôles plus stricts, passage des paramètres par références Il permet de compiler des programmes écrits en C-ANSI (à quelques incompatibilités près). Contrairement à ADA qui a été normalisé sans avoir de compilateur, le C++ a vu de nombreuses versions de compilateur sans normalisation. On assiste en ce moment à la phase finale de normalisation du C++ (celle ci devrait normalement intervenir en 1998, le vote du document final ayant eut lieu en décembre 97). Ne nous faisons pas d'illusions : le langage C++ est et restera un langage complexe et long à maîtriser. L'apprentissage du C++ n'est donc pas à la portée de tout le monde et une formation de plusieurs mois est requise pour pouvoir programmer en C++. 1.2 LA CLASSE La classe est la structure de base de la programmation orientée par les objets (POO). La classe d'objets est un "moule" à partir duquel des objets ayant des propriétés analogues seront formés. Un objet est une instance d'une classe, c'est à dire qu'il est un élément conforme au modèle que constitue sa classe. La programmation orientée par les objets est une approche générale de la programmation. C'est une forme de programmation modulaire dans laquelle l'unité de modularité est fortement liée aux structures des données manipulées par le programme. Un langage à objets utilise les notions de classe et d'instance (ou d'objet), que l'on peut comparer aux notions de type et de variable d'un langage tel que le C. A. DANCEL - Le langage C++ 1
Au lieu d'appliquer des procédures ou des fonctions globales à des variables, en programmation orientée par les objets on invoque les méthodes des instances d'une classe. Cette invocation est souvent appelée "envoi de message" à une instance d'une classe. L'abstraction et l'encapsulation des données sont les deux premiers principes fondateurs de la programmation orientée par les objets. Au lieu de résoudre, comme en C, un problème par des variables et des fonctions sans lie n explicite, l'abstraction des données nous propose de réunir dans une même structure les données membres (attributs) et les fonctions membres (méthodes) les manipulant. Pour éviter toute altération non désirée, l'encapsulation rend les données privées. L'utilisateur d'un objet n'aura pas un accès direct aux données, il devra passer par les méthodes publiques. L'ensemble des méthodes publiques est appelé interface. Cette dernière spécifie ce qu'un utilisateur peut faire avec un objet. Le bon programmeur OO devra donc se concentrer sur le "ce que peut faire un objet" plutôt que de se pencher sur le "comment faut-il le faire". On peut alors manipuler les classes comme des "boites noires" nous permettant ainsi de créer de nouveaux types : types abstraits de données. La réutilisabilité est un autre atout essentiel. Les langages à objets permettent une réutilisation bien plus importante que la plupart des autres langages. 1.3 L'HERITAGE L'un des avantages principaux de la POO, la réutilisation du code, est permis grâce à l'héritage. Au lieu de développer une nouvelle application en écrivant du code à partir de zéro, nos classes héritent des données et des opérations issues d'une classe de base intéressante et ajoute ensuite de nouvelles fonctionnalités en décrivant en quoi la nouvelle classe (dite classe dérivée) diffère de cette classe de base. Une classe dérivée peut se différencier de la classe de base par : ajout de méthodes ajout de données membres redéfinition de méthodes héritées de la classe de base Une classe dérivée peut à son tour servir de classe de base pour d'autres classes dérivées, nous permettant ainsi de définir une organisation hiérarchique de classes. Ceci permet d'organiser les classes de manière cohérente et de partager (factoriser) entre elles du code commun. C++ permet aussi à une classe : d'avoir plusieurs classes de base : héritage multiple de ne servir que de classe de base : classe abstraite (pas d'instanciation possible) REMARQUE : il ne faut pas confondre l'héritage avec l'agrégation. Une agrégation est une forme d'association qui relie une entité avec ses composants. Elle est définie comme la relation "est composée de". Exemple : une voiture est composée d'un moteur. Alors que l'héritage est défini comme la relation "est une sorte de". Exemple : une voiture est une sorte de véhicule. A. DANCEL - Le langage C++ 2
1.4 LE POLYMORPHISME Le polymorphisme permet de décrire le comportement d'une fonction de façon indépendante de la nature des paramètres. Il existe plusieurs formes de polymorphisme : Polymorphisme (d'héritage) : non seulement une classe de base ayant plusieurs classes dérivées élimine le code redondant, mais elle donne en plus le moyen de rendre les programmes clients plus généraux. Ainsi, grâce au polymorphisme d'héritage, des fonctions n'utilisant que des méthodes d'une classe de base peuvent agir sur des instances de toute classe dérivée de cette classe de base, comme s'il s'agissait vraiment d'instances de la classe de base. Surcharge (surdéfinition de nom de fonction) : la surcharge réduit le nombre de fonctions dont un programmeur doit se souvenir. Elle permet de définir des fonctions (ou des opérateurs) ayant le même nom, dès lors que chacun exige des types et/ou un nombre d'arguments différents. Ainsi en programmation système sous UNIX les fonctions execl(), execle(), execlp(), execv(), execve(), excecvp() pourraient toutes s'appeler exec(). Généricité (modèle, patrons) : consiste à définir un modèle de fonction en utilisant des types comme paramètres. Elle nous permet, par exemple, d'écrire une classe générique Stack permettant à l'utilisateur de cette classe de spécifier le type des données de la pile. Son utilisation conjointe avec le polymorphisme d'héritage nous permet d'obtenir des structures de données polymorphes. 1.5 RESUME Le langage C++ est compatible ascendant avec le langage C auquel il ajoute : Un type plus fort L'abstraction de données (la classe) Le support (facultatif) pour la programmation orientée par les objets (héritage, généricité, polymorphisme...) Le traitement des erreurs à l'exécution (exception) Et tout ceci sans perte d'efficacité. Cependant, contrairement à un langage comme Java, il n'y a pas de support explicite dans le langage pour la concurrence (thread), pour le réseau, la distribution (CORBA), la persistance, les fuites mémoires ("garbage collection"), etc Sa compatibilité avec les librairies écrites en C-ANSI lui permet de faire tout cela facilement et de s'interfacer avec X-windows, d'appeler les primitives du système d'exploitation... Son avenir? Java? A. DANCEL - Le langage C++ 3
2 - UN MEILLEUR C Le langage C++ apporte un certain nombre d'améliorations et de nouveautés par rapport au langage C, indépendamment de la POO, qui rendent la vie du programmeur plus agréable. 2.1 LES COMMENTAIRES Le langage C++ offre une nouvelle façon d'ajouter des commentaires. En plus des symboles /* et */ utilisés en C, le langage C++ offre les symboles // qui permettent d'ignorer tout jusqu'à la fin de la ligne. /* commentaire traditionnel */ sur plusieurs lignes valide en C et C++ // commentaire de fin de ligne valide en C++ #if 0 // Une partie d'un programme en C ou C++ peut toujours // être ignorée par les directives au préprocesseur // #if... #endif #endif Il est préférable d'utiliser les symboles // pour la plupart des commentaires et de n'utiliser les commentaires C ( /* */ ) que pour isoler des blocs importants d'instructions. Il est très important que tout fichier C++ (et C) commence par des commentaires décrivant le fichier. La forme dépend de vous et de votre entreprise, mais devra comporter au minimum : Le nom du programme, Le nom de la classe ou de la fonction, Le nom du fichier, de l'auteur etc La description du fonctionnement, Les dates de mise à jour, Les éventuelles remarques. /////////////////////////////////////////////////////////////// // PROGRAMME : essai // FICHIER : Commentaire2.cpp // AUTEUR : Alain Dancel - ENAC // DESCRIPTION : essai des différents styles de commentaires A. DANCEL - Le langage C++ 4
// MISES A JOUR : 15/03/98 Version 0.99 // STATUS : en cours de validation // REMARQUES : compilation par : // g++ -o essai Commentaire2.cpp /////////////////////////////////////////////////////////////// int main() { return 0; Conseils : éviter les commentaires triviaux qui surchargent inutilement le code de votre programme et n'oubliez surtout pas de mettre à jour les commentaires de début de fichier chaque fois que votre programme est modifié. 2.2 ENTREES/SORTIES SIMPLES Il est possible d'utiliser les fonctions scanf et printf pour effectuer les entrées/sorties de vos programmes, mais cependant les programmeurs C++ préfèrent les entrées/sorties par flux (flot ou stream). Quatre flots sont prédéfinis lorsque vous avez inclus le fichier d'en-tête iostream.h : 1. cout qui correspond à la sortie standard. 2. cin qui correspond à l'entrée standard. 3. cerr qui correspond à la sortie standard d'erreur non tamponné. 4. clog qui correspond à la sortie standard d'erreur tamponnée. L opérateur (surchargé) << permet d'envoyer des valeurs dans un flot de sortie : #include <iostream.h> int i = 123; float f = 456.789; char ch[80] = «Bonjour\n»; cout << i; // affiche : 123 cout << «f=» << f; // affiche : 456.789 cout << ch; // affiche : Bonjour L'opérateur >> permet d'extraire des valeurs d'un flot d'entrée : #include <iostream.h> int i; float f; char ch[80]; A. DANCEL - Le langage C++ 5
cout << "i =? "; cin >> i; // lecture d'un entier cout << "f =? "; cin >> f; // lecture d'un réel cout << "ch =? "; cin >> ch; // lecture du premier mot d'une chaîne cout << "ch= " << ch; // c'est bien le premier mot... Tout comme pour la fonction scanf, les espaces sont considérés comme des séparateurs entre les données par le flux cin. Notez l'absence de l'opérateur & dans la syntaxe du cin. Ce dernier n'a pas besoin de connaître l'adresse de la variable à lire. Les programmeurs C++ ne préfèrent pas utiliser les fonctions scanf et printf de la librairie standard à cause de : 1 la vitesse d'exécution plus rapide des flux C++ : la fonction printf doit analyser à l'exécution la chaîne de formatage, tandis qu'avec les flux C++, la traduction est faite à la compilation. 2 la vérification de type : pas d'affichage erroné comme dans l'exemple ci dessous : #include <stdio.h> #include <iostream.h> double d=567.89; printf("d = %d!!!!\n", d); // Affiche : d = -5243!!!! // ^erreur: %lf normalement cout << "d = " << d; // Affiche : d = 567.89 3 la taille mémoire réduite : seul le code nécessaire est mis par l'éditeur de liens, alors que pour la fonction printf tout le code correspondant à toutes les possibilités d'affichage est mis. 4 et surtout, on préfère les flux C++, parce que l'on peut utiliser ceux ci avec ses propres classes (surcharge possible de >> et <<) et l'on pourra donc écrire : Avion av; // une instance de la classe Avion // Initialisation de av cout << "Avion " << av ; 2.3 LES MANIPULATEURS Les manipulateurs sont des éléments qui modifient la façon dont les éléments sont lus ou écrits dans le flot. Les principaux manipulateurs sont : dec lecture/écriture d'un entier en décimal A. DANCEL - Le langage C++ 6
oct hex endl setw(int n) lecture/écriture d'un entier en octal lecture/écriture d'un entier en hexadécimal insère un saut de ligne et vide les tampons affichage de n caractères setprecision(int n) affichage de la valeur avec n chiffres avec éventuellement un arrondi de la valeur setfill(char) définit le caractère de remplissage flush vide les tampons après écriture #include <iostream.h> #include <iomanip.h> int i=1234; float p=12.3456; cout << " " << setw(8) << setfill('*') << hex << i << " \n" << " " << setw(6) << setprecision(4) << p << " " << endl; /*-- résultat de l'exécution ------------------- *****4d2 *12.35 ------------------------------------------------*/ 2.4 LES CONVERSIONS EXPLICITES En C++, comme en langage C, il est possible de faire des conversions explicites de type, bien que le langage soit plus fortement typé. Le C++ offre aussi une notation fonctionnelle pour faire une conversion explic ite de type : double d = 3.14159; int i ; i = int(d) / 2 ; Cette notation ne marche que pour les types simples et les types utilisateurs. Pour les types pointeurs ou tableaux le problème peut être résolu en définissant un nouveau type : double d; int *i; typedef int *ptr_int; // ptr_int est du type: pointeur sur entier i = ptr_int(&d); La conversion explicite de type est surtout utile lorsqu'on travaille avec des pointeurs du type void *. Mais en C++, seule la conversion implicite T * vers void * existe (pas l inverse contrairement au langage C) : void *addr ; int *ptr ; A. DANCEL - Le langage C++ 7
addr = ptr ; // OK en C et C++ ptr = addr ; // OK en C, illégal en C++ 2.5 DEFINITION DE VARIABLES En C++ vous pouvez déclarer les variables ou fonctions n'importe où dans le code. La portée de telles variables va de l'endroit de la déclaration jusqu'à la fin du bloc courant. Ceci permet : De définir une variable aussi près que possible de son utilisation afin d'améliorer la lisibilité. C'est particulièrement utile pour des grosses fonctions ayant beaucoup de variables locales. D'initialiser un objet avec une valeur obtenue par calcul ou saisie : int i; cin >> i; int j = 2 * i; j++; // définition d'une variable // instruction // définition d'une autre variable // instruction En C++, il est possible d initialiser une variable globale (ou de classe static) avec une valeur non évaluable par le compilateur (contrairement au langage C) : static int i = 3; static int j = i + 1; // illégal en C static int k = nbjours(2*j); // illégal en C void f1( int nbre ) { static int N = 2 * nbre; // illégal en C 2.6 VARIABLE DE BOUCLE On peut déclarer une variable de boucle directement dans l'instruction for. Ceci permet de n'utiliser cette variable que dans le bloc de la boucle. #include <iostream.h> for(int i=0; i<10; i++) cout << i << ' '; // i n'est pas utilisable à l'extérieur du bloc for /*-- Résultat de l'exécution -------- 0 1 2 3 4 5 6 7 8 9 -------------------------------------*/ On peut faire la même chose dans un if ou un while. A. DANCEL - Le langage C++ 8
2.7 VISIBILITE DES VARIABLES L'opérateur de résolution de portée :: permet d'accéder aux variables globales plutôt qu'aux variables locales. #include <iostream.h> int i = 22; int i = 44; { int i = 55; cout << ::i << " " << i; // affiche : 22 55 cout << ::i << " " << i ; // affiche : 22 44 L'utilisation abusive de cette technique n'est pas une bonne pratique de programmation (lisibilité). Il est préférable de donner des noms différents plutôt que de réutiliser les mêmes noms. En fait, on utilise beaucoup cet opérateur pour définir hors d'une classe les fonctions membres ou pour accéder à un identificateur dans un espace de noms (cf. espace de noms). 2.8 LES CONSTANTES Les habitués du C ont l'habitude d'utiliser la directive du pré processeur #define pour définir des constantes. Il est reconnu que l'utilisation du pré processeur est une source d'erreurs difficiles à détecter. En C++, l'utilisation du pré processeur se limite aux cas les plus sûrs : Inclusion de fichiers. Compilation conditionnelle. Le mot réservé const permet de définir une constante. L'objet ainsi spécifié ne pourra pas être modifié durant toute sa durée de vie et pourra être traitée par un débogueur. Il est indispensable d'initialiser la constante au moment de sa définition. const int N = 10; // N est un entier constant. const int MOIS=12, AN=1995; // 2 constantes entières int tab[2 * N]; // autorisé en C++ (interdit en C) Mais attention, un pointeur peut toujours «violer» une constante : const double salaire = 10000.0; *(double *)&salaire = 50000.0; // C est mieux payé 2.9 CONSTANTES ET POINTEURS Il faut distinguer ce qui est pointé du pointeur lui-même. Le mot const permet de protéger le pointeur ou la valeur pointée ou les deux : La donnée pointée est constante : A. DANCEL - Le langage C++ 9
const char *ptr1 = "QWERTY"; ptr1++; // Autorisé *ptr1 = 'A'; // ERROR: assignment to const type Le pointeur est constant : char * const ptr2 = "QWERTY"; ptr2++; // ERROR: increment of const type *ptr2 = 'A'; // autorisé Le pointeur et la donnée sont constants : const char * const ptr3 = "QWERTY"; ptr3++; // ERROR: increment of const type *ptr3 = 'A'; // ERROR: assignment to const type 2.10 LES TYPES 2.10.1 Le type booléen C++ dispose d un type booléen nommé bool, dont les valeurs possibles sont true et false. Les conditions du if, while, for et de l opérateur conditionnel? : sont des expressions du type bool. Les opérateurs relationnels ==,!=, <, etc. retournent un résultat de type bool. Les opérateurs logiques &&, et! ont leurs opérandes et leurs résultats du type bool. Il existe une conversion implicite du type bool vers le type int (false donne 0 et true donne 1). La conversion inverse existe et est à éviter. bool b; b = 2 > 3; // b = false if ( b ) 2.10.2 Les types composés En C++, comme en langage C, le programmeur peut définir des nouveaux types en définissant des struct, des enum ou des union. Mais contrairement au langage C, l'utilisation de typedef n'est plus obligatoire pour renommer un type. Cela permet d alléger l écriture des définitions et des transtypages. struct FICHE { char *nom, *prenom; ; // en C, il faut ajouter la ligne : typedef struct FICHE FICHE; FICHE adherent, *liste, repertoire[100]; enum BOOLEEN { FAUX, VRAI ; // en C, il faut ajouter la ligne : typedef enum BOOLEEN BOOLEEN; BOOLEEN trouve = FAUX; Chaque énumération enum est un type particulier, différent de int et ne peut prendre que les valeurs énumérées dans sa définition : A. DANCEL - Le langage C++ 10
enum Jour {DIMANCHE, LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI; enum Couleur {NOIR, BLEU, VERT, CYAN, ROUGE, MAGENTA, BRUN, GRIS; Jour j; // définition d'une variable de type Jour j = LUNDI; // OK j = 2; // ERREUR en C++ (légal en C) int i = MARDI; // OK il existe une conversion implicite vers le type int Couleur c; // définition d'une variable de type Couleur c = j; // ERREUR en C++ (légal en C) 2.10.3 Les références En plus des variables normales et des pointeurs, le C++ offre les variables références. Une variable référence permet de créer une variable qui est un "synonyme" d'une autre. Dès lors, une modification de l'une affectera le contenu de l'autre. int i; int & ir = i; // ir est une référence à i i=2; cout << "i= " << i << " ir= " << ir; // affiche : i= 2 ir= 2 ir=3; cout << "i= " << i << " ir= " << ir; // affiche : i= 3 ir= 3 int *ptr = &ir; *ptr = 4; cout << "i= " << i << " ir= " << ir; // affiche : i= 4 ir= 4 Une variable référence doit obligatoirement être initialisée et le type de l'objet initial doit être le même que l'objet référence. Intérêt : Passage des paramètres par référence, Utilisation d'une fonction en lvalue. 2.11 ALLOCATION MEMOIRE Le C++ met à la disposit ion du programmeur deux opérateurs new et delete pour remplacer respectivement les fonctions malloc et free (bien qu'il soit toujours possible de les utiliser). 2.11.1 L opérateur new L'opérateur new réserve l'espace mémoire qu'on lui demande et l'initialise. Il retourne l'adresse de début de la zone mémoire allouée. A. DANCEL - Le langage C++ 11
int *pt1, *pt2, *pt3; pt1 = new int; // allocation dynamique d'un entier pt2 = new int [10]; // allocation d'un tableau de 10 entiers /////// à ne pas confondre avec : pt3 = new int(10); // allocation d'un entier avec initialisation à 10 struct date {int jour, mois, an; ; date *pt4, *pt5, *pt6, d = {25, 4, 1952; pt4 = new date; // allocation dynamique d'une structure pt5 = new date[10]; // allocation d'un tableau de structure pt6 = new date(d); // allocation dynamique avec initialisation En cas d'erreur d'allocation par new, une exception bad_alloc est lancée s'il n'y a pas de fonction d'interception définie par l'utilisateur. L'allocation des tableaux à plusieurs dimensions est possible : typedef char T[80]; // T est un synonyme de : tableau de 80 char T *ecran; ecran = new T[25]; // ecran est un tableau de 25 fois 80 char ecran[24][79]='$'; cout << ecran[24][79]; // 2 ème solution : char (*ecran)[80] = new char[25][80]; ecran[24][79]='$'; cout << ecran[24][79]; 2.11.2 L opérateur delete L'opérateur delete libère l'espace mémoire d un objet alloué par new, tandis que l'opérateur delete[] libère l'espace mémoire alloué à un tableau d'objets : delete ptr1; // libération d'un entier delete[] ptr2; // libération d'un tableau d'entier L'application de l'opérateur delete à un pointeur nul est légale et n entraîne aucune conséquence fâcheuse (l'opération est tout simplement ignorée). A chaque instruction new doit correspondre une instruction delete. Il est important de libérer l'espace mémoire dès que celui ci n'est plus nécessaire. La mémoire allouée en cours de programme sera libérée automatiquement à la fin du programme. Tout ce qui est alloué avec new [], doit être libéré avec delete[], sinon ce n est que le premier objet du tableau qui est libéré (fuite mémoire). typedef char LIGNE[80]; // LIGNE est un tableau de 80 caractères LIGNE *ptr = new LIGNE; A. DANCEL - Le langage C++ 12
//... delete[] ptr; // ptr est un tableau... 2.11.3 La fonction d'interception set_new_handler : Si une allocation mémoire par new échoue, une fonction d'erreur utilisateur peut être appelée. La fonction set_new_handler, déclarée dans new.h, permet d'installer votre propre fonction d'erreur : #include <iostream.h> #include <stdlib.h> // exit() #include <new.h> // set_new_handler() // fonction d'erreur d'allocation mémoire dynamique void erreur_memoire( void) { cerr << "\nla mémoire disponible est insuffisante!!!" << endl; exit(1); set_new_handler( erreur_memoire ); double *tab = new double [100000000]; Si la fonction d'interception erreur_memoire ne comporte pas un exit, une nouvelle demande d'allocation mémoire est faite, et cela jusqu'à ce que l'allocation réussisse. A. DANCEL - Le langage C++ 13
3 - LES FONCTIONS 3.1 DECLARATION DES FONCTIONS Le langage C++ impose au programmeur de déclarer le nombre et le type des arguments de la fonction. Ces déclarations sont identiques aux prototypes de fonctions de la norme C-ANSI et sont par ailleurs obligatoires avant utilisation (contrairement à la norme C-ANSI). Ceci permet d éliminer un certain nombre d erreurs C. En C+, une fonction déclarée avec une liste d'arguments vide impose qu elle ne puisse pas être appelée avec des paramètres : int f1(); // déclaration équivalente à : int f1( void ); f1("toto", 123.45); // ERREUR en C++, légal en C La norme C-ANSI considère que f1 est une fonction qui peut recevoir un nombre quelconque d'arguments, eux mêmes de type quelconques, comme si elle était déclarée int f1(... ); Une fonction dont le type de la valeur de retour n'est pas void, doit obligatoirement retourner une valeur. 3.2 PASSAGE PAR REFERENCE En plus du passage par valeur, le C++ définit le passage par référence. Lorsque l'on passe à une fonction un paramètre par référence, cette fonction reçoit un "synonyme" du paramètre réel. Toute modification du paramètre référence est répercutée sur le paramètre réel. void echange(int &n1, int &n2); // déclaration de la fonction int i=2, j=3; echange( i, j ); // appel de la fonction echange cout << "i= " << i << " j= " << j; // affiche : i= 3 j= 2 void echange(int &n1, int &n2) { // n1 et n2 sont des alias des paramètres réels i et j int temp = n1; n1 = n2; // toute modification de n1 est répercutée sur i n2 = temp; // toute modification de n2 est répercutée sur j Comme vous le remarquez, l'appel se fait de manière très simple. Le passage par référence permet de simplifier l écriture des fonctions mais ne permet pas de rendre explicite le passage des paramètres. Quand il est écrit dans le programme ci dessus echange(i,j); on ne sait pas si les paramètres sont passés par valeur ou par référence. Cette facilité augmente la puissance du langage mais doit être utilisée avec précaution, car elle ne protège plus la valeur du paramètre réel transmis par référence. A. DANCEL - Le langage C++ 14
L'utilisation du mot réservé const permet d'annuler ces risques, lorsque l on passe des objets de grande taille par référence, dans un but d efficacité, et ne devant pas être modifiés dans la fonction. struct FICHE { char nom[30], prenom[20], email[256]; ; void affiche(const FICHE &f) { // passage par référence (plutôt que par valeur) pour des // raisons d'efficacité. Une modification du paramètre f // dans la fonction provoque une erreur de compilation cout << f.nom << " " << f.prenom; cout << " " << f.adresse << endl; FICHE user = { "Dancel", "Alain", "dancel@enac.fr" ; affiche(user); Références et pointeurs peuvent se combiner : int ouverture(file *&f, const char *nf, const char *mode) { // passage par référence d'un pointeur. f = fopen(nf, mode); return (f == null)? -1 : 0; int main() { FILE *fic; if (ouverture(fic, "/tmp/toto.fic", "r") == -1) exit(1); int i; fscanf(fic, "%d", &i); Utilisez les références quand vous pouvez, et n utiliser les pointeurs que quand vous devez. 3.3 VALEUR PAR DEFAUT DES PARAMETRES Certains arguments d'une fonction peuvent prendre souvent la même valeur. Pour ne pas avoir à spécifier ces valeurs à chaque appel de la fonction, le C++ permet de déclarer des valeurs par défaut dans le prototype de la fonction. void print(long valeur, int base = 10); print(16); // affiche 16 (16 en base 10) print(16, 2); // affiche 10000 (16 en base 2) void print(long valeur, int base){ affichage de la valeur dans la base Les paramètres par défaut sont obligatoirement les derniers de la liste. A. DANCEL - Le langage C++ 15
Ils ne sont déclarés que dans le prototype de la fonction et pas dans sa définition. Pourquoi? les prototypes sont dans un fichier.h modifiable par l utilisateur. La valeur par défaut est ainsi modifiable selon le projet, sans avoir à remettre en cause le module objet correspondant. 3.4 FONCTION INLINE Le mot clé inline remplace avantageusement l'utilisation de #define du pré processeur pour définir des pseudo fonctions. Afin de rendre l'exécution plus rapide d'une fonction et à condition que celle ci soit de courte taille, on peut définir une fonction avec le mot réservé inline. Le compilateur générera, à chaque appel de la fonction, le code de celle ci. Les fonctions inline se comportent comme des fonctions normales et donc, présentent l'avantage de vérifier les types de leurs arguments, ce que ne fait par la directive #define. #include <iostream.h> inline int carre(int n); // déclaration cout << carre(10) << endl; // inline facultatif à la définition, mais préférable inline int carre(int n) { return n * n; ATTENTION : Contrairement à une fonction normale, la portée d'une fonction inline est réduite au module dans lequel elle est déclarée. 3.5 SURCHARGE DE FONCTIONS Une fonction se définit par : 1 son nom, 2 sa liste typée de paramètres formels, 3 le type de la valeur qu'elle retourne. Mais seuls les deux premiers critères sont discriminants. On dit qu'ils constituent la signature de la fonction. On peut utiliser cette propriété pour donner un même nom à des fonctions qui ont des paramètres différents : int somme( int n1, int n2) { return n1 + n2; int somme( int n1, int n2, int n3) { return n1 + n2 + n3; A. DANCEL - Le langage C++ 16
double somme( double n1, double n2) { return n1 + n2; cout << "1 + 2 = " << somme(1, 2) << endl; cout << "1 + 2 + 3 = " << somme(1, 2, 3) << endl; cout << "1.2 + 2.3 = " << somme(1.2, 2.3) << endl; Le compilateur sélectionnera la fonction à appeler en fonction du type et du nombre des arguments qui figurent dans l'appel de la fonction. Ce choix se faisant à la compilation, fait que l'appel d'une fonction surchargée procure des performances identiques à un appel de fonction "classique". On dit que l'appel de la fonction est résolu de manière statique. Autres exemples : enum Jour {DIMANCHE, LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI; enum Couleur {NOIR, BLEU, VERT, CYAN, ROUGE, MAGENTA, BRUN, GRIS; void f1(jour j); void f1(couleur c); // OK : les types énumérations sont tous différents void f2(char *str) { /*... */ void f2(char ligne[80]) { /*... */ // Erreur: redéfinition de la fonction f // char * et char [80] sont considérés de même type int somme1(int n1, int n2) {return n1 + n2; int somme1(const int n1, const int n2) {return n1 + n2; // Erreur: la liste de paramètres dans les déclarations // des deux fonctions n'est pas assez divergente // pour les différencier. int somme2(int n1, int n2) {return n1 + n2; int somme2(int & n1, int & n2) {return n1 + n2; // Erreur: la liste de paramètres dans les déclarations // des deux fonctions n'est pas assez divergente // pour les différencier. int somme3(int n1, int n2) {return n1 + n2; double somme3(int n1, int n2) {return (double) n1 + n2; // Erreur: seul le type des paramètres permet de faire la distinction // entre les fonctions et non pas la valeur retournée. A. DANCEL - Le langage C++ 17
int somme4(int n1, int n2) {return n1 + n2; int somme4(int n1, int n2=8) {return n1 + n2; // Erreur: la liste de paramètres dans les déclarations // des deux fonctions n'est pas assez divergente // pour les différencier. typedef int entier; // entier est un alias de int int somme5(int n1, int n2) {return n1 + n2; int somme5(entier e1, entier e2) {return e1 + e2;; // Erreur : redéfinition de somme5 : des alias de type ne // sont pas considérés comme des types distincts 3.6 RETOUR D'UNE REFERENCE 3.6.1 Fonction retournant une référence Une fonction peut retourner une valeur par référence et on peut donc agir, à l'extérieur de cette fonction, sur cette valeur de retour. La syntaxe qui en découle est plutôt inhabituelle et déroutante au début. #include <iostream.h> int t[20]; // variable globale - beurk!!! int & nieme(int i) { return t[i]; nieme(0) = 123; nieme(1) = 456; cout << t[0] << " " << ++nieme(1); // affiche : 123 457 Tout se passe comme si nieme(0) était remplacé par t[0]. Une fonction retournant une référence (non constante) peut être une lvalue. La variable dont la référence est retournée doit avoir une durée de vie permanente, ce qui explique pourquoi j utilise une variable globale. En C, pour réaliser la même chose, nous aurions du écrire : int t[20]; // variable globale - beurk!!! int * nieme(int i) { return &t[i]; *nieme(0) = 123; A. DANCEL - Le langage C++ 18
*nieme(1) = 456; printf("%d %d\n", t[0] ++(*nieme(1)) ); ce qui est moins lisible et pratique... 3.6.2 Retour d'une référence constante Afin d'éviter la création d'une copie, dans la pile, de la valeur retournée lorsque cette valeur est de taille importante, il est possible de retourner une valeur par référence constante. Le préfixe const, devant le type de la valeur retournée, signifie au compilateur que la valeur retournée est constante. const Big & f1() { static Big b; // notez le static... return b; f1() = 12; // erreur 3.7 UTILISATION D UNE FONCTION ECRITE EN C Le compilateur C++ génère pour chaque fonction un nom dont l'éditeur de liens aura besoin. Le nom généré en C++ se fait à partir de la signature de la fonction. Ainsi, à une fonction surchargée 2 fois, correspondront 2 fonctions de noms différents dans le module objet. En C, la signature de la fonction ne comporte que le nom de la fonction. Pour pouvoir utiliser dans un programme C++ des fonctions compilées par un compilateur C, il faut déclarer ces fonctions de la façon suivante : ou extern "C" int f1(int i, char c); extern "C" void f2(char *str); extern "C" { int f1(int i, char c); void f2(char *str); Par contre, les fonctions f1 et f2 ne pourront pas être surchargées. 3.8 FICHIER D'EN-TETES POUR C ET C++ Le symbole cplusplus est défini pour les compilateurs C++ uniquement. Il facilite l'écriture de code commun aux langages C et C++, et plus particulièrement pour les fichiers d'en-têtes : #ifdef cplusplus extern "C" { #endif A. DANCEL - Le langage C++ 19
void fail(char *msg); int calcul(double d1, double d2); #ifdef cplusplus #endif Cet exemple peut être compilé aussi bien en C qu'en C++. A. DANCEL - Le langage C++ 20
4 - LES CLASSES 4.1 DEFINITION D'UNE CLASSE La classe décrit le modèle structurel d'un objet comprenant l'ensemble des attributs (ou champs ou données membres) décrivant sa structure opérations (ou méthodes ou fonctions membres) qui lui sont applicables. class Avion { void init(char [], char *, float); // fonction membre void affiche(); // fonction membre char immatriculation[6], *type; // données membres float poids; ; // N oubliez pas ce ; après l'accolade 4.2 DROITS D'ACCES // donnée membre L'encapsulation consiste à masquer l'accès à certains attributs et méthodes d'une classe. Elle est réalisée à l'aide des mots clés : private les membres privés ne sont accessibles que par les fonctions membres de la classe. La partie privée est aussi appelée réalisation. Les données membres doivent être privées -> encapsulation. On y accède par des méthodes. protected les membres protégés sont comme les membres privés. Mais ils sont aussi accessibles par les fonctions membres des classes dérivées (voir l'héritage). public les membres publics sont accessibles par tous. L'ensemble des méthodes de la partie publique est appelé interface. C'est la vue de l'utilisateur. Le concepteur à accès à tous les membres de la classe (privés, protégés et publics). Les sections private, protected et public peuvent figurer plusieurs fois dans la déclaration de la classe et le droit d'accès ne change pas tant qu'un nouveau droit n'est pas spécifié. 4.3 RECOMMANDATIONS DE STYLE Elles sont propres à l'entreprise. Les plus couramment utilisées sont : la première lettre du nom de la classe en majuscule la liste des membres publics en premier les noms des méthodes en minuscules le caractère _ comme premier caractère du nom d'une donnée membre. Cette dernière règle pose parfois des problèmes avec certains #define des fichiers standards. A. DANCEL - Le langage C++ 21
4.4 DEFINITION DES FONCTIONS MEMBRES En général, la déclaration d'une classe contient simplement les prototypes des fonctions membres de la classe. ////////////////// Avion.h class Avion { public : void init(char [], char *, float); void affiche(); private : char _immatriculation[6], *_type; float _poids; void erreur(char *msg); ; // méthode privée Les fonctions membres sont définies dans un module séparé ou plus loin dans le code source. L'ordre de leurs définitions n'a plus d'importance (mais devrait respecter l'ordre de la déclaration pour des raisons évidentes de lisibilité). ////////////////// Avion.cpp ///////////////////////////// Définition de la méthode init void Avion::init(char im[], char *type, float poids) { if ( strlen(im)!= 5 ) { erreur("immatriculation invalide"); // appel d'une méthode strcpy(_immatriculation, "?????"); else strcpy(_immatriculation, m); _type = new char [strlen(type)+1]; strcpy(_type, type); _poids = poids; ///////////////////////////// Définition de la méthode affiche void Avion::affiche() { cout << _immatriculation << " " << _type << " " << _poids << endl; ///////////////////////////// Définition de la méthode erreur void Avion::erreur(char *msg) { cerr << "ERREUR : " << msg << endl; exit(1); La définition de la méthode peut aussi avoir lieu à l'intérieur de la déclaration de la classe. Dans ce cas, ces fonctions sont automatiquement traitées par le compilateur comme des fonctions inline. Une fonction membre définie à l'extérieur de la classe peut être aussi qualifiée explicitement de fonction inline. A. DANCEL - Le langage C++ 22
//////////////////// Nombre.h class Nombre { public : void setnbre(int n) { nbre = n; // fonction inline int getnbre() { return nbre; // fonction inline void affiche(); private : int nbre; ; inline void Nombre::affiche() { // fonction inline cout << "Nombre = " << nbre << endl; Rappel : la visibilité d'une fonction inline est restreinte au module seul dans laquelle elle est définie. Dans l'exemple ci dessus, il faudra que la définition de la méthode affiche soit dans un fichier inc lus par le module l'utilisant. 4.5 INSTANCIATION D'UNE CLASSE De façon similaire à une struct ou à une union, le nom de la classe représente un nouveau type de donnée. On peut donc définir des variables de ce nouveau type; on dit alors que vous créez des objets ou des instances de cette classe. Avion av1; // une instance simple (statique) Avion *av2; // un pointeur (non initialisé...) Avion compagnie[10]; // un tableau d'instances av2 = new Avion; // création (dynamique) d'une instance 4.6 UTILISATION DES OBJETS Après avoir créé une instance (de façon statique ou dynamique) on peut accéder aux attributs et méthodes de la classe. Cet accès se fait comme pour les structures à l'aide de l'opérateur (point) ou - > (tiret supérieur). av1.init("fgbcd", "TB20", 1.47); av2->init("fgdef", "ATR 42", 80.0); compagnie[0].init("fefgh","a320", 150.0); av1.affiche(); av2->affiche(); compagnie[0].affiche(); av1.poids = 0; // erreur, poids est un membre privé 4.7 FONCTIONS MEMBRES CONSTANTES Certaines méthodes d'une classe ne doivent (ou ne peuvent) pas modifier les valeurs des données membres de la classe, ni retourner une référence non constante ou un pointeur non constant d'une donnée membre. On dit que ce sont des fonctions membres constantes. Ce type de déclaration renforce les contrôles effectués par le compilateur et permet donc une programmation plus sûre sans surcoût d'exécution. Il est donc très souhaitable d'en déclarer aussi souvent que possible dans les classes. A. DANCEL - Le langage C++ 23
class Nombre { public : void setnbre(int n) { nbre = n; // méthodes constantes int getnbre() const { return nbre; void affiche() const; private : int nbre; ; inline void Nombre::affiche() const { cout << "Nombre = " << nbre << endl; Une fonction membre const peut être appelée sur des objets constants ou pas, alors qu'une fonction membre non constante ne peut être appelée que sur des objets non constants. const Nombre n1; // un objet constant qu'on ne peut pas // initialiser pour l'instant... n1.setnbre(15); // ERREUR: seule les fonctions const peuvent // être appelées pour un objet constant n1.affiche(); // OK Nombre n2; n2.setnbre(15); // OK n2.affiche(); // OK 4.7.1 Surcharge d'une méthode par une méthode constante Une méthode déclarée comme constante permet de surcharger une méthode non constante avec le même nombre de paramètres du même type. class String { ; public : // etc... char & nieme(int n); // (1) char nieme(int n) const; // (2) // etc... private : char *_str; S'il n'y a pas l'attribut const dans la deuxième méthode, le compilateur génère une erreur ("String::nieme() cannot be redeclared in class"). Cette façon de faire permet d'appliquer la deuxième méthode nieme() à des objets constants et la première méthode nieme() à des objets variables : String ch1; // Initialisation de ch1 A. DANCEL - Le langage C++ 24
const String ch2; // Initialisation de ch2 cout << ch1.nieme(1); // appel de la méthode (1) cout << ch2.nieme(1); // appel de la méthode (2) L'écriture d'une instruction comme : ch2.nieme(3) = 'u'; ne sera pas compilable, car la méthode (2) ne retourne pas une référence. Si pour des raisons d'efficacité, la méthode (2) doit retourner une référence, on retournera alors une référence constante et l'on écrira donc : const char & nieme(int n) const; // (2 bis) Et que se passe-t-il si j'écris : au lieu de : char & nieme(int n) const; // (2 bis) //on retourne une référence non constante const char & nieme(int n) const; // (2 bis) //on retourne une référence constante Le programme suivant se compile parfaitement et donne un résultat surprenant : const String ch2; // initialisation de ch2 avec la chaîne "coco" ch2.nieme(3) = 'u'; ch2.affiche(); // affiche "cocu"!! // vous n'avez vraiment pas de chance!!! La question maintenant est : comment une méthode constante a-t-elle pu modifier la valeur d'un objet constant? Quand on déclare une méthode constante, le compilateur vérifie que la méthode ne modifie pas une donnée membre. Et c'est le cas de la méthode nieme(). Pour générer une erreur il aurait fallu qu'elle change la valeur de _str, chose qu'elle ne fait pas. Elle ne peut changer la valeur que d'un caractère pointé par _str. 4.8 EXEMPLE : PILE D'ENTIERS (1) // IntStack.h : déclaration d'une pile d'entiers class IntStack { void init(int taille = 10); // création d'une pile A. DANCEL - Le langage C++ 25
void push(int n); // empile un entier au sommet de la pile int pop(); // retourne l'entier au sommet de la pile bool vide() const; // vrai, si la pile est vide bool pleine() const;// vrai, si la pile est pleine int getsize() const { return _taille; private: int _taille; // taille de la pile int _sommet; // position de l'entier à empiler int *_addr; // adresse de la pile ; // IntStack.cpp : définition d'une pile d'entiers // Compilation : g++ -Wall c IntStack.cpp #include <iostream.h> #include <assert.h> #include "IntStack.h" void IntStack::init(int taille ) { _addr = new int [ _taille = taille ]; // chasseur de ns assert( _addr!= 0 ); _sommet = 0; void IntStack::push(int n) { if (! pleine() ) _addr[ _sommet++ ] = n; // chasseur de ns int IntStack::pop() { return (! vide() )? _addr[ --_sommet ] : 0; int IntStack::vide() const { return ( _sommet == 0 ); int IntStack::pleine() const { return ( _sommet == getsize() ); Exemple d'utilisation de cette classe : // EssaiPile.cpp // compilation : g++ -Wall EssaiPile.cpp IntStack.o #include <iostream.h> #include <stdlib.h> // rand() #include "IntStack.h" IntStack pile1; A. DANCEL - Le langage C++ 26
pile1.init(15); // pile de 15 entiers while (! pile1.pleine() ) // remplissage de la pile pile1.push( rand() % 100 ); while (! pile1.vide() ) // Affichage (et dépilage) cout << pile1.pop() << " "; cout << endl; 4.9 CONSTRUCTEUR ET DESTRUCTEUR Les données membres d'une classe ne peuvent pas être initialisées; il faut donc prévoir une méthode d'initialisation de celles-ci (voir la méthode init de l'exemple précédent). Si l'on oublie d'appeler cette fonction d'initialisation, le reste n'a plus de sens et il se produira très certainement des surprises fâcheuses dans la suite de l'exécution... De même, après avoir fini d'utiliser l'objet, il est bon de prévoir une méthode permettant de détruire l'objet (libération de la mémoire dynamique...). 4.9.1 Constructeur Le constructeur est une fonction membre spécifique de la classe qui est appelée implicitement à l'instanciation de l'objet, assurant ainsi une initialisation correcte de l'objet. Ce constructeur est une fonction qui porte comme nom, le nom de la classe et qui ne retourne pas de valeur (pas même un void). On appelle constructeur par défaut un constructeur n'ayant pas de paramètre ou ayant des valeurs par défaut pour tous les paramètres. Ce constructeur est sollicité automatiquement dans certaines situations. Si le concepteur de la classe ne spécifie aucun constructeur, le compilateur générera un constructeur par défaut. class Nombre { ; public : Nombre(); // constructeur par défaut private : int _i; Nombre::Nombre() { // définition du constructeur _i = 0; Comme les autres fonctions, les constructeurs peuvent être surchargés. class Nombre { public : Nombre() { _i = 0; // constructeur par défaut Nombre(int i) { _i = i; // constructeur à un paramètre private : int _i; ; A. DANCEL - Le langage C++ 27
ou mieux : class Nombre { public : Nombre(int i=0) { _i = i; private : int _i; ; Le constructeur est appelé à l'instanciation de l'objet. Nombre n1; // correct, appel du constructeur par défaut Nombre n2(10); // correct, appel du constructeur à 1 paramètre Nombre tab1[10]; // chaque objet du tableau est initialisé // par un appel au constructeur par défaut Nombre tab2[3] = { Nombre(10), Nombre(20), Nombre(30) ; // initialisation des 3 objets du tableau // par les nombres 10, 20 et 30 Il n'est pas appelé quand on définit un pointeur sur un objet... Nombre *ptr1, *ptr2; // correct, pas d'appel aux constructeurs ptr1 = new Nombre; // appel au constructeur par défaut ptr2 = new Nombre(12); // appel du constructeur à 1 paramètre 4.9.2 Destructeur De la même façon que pour les constructeurs, le destructeur est une fonction membre spécifique de la classe qui est appelée implicitement à la destruction de l'objet. Ce destructeur est une fonction : qui porte comme nom, le nom de la classe précédé du caractère (tilde) qui ne retourne pas de valeur (pas même un void ) qui n'accepte aucun paramètre (le destructeur ne peut donc pas être surchargé) class Nombre { ; public : ~Nombre(); // Déclaration du destructeur private : Nombre::~Nombre() { // Définition du destructeur Comme pour le constructeur, le compilateur générera un destructeur par défaut si le concepteur de la classe n'en spécifie pas un. A. DANCEL - Le langage C++ 28
4.9.3 Remarques Les constructeurs et destructeurs sont les seules méthodes non constantes qui peuvent être appelées pour des objets constants. Attention aux fautes de frappe, particulièrement dans l'identificateur du constructeur : class Essai { ; private: char *_ptr; essai() { _ptr = new char[80]; // méthode ordinaire // à cause de l'erreur void init(char c) {for(int i=0; i<80; i++) _ptr[i]=c; ~Essai() { delete[] _ptr; // destructeur Essai e; e.init('\0'); // Aïe, Aïe, Aïe... explosion assurée A cause de la faute de frappe dans le nom du constructeur, celui ci n'a pas été appelé à la création de l'objet e. Certains compilateurs signalent cette erreur : warning: Essai has Essai::~Essai() but no constructor (273) error: no value returned from Essai::essai() (1404) 4.10 EXEMPLE : PILE D'ENTIERS (2) // Pile d'entiers avec constructeurs et destructeur. // IntStack.h : déclaration d'une pile d'entiers class IntStack { IntStack(int taille = 10); // constructeur ~IntStack() { delete[] _addr; // destructeur void push(int n); // empile un entier au sommet de la pile int pop(); // retourne l'entier au sommet de la pile bool vide() const; // vrai, si la pile est vide bool pleine() const;// vrai, si la pile est pleine int getsize() const { return _taille; private: int _taille; // taille de la pile int _sommet; // position de l'entier à empiler int *_addr; // adresse de la pile ; // IntStack.cpp : définition d'une pile d'entiers // Compilation : g++ -Wall c IntStack.cpp A. DANCEL - Le langage C++ 29
#include <iostream.h> #include <assert.h> #include "IntStack.h" IntStack::IntStack(int taille ) { //// Constructeur _addr = new int [ _taille = taille ]; // chasseur de ns assert( _addr!= 0 ); _sommet = 0; Sans changement pour les autres méthodes Exemple d'utilisation de cette classe : // EssaiPile.cpp // compilation : g++ -Wall EssaiPile.cpp IntStack.o #include <iostream.h> #include <stdlib.h> // rand() #include "IntStack.h" IntStack pile0; IntStack pile1(15); // Pile de 10 entiers // Pile de 15 entiers // etc... 4.11 CONSTRUCTEUR COPIE 4.11.1 Présentation du problème Reprenons la classe IntStack avec un constructeur et un destructeur et écrivons une fonction AfficheSommet qui affiche la valeur de l'entier au sommet de la pile qui est passée en paramètre. Exemple d'appel de cette fonction : IntStack pile1(15); // création d'une pile de 15 entiers AfficheSommet( pile1 ); Une version fausse (pour l'instant) de cette fonction peut ressembler au code qui suit : void AfficheSommet( IntStack pile ) { cout << "Sommet de la pile : " << pile.pop() << endl; A. DANCEL - Le langage C++ 30
Ici, la pile est passée par valeur, donc il y a création de l'objet temporaire nommé pile créé en copiant les valeurs du paramètre réel pile1. L'affichage de la valeur au sommet de la pile marche bien, mais la fin de cette fonction fait appel au destructeur de l'objet local pile qui libère la mémoire allouée par l'objet pile1 parce que les données membres de l'objet pile contiennent les mêmes valeurs que celles de l'objet pile1. Pour éviter ce mécanisme de recopie et l'appel du destructeur à la fin de la fonction AfficheSommet, il faut définir la fonction AfficheSommet comme : void AfficheSommet( IntStack & pile ) { // Version 2 cout << "Sommet de la pile : " << pile.pop() << endl; Mais une opération comme pile.pop() dépile un entier de la pile pile1!!! Solution : il faut faire une copie intelligente : création du constructeur de copie. 4.11.2 Création du constructeur de copie Le constructeur de copie est invoqué implicitement à chaque fois que l'on doit construire un objet à partir d'un objet existant de la même classe. Nombre n1(10); // appel du constructeur à 1 paramètre Nombre n2(n1); // appel du constructeur de copie Nombre n3 = n1; // appel du constructeur de copie Le constructeur de copie est appelé aussi pour le passage d'arguments par valeur et le retour de valeur : Nombre traitement(nombre n) { static Nombre nbre; return nbre; // appel du constructeur de copie Nombre n1, n2; n2 = traitement( n1 ); // appel du constructeur de copie Le constructeur de copie d'une classe X est un constructeur dont le premier paramètre est de type référence (évidemment) sur X et dont tous les (éventuels) paramètres suivants ont une valeur par défaut Le compilateur C++ génère par défaut un constructeur de copie "bête" qui effectue une copie "superficielle", c'est à dire membres à membres. Cela marche bien pour des classes simples, comme la classe Nombre ci dessus. Pour les classes dont la copie superficielle n'est pas acceptable, il faut impérativement le définir explicitement, comme pour la classe IntStack ci dessous. A. DANCEL - Le langage C++ 31
4.11.3 Constructeur de copie de la classe IntStack class IntStack { IntStack(int taille = 10); // constructeur par défaut IntStack(const IntStack & s); // constructeur de copie ~IntStack() { delete[] _addr; // destructeur private: int _taille; // taille de la pile int _sommet; // position de l'entier à empiler int *_addr; // adresse de la pile ; //////////////////////// Définition du constructeur de copie IntStack::IntStack(const IntStack & s) { // Création de la zone mémoire _addr = new int [ _taille = s._taille ]; // Copie de la valeur du sommet _sommet = s._sommet; // Recopie de tous les éléments for (int i=0; i< _sommet; i++) _addr[i] = s._addr[i]; 4.12 CLASSES IMBRIQUEES Il est possible de créer une classe par une relation d'appartenance : relation "a un" ou "est composée de". Exemple : une voiture a un moteur, a des roues... class Moteur { /*... */ ; class Roue { /*... */ ; class Voiture {. private: Moteur _moteur; Roue _roue[4];. ; Des types peuvent aussi être définis dans la partie privée ou publique d'une autre classe. L'accès aux types imbriqués suit les mêmes contrôles que ceux des membres de la classe. La classe englobante n'a pas de droit particulier sur le type englobé (et inversement). A. DANCEL - Le langage C++ 32
Pour pouvoir utiliser les types (publics) de l'extérieur de la classe, il est obligatoire d'utiliser l'opérateur de résolution de portée ::. class Pixel { // A titre pédagogique, pour illustrer les public : // types imbriqués. enum Color { NOIR, BLEU, VERT, ROUGE, CYAN ; class Point { public : Point(int x=0, int y=0) { _x = x; _y = y; private : short _x, _y; ; Pixel(Point p, Color c); // Constructeur Color getcolor() const; Point getpoint() const; private : Point _p; Color _c; ; Pixel p1( Point(100, 200), Pixel::ROUGE); Pixel::Color c; // Définition d'une variable Color Pixel::Point p(10, 20); // Création d'un point c = Pixel::NOIR; Pixel p2( p, c); 4.13 AFFECTATION ET INITIALISATION Le langage C++ fait la différence entre l'initialisation et l'affectation. l'affectation consiste à modifier la valeur d'une variable (et peut avoir lieu plusieurs fois) l'initialisation est une opération qui n'a lieu qu'une fois immédiatement après que l'espace mémoire de la variable ait été alloué. Cette opération consiste à donner une valeur initiale à l'objet ainsi créé. C'est le constructeur qui effectue cette initialisation. Nombre n1(123); // initialisation Nombre n2; Nombre n3 = n1; // initialisation n2 = n3; // Affectation 4.14 LISTE D'INITIALISATION D'UN CONSTRUCTEUR Soit la classe : A. DANCEL - Le langage C++ 33
class Y { /*... */ ; class X { X(int a, int b, const Y &y); ~X();. private: const int _x; Y _y; int _z; ; X::X(int a, int b, const Y &y) { _x = a; // ERREUR: l'affectation à une constante est interdite _z = b; // OK : affectation // et comment initialiser l'objet membre _y??? Comment peut-on initialiser la donnée membre constante _x? Comment peut-on appeler le constructeur de la classe Y? Réponse : la liste d'initialisation. La phase d'initialisation de l'objet utilise une liste d'initialisation qui est spécifiée dans la définition du constructeur. Syntaxe : nom_classe::nom_constructeur( args... ) : liste_d_initialisation { // corps du constructeur Exemple : X::X(int a, int b, const Y &y) : _x( a ), _y( y ), _z( b ) { // rien d'autre à faire pour cet exemple L'expression _x( a ) indique au compilateur d'initialiser la donnée membre _x avec la valeur du paramètre a. L'expression _y( y ) indique au compilateur d'initialiser la donnée membre _y par un appel au constructeur (avec un argument) de la classe Y. 4.15 LE POINTEUR this Toute méthode d'une classe X a un paramètre caché : le pointeur this. Celui contient l'adresse de l'objet qui l'a appelé, permettant ainsi à la méthode d'accéder aux membres de l'objet. Il est implicitement déclaré comme (pour une variable) : X * const this; A. DANCEL - Le langage C++ 34
et comme (pour un objet constant) : const X * const this; et initialisé avec l'adresse de l'objet sur lequel la méthode est appelée. Il peut être explicitement utilisé : classe X { int f1() const { return this->i; // idem que : int f1() { return i; private: int i; ; Une fonction membre qui retourne le pointeur this peut être chaînée, étant donné que les opérateurs de sélection de membres. (point) et -> (tiret supérieur) sont associatifs de gauche à droite : classe X { ; X f1() const { cout << "X "; return *this; private: X x; x.f1().f1().f1(); // affiche : X X X La fonction membre f1 a tout intérêt de retourner une référence : const X &f1() const { cout << "X "; return *this; La valeur ne this ne peut ni être changée Le pointeur this ne peut pas être explicitement déclaré. 4.16 LES MEMBRES STATIQUES Ces membres sont utiles lorsque l'on a besoin de gérer des données ou des méthodes communes à toutes les instances d'une même classe. 4.16.1 Données membres statiques Si l'on déclare une donnée membre comme static, elle aura la même valeur pour toutes les instances de cette classe. A. DANCEL - Le langage C++ 35
class Ex1 { Ex1() { nb++; /*... */ ~Ex1() { nb--; /*... */ private: static int nb; // initialisation impossible ici ; L'initialisation de cette donnée membre statique se fera en dehors de la classe et en global par une déclaration : int Ex1::nb = 0; // initialisation du membre static 4.16.2 Fonctions membres statiques De même que les données membres statiques, il existe des fonctions membres statiques. ne peuvent accéder qu'aux membres statiques, ne peuvent pas être surchargés, existent même s'il n'y a pas d'instance de la classe. class Ex1 { Ex1() { nb++; /*... */ ~Ex1() { nb--; /*... */ static void affiche() { // fonction membre statique cout << nb << endl; private: static int nb; ; int Ex1::nb = 0; // initialisation du membre static (en global) Ex1.affiche(); // affiche 0 (pas d'instance de Ex1) Ex1 a, b, c; Ex1.affiche(); // affiche 3 a.affiche(); // affiche 3 A. DANCEL - Le langage C++ 36
4.17 CLASSES ET FONCTIONS AMIES "Un ami est quelqu'un qui peut toucher vos parties privées." Dans la définition d'une classe il est possible de désigner des fonctions (ou des classes) à qui on laisse un libre accès à ses membres privés ou protégés. C'est une infraction aux règles d'encapsulation pour des raisons d'efficacité. class Nombre { // ici je désigne les fonctions qui pourront accéder // aux membres privés de ma classe friend int manipule_nbre(); // fonction amie public : int getnbre(); private : int _nbre; ; int manipule_nbre(nombre n) { return n._nbre + 1; // je peux le faire en tout légalité // parce que je suis une fonction amie // de la classe Nombre. Classe amie : class Window; // déclaration de la classe Window class Screen { friend class Window; //... private : //... // Classe amie ; Les fonctions membres de la classe Window peuvent accéder aux membres non publics de la classe Screen. A. DANCEL - Le langage C++ 37
A. DANCEL - Le langage C++ 38
5 - LES OPERATEURS Les opérateurs forment une famille particulière de fonctions. Il faut offrir tous les opérateurs possibles à une classe permettant ainsi de la considérer comme un type de base. 5.1 INTRODUCTION A LA SURCHARGE D'OPERATEURS Le concepteur d'une classe doit fournir à l'utilisateur de celle ci toute une série d'opérateurs agissant sur les objets de la classe. Ceci permet une syntaxe intuitive de la classe. Par exemple, il est plus intuitif et plus clair d'additionner deux matrices en surchargeant l'opérateur d'addition et en écrivant : Matrice m0, m1, result; result = m0 + m1; que d'écrire matrice_add(result, m0, m1); Surcharger un opérateur revient à écrire une fonction (ou une méthode suivant le cas). Toutes les règles relatives aux fonctions (sauf pour la valeur par défaut des paramètres) s'appliquent donc à la surcharge des opérateurs. 5.2 REGLES D'UTILISATION Il faut veiller à respecter l'esprit de l'opérateur. Il faut faire avec les types utilisateurs des opérations identiques à celles que font les opérateurs avec les types prédéfinis. La plupart des opérateurs sont surchargeables. Les opérateurs suivants ne sont pas surchargeables : ::..*?: sizeof # ## Il n'est pas possible de : changer sa priorité changer son associativité changer sa pluralité (unaire, binaire, ternaire) créer de nouveaux opérateurs Par contre la surcharge d'un opérateur ne conserve pas : Sa commutativité : l'opérateur d'addition défini pour effecteur X + Y (le type de X et de Y étant différent) ne permet pas d'effectuer Y + X A. DANCEL - Le langage C++ 39
Les relations avec les autres opérateurs induits : la surcharge de + est totalement indépendante de celle de +=. Il faudra, dans ce cas, penser également à surcharger l'opérateur +=. 5.3 SURCHARGE D'UN OPERATEUR Quand l'opérateur + (par exemple) est appelé, le compilateur génère un appel à la fonction operator+. Cette fonction peut être : Une méthode de la classe du premier opérande : Nombre n; n + 3; // équivalent à : n.operator+(3); Une fonction globale : Nombre n; n + 3; // équivalent à : operator+(n, 3); 5.3.1 Surcharge par une fonction membre Par exemple, la surcharge des opérateurs + et = par des fonctions membres de la classe Matrice s'écrit : class Matrice { // matrice 2 x 2 d'entiers const Matrice &operator=(const Matrice &n2); Matrice operator+(const Matrice &n2); private: int _matrice[2][2]; ; Matrice a, b, c; b + c ; // appel à : b.operator+( c ); a = b + c; // appel à : a.operator=( b.operator+( c ) ); Les opérateurs = () [] -> new delete ne peuvent être surchargés que comme des fonctions membres. Une fonction membre (non statique) peut toujours utiliser le pointeur caché this. Dans le code ci dessus, this fait référence à l'objet b pour l'opérateur + et à l'objet a pour l'opérateur =. Définition de la fonction membre operator+ : Matrice Matrice::operator+(const Matrice & m) { Matrice temp; A. DANCEL - Le langage C++ 40
for(int i = 0; i < 2; i++) for(int j = 0; j < 2; j++) temp._matrice[i][j] = _matrice[i][j] + m._matrice[i][j]; return temp; Quand on a le choix, l'utilisation d'une fonction membre pour surcharger un opérateur est préférable. Une fonction membre renforce l'encapsulation. Les opérateurs surchargés par des fonctions membres se transmettent aussi par héritage (sauf l'affectation). La fonction membre operator+ peut elle même être surchargée, pour dans l'exemple qui suit, additionner à une matrice un vecteur : class Matrice { // matrice 2 x 2 entiers ; Matrice operator=(const Matrice &n2); Matrice operator+(const Matrice &n2); Matrice operator+(const Vecteur &n2); Matrice b, c; Vecteur v; b + c; // appel à b.operator+(c); b + v; // appel à b.operator+(v); addition entre une matrice // et un vecteur v + b; // appel à v.operator+(b); --> ERREUR si la classe // Vecteur n'a pas défini l'addition entre un vecteur et // une matrice 5.3.2 Surcharge par une fonction globale Cette façon de procéder est plus adaptée à la surcharge des opérateurs binaires. En effet, elle permet d'appliquer des conversions implicites au premier membre de l'expression. Le plus souvent, pour des raisons d'efficacité, cette fonction sera déclarée amie de la classe. Class Nombre{ friend Nombre operator+(const Nombre &, const Nombre &); Nombre(int n = 0) { _nbre = n; //... private: int _nbre; ; Nombre operator+(const Nombre &nbr1, const Nombre &nbr2) { A. DANCEL - Le langage C++ 41
Nombre n; n._nbre = nbr1._nbre + nbr2._nbre; return n; Nombre n1(10); n1 + 20; // OK appel à : operator+( n1, Nombre(20) ); 30 + n1; // OK appel à : operator+( Nombre(30), n1 ); 5.4 OPERATEUR D'AFFECTATION C'est le même problème que pour le constructeur de copie. Le compilateur C++ construit par défaut un opérateur d'affectation "bête". L'opérateur d'affectation est obligatoirement une fonction membre et il doit fonctionner correctement dans les deux cas suivants : XXX x1, x2, x3; // 3 instances de la classe XXX x1 = x1; // Qui donc écrit des trucs comme cela??? x1 = x2 = x3; // Ca, oui, c'est utile Exemple : Opérateur d'affectation de la classe Matrice : class Matrice { // matrice 2 x 2 entiers const Matrice &operator=(const Matrice &m); private: int _matrice[2][2]; ; const Matrice &Matrice::operator=(const Matrice &m) { if ( &m!= this ) { // traitement du cas : x1 = x1 for(int i = 0; i < 2; i++) // copie de la matrice for(int j = 0; j < 2; j++) this->_matrice[i][j] = m._matrice[i][j]; return *this; // traitement du cas : x1 = x2 = x3 A. DANCEL - Le langage C++ 42
5.5 CAS PARTICULIERS 5.5.1 Surcharge de ++ et -- notation préfixée : fonction membre : X operator++(); fonction globale : X operator++(x &); notation postfixée : fonction membre : X operator++(int); fonction globale : X operator++(x &, int); L'argument entier passé ne sert rien, sauf à faire la différence dans la surcharge de la fonction operator++. class BigNombre { ; BigNombre operator++(); // notation préfixée BigNombre operator++(int); // notation postfixée BigNombre n1; n1++; // notation postfixée ++n1; // notation préfixée 5.5.2 Surcharge de l'opérateur [] L'opérateur [] doit obligatoirement être défini comme une fonction membre. Son paramètre est l'indice. class IntStack { ; // le retour int &operator [](int i) { assert(i>=0 && i<_sommet); return _addr[i]; private: int _taille; // taille de la pile int _sommet; // position de l'entier à empiler int *_addr; // adresse de la pile A. DANCEL - Le langage C++ 43
Il n'est pas possible de définir un opérateur [] avec plusieurs indices car les tableaux à 2 dimensions n'existent pas, nous ne pouvons avoir que des tableaux de tableaux. Pour résoudre ce problème on définira [] qui renvoie un objet pouvant lui même être indicé. class Matrice { // matrice 2 x 2 d'entiers ; class Tab2; // Déclaration de classe Tab2 &operator [](int i) { return t1[i]; private: class Tab2 { ; public : int &operator [](int j) { return t2[j]; private : int t2[2]; Tab2 t1[2]; // tableau de tableau // classe imbriquée Matrice m; m[0][0] = 123; cout << m[0][0] << endl; 5.5.3 Surcharge de >> et << Les opérateurs >> et << sont définis pour tous les types prédéfinis. Ils permettent d'extraire ou d'injecter des informations formatées du flux. Pour pouvoir lire et écrire dans un flux une donnée de type T il suffit de définir les fonctions globales suivantes qui surchargent les opérateurs d'extraction et d'injection : ostream &operator <<(ostream &os, const T &t); // injection istream &operator >>(istream &is, T &t); // extraction Exemple pour la classe IntStack : class IntStack { // Le retour IV friend ostream &operator <<(ostream &os, const IntStack &s); friend istream &operator <<(istream &is, IntStack &s); public : private : int _sommet, _taille; int *_tab; ; A. DANCEL - Le langage C++ 44
////////////////////////////////////// Injection dans un flux ostream &operator <<(ostream &os, const IntStack &s) { for( int i=0; i<_sommet; i++) os << _tab[i]; return os; //////////////////////////////////////// Extraction d'un flux istream &operator <<(istream &is, IntStack &s) { int v; for( int i=_sommet; i<_taille; i++) { is >> v; s.push(v); return is; 5.5.4 Surcharge de new et delete Si les opérateurs new et delete sont surchargés pour une classe, ils seront automatiquement appelés à chaque création ou destruction d'un objet dynamique de la classe considérée. Ils doivent obligatoirement être surchargés sous forme de fonction membre statique. Ils n'auront donc pas accès au pointeur this ou au membres non static de la classe. Surcharge de new : La méthode operator new est appelée avant le constructeur de la classe. Son paramètre est initialisé par le compilateur avec la taille de la zone mémoire nécessaire pour construire un objet de cette classe. class XXX { ; public : static void *operator new(size_t n); // allocation simple static void *operator new[](size_t n); // pour les tableaux // à titre d'exemple on ne fait rien de spécial void *XXX::operator new(size_t n) { // définition de new return ::operator new(n); // appel à l'opérateur global new void *XXX::operator new[](size_t n) { // définition de new return ::operator new[](n); // appel à l'opérateur global new Surcharge de delete : La méthode operator delete est appelée après le destructeur de la classe. Son paramètre est initialisé par le compilateur avec l'adresse de début de la zone à libérer. A. DANCEL - Le langage C++ 45
class XXX { public : static void operator delete(void *); // libération simple static void operator delete[](void *); // pour les tableaux ; void XXX::operator delete(void *ptr) { // définition de delete ::operator delete(ptr); // appel à l'opérateur global delete void XXX::operator delete[](void *ptr) { // définition de delete ::operator delete[](ptr); // appel à l'opérateur global delete 5.5.5 Surcharge de ( ) La surcharge de l'opérateur () permet de simplifier l'appel d'une méthode particulière d'une classe, comme si l'objet était une fonction. Ces objets, dont l'opérateur () est défini, sont appelés objetsfonctions. Ce concept est largement utilisé dans la bibliothèque standard du C++. Cet opérateur doit être défini comme une fonction membre, et peut, elle aussi, être surchargée. class String { public : // Retourne une chaîne de n caractères String operator() (int debpos, int n=1); String s1, s2("abcdef"); s1 = s2(3, 2); // interprété comme : // s1.operator=( s2.operator() (3, 2) ); 5.6 OPERATEURS DE CONVERSION Dans la définition complète d'une classe, il ne faut pas oublier de définir des opérateurs de conversions de types. Il existe deux types de conversions de types : La conversion de type prédéfini (ou défini préalablement) vers le type classe en question. Ceci sera effectué grâce au constructeur de conversion. La conversion du type classe vers un type prédéfini (ou défini préalablement). Cette conversion sera effectuée par des fonctions de conversion. A. DANCEL - Le langage C++ 46
5.6.1 Constructeur de conversion : Un constructeur avec un seul argument de type T permet de réaliser une conversion d'une variable de type T vers un objet de la classe du constructeur. class Nombre { ; Nombre(int n) { _nbre = n; private: int _nbre; void f1(nombre n) { /*... */ Nombre n = 2; // idem que : Nombre n(2); f1(3); // Appel du constructeur Nombre(int) pour réaliser // la conversion de l'entier 3 en un Nombre. // Pas d'appel du constructeur de copie Il est préférable d'utiliser une conversion explicite : f1(nombre(3)); Dans cet exemple, le constructeur avec un argument permet donc d'effectuer des conversions d'entier en Nombre. C'est pour cette raison qu'on appelle cette sorte de constructeur : constructeur de conversion. 5.6.2 Fonction de conversion Une fonction de conversion est une méthode qui effectue une conversion vers un type T. Elle est nommé operator T(). Cette méthode n'a pas de type de retour (comme un constructeur), mais doit cependant bien retourner une valeur du type T. class Article { ; public : Article(double prix=0.0):_prix(prix) { operator double() const { return _prix; private : double _prix; double total; Article biere(17.50); // utilisation implicite de la conversion Article -> double total = 7 * biere; A. DANCEL - Le langage C++ 47
5.6.3 Conversion explicite La conversion implicite effectuée par le compilateur est parfois gênante et source d'ennuis. Le mot réservé explicit peut être utilisé pour empêcher qu'un constructeur de conversion ou qu'un opérateur de conversion puissent être utilisés pour des conversions implicites : class XXX { ; public : explicit XXX(int n); // constructeur explicit operator int(); XXX x; int i = x; // ERREUR: il faut écrire : int i = int(x); void f(xxx x); // déclaration d'une fonction f( 234 ); // ERREUR: il faut écrire : f( XXX(234) ); A. DANCEL - Le langage C++ 48
A. DANCEL - Le langage C++ 49
6 - LES MODELES Les modèles (ou patrons ou gabarits) permettent de définir des fonctions ou des classes paramétrées par un type (ou une classe). C'est une mise en œuvre de la généricité. 6.1 LES MODELES DE FONCTIONS (patron de fonction ou fonction générique ou fonction template) Lorsque l'algorithme est le même pour plusieurs types de données, il est possible de créer un modèle de fonction. C'est un modèle à partir duquel le compilateur générera les fonctions qui lui seront nécessaires. Exemple 1 : template <class TTT> void affiche(ttt tab[], int nbre) { for(int i = 0; i < nbre; i++) cout << tab[i] << " "; cout << endl; La fonction modèle affiche marchera pour tous les types de base et les classes ayant redéfini l'opérateur []. int tabi[6] = {25, 4, 52, 18, 6, 55; affiche(tabi, 6); double tabd[3] = {12.3, 23.4, 34.5; affiche(tabd, 3); char *tabs[] = {"Bjarne", "Stroustrup"; affiche(tabs, 2); Avion compagnie[5]; // Remplissage du tableau d'avions affiche(compagnie, 5); Exemple 2 : exemple de déclaration puis de définition de fonction modèle. template <class TTT> TTT min(ttt, TTT); // déclaration du patron la suite de votre programme A. DANCEL - Le langage C++ 50
template <class TTT> TTT min(ttt t1, TTT t2) { return t1 < t2? t1 : t2; // définition du patron La fonction modèle affiche marchera pour tous les types de base et les classes (ayant redéfini l'opérateur relationnel <). Exemple 3 : le modèle peut avoir plusieurs paramètres de type : template <class X, class Y> int valeur(x x, Y y) { Les fonctions génériques peuvent, tout comme les fonctions normales, être surchargées. Ainsi il est possible de spécialiser une fonction à une situation particulière, comme dans l'exemple qui suit : template <class TTT> TTT min(ttt t1, TTT t2) { return t1 < t2? t1 : t2; char *min(char *s1, char *s2) { // fonction spécialisée pour les chaînes. Il est évident // que la comparaison des adresses des chaînes est absurde... return strcmp(s1, s2)<0? s1 : s2; cout << min(10, 20) << " " << min("alain", "Bjarne") << endl; 6.2 LES CLASSES PARAMETREES Les classes paramétrées permettent de créer des classes générales et de transmettre des types comme paramètres à ces classes pour construire une classe spécifique. Par exemple la classe Pile n'a pas à se soucier du type réel de ses éléments. Elle ne doit s'occuper que d'empiler, dépiler... ses éléments. On a donc intérêt à paramétrer cette classe par le type des éléments qu'elle stocke. template <class TTT> class Stack { ; stack(int n=10); void push(const TTT &e); TTT &pop(); private: TTT *_tab; A. DANCEL - Le langage C++ 51
La définition des méthodes se fait de la façon suivante : template <class TTT> Stack<TTT>::Stack(int n) { _tab = new TTT[n]; template <class TTT> void Stack<TTT>::push(const TTT &e) { /*...*/ template <class TTT> TTT &Stack<TTT>::pop() { /*...*/ Une utilisation de cette classe sera, par exemple : Stack<int> resultat(20); // Pile de 20 entiers Stack<Employe> r; // Pile de 10 employés resultat.push(234); Comme avec les fonctions modèles, il est possible de spécialiser une classe à une situation particulière : class Stack<char *> { // classe Stack spécialisée pour les char * ; public : 6.3 ARGUMENTS D UNE FONCTION MODELE Outre des types de données, les arguments d'une fonction modèle ou d'une classe modèle peuvent également représenter des valeurs : template<class TTT, int size> class Stack { public : Stack() { // constructeur _tab = new TTT[size]; ; Stack<Employe, 30> p; // Pile de 30 employés Les arguments du modèle peuvent aussi avoir des valeurs par défaut : template<class TTT=int, int size=10> class Stack { A. DANCEL - Le langage C++ 52
; Stack<char *,20> // Pile de 20 chaînes Stack<float> pf; // Pile de 10 réels Stack<> pi; // Pile de 10 entiers Les arguments du modèle peuvent être eux mêmes des instances de modèle permettant ainsi d'écrire : Stack< Stack<int, 20>, 30> p; // p est une pile de 30 piles de 20 entiers. A. DANCEL - Le langage C++ 53
7 - L'HERITAGE "Il est plus facile de modifier que de réinventer" 7.1 L'HERITAGE L'héritage, également appelé dérivation, permet de créer une nouvelle classe à partir d'une classe déjà existante, la classe de base (ou super classe). La nouvelle classe (ou classe dérivée ou sous classe) hérite de tous les membres, qui ne sont pas privés, de la classe de base et ainsi réutiliser le code déjà écrit pour la classe de base. On peut aussi lui ajouter de nouveaux membres ou redéfinir des méthodes. Syntaxe : La classe B hérite de façon publique de la classe A. Tous les membres publics ou protégés de la classe A font partis de l'interface de la classe B. 7.2 MODE DE DERIVATION Lors de la définition de la classe dérivée il est possible de spécifier le mode de dérivation par l'emploi d'un des mots clés suivants : public, protected ou private. Ce mode de dérivation détermine quels membres de la classe de base sont accessibles dans la classe dérivée. Au cas où aucun mode de dérivation n'est spécifié, le compilateur C++ prend par défaut le mot clé private pour une classe et public pour une structure. Les membres privés de la classe de base ne sont jamais accessibles par les membres des classes dérivées. A. DANCEL - Le langage C++ 54
7.2.1 Héritage public Il donne aux membres publics et protégés de la classe de base le même statut dans la classe dérivée. C'est la forme la plus courante d'héritage, car il permet de modéliser les relations "Y est une sorte de X" ou "Y est une spécialisation de la classe de base X". class Vehicule { ; void pub1(); protected: void prot1(); private: void priv1(); class Voiture : public Vehicule { int pub2() { pub1(); // OK prot1(); // OK priv1(); // ERREUR ; Voiture safrane; safrane.pub1(); // OK safrane.pub2(); // OK 7.2.2 Héritage privé Il donne aux membres publics et protégés de la classe de base le statut de membres privés dans la classe dérivée. Il permet de modéliser les relations "Y est composé de un ou plusieurs X". Plutôt que d'hériter de façon privée de la classe de base X, on peut faire de la classe de base une donnée membre (composition). class String { ; int length(); class Telephone_number : private String { void f1() { i = length(); // OK ; Telephone_number tn; A. DANCEL - Le langage C++ 55
cout << tn.length(); // ERREUR 7.2.3 Héritage protégé Il donne aux membres publics et protégés de la classe de base le statut de membres protégés dans la classe dérivée. L'héritage fait partie de l'interface mais n'est pas accessible aux utilisateurs. class String { ; protected: int n; class Telephone_number : protected String { protected: void f2() { n++; // OK ; class Local_number : public Telephone_number { protected: void f3() { n++; // OK ; 7.2.4 Tableau résumé de l'accès aux membres public Statut dans la classe de base public protected private Statut dans la classe dérivée public protected inaccessible mode public protected de protected protected protected dérivation private inaccessible public private private protected private private inaccessible A. DANCEL - Le langage C++ 56
7.3 REDEFINITION DE METHODES On peut redéfinir une fonction dans une classe dérivée si on lui donne le même nom que dans la classe de base. Il y aura ainsi, comme dans l'exemple ci après, deux fonctions f2(), mais il sera possible de les différencier avec l'opérateur :: de résolution de portée. class X { ; void f1(); void f2(); protected: int xxx; class Y : public X { void f2(); void f3(); ; void Y::f3() { X::f2(); // f2 de la classe X X::xxx = 12; // accès au membre xxx de la classe X f1(); // appel de f1 de la classe X f2(); // appel de f2 de la classe Y 7.4 AJUSTEMENT D'ACCES Lors d'un héritage protégé ou privé, nous pouvons spécifier que certains membres de la classe ancêtre conservent leur mode d'accès dans la classe dérivée. Ce mécanisme, appelé déclaration d'accès, ne permet en aucun cas d'augmenter ou de diminuer la visibilité d'un membre de la classe de base. class X { ; void f1(); void f2(); protected: void f3(); void f4(); class Y : private X { X::f1; // f1() reste public dans Y X::f3; // ERREUR: un membre protégé ne peut pas devenir public protected: X::f4; // f3() reste protégé dans Y X::f2; // ERREUR: un membre public ne peut pas devenir protégé ; A. DANCEL - Le langage C++ 57
7.5 HERITAGE DES CONSTRUCTEURS / DESTRUCTEURS Les constructeurs, constructeur de copie, destructeurs et opérateurs d'affectation ne sont jamais hérités. Les constructeurs par défaut des classes de bases sont automatiquement appelés avant le constructeur de la classe dérivée. Pour ne pas appeler les constructeurs par défaut, mais des constructeurs avec des paramètres, vous devez employer une liste d'initialisation. L'appel des destructeurs se fera dans l'ordre inverse des constructeurs. class Vehicule { ; Vehicule() { cout<< "Vehicule" << endl; ~Vehicule() { cout<< "~Vehicule" << endl; class Voiture : public Vehicule { Voiture() { cout<< "Voiture" << endl; ~Voiture() { cout<< "~Voiture" << endl; ; Voiture *R21 = new Voiture; delete R21; /********** se programme affiche : Vehicule Voiture ~Voiture ~Vehicule ***********************************************/ Exemple d'appel des constructeurs avec paramètres : class Vehicule { Vehicule(char *nom, int places); //... ; class Voiture : public Vehicule { private: int _cv; // puissance fiscale Voiture(char *n, int p, int cv); ; Voiture::Voiture(char *n, int p, int cv): Vehicule(n, p), _cv(cv) A. DANCEL - Le langage C++ 58
{ /*... */ 7.6 HERITAGE ET AMITIE L'amitié pour une classe s'hérite, mais uniquement sur les membres de la classe hérités, elle ne se propage pas aux nouveaux membres de la classe dérivée et ne s'étend pas aux générations suivantes. class A { ; friend class test1; A( int n= 0): _a(n) { private: int _a; class test1 { test( int n= 0): a0(n) { void affiche1() { cout << a0._a << // OK: test1 est amie de A private: A a0; ; class test2: public test { test2( int z0= 0, int z1= 0): test( z0), a1( z1) { void Ecrit() { cout << a1._a; // ERREUR: test2 n'est pas amie de A private: A a1; ; L'amitié pour une fonction ne s'hérite pas. A chaque dérivation, vous devez redéfinir les relations d'amitié avec les fonctions. 7.7 CONVERSION DE TYPE Il est possible de convertir implicitement une instance d'une classe dérivée en une instance de la classe de base si l'héritage est public. L'inverse est interdit car le compilateur ne saurait pas comment initialiser les membres de la classe dérivée. class Vehicule { A. DANCEL - Le langage C++ 59
; void f1(); class Voiture : public Vehicule { int f1(); ; void traitement1(vehicule v) { v.f1(); // OK Voiture R25; traitement1( R25 ); De la même façon on peut utiliser des pointeurs : Un pointeur (ou une référence) sur un objet d'une classe dérivée peut être implicitement converti en un pointeur (ou une référence) sur un objet de la classe de base. Cette conversion n'est possible que si l'héritage est public, car la classe de base doit posséder des membres public accessibles (ce n'est pas le cas d'un héritage protected ou private). C'est le type du pointeur qui détermine laquelle des méthodes f1() est appelée. void traitement1(vehicule *v) { v->f1(); // OK Voiture R25; traitement1( &R25 ); 7.8 HERITAGE MULTIPLE En langage C++, il est possible d'utiliser l'héritage multiple. Il permet de créer des classes dérivées à partir de plusieurs classes de base. Pour chaque classe de base, on peut définir le mode d'héritage. class A { void fa() { /*... */ protected: A. DANCEL - Le langage C++ 60
; int _x; class B { void fb() { /*... */ protected: int _x; ; class C: public B, public A { void fc(); ; void C::fc() { int i; fa(); i = A::_x + B::_x; // résolution de portée pour lever l'ambiguïté 7.8.1 Ordre d'appel des constructeurs Dans l'héritage multiple, les constructeurs sont appelés dans l'ordre de déclaration de l'héritage. Dans l'exemple suivant, le constructeur par défaut de la classe C appelle le constructeur par défaut de la classe B, puis celui de la classe A et en dernier lieu le constructeur de la classe dérivée, même si une liste d'initialisation existe. class A { ; A(int n=0) { /*... */ class B { B(int n=0) { /*... */ ; class C: public B, public A { // ^^^^^^^^^^^^^^^^^^ // ordre d'appel des constructeurs des classes de base // C(int i, int j) : A(i), B(j) { /*... */ ; A. DANCEL - Le langage C++ 61
C objet_c; // appel des constructeurs B(), A() et C() Les destructeurs sont appelés dans l'ordre inverse de celui des constructeurs. 7.9 HERITAGE VIRTUEL Un objet de la classe D contiendra deux fois les données héritées de la classe de base A, une fois par héritage de la classe B et une autre fois par C. Il y a donc deux fois le membre _base dans la classe D. L'accès au membre _base de la classe A se fait en levant l'ambiguïté. D od; od._base = 0; od.b::_base = 1; // OK od.c::_base = 2; // OK // ERREUR, ambiguïté Il est possible de n'avoir qu'une occurrence des membres de la classe de base, en utilisant l'héritage virtuel. Pour que la classe D n'hérite qu'une seule fois de la classe A, il faut que les classes B et C héritent virtuellement de A. Permet de n'avoir dans la classe D qu'une seule occurrence des données héritées de la classe de base A. D od; od._base = 0; // OK, pas d'ambiguïté Il ne faut pas confondre ce statut "virtual" de déclaration d'héritage des classes avec celui des membres virtuels que nous allons étudier. Ici, Le mot clé virtual précise au compilateur les classes à ne pas dupliquer. 7.10 POLYMORPHISME L'héritage nous permet de réutiliser le code écrit pour la classe de base dans les autres classes de la hiérarchie des classes de votre application. Le polymorphisme rendra possible l'utilisation d'une même instruction pour appeler dynamiquement des méthodes différentes dans la hiérarchie des classes. En C++, le polymorphisme est mis en œuvre par l'utilisation des fonctions virtuelles. A. DANCEL - Le langage C++ 62
7.10.1 Fonctions virtuelles class ObjGraph { void print() const { cout <<"ObjGraph::print()"; ; class Bouton: public ObjGraph { void print() const { cout << "Bouton::print()"; ; class Fenetre: public ObjGrap { void print() const { cout << "Fenetre::print()"; ; void traitement(const ObjGraph &og) { og.print(); // Qu'affiche ce programme??? Bouton OK; Fenetre windows97; traitement(ok); // affichage de... traitement(window97); // affichage de... Comme nous l'avons déjà vu, l'instruction og.print() de traitement() appelera la méthode print() de la classe ObjGraph. La réponse est donc : traitement(ok); traitement(window97); // affichage de ObjGraph::print() // affichage de ObjGraph::print() Si dans la fonction traitement() nous voulons appeler la méthode print() selon la classe à laquelle appartient l'instance, nous devons définir, dans la classe de base, la méthode print() comme étant virtuelle : class ObjGraph { ; virtual void print() const { cout<< "ObjetGraphique::print()" << endl; Pour plus de clarté, le mot-clé virtual peut être répété devant les méthodes print() des classes Bouton et Fenetre : A. DANCEL - Le langage C++ 63
class Bouton: public ObjGraph { virtual void print() const { cout << "Bouton::print()"; ; class Fenetre: public ObjGrap { virtual void print() const { cout << "Fenetre::print()"; ; On appelle ce comportement, le polymorphisme. Lorsque le compilateur rencontre une méthode virtuelle, il sait qu'il faut attendre l'exécution pour déterminer la bonne méthode à appeler. 7.10.2 Destructeur virtuel Il ne faut pas oublier de définir le destructeur comme "virtual" lorsque l'on utilise une méthode virtuelle : class ObjGraph { ; //... virtual ~ObjGraph() { cout << "fin de ObjGraph\n"; class Fenetre : public ObjGraph { ; ~Fenetre() { cout << "fin de Fenêtre "; Fenetre *Windows97 = new Fenetre; ObjGraph *og = Windows97; delete og; // affichage de : fin de Fenêtre fin de ObjGraph // si le destructeur n'avait pas été virtuel, // l'affichage aurait été : fin de ObjGraph Un constructeur, par contre, ne peut pas être déclaré comme virtuel. Une méthode statique ne peut, non plus, être déclarée comme virtuelle. Lors de l'héritage, le statut de l'accessibilité de la méthode virtuelle (public, protégé ou privé) est conservé dans toutes les classes dérivée, même si elle est redéfinie avec un statut différent. Le statut de la classe de base prime. A. DANCEL - Le langage C++ 64
7.11 CLASSES ABSTRAITES Il arrive souvent que la méthode virtuelle définie dans la classe de base serve de cadre générique pour les méthodes virtuelles des classes dérivées. Ceci permet de garantir une bonne homogénéité de votre architecture de classes. Une classe est dite abstraite si elle contient au moins une méthode virtuelle pure. On ne peut pas créer d'instance d'une classe abstraite et une classe abstraite ne peut pas être utilisée comme argument ou type de retour d'une fonction. Par contre, les pointeurs et les références sur une classe abstraite sont parfaitement légitimes et justifiés. 7.11.1 Méthode virtuelle pure Une telle méthode se déclare en ajoutant un = 0 à la fin de sa déclaration. class ObjGraph { virtual void print() const = 0; ; ObjGraph og; // ERREUR On ne peut utiliser une classe abstraite qu'à partir d'un pointeur ou d'une référence. Contrairement à une méthode virtuelle "normale", une méthode virtuelle pure n'est pas obligé de fournir une définition pour ObjGraph::print(). Une classe dérivée qui ne redéfinit pas une méthode virtuelle pure est elle aussi abstraite. A. DANCEL - Le langage C++ 65
8 - LES FLUX 8.1 GENERALITES Un flux (ou stream) est une abstraction logicielle représentant un flot de données entre : une source produisant de l'information une cible consommant cette information. Il peut être représenté comme un buffer et des mécanismes associés à celui-ci et il prend en charge, quand le flux est créé, l'acheminement de ces données. Comme pour le langage C, les instructions entrées/sorties ne font pas partie des instructions du langage. Elles sont dans une librairie standardisée qui implémente les flots à partir de classes. Par défaut, chaque programme C++ peut utiliser 3 flots : cout qui correspond à la sortie standard cin qui correspond à l'entrée standard cerr qui correspond à la sortie standard d'erreur. Pour utiliser d'autres flots, vous devez donc créer et attacher ces flots à des fichiers (fichiers normaux ou fichiers spéciaux) ou à des tableaux de caractères. 8.2 FLOTS ET CLASSES 8.2.1 Iostream.h Classes déclarées dans iostream.h et permettant la manipulation des périphériques standards : ios classe de base des entrées/sorties par flot. Elle contient un objet de la classe streambuf pour la gestion des tampons d'entrées/sorties. istream classe dérivée de ios pour les flots en entrée. ostream classe dérivée de ios pour les flots en sortie. iostream classe dérivée de istream et de ostream pour les flots bidirectionnels. istream_withassign, ostream_withassign et iostream_withassign : classes dérivées respectivement de istream, ostream et iostream et qui ajoute l'opérateur d'affectation. Les flots standard cin, cout et cerr sont des instances de ces classes. A. DANCEL - Le langage C++ 66
8.2.2 fstream.h Classes déclarées dans fstream.h permettant la manipulation des fichiers disques : fstreambase classe de base pour les classes dérivées ifstream, ofstream et fstream. Elle même est dérivée de ios et contient un objet de la classe filebuf (dérivée de streambuf). ifstream ofstream fstream classe permettant d'effectuer des entrées à partir des fichiers. classe permettant d'effectuer des sorties sur des fichiers. classe permettant d'effectuer des entrées/sorties à partir des fichiers. 8.2.3 Strstream.h Classes déclarées dans strstream.h permettant de simuler des opérations d'entrées/sorties avec des tampons en mémoire centrale. Elles opèrent de la même façon que les fonctions du langage C sprintf() et sscanf() : strtreambase classe de base pour les classes suivantes. Elle contient un objet de la classe strstreambuf (dérivée de streambuf). istrstream classe dérivée de strtreambase et de istream permettant la lecture dans un tampon mémoire (à la manière de la fonction sscanf). A. DANCEL - Le langage C++ 67
ostrstream classe dérivée de strtreambase et de ostream permettant l'écriture dans un tampon mémoire (à la manière de la fonction sprintf). strstream classe dérivée de istrstream et de iostream permettant la lecture et l'écriture dans un tampon mémoire. 8.3 LE FLOT DE SORTIE : ostream Il fournit des sorties formatées et non formatées (dans un streambuf). Il redéfinit l'opérateur << Il gère les types prédéfinis du langage C++ On peut (doit) le surdéfinir pour ses propres types 8.3.1 Surdéfinition de l'opérateur d'injection dans un flot : class Exemple { friend ostream &operator<<(ostream &os, const Exemple &ex); public : private : char _nom[20]; int _valeur; ; ostream &operator<<(ostream &os, const Exemple &ex) { return os << ex._nom << " " << ex._valeur; Exemple e1; cout << "Exemple = " << e1 << endl; La fonction operator<< doit retourner une référence sur l'ostream pour lequel il a été appelé afin de pouvoir le cascader dans une même instruction, comme dans la fonction main() ci-dessus. 8.3.2 Principales méthodes de ostream En plus de l'opérateur d'injection (<<), la classe ostream contient les principales méthodes suivantes : ostream & put(char c); : insère un caractère dans le flot : cout.put('\n'); ostream & write(const char *, int n); : insère n caractères dans le flot : A. DANCEL - Le langage C++ 68
cout.write("bonjour", 7); streampos tellp(); : retourne la position courante dans le flot ostream & seekp(streampos n); : se positionne à n octet(s) par rapport au début du flux. Les positions dans un flot commencent à 0 et le type streampos correspond à la position d'un caractère dans le fichier. ostream & seekp(streamoff dep, seek_dir dir); : se positionne à dep octet(s) par rapport : au début du flot : dir = beg à la position courante : dir = cur à la fin du flot : dir = end (et dep est négatif!) : streampos old_pos = fout.tellp(); // mémorise la position fout.seekp(0, end); // se positionne à la fin du flux cout << "Taille du fichier : " << fout.tellp() << " octet(s)\n"; fout.seekp(old_pos, beg); ostream & flush(); : vide les tampons du flux // se repositionne comme au départ 8.4 LE FLOT D'ENTREE : istream Il fournit des entrées formatées et non formatées (dans un streambuf) Il redéfinit l'opérateur >> Il gère les types prédéfinis du langage C++ On peut (doit) le surdéfinir pour ses propres types Par défaut >> ignore tous les espaces (voir ios::skipws) 8.4.1 Surdéfinition de l'opérateur d'extraction >> : class Exemple { friend istream &operator>>(istream &is, Exemple &ex); public : private : char _nom[20]; int _valeur; ; istream &operator>>(istream &is, Exemple &ex) { return is >> ex._nom >> ex._valeur; Exemple e1; cout << "Entrez un nom et une valeur : "; A. DANCEL - Le langage C++ 69
cin >> e1; La fonction operator>> doit retourner une référence sur l' istream pour lequel il a été appelé afin de pouvoir le cascader dans une même instruction. 8.4.2 Principales méthodes de istream En plus de l'opérateur d'extraction (>>), la classe istream contient les principales méthodes suivantes : Lecture d'un caractère : int get(); : retourne la valeur du caractère lu (ou EOF si la fin du fichier est atteinte) istream & get(char &c); : extrait le premier caractère du flux (même si c'est un espace) et le place dans c. int peek(); : lecture non destructrice du caractère suivant dans le flux. Retourne le code du caractère ou EOF si la fin du fichier est atteinte. Lecture d'une chaîne de caractères : istream & get(char *ch, int n, char delim='\n'); : extrait n -1 caractères du flux et les place à l'adresse ch. La lecture s'arrête au délimiteur qui est par défaut le '\n' ou la fin de fichier. Attention : Le délimiteur ('\n' par défaut) n'est pas extrait du flux. istream & getline(char *ch, int n, char delim='\n'); : comme la méthode précédente sauf que le délimiteur est extrait du flux mais n'est pas recopié dans le tampon. istream & read(char *ch, int n); : extrait un bloc d' au plus n octets du flux et les place à l'adresse ch. Le nombre d'octets effectivement lus peut être obtenu par la méthode gcount(). int gcount(); : retourne le nombre de caractères non formatés extraits lors de la dernière lecture streampos tellg(); : retourne la position courante dans le flot. istream & seekg(streampos n); : se positionne à n octet(s) par rapport au début du flux. Les positions dans un flot commencent à 0 et le type streampos correspond à la position d'un caractère dans le fichier. istream & seekg(streamoff dep, seek_dir dir); : se positionne à dep octet(s) par rapport : au début du flot : dir = beg à la position courante : dir = cur à la fin du flot : dir = end (et dep est négatif!) istream & flush(); : vide les tampons du flux. 8.5 CONTROLE DE L'ETAT D'UN FLUX La classe ios décrit les aspects communs des flots d'entrée et de sortie. C'est une classe de base virtuelle pour tous les objets flots. Vous n'aurez jamais à créer des objets de la classe ios. Vous utiliserez ses méthodes pour tester l'état d'un flot ou pour contrôler le formatage des informations. A. DANCEL - Le langage C++ 70
8.5.1 Méthodes de la classe de base ios : int good(); : retourne une valeur différente de zéro si la dernière opération d'entrée/sortie s'est effectuée avec succès et une valeur nulle en cas d'échec. int fail(); : fait l'inverse de la méthode précédente int eof(); : retourne une valeur différente de zéro si la fin de fichier est atteinte et une valeur nulle dans le cas contraire. int bad(); : retourne une valeur différente de zéro si vous avez tenté une opération interdite et une valeur nulle dans le cas contraire. int rdstate(); : retourne la valeur de la variable d'état du flux. Retourne une valeur nulle si tout va bien. void clear(); : remet à zéro l'indicateur d'erreur du flux. C'est une opération obligatoirement à faire après qu'une erreur se soit produite sur un flux. surdéfinition de () et! Exemples : if ( f1 )... // équivalent à : if ( f1.good() ) if (!f1 )... // équivalent à : if (!f1.good() ) if (! (cin >> x) )... 8.6 ASSOCIER UN FLOT D'E/S A UN FICHIER Il est possible de créer un objet flot associé à un fichier autre que les fichiers d'entrées/sorties standards. 3 classes permettant de manipuler des fichiers sur disque sont définies dans fstream.h : ifstream : (dérivée de istream et de fstreambase) permet l'accès du flux en lecture seulement ofstream : (dérivée de ostream et de fstreambase) permet l'accès du flux en écriture seulement fstream : (dérivée de iostream et de fstreambase) permet l'accès du flux en lecture/écriture. Chacune de ces classes utilise un buffer de la classe filebuf pour synchroniser les opérations entre le buffer et le flot. La classe fstreambase offre un lot de méthodes communes à ces classes (open, close, attach...). 8.6.1 Ouverture du fichier et association avec le flux : C'est la méthode open() qui permet d'ouvrir le fichier et d'associer un flux avec ce dernier. void open(const char *name, int mode,int prot=filebuf::openprot); name : nom du fichier à ouvrir mode : mode d'ouverture du fichier (enum open_mode de la classe ios) : enum open_mode { app, ate, // ajout des données en fin de fichier. // positionnement à la fin du fichier. A. DANCEL - Le langage C++ 71
in, out, // ouverture en lecture (par défaut pour ifstream). // ouverture en écriture (par défaut pour ofstream). binary, // ouverture en mode binaire (par défaut en mode texte). trunc, // détruit le fichier s'il existe et le recrée (par défaut // si out est spécifié sans que ate ou app ne soit activé). nocreate, // si le fichier n'existe pas, l'ouverture échoue. noreplace // si le fichier existe, l'ouverture échoue, // sauf si ate ou app sont activés. ; Pour les classes ifstream et ofstream le mode par défaut est respectivement ios::in et ios::out. prot : il définit les droits d'accès au fichier (par défaut les permissions de lecture/écriture sont positionnés) dans le cas d'une ouverture avec création (sous UNIX). Exemple : #include <fstream.h> ifstream f1; f1.open("essai1.tmp"); // ouverture en lecture du fichier ofstream f2; f2.open("essai2.tmp"); // ouverture en écriture du fichier fstream f3; f3.open("essai3.tmp", ios::in ios::out); // ouverture en // lecture/écriture On peut aussi appeler les constructeurs des différentes classes et combiner les 2 opérations de définition du flot et d'ouverture. #include <fstream.h> ifstream f1("essai1.tmp"); // ouverture en lecture du fichier ofstream f2("essai2.tmp"); // ouverture en écriture du fichier fstream f3("essai3.tmp", ios::in ios::out); // ouverture en // lecture/écriture 8.7 FORMATAGE DE L'INFORMATION Chaque flot conserve en permanence un ensemble d'indicateurs spécifiant l'état de formatage. Ceci permet de donner un comportement par défaut au flot, contrairement aux fonctions printf() et scanf() du langage C, dans lesquelles on fournissait pour chaque opération d'entrée/sortie l'indicateur de format. L'indicateur de format du flot, est un entier long défini (en protected) dans la classe ios, dont les classes stream héritent. A. DANCEL - Le langage C++ 72
Extrait de ios : class ios { public : enum { skipws, // ignore les espaces en entrée left, // justifie les sorties à gauche right, // justifie les sorties à droite internal, // remplissage après le signe ou la base dec, // conversion en décimal oct, // conversion en octal hex, // conversion en hexadécimal showbase, // affiche l'indicateur de la base showpoint, // affiche le point décimal avec les réels uppercase, // affiche en majuscules les nombres hexadécimaux showpos, // affiche le signe + devant les nombres positifs scientific,// notation 1.234000E2 avec les réels fixed, // notation 123.4 avec les réels unitbuf, // vide les flots après une insertion stdio // permet d'utiliser stdout et cout ; //... protected : long x_flags; // indicateur de format //... ; Ces valeurs peuvent se combiner avec l'opérateur comme dans l'exemple : ios::showbase ios::showpoint ios::showpos Des constantes sont aussi définies dans la classe ios pour accéder à un groupe d'indicateurs : static const long basefield; : permet de choisir la base (dec, oct ou hex) static const long adjustfield; permet de choisir son alignement (left, right ou internal) static const long floatfield; permet de choisir sa notation pour les réels (scientific ou fixed) Les méthodes suivantes (définies dans la classe ios) permettent de lire ou de modifier la valeur des indicateurs de format : long flags(); : retourne la valeur de l'indicateur de format long flags(long f); : modifie l'ensemble des indicateurs en concordance avec la valeur de f. Elle retourne l'ancienne valeur de l'indicateur. long setf(long setbits, long field); : remet à zéro les bits correspondants à field (basefield, adjustfield ou floatfield) et positionne ceux désignés par setbits. Elle retourne l'ancienne valeur de l'indicateur de format. cout << setf(ios::dec, ios::basefield) << i; cout << setf(ios::left, ios::adjustfield)<< hex << 0xFF; A. DANCEL - Le langage C++ 73
// affiche : 000xFF cout << setf(ios::internal, ios::adjustfield) << hex << 0xFF; // affiche : 0x00FF cout << setf(ios::scientific, ios::floatfield) << f; long old = cout.setf(ios::left, ios::adjustfield); cout << data; cout.setf(old, ios::adjustfield); cout.setf(0l, ios::basefield); // état par défaut de basefield long setf(long f); : positionne l'indicateur de format. Elle retourne l'ancienne valeur de l'indicateur de format. cout.setf( ios::skipws ); cout.setf( ios::dec ios::right ); long unsetf(long); : efface les indicateurs précisés. Méthodes de la classe ios manipulant le format des informations : int width(int); : positionne la largeur du champ de sortie. int width(); : retourne la largeur du champ de sortie. char fill(char); : positionne le caractère de remplissage char fill(); : retourne la valeur du caractère de remplissage int precision(int); : positionne le nombre de caractères (non compris le point décimal) qu'occupe un réel. int precision(); : retourne la précision (voir ci-dessus). cout << "Largeur par défaut du champ de sortie : "; cout << cout.width() << endl; // affiche: 0 cout.width(10); cout.fill('*'); cout << ' ' << 1234 << " \n"; // affiche: ******1234 cout.setprecision(6); cout << 12.345678 << endl; // affiche: 12.3457 // (noter l'arrondi à l'affichage) 8.8 LES MANIPULATEURS L'usage des méthodes de formatage de l'information rend les instructions lourdes à écrire et un peu trop longues. Les manipulateurs permettent d'écrire un code plus compact et plus lisible. Ainsi, au lieu d'écrire : cout.width(10); cout.fill('*'); cout.setf(ios::hex, ios::basefield); cout << 123 ; A. DANCEL - Le langage C++ 74
cout.flush(); on préfèrera écrire l'instruction équivalente : cout << setw(10) << setfill('*') << hex << 123 << flush; dans laquelle, setw et setfill sont des manipulateurs avec un argument alors que hex et flush sont des manipulateurs sans argument. De plus nous pourrons écrire nos propres manipulateurs. 8.8.1 Comment cela marche-t il? Les manipulateurs les plus célèbres sont endl et flush. Vous les connaissez certainement. Voici comment ils sont définis : ostream &flush(ostream &ps) { return os.flush(); ostream &endl(ostream &os) { return os << '\n' << flush; Pour pouvoir les utiliser dans une instruction comme : cout << end; La fonction operator<< est surchargée dans la classe ostream comme : ostream &ostream::operator<<(ostream& (* f)(ostream &)) { (*f)(*this); return *this; Il est donc très simple de définir son propre manipulateur (sans paramètre) : Exemple : création du manipulateur tab : ostream &tab(ostream &os){ return os << '\t'; que l'on peut utiliser ainsi : cout << 12 << tab << 34; 8.8.2 Manipulateurs prédéfinis Les classes ios, istream et ostream implémentent les manipulateurs prédéfinis. Le fic hier d'entête iomanip.h défini un certain nombre de ces manipulateurs et offre la possibilité de créer ses propres manipulateurs. dec : la prochaîne E/S utilise la base décimale hex : la prochaîne E/S utilise la base hexadécimale oct : la prochaîne E/S utilise la base octale endl : écrit un '\n' puis vide le flot ends : écrit un '\0' puis vide le flot flush : vide le flot ws : saute les espaces (sur un flot en entrée uniquement) A. DANCEL - Le langage C++ 75
setbase(int b) : positionne la base b pour la prochaine sortie. n vaut 0 pour le décimal, 8 pour l'octal et 16 pour l'hexadécimal. setfill(int c) : positionne le caractère de remplissage c pour la prochaîne E/S setprecision(int p) : positionne la précision à p chiffres pour la prochaîne E/S setw(int l) : positionne la largeur à n caractères. setiosflags(long n) : active les bits de l'indicateur de format spécifiés par l'entier n. On l'utilise comme la méthode flags(). resetiosflags(long b) : désactive les bits de l'indicateur de format. 8.8.3 Création d'un nouveau manipulateur (avec un paramètre) L'implémentation d'un nouveau manipulateur est implanté en 2 parties : le manipulateur : sa forme générale est (pour un ostream) : ostream &nom_du_manipulateur(ostream &, type ); type est le type du paramètre du manipulateur. Cette fonction ne peut pas être appelée directement par une instruction d'entrée/sortie. Elle sera appelée seulement par l'applicateur. l'applicateur : il appelle le manipulateur. C'est une fonction globale. Sa forme générale est : xxxmanip( type ) nom_du_manipulateur(type arg) { return xxxmanip(type) (nom_du_manipulateur, arg); avec xxx valant O pour les flots manipulant un ostream (ou ses dérivées), I, S et IO pour respectivement les flots manipulant un istream, ios et un iostream. Exemple : un manipulateur pour afficher un entier en binaire : #include <iomanip.h> #include <limits.h> // ULONG_MAX ostream &bin(ostream &os, long val) { unsigned long masque = ~(ULONG_MAX >> 1); while ( masque ) { os << ((val & masque)? '1' : '0'); masque >>= 1; return os; OMANIP(long) bin(long val) { return OMANIP(long) (bin, val); cout << "1997 en binaire = " << bin(1997) << endl; A. DANCEL - Le langage C++ 76
9 - LES EXCEPTIONS 9.1 GENERALITES Le langage C++ nous propose une gestion efficace des erreurs pouvant survenir pendant l'exécution : erreurs matérielles : saturation mémoire, disque plein... erreurs logicielles : division par zéro... La solution habituellement pratiquée est l'affichage d'un message d'erreur et la sortie du programme avec le renvoi d'un code d'erreur au programme appelant. void lire_fichier(const char *nom) { ifstream f_in(nom); // ouverture en lecture if (! f_in.good()) { cerr << "Problème à l'ouverture du fichier " << nom << endl; exit(1); // lecture du fichier... En C++, c est une nouvelle structure de contrôle qui permet la gestion des erreurs d'exécution : la structure try... catch 9.2 SCHEMA DU MECANISME D'EXCEPTION constuction d'un objet (d'un type quelconque) qui représente l'erreur lancement de l'exception (throw) l'exception est alors propagée dans la structure de contrôle try... catch englobante cette structure essaye attraper (catch) l'objet si elle n'y parvient pas, la fonction terminate() est appelée. try { throw objet // lancement de l'exception catch ( type ) { // traitement de l'erreur A. DANCEL - Le langage C++ 77
9.3 LA STRUCTURE try... catch Quand une exception est détectée dans un bloc try, le contrôle de l'exécution est donné au bloc catch correspondant au type de l'exception (s'il existe). Un bloc try doit être suivi d'au moins un bloc catch Si plusieurs blocs catch existent, ils doivent intercepter un type d'exception différent. Quand une exception est détectée, les destructeurs des objets inclus dans le bloc try sont appelés avant d'appeler un bloc catch. A la fin du bloc catch, le programme continue son exécution sur l'instruction qui suit le dernier bloc catch. Exemple : interception d'une exception de type bad_alloc : (Cette exception est lancée en cas d'echec d'allocation mémoire) #include <exception.h> try { char *ptr = new char[1000000000]; / suite en cas de succès de new (improbable...) catch ( bad_alloc ) { // en cas d'échec d'allocation mémoire par new // une exception bad_alloc est lancée par new // traitement de l'erreur d'allocation 9.3.1 Syntaxe de catch catch ( TYPE ) : intercepte les exceptions du type TYPE, ainsi que celles de ses classes dérivées. catch ( TYPE o) : intercepte les exceptions du type TYPE, ainsi que celles de ses classes dérivées. Dans le catch un objet o est utilisable pour extraire d'autres informations sur l'exception. catch (... ) : intercepte les exceptions de tous types, non traitées par les bocs catch précédents. class Erreur { int get_erreur() { /*... */ ; try { // instruction(s) douteuse(s) qui peu(ven)t lancer une exception catch ( xalloc ) { catch ( Erreur err ) { A. DANCEL - Le langage C++ 78
cerr << err.get_erreur() << endl; catch (...) { // traitement des autres exceptions 9.3.2 Syntaxe de throw throw obj : elle permet de lever une exception donnée #include <except.h> #include <iostream.h> class Essai { public : class Erreur { ; void f1() { throw Erreur(); // construction d'une instance de Erreur // et lancement de celle-ci ; try { Essai e1; e1.f1(); catch ( Essai::Erreur ) { cout << "interception de Erreur" << endl; throw : l'instruction throw sans paramètre s'utilise si l'on ne parvient pas à résoudre une exception dans un bloc catch. Elle permet de relancer la dernière exception. 9.4 EXCEPTIONS LEVEES PAR UNE FONCTION Il faut déclarer les exceptions levées par une fonction. Cette déclaration permet de spécifier le type des exceptions pouvant être éventuellement levées par votre fonction (ou méthode). Exemples de déclaration : void f1() throw (Erreur); void f2() throw (Erreur, Division_zero); void f3() throw (); void f4(); Remarques : A. DANCEL - Le langage C++ 79
Par défaut de déclaration (comme dans le cas de la fonction f4()), toute exception peut être lancée par une fonction. La fonction f3(), déclarée ci dessus, ne peut lancer aucune exception. La fonction f1() ne peut lancer que des exceptions de la classe Erreur (ou de classes dérivées). Si une fonction lance une exception non déclarée dans son entête, la fonction unexpected() est appelée. A la définition de la fonction, throw et ses paramètres doivent être de nouveau spécifié. 9.5 LA FONCTION terminate() Si aucun bloc catch ne peut attraper l'exception lancée, la fonction terminate() est alors appelée. Par défaut elle met fin au programme par un appel à la fonction abort(). On peut définir sa propre fonction terminate() par un appel à la fonction set_terminate() définie dans except.h comme : typedef void (*terminate_function)(); terminate_function set_terminate(terminate_function t_func); Exemple : #include <except.h> #include <iostream.h> void my_terminate() { cout << "my_terminate" << endl; exit(1); // elle ne doit pas retourner à son appelant try { set_terminate((terminate_function) my_terminate); catch ( Erreur ) { 9.6 LA FONCTION unexpected() Si une fonction (ou une méthode) lance une exception qui n'est pas déclarée par un throw dans son entête, la fonction unexpected() est alors appelée. Par défaut elle met fin au programme par un appel à la fonction abort(). On peut définir sa propre fonction unexpected() par un appel à la fonction set_unexpected() définie dans except.h comme : typedef void (*unexpected_function)(); unexpected_function set_unexpected(unexpected_function t_func); A. DANCEL - Le langage C++ 80
Exemple : #include <except.h> #include <iostream.h> void my_unexpected() { cout << "my_unexpected" << endl; exit(1); // elle ne doit pas retourner à son appelant class Erreur {; class Toto {; class Essai { void f1() throw (Erreur); ; void Essai::f1() throw (Erreur) { throw Toto(); try { set_unexpected( (unexpected_function) my_unexpected); Essai e1; e1.f1(); catch ( Erreur ) { 9.7 EXEMPLE COMPLET #include <except.h> #include <iostream.h> class Essai { public : class Erreur { Erreur(int n=0): _val(n) { int get_val() { return _val; private: int _val; ; Essai() { cout << "Constructeur d'essai" << endl; A. DANCEL - Le langage C++ 81
; ~Essai() { cout << "Destructeur d'essai" << endl; void f1() { throw Erreur(10);//construction d'une instance de Erreur //initialisée à 10 et lancement de celle-ci try { Essai e1; e1.f1(); cout << "bla bla bla" << endl; // catch ( Essai::Erreur e ) { cout << "Erreur numéro : " << e.get_val() << endl; /* résultat de l'exécution *********** Constructeur d'essai Destructeur d'essai Erreur numéro : 10 *********************************/ A. DANCEL - Le langage C++ 82
TABLE DES MATIERES 1 - GENERALITES...0 1.1 INTRODUCTION... 1 1.2 LA CLASSE... 1 1.3 L'HERITAGE... 2 1.4 LE POLYMORPHISME... 3 1.5 RESUME... 3 2 - UN MEILLEUR C...4 2.1 LES COMMENTAIRES... 4 2.2 ENTREES/SORTIES SIMPLES... 5 2.3 LES MANIPULATEURS... 6 2.4 LES CONVERSIONS EXPLICITES... 7 2.5 DEFINITION DE VARIABLES... 8 2.6 VARIABLE DE BOUCLE... 8 2.7 VISIBILITE DES VARIABLES... 9 2.8 LES CONSTANTES... 9 2.9 CONSTANTES ET POINTEURS... 9 2.10 LES TYPES... 10 2.11 ALLOCATION MEMOIRE... 11 3 - LES FONCTIONS...14 3.1 DECLARATION DES FONCTIONS... 14 3.2 PASSAGE PAR REFERENCE... 14 3.3 VALEUR PAR DEFAUT DES PARAMETRES... 15 3.4 FONCTION INLINE... 16 3.5 SURCHARGE DE FONCTIONS... 16 3.6 RETOUR D'UNE REFERENCE... 18 3.7 UTILISATION D UNE FONCTION ECRITE EN C... 19 3.8 FICHIER D'EN -TETES POUR C ET C++... 19 4 - LES CLASSES...21 4.1 DEFINITION D'UNE CLASSE... 21 4.2 DROITS D'ACCES... 21 4.3 RECOMMANDATIONS DE STYLE... 21 4.4 DEFINITION DES FONCTIONS MEMBRES... 22 4.5 INSTANCIATION D'UNE CLASSE... 23 4.6 UTILISATION DES OBJETS... 23 4.7 FONCTIONS MEMBRES CONSTANTES... 23 4.8 EXEMPLE : PILE D'ENTIERS (1)... 25 4.9 CONSTRUCTEUR ET DESTRUCTEUR... 27 4.10 EXEMPLE : PILE D'ENTIERS (2)... 29 4.11 CONSTRUCTEUR COPIE... 30 4.12 CLASSES IMBRIQUEES... 32 4.13 AFFECTATION ET INITIALISATION... 33 4.14 LISTE D'INITIALISATION D'UN CONSTRUCTEUR... 33 4.15 LE POINTEUR THIS... 34 4.16 LES MEMBRES STATIQUES... 35 4.17 CLASSES ET FONCTIONS AMIES... 37 5 - LES OPERATEURS...39 5.1 INTRODUCTION A LA SURCHARGE D'OPERATEURS... 39 5.2 REGLES D'UTILISATION... 39 A. DANCEL - Le langage C++ 83
5.3 5.4 SURCHARGE D'UN OPERATEUR... 40 OPERATEUR D'AFFECTATION... 42 5.5 5.6 CAS PARTICULIERS... 43 OPERATEURS DE CONVERSION... 46 6 - LES MODELES...50 6.1 LES MODELES DE FONCTIONS... 50 6.2 LES CLASSES PARAMETREES... 51 6.3 ARGUMENTS D UNE FONCTION MODELE... 52 7 - L'HERITAGE...54 7.1 L'HERITAGE... 54 7.2 MODE DE DERIVATION... 54 7.3 REDEFINITION DE METHODES... 57 7.4 AJUSTEMENT D'ACCES... 57 7.5 HERITAGE DES CONSTRUCTEURS / DESTRUCTEURS... 58 7.6 HERITAGE ET AMITIE... 59 7.7 CONVERSION DE TYPE... 59 7.8 HERITAGE MULTIPLE... 60 7.9 HERITAGE VIRTUEL... 62 7.10 POLYMORPHISME... 62 7.11 CLASSES ABSTRAITES... 65 8 - LES FLUX...66 8.1 GENERALITES... 66 8.2 FLOTS ET CLASSES... 66 8.3 LE FLOT DE SORTIE : OSTREAM... 68 8.4 LE FLOT D'ENTREE : ISTREAM... 69 8.5 CONTROLE DE L'ETAT D'UN FLUX... 70 8.6 ASSOCIER UN FLOT D'E/S A UN FICHIER... 71 8.7 FORMATAGE DE L'INFORMATION... 72 8.8 LES MANIPULATEURS... 74 9 - LES EXCEPTIONS...77 9.1 GENERALITES... 77 9.2 SCHEMA DU MECANISME D'EXCEPTION... 77 9.3 LA STRUCTURE TRY... CATCH... 78 9.4 EXCEPTIONS LEVEES PAR UNE FONCTION... 79 9.5 LA FONCTION TERMINATE()... 80 9.6 LA FONCTION UNEXPECTED()... 80 9.7 EXEMPLE COMPLET... 81 TABLE DES MATIERES...83 A. DANCEL - Le langage C++ 84