COURS DE C++ Christophe LÉGER 2004−2005 1. ENCAPSULATION 1.1 INTRODUCTION C++ est un langage orienté objet dérivé du langage C. Le langage C a été développé dans les années 1970 par B. Kernighan et D. Ritchie pour en faire le langage de programmation structurée du système UNIX. Mais son utilisation est aujourd'hui beaucoup plus répandue. Il est employé pour l'écriture de logiciels dans des domaines très divers (programmation scientifique, informatique industrielle, gestion, ...) et avec des systèmes d'exploitation différents (UNIX bien sûr, mais aussi MSDOS sur PC, ...). Le langage C est donc réputé pour être facilement portable sur de multiples plates-formes (PC, SUN, HP, IBM, DOS, UNIX, ...). Il est également renommé pour son efficacité (code compact et proche des instructions machine), mais aussi pour son grand laxisme (le programmeur ne dispose pas des garde-fous du Pascal) qui peut conduire à écrire des programmes illisibles ou faux. A chaque langage sont associés des modèles de programmation qui présentent l'ensemble des techniques à appliquer lors de la conception et l'implémentation des programmes. Le C++ a été développé en 1983 par Bjarne Stroustrup, des laboratoires AT&T Bell, USA. Il permet d'améliorer la qualité des programmes écrits en C, grâce à l'ajout des principes de la programmation orientée objets : la modularité, l'encapsulation des données, l'héritage, la surcharge des fonctions et le polymorphisme. Le passage du C au C++ correspond à une évolution naturelle de la programmation structurée classique vers la programmation orientée objets. C'est pourquoi le C++ représente de plus en plus la nouvelle manière de programmer des développeurs en C. Son apprentissage direct se justifie donc pleinement, surtout pour les programmeurs déjà familiarisés à la programmation classique. 1.2 ABSTRACTION DES DONNEES L'abstraction des données est un des concepts fondamentaux de la POO [Programmation Orientée Objets]. En programmation structurée, les données qui ont un lien logique sont regroupées en structure, les enregistrements. De la même manière, les instructions qui ont un lien logique sont rassemblées dans des sous-programmes. Ainsi, les données et le code sont structurés, et on atteint un certain niveau d'abstraction puisqu'un seul identificateur regroupe plusieurs champs d'un enregistrement ou plusieurs instructions d'un sous-programme. L'exécution d'un programme consiste alors à utiliser des sous-programmes pour initialiser, et/ou modifier, et/ou afficher des données. Ces sous-programmes doivent donc connaître la structure des données qu'ils utilisent. Pour cela, il faut définir les structures des données avant celles des sous-programmes, et déclarer ces données à travers les interfaces des sous-programmes (paramètres). On fait donc coexister de manière autonome des éléments qui ont un lien logique. Grâce à l'encapsulation, la POO propose de relier données et code structurés dans des structures nouvelles qui permettent d'utiliser des sous-programmes sans connaître la structure des données qu'ils manipulent. – Page 3 – Cours de C++ 1.3 ENCAPSULATION Pour permettre l'abstraction des données, les LOOs [Langage Orienté Objets] fournissent une structure qui regroupe (ou encapsule) les données et les sous-programmes qui les utilisent. En C++, une telle structure est appelée classe, et l'instance d'une classe (la déclaration d'une variable de type classe) est appelée objet. Les classes sont composées de données membres (que l'on peut comparer aux champs des enregistrements de la programmation structurée) et de fonctions membres, qui définissent les opérations à réaliser sur les données membres. Les programmes qui manipulent des objets ne connaissent pas les identificateurs des données membres de ces objets. Les échanges de données entre le programme et les objets se font au moyen d'une interface clairement définie (les fonctions membres) et donc indépendante des données membres. Aussi longtemps que cette interface ne change pas, les données-membres et le code des fonctions membres de l'objet peuvent changer sans nécessiter la modification du programme qui utilise l'objet. Cela permet une modularité plus grande et une indépendance vis à vis du choix de la représentation interne des données. 1.4 CLASSES Une classe est composée d'un ensemble de données membres et de fonctions membres. La syntaxe de déclaration d'une classe est la suivante : class identificateur { private: (par défaut) <déclarations> protected: <déclarations> public: <déclarations> }; où <déclarations> est de la forme : type1 donnée_membre1; type2 donnée_membre2; ... fonction_membre1 (paramètres); fonction_membre2 (paramètres); ... Par exemple : class booleen { short b; public: void init (short); void not (); void affiche (); }; 1.4.1 Contrôle d'accès Par défaut, les données membres et les fonctions membres des classes sont privées, c'est à dire inaccessibles de l'extérieur de la classe, ce qui permet d'imposer l'abstraction des données. Mais il est possible de contrôler l'accès des différents membres à l'aide des mots réservés private, protected et public. – Page 4 – Encapsulation Généralement, les membres privés sont les données et les membres public sont les fonctions (nous reviendrons plus loin sur les membres protected). En effet, pour bénéficier pleinement de l'abstraction des données, il faut interdire la manipulation directe des données membres à l'extérieur d'une classe (celles-ci ne doivent être utilisées que par les fonctions membres). Par contre, les fonctions membres étant la seule interface entre la classe et l'extérieur, celles-ci doivent être déclarées publiques. Ainsi, il est possible de les référencer à l'extérieur de la classe. Les déclarations privées, publiques ou protégées peuvent se faire plusieurs fois et dans n'importe quel ordre. Cependant, pour des raisons de lisibilité, on les utilisera dans l'ordre private, protected, public, d'abord pour les données membres, puis pour les fonctions membres. Conséquence directe du fait que l'on s'interdit d'accéder directement aux données membres d'une classe, il faut toujours déclarer une fonction membre qui permet d'initialiser les données membres de cette classe. De la même manière, mais moins systématiquement, on déclare souvent une fonction membre qui, à la fin du programme, retourne ou affiche les données membres. 1.5 DONNEES MEMBRES Les données membres comprennent n'importe quelle donnée de type fondamental (type de base du langage), composé ou défini par l'utilisateur. Elles sont déclarées de la même manière que les variables, mais ne peuvent être automatiquement initialisées lors de leur déclaration. Les données membres doivent rester privées. 1.5.1 Membres statiques Une donnée membre d'une classe peut être précédée du mot réservé static, afin de spécifier que cette donnée est partagée par toutes les instances de la classe. Il s'agit donc d'une zone de donnée unique, commune à toutes les variables de la classe. Mais attention, cette utilisation de données membres statiques doit rester très exceptionnelle car elle nuit à la lisibilité des programmes. 1.6 FONCTIONS MEMBRES Une fonction membre d'une classe peut accéder à tous les membres (données ou fonctions) de cette classe, qu'ils soient privés, protégés ou publics. Pour définir une fonction membre, il existe deux possibilités : les fonctions inline et les fonctions "non inline". 1.6.1 Fonctions « non inline » Elles sont déclarées dans la définition d'un classe puis définies à l'extérieur de cette classe. Comme plusieurs classes peuvent définir des fonctions ayant même identificateur, il faut spécifier l'appartenance des fonctions lors de leur définition. Ceci est réalisé en faisant précéder le nom de la fonction du nom de la classe et de l'opérateur ::, appelé opérateur de résolution de portée. – Page 5 – Cours de C++ Exemple : void booleen::init (short bi=0) { b = bi; } void booleen::not () { b = !b; } void booleen::affiche () { if (b==0) cout << "Faux"; else cout << "Vrai"; } 1.6.2 Fonctions inline L'utilisation de fonctions dans un programme améliore énormément la lisibilité, mais peut dans certains cas nuire à l'efficacité des programmes (appel de la fonction plus long que son exécution). Le spécificateur inline permet d'améliorer la lisibilité en demandant au compilateur de remplacer l'appel de la fonction par son code, à l'endroit de l'appel. Les fonctions inline ne doivent généralement comporter que peu d'instructions. Syntaxe : il suffit de définir le corps de la fonction lors de sa déclaration, dans la classe, ou alors de faire précéder la définition de la fonction par le mot réservé inline. Exemple : class booleen { short b; public: void init (short); void not () { b != b; }; void affiche (); }; inline void booleen::affiche () { ... } 1.6.3 Fonctions amies (friend) Une fonction amie d'une classe est une fonction (membre ou non d'une autre classe) qui bénéficie des mêmes droits d'accès que les membres de cette classe. Les fonctions amies résolvent par exemple le problème suivant : lorsque plusieurs classes sont définies dans un même programme, il peut arriver que les fonctions d'une classe travaillent sur les objets d'une autre classe. Pourtant, l'utilisation des données membres (privées) de cette classe sont – Page 6 – Encapsulation interdites à l'extérieur de la classe, et une même fonction ne peut être simultanément membre de plusieurs classes. Les fonctions amies permettent donc d'accéder de l'extérieur aux membres privés d'une classe. Pour déclarer une fonction membre amie, il suffit de faire précéder sa définition par le mot réservé friend. Bien évidemment, le corps des fonctions amies d'une classe n'est pas défini lors de la déclaration de cette classe. Pour résoudre les éventuels problèmes d'appartenance, on utilise l'opérateur ::. L'utilisation des fonctions amies nuit énormément à la bonne POO. Il ne faut donc les utiliser qu'en dernier recours. 1.7 DECLARATION D'INSTANCES La déclaration et l'utilisation d'un objet (instance d'une classe) se font de la manière suivante : Syntaxe : class identificateur_de_classe { ... public: fonction_membre1 (paramètres); fonction_membre2 (paramètres); ... }; main () { identificateur_de_classe objet; Exemple : class booleen { short b; public: void init (short) void not (); void affiche (); }; main () { booleen fini; objet.fonction_membre1 (paramètres); objet fonction_membre2 (paramètres); ... } fini.init (1); fini.not (); fini.affiche (); } Grâce à l'encapsulation, les fonctions membres deviennent l'interface entre les classes et le monde extérieur. La programmation se réduit alors à une succession d'ordres. C'est pour cela que l'on appelle souvent les LOOs "langages acteurs". 1.8 MOTS CLEFS Abstraction des données Classe Déclaration Définition Donnée membre Encapsulation Fonction amie Fonction inline Fonction membre – Page 7 – Cours de C++ Identificateur Instanciation Objet Paramètres Structure du code Structure des données – Page 8 – 2. ÉLÉMENTS DE SYNTAXE 2.1 STRUCTURE D'UN PROGRAMME C++ Voici deux exemples de programme destinés à donner un aperçu de ce qu'est un programme en C++. Il faut simplement noter qu'un programme écrit en C++ est constitué d'une suite de fonctions. Chaque fonction est composée d'un entête et d'un corps. L'entête contient le nom et le type des paramètres, le corps est composé de déclarations et d'instructions. Parmi toutes les fonctions, l'une doit être principale. C'est celle qui sera exécutée en premier ; son nom doit obligatoirement être main. Exemple 1 : architecture minimale d'un programme. main () { } Toute déclaration de fonction doit être suivie des parenthèses (), même si elle ne comporte pas de paramètre. Le corps de la fonction est délimité par les accolades {} ({ équivaut au begin du Pascal, et } au end). Exemple 2 : programme qui affiche la chaîne de caractères POLYTECH. #include <iostream.h> main () { cout << "POLYTECH" << endl; } La ligne #include <iostream.h> sert à inclure le fichier de définition pour les opérations d'entrées/sorties (pour utiliser cout). 2.2 ELEMENTS DE BASE 2.2.1 Caractères Le compilateur C++ utilise l'ensemble des caractères alphabétiques ou numériques suivants : abcdefghijklmnopqrstuvwxyz0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ Attention, contrairement au compilateur Pascal, C++ fait la différence entre les minuscules et les majuscules. C++ reconnait aussi les caractères spéciaux : ! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ _ { } ~ | espace tab entrée – Page 9 – Cours de C++ Comme certains caractères spéciaux ne sont pas disponibles sur tous les claviers, il est possible d'en remplacer certains par la combinaison ??x : # ≡ (équivaut à) ??=, [ ≡ ??(, ] ≡ ??), \ ≡ ??/, ^ ≡ ??', | ≡ ??!, { ≡ ??<, } ≡ ??>, ~ ≡ ??D'autre part, certains caractères "non imprimables" (rassemblés sous l'appellation séquence d'échappement) sont néanmoins disponibles en les remplaçant par la combinaison \x : \a ≡ sonnerie, \b ≡ retour arrière, \f ≡ nouvelle page, \n ≡ nouvelle ligne, \r ≡ entrée, \t ≡ tabulation horizontale, \v ≡ tabulation verticale, \' ≡ ', \" ≡ ", \? ≡ ?, \\ ≡ \ De cette manière, il est aussi possible de spécifier directement la valeur du code ASCII d'un caractère (en octal ou hexadécimal). Exemple : \141 ou \x61 représentent le caractère a de code ASCII 97. 2.2.2 Commentaires Pour placer une seule ligne en commentaire, on utilise //. Le commentaire commence après // et se termine à la fin de la ligne. Pour placer plusieurs lignes en commentaire, on utilise /* (début de commentaire) et */ (fin de commentaire). 2.2.3 Délimiteurs Les délimiteurs sont des caractères qui permettent au compilateur de reconnaître les différents éléments syntaxiques du langage. Les principaux délimiteurs sont les suivants : ; : termine une déclaration de variable ou une instruction, , : sépare deux éléments dans une liste, () : encadre une liste de paramètres, [] : encadre la dimension ou l'indice d'un tableau, {} : encadre un bloc d'instructions ou une liste de valeurs d'initialisation. 2.2.4 Identificateurs Les identificateurs définis par le programmeur peuvent être constitués de n'importe quelle combinaison de caractères alphabétiques ou numériques, mais doivent obligatoirement commencer par une lettre. Le caractère souligné _ est considéré comme une lettre. Il n'y a pas de taille limite maximale pour les identificateurs, mais ceux-ci doivent être choisis courts et représentatifs de ce qu'ils identifient. C++ fait la différence entre les majuscules et les minuscules. Par exemple, true, et TRUE sont des identificateurs différents. 2.2.5 Mots réservés Les mots réservés du C++ sont des mots privilégiés du langage qui ne doivent pas être utilisés comme identificateurs. En voici la liste : asm continue float new signed try auto default for operator sizeof typedef break delete friend private static union case do goto protected struct unsigned catch double if public switch virtual char else inline register template void class enum int return this volatile const extern long short throw while – Page 10 – Eléments de syntaxe 2.2.6 Types de base Il existe trois types de base : caractère, entier et double. TYPE : CARACTÈRE Signed char Unsigned char (char) TAILLE 1 octet 1 octet LIMITES -128 à 127 0 à 255 TYPE : ENTIER Signed short (short) Unsigned short Signed int (int) Unsigned int Signed long (long) Unsigned long TAILLE 2 octets 2 octets 4 octets 4 octets 4 octets 4 octets LIMITES -32768 à 32767 0 à 65535 -214783648 à 214783647 0 à 4294967295 -214783648 à 214783647 0 à 4294967295 TYPE : RÉEL TAILLE Float 4 octets LIMITE EXPOSANT -38 à 38 Double 8 octets -308 à 308 Long double 8 octets -308 à 308 LIMITE MANTISSE 1.175494350822875 10-38 à 3.4028234663852886 10+38 2.2250738585072015 10-308 à 1.7976931348623158 10+308 2.2250738585072015 10-308 à 1.7976931348623158 10+308 2.2.7 Valeurs littérales A chaque type de base, peuvent être associées des valeurs littérales correspondantes. En voici la liste : Valeur caractère : Une valeur caractère est représentée par un caractère ou une séquence d'échappement (voir paragraphe caractères) entouré d'apostrophes. Exemple : 'X', '\'', '0', '\117'. Valeur entière : Pour les valeurs entières, le suffixe u ou U indique que la valeur est signée. De même, le suffixe l ou L indique que la valeur est de type long. décimales : Les valeurs entières décimales sont composées d'une séquence de chiffres de 0 à 9. Elles ne doivent pas commencer par 0. Exemple : 128, 799, 128L, 32744u. octales: Les valeurs entières octales sont composées d'une séquence de chiffres de 0 à 7. Elles doivent obligatoirement commencer par 0. Exemple : 0732, 0899. hexa : Les valeurs entières hexadécimale sont composées d'une séquence de chiffres de 0 à 9 ou de lettres de A à F (majuscules ou minuscules). Elles doivent obligatoirement commencer par 0x ou 0X. Exemple : 0x3F, 0Xe2d4. – Page 11 – Cours de C++ Valeur réelle : Les valeurs réelles sont constituées d'une partie entière, d'un point, d'une partie fractionnaire, et d'une partie exponentielle, signée ou non, précédée de la lettre e ou E. La partie entière ou la partie fractionnaire peut être omise, mais pas les deux. Le suffixe f ou F indique une valeur de type float. Le suffixe l ou L indique une valeur de type long double. Sans suffixe, la valeur est de type double. Exemple : .5E1 (5.0, double), 50e-1F (5.0, float), 5.0L (5.0, long double). Valeur chaîne de caractères : Une valeur chaîne de caractères est constituée d'une suite de caractères (lettres ou chiffres ou signes de ponctuation) placée entre guillemets. Pour continuer une chaîne de caractères sur une ligne suivante, utiliser \ à la fin de la première ligne suivi d'un retour chariot. Le caractère \0 est ajouté à la fin de chaque chaîne de caractères. Exemple : "Le C++ est un LOO" est une chaîne de 18 caractères (\0 à la fin). 2.2.8 Déclaration des variables Syntaxe : type identificateur; ou type identificateur1, identificateur2, ..., identificateurn; Remarque : Dans un programme en C++, les variables peuvent être déclarées n'importe où dans le programme du moment qu'elles le sont avant leur utilisation. Déclarer une variable revient à donner un identificateur à un emplacement mémoire dont la taille est fixée par type, et à préciser le format interne de stockage des données. Exemple : int i, j; double t, r; 2.2.9 Attributs des variables Il est possible de spécifier des attributs de variables. Ceux-ci sont au nombre de quatre : static, auto, register et extern. static : auto : register : Les variables statiques ont un emplacement mémoire réservé au moment de la compilation. Leur allocation est donc permanente en mémoire, et leur contenu, qui peut évoluer, est conservé pendant toute la durée d'exécution d'un programme. Les variables statiques sont surtout utilisées pour les variables locales des fonctions. Elles permettent en effet de conserver la valeur d'une variable entre deux appels d'une même fonction. Mais attention, ceci nuit énormément à la lisibilité des programmes et ne doit être utilisé qu'en dernier recours. Exemple : static int i; Les variables auto, propres à une fonction, sont allouées lors de l'appel de cette fonction, puis libérées au retour de la fonction. Leur contenu est donc perdu entre deux appels consécutifs à la fonction. Les variables locales des fonctions sont auto par défaut. Exemple : auto int n; L'attribut register permet de placer une variable dans un registre interne du microprocesseur. Il est généralement employé avec des variables intensément – Page 12 – Eléments de syntaxe extern : utilisées comme les indices de boucles. A cause du nombre limité de registres, quelques variables seulement peuvent être placées dans les registres. D'autre part, il faut bien évidemment que la taille de la variable corresponde à la taille des registres (16 ou 32 bits). Exemple : register int i; L'utilisation de variables externes permet de référencer des variables définies dans d'autres fichiers. Une déclaration extern ne remplace pas une définition. Elle décrit simplement une variable définie ailleurs. Exemple : extern int i; 2.2.10 Initialisation des variables Les variables peuvent être initialisées à l'aide de l'opérateur d'affectation =. L'expression à droite du signe = doit être évaluable, c'est à dire représenter une valeur. Le membre à gauche de l'affectation doit lui représenter une variable, c'est à dire un contenu. Toutes les variables doivent être initialisées avant leur utilisation. Il est possible d'initialiser les variables au moment de leur déclaration. Exemple : main () { int i, j=0; double e=2.71828, a; j = 3; a = 5.6; } 2.2.11 Constantes typées Pour déclarer une constante typée en C++, il suffit de faire précéder la déclaration d'une variable initialisée au moment de sa déclaration par le mot réservé const. Ainsi, la valeur de cette variable ne peut plus être modifiée. Exemple : const double rac2 = 1.41421. On verra la déclaration et l'utilisation des constantes non typées au paragraphe sur les directives pour le préprocesseur. 2.3 INSTRUCTIONS Les fonctions d'un programme C++ sont constituées d'un ensemble de données et d'instructions exécutables. Une instruction est une séquence d'opérateurs, opérandes et signes de ponctuation. Une expression peut conduire ou non à un résultat. Le déroulement des instructions dans le corps d'une fonction est séquentiel, sauf dans le cas de rupture de séquence, de terminaison de boucle ou de retour de fonction. Toute instruction doit obligatoirement se terminer par un point virgule (;) qui est un terminateur d'instruction. Une instruction composée regroupe plusieurs instructions entourées par des accolades ({ et }). Une instruction composée ne se termine pas par un point virgule. 2.4 OPERATEURS C++ offre une grande richesse d'opérateurs. En voici la liste. – Page 13 – Cours de C++ 2.4.1 Opérateurs arithmétiques + − * / % : : : : : addition soustraction multiplication division modulo ou reste de la division 2.4.2 Opérateurs de manipulation de bits & : | ^ << >> ~ : : : : : ET logique (AND). Cet opérateur est utilisé conjointement avec un masque de bits. OU logique (OR) OU logique exclusif (XOR) décalage vers la gauche décalage vers la droite complémentation à un unaire 2.4.3 Opérateurs d'affectation, d'incrémentation et de décrémentation = : ++ : −− : affectation. Des simplifications peuvent survenir lorsque la partie gauche du signe = se répète à droite. On peut remplacer par exemple x = x + 1; par x += 1;. On a de la même manière -=, *=, /=, %=, &=, |=, ^=, ~=, <<= et >>=. incrémentation. x = x + 1; peut s'écrire x += 1;, mais aussi ++x; (préfixe) ou x++; (suffixe). En préfixe, la variable est incrémentée avant son utilisation, alors qu'en suffixe elle est incrémentée après son utilisation. Par exemple : x = 3; y = x++; conduira à avoir 3 dans y et 4 dans x alors que x=3; y = ++x; conduira à avoir 4 dans x et y. décrémentation. x = x − 1; peut s'écrire x −= 1;, mais aussi −−x; (préfixe) ou x−−; (suffixe). En préfixe, la variable est décrémentée avant son utilisation, alors qu'en suffixe elle est décrémentée après son utilisation. Par exemple : x = 3; y = x−−; conduira à avoir 3 dans y et 2 dans x alors que x=3; y = −−x; conduira à avoir 2 dans x et y. 2.4.4 Opérateurs relationnels Les opérateurs relationnels mettent en relation deux expressions, et le résultat est une expression booléenne fausse (0) ou vraie (tout entier différent de 0). > : supérieur >= : supérieur ou égal < : inférieur <= : inférieur ou égal == : égalité != : inégalité Attention, en C++, de nombreuses erreurs proviennent de la confusion des opérateurs d'affectation (=) et d'égalité (==). – Page 14 – Eléments de syntaxe 2.4.5 Opérateurs logiques Les expressions reliées par ces opérateurs sont évaluées de la gauche vers la droite. L'évaluation prend fin dès que le résultat d'une seule expression entraîne un résultat définitif pour l'expression globale. ! : && || : : négation unaire (NOT). Cet opérateur a pour effet d'inverser la valeur du résultat de l'expression qui le suit. ET logique (AND) OU logique (OR) 2.4.6 Opérateur conditionnel L'opérateur conditionnel est un opérateur ternaire mettant en relation trois expressions. L'expression résultante est booléenne. Cet opérateur est composé des deux signes ? et :. Exemple : (expression1) ? (expression2) : (expression3) qui peut se traduire par si (expression1) alors (expression2) sinon (expression3). Voici un exemple de calcul d'une valeur absolue : val = (n>0) ? n : -n; 2.4.7 Opérateur sizeof sizeof donne la taille en octets de l'opérande qui lui est associé. Par exemple, sizeof (double) donne 8, ou int i; sizeof (i) donne 4. 2.4.8 Opérateur , L'opérateur , permet de manipuler une liste d'expressions comme une seule expression. Deux expressions séparées par une virgule sont évaluées de gauche à droite et l'ensemble a comme valeur celle de l'expression la plus à droite. Exemple c = (a = 1, b = 2); affecte 1 a "a" et 2 a "b", et affecte 2 a "c". 2.4.9 Opérateur . L'opérateur . est utilisé pour accéder aux membres (données ou fonctions) d'une classe, d'une structure ou d'une union. Exemple : class X x; x.init (); x.done (); 2.4.10 Opérateur :: L'opérateur :: est utilisé pour relier la définition d'une fonction à la déclaration d'une classe. Exemple : class X {...} int X::x()... 2.4.11 Opérateur ( ) : conversion de type L'opérateur ( ) permet de forcer le type d'une expression. Par exemple, après la déclaration int i; double a;, on peut convertir la valeur de a en entier pour la stocker dans i en écrivant : i = (int) a; 2.4.12 Autres opérateurs Il existe d'autres opérateurs comme new, delete, throw, ->, ., [ ], &, * sur lesquels nous reviendrons plus loin. – Page 15 – Cours de C++ 2.4.13 Précédence des opérateurs Dans une expression qui comporte plusieurs opérateurs, l'évaluation s'effectue suivant un ordre de priorité décroissant. A chaque opérateur est associé une priorité appelée précédence d'opérateur. Dans le tableau suivant, les opérateurs placés sur une même ligne sont d'égale précédence, les lignes étant disposées par priorité décroissante. Opérateur Associativité ( ) { } -> . Gauche → Droite ! ~ ++ -- - ( ) * & sizeof Droite → Gauche * / % Gauche → Droite + Gauche → Droite << >> Gauche → Droite < <= > >= Gauche → Droite == != Gauche → Droite & Gauche → Droite ^ Gauche → Droite | Gauche → Droite && Gauche → Droite || Gauche → Droite ?: Droite → Gauche = += -= *= /= %= >>= <<= &= Droite → Gauche |= ^= , Gauche → Droite 2.5 STRUCTURES CONDITIONNELLES 2.5.1 if ... else ... Syntaxe : if (expression) instruction1; ou if (expression) instruction1; else instruction2; instruction peut être une instruction composée de la forme : { instruction1; instruction2; ... ; } Remarques : expression doit être de type entier. Si expression est vraie, instruction1 est exécutée. Sinon, c'est instruction2 qui est exécutée. Lorsque plusieurs if sont imbriqués, else se réfère au dernier if. Exemple : if (score1==score2) nul = 1; else nul = 0; 2.5.2 switch Syntaxe : switch (expression) { case valeur1 : instruction; – Page 16 – Eléments de syntaxe case valeur2 : instruction; case valeur3 : instruction; ... default : instruction; } instruction peut être une instruction composée de la forme : { instruction1; instruction2; ... ; }. Les accolades peuvent alors même être omises. Remarques : L'évaluation a lieu dans l'ordre des "case". expression doit être de type entier. Dès que la valeur de l'expression correspond à la valeur qui suit un "case", toutes les instructions qui suivent ce "case" sont exécutées (même celles des autres "case"). Pour provoquer une sortie immédiate du switch, on utilise l'instruction break (voir paragraphe branchements). L'instruction qui suit le "default" est exécutée si aucun branchement dans un "case" n'est réalisé. Exemple : switch (c) { case 'a' : cout << "Lettre a"; break; case 'b' : cout << "Lettre b"; break; default : cout << "Erreur"; } 2.6 STRUCTURES ITERATIVES 2.6.1 Boucle while Syntaxe : while (expression) instruction; instruction peut être une instruction composée de la forme : { instruction1; instruction2; ... ; } Remarques : Le test de l'expression a lieu avant exécution de l'instruction. Lorsque l'expression est fausse, la boucle while ... do n'est pas exécutée. L'expression à tester doit être de type entier. Exemple : int i=0; while (i<N) cout << "i = " << i++; 2.6.2 Boucle do ... while Syntaxe : do instruction; while (expression) instruction peut être une instruction composée de la forme : { instruction1; instruction2; ... ; } Remarques : Le test de l'expression a lieu après l'exécution de l'instruction. La boucle do ... while est toujours exécutée au moins une fois. – Page 17 – Cours de C++ Exemple : L'expression à tester doit être de type entier. int i=10; do cout << "i = " << i--; while (i>0); 2.6.3 Boucle for Syntaxe : for (expression1 ; expression2 ; expression3) instruction instruction peut être une instruction composée de la forme : { instruction1; instruction2; ... ; } expression1 sert d'initialisation, expression2 réalise le test de la boucle, expression3 est exécutée à chaque itération. Remarques : On choisira une boucle for lorsque le nombre d'itérations est connu à l'entrée dans la boucle. Les différentes expressions peuvent être omises (exemple : boucle sans fin si expression2 est toujours vraie, boucle sans initialisation si expression1 n'apparait pas). Plusieurs expressions peuvent être regroupées pour former expression1, expression2 ou expression3. Elles sont alors séparées pas des virgules (exemple : for (i=0,j=N;i<N;i++,j--) instruction). La boucle for n’est pas exécutée si la valeur de départ du compteur est supérieure à la valeur de fin. L'expression à tester doit être de type entier. Exemple : for (i=0;i<N;i++) cout << i; 2.7 BRANCHEMENTS Toutes les instructions de branchement nuisent à la clarté d'un programme. Il faut donc s'interdire de les utiliser. Toutefois dans des cas très particuliers, on peut être amené à les utiliser (exemple : break dans l'instruction switch). 2.7.1 break L'instruction break permet de quitter une instruction itérative (do, for ou while), ou un switch. L'instruction qui est exécutée après un break est la première instruction après la boucle ou le switch. Remarque : L'instruction break est généralement utilisée dans les instructions switch. Exemple : for (i=0;i<5;i++) { if (string[i] == '\0') break; length++; } Lorsque le caractère nul est rencontré dans string, l'exécution se poursuit sur l'instruction qui suit la boucle for. – Page 18 – Eléments de syntaxe 2.7.2 continue L'instruction continue impose le passage immédiat à l'itération suivante dans une boucle (do, for ou while). Exemple : for (i=0;i<N;i++) { k=i-1; if (k==0) continue; a = 1/k; } Lorsque k est égal à zéro, on passe directement à l'itération suivante de la boucle (i est incrémenté et l'instruction k=i-1 est exécutée). Tout se passe comme si les instructions de la boucle qui suivent continue n'étaient pas exécutées. 2.7.3 goto L'instruction goto permet un saut inconditionnel vers l'instruction qui suit l'étiquette spécifiée avec le goto. Syntaxe : goto label; ... label : instruction Remarque : L'utilisation d'un goto dans un programme du module C++ conduit immédiatement a un zéro dans ce module. Exemple : for (j=0;j<=N;j++) { if ( (j<0) || (j>N) ) goto etiq1; } etiq1 : cout << "Erreur : cas impossible"; 2.7.4 return L'instruction return arrête l'exécution d'une fonction. Syntaxe : return ou return valeur Remarque : L'utilisation de return dans les fonctions est facultative, mais ne pas en mettre dans les fonctions typées provoque un warning. Utiliser return dans les fonctions déclarées void provoque une erreur. Une fonction peut contenir plusieurs return. Exemple : int somme (int a, int b) { return (a+b); – Page 19 – Cours de C++ } return permet de quitter somme en rendant la main au (sous-) programme d'appel, après avoir affecté la valeur entière a+b à la fonction. 2.8 DIRECTIVES POUR LE PREPROCESSEUR Ces directives ne sont pas des instructions du C++. Elles ne sont utilisées que par le préprocesseur qui réalise la première phase de la compilation, et non par le compilateur luimême. 2.8.1 #define et #undef #define permet de remplacer dans un fichier toutes les occurrences d'un symbole par une suite de caractères. Exemple : #define N 64 remplacera tous les caractères N du texte qui suit #define par les caractères 64. #undef annule la définition d'un symbole pour le reste d'un fichier. Exemple : #undef N. 2.8.2 #include #include permet d'insérer, à la place de la ligne #include, le contenu d'un fichier source courant à compiler (généralement un fichier de définitions .h). Deux syntaxes existent : #include "nom de fichier" ou #include <nom de fichier>. Dans la première forme, le fichier appartient à l'utilisateur et doit se trouver dans le répertoire courant, ou alors il faut spécifier son chemin d'accès complet. Dans la seconde, le fichier est recherché dans le répertoire /usr/include. Exemple : #include <iostream.h>. 2.8.3 Compilation conditionnelle #if/#ifdef/#ifndef ... #else ... #endif Ces directives de compilation permettent de compiler des lignes d'un programme si une condition est vérifiée. Syntaxe : #if expression séquence compilée si expression est vraie (≠0) #else séquence compilée si expression est fausse (=0) #endif poursuite de la compilation Syntaxe : #ifdef symbole séquence compilée si symbole a été défini par #define #else séquence compilée si symbole n'a pas été défini par #define #endif poursuite de la compilation Syntaxe : #ifndef symbole séquence compilée si symbole n'a pas été défini par #define #else – Page 20 – Eléments de syntaxe séquence compilée si symbole a été défini par #define #endif poursuite de la compilation Remarque : L'expression qui suit #if peut contenir des parenthèses, des opérateurs unaires, binaires ou ternaire. 2.9 MOTS CLEFS Attribut de variable Constante non typée Constante typée Délimiteur Directive Identificateur Mot réservé Opérateur Préprocesseur Variable – Page 21 – 3. POINTEURS ET FONCTIONS 3.1 POINTEURS ET VARIABLES DYNAMIQUES Les variables de type pointeur sont des variables qui contiennent des adresses. Avec les opérateurs de réservation et de libération mémoire, les pointeurs permettent de manipuler des variables dynamiques, c'est à dire créées et détruites, selon les besoins, au fur et à mesure des programmes. 3.1.1 Type pointeur Une variable de type pointeur est une variable qui contient l'adresse d'une zone mémoire. La plupart des pointeurs en C++ sont typés, c'est à dire qu'ils représentent à la fois une zone mémoire et le type d'interprétation de cette zone (zone de stockage d'un réel, d'un entier, d'un objet, d'une fonction, ...). Pour déclarer une variable de type pointeur, il suffit de faire précéder son identificateur du caractère *. Syntaxe : type *identificateur_de_variable; Remarques : La valeur d'une variable pointeur peut être soit la valeur réservée NULL (pointeur sur rien), soit l'adresse d'une variable. L'erreur la plus fréquente est d'omettre l'initialisation des variables pointeurs qui pointent alors n'importe où en mémoire (données, code, système d'exploitation, etc ...). Attention : déclarer une variable pointeur revient à réserver un emplacement qui permet de stocker une adresse, mais pas l'emplacement pointé par cette adresse (voir opérateur new). Exemples : int *pi; définit une variable pi qui contient l'adresse d'un emplacement mémoire pouvant contenir un entier, double *px; définit un pointeur px sur un double, float **pa; définit une variable pa qui pointe sur un pointeur qui pointe sur un float. 3.1.2 Pointeur sur void Le type particulier "pointeur sur void" définit un pointeur qui pointe sur une variable sans type prédéfini. Syntaxe : void *identificateur_de_variable; Exemple : void *p; – Page 23 – Cours de C++ 3.1.3 Opérateur & L'opérateur & crée un pointeur sur une variable. & est un opérateur unaire ayant comme opérande un identificateur de variable ou de fonction. Il renvoie un pointeur void qui peut par conséquent être affecté à toute variable pointeur. Syntaxe : &identificateur Remarque : On dit aussi que & retourne l'adresse de son opérande. Exemple : int i, *pi = &i; le pointeur sur un entier pi contient l'adresse de la variable entière i. 3.1.4 Opérateur * L'opérateur * (appelé parfois opérateur de déréférenciation) permet d'accéder au contenu de la zone mémoire dont l'adresse est dans un pointeur. Il suffit pour cela de faire précéder l'identificateur du pointeur par le symbole *. Syntaxe : *variable_pointeur Remarque : on dit souvent que l'opérateur * permet de passer du pointeur à la variable pointée. Exemple : int *pi, i = 4, j; pi = &i; (*p)i ++; j = *pi; // la variable i contient 4 // pi contient l'adresse de i : il pointe sur i // le contenu pointé par pi (donc i) est incrémenté // la valeur 5 est affectée à j 3.1.5 Variables dynamiques, opérateurs new et delete L'opérateur new crée une variable dynamique et initialise un pointeur sur cette variable. La zone allouée par new n'est pas initialisée automatiquement, mais pour faciliter l'initialisation, il est possible de spécifier une valeur entre parenthèses. Syntaxe : pointeur = new type (valeur); Remarques : En cas de succès, new retourne l'adresse de la variable créée ; en cas d'échec, (par exemple lorsqu'il n'y pas assez de mémoire disponible), new retourne la valeur NULL. Comme pour toutes les variables, c'est au programmeur de veiller à leur initialisation. Dans le cas de variables dynamiques celui-ci doit veiller à initialiser à la fois le pointeur sur la variable dynamique et la variable dynamique elle-même. Exemple : double *pi *e; // pointeurs sur un double, non initialisés pi = new double; // réservation mémoire et initialisation du pointeur *pi = 3.1415927; // initialisation de la variable dynamique e = new double (2.71828); // méthode plus rapide, rationnelle et sûre A l'inverse de new, delete libère la mémoire d'une variable dynamique. – Page 24 – Pointeurs et fonctions Syntaxe : delete pointeur; Remarques : L'opérande de delete doit être un pointeur initialisé par new. Si une tentative d'allocation par new échoue, le pointeur retourné par new (NULL) peut quand même être utilisé avec delete. Lorsque la variable pointée est un tableau, il est préférable d'utiliser l'opérateur delete [ ]. Exemple : (suite de l'exemple précédent) delete pi; // libération mémoire e = delete e; // plus rationnel puisque e pointe sur NULL 3.1.6 Pointeurs constants Pour déclarer un pointeur constant, il faut, lors de la déclaration de ce pointeur, placer le mot réservé const entre le caractère * et son identificateur. Exemple : int z = 10; int * const y; // y est un pointeur constant sur un entier De même, pour déclarer un pointeur sur une donnée constante, il faut placer le mot réservé const avant le caractère * et l'identificateur du pointeur. Exemple : const int *y; // y est un pointeur sur un entier constant L'instruction *y = z; est autorisée dans le premier exemple, mais pas dans le second. Inversement, l'instruction y = &z est correcte dans le second exemple mais pas dans le premier. 3.1.7 Opérations sur les pointeurs Il est possible de réaliser des additions et des soustractions avec des variables de type pointeur. Par exemple px + 1 pointe l'emplacement mémoire qui suit celui pointé par px (le décalage (en octets) réalisé dépend de la taille du type de px : 4 pour int, 8 pour double, ...). Exemple 1 : Exemple 2 : int *pi, *pj; *(pi+1) = 3; // affecte 3 dans pj : utilisation très discutable while (*s++=*t++); // copie d'une chaîne t dans une chaîne s 3.2 TABLEAUX Un tableau est une donnée structurée dont tous les éléments sont de même type. Les tableaux peuvent être classés en deux catégories : les tableaux unidimensionnels et les tableaux multidimensionnels. Un tableau est caractérisé par 3 éléments : son identificateur, son type et sa dimension. En C++, la dimension d'un tableau est une constante entière qui donne le nombre d'éléments, le premier élément étant toujours indicé zéro. – Page 25 – Cours de C++ 3.2.1 Déclaration La déclaration d'un tableau se fait de la manière suivante : Syntaxe : type identificateur [dimension]; ou type identificateur [dim1] [dim2] [dim3]... // tableau unidimensionnel // multidimensionnel Remarques : Rappel : le premier indice d'un tableau est toujours 0. Il est possible de ne pas donner de dimension lors de la déclaration d'un tableau. Dans ce cas, aucun espace mémoire n'est alloué. Ceci permet de référencer un tableau dont la dimension est définie ailleurs, par exemple avec l'opérateur new. Lors de la réservation mémoire d'un tableau, l'adresse retournée par new est l'adresse du premier élément de ce tableau. La réservation mémoire d'un tableau par new se fait de la manière suivante : new type [dimension]; Les tableaux multidimensionnels sont vectorisés. Par exemple, la déclaration double a[3][6]; conduit à l'équivalence, très utilisée en C++, a[i][j] ≡ a[i*6+j]; (6 groupes de 3 éléments chacun). Exemple : int ti[8]; char tc[80]; extern double td[10]; unsigned char ima[512][512]; // tableau de 8 entiers // tableau de 80 caractères // tableau externe de 10 réels // tableau carré 512´512 (pixels) for (i=0;i<10;i++) td[i] = (double) i; // initialisation de td for (i=0;i<512;i++) for (j=0;j<512;j++) ima[i][j] = 0; // initialisation de ima 3.2.2 Relation entre tableaux et pointeurs En C++, les pointeurs et les tableaux sont des variables de même nature. Un tableau est en fait un pointeur constant qui contient l'adresse du premier élément du tableau. La seule différence entre pointeurs et tableaux réside dans le fait que l'adresse du premier élément du tableau ne peut être modifiée. Par exemple, s'il est possible d'incrémenter la valeur d'un pointeur (p++), cela est interdit pour un tableau. Pour fixer les esprits, le tableau suivant présente sous forme synthétique certaines équivalences d'écriture entre pointeurs et tableaux. test identique à &(t[0]) test l'adresse de t[0] t+1 est l'adresse de t[1] t+iest l'adresse de t[i] *(t+i) = 1 est identique à t[i] = 1 t ayant été déclaré par int t[10]; ou t ayant été déclaré par int *t; et alloué par t = new int [10] ; – Page 26 – Pointeurs et fonctions 3.2.3 Tableau de pointeurs Il est possible de déclarer des tableaux de pointeurs en utilisant la syntaxe : Syntaxe : type *nom[dimension]; Remarques : On obtient des tableaux bidimensionnels à structure non régulière ; le nombre de composants est variable. Exemple : char * t1[10]; char (*t2)[10]; // t1 est un tableau de 10 pointeurs sur caractère // à ne pas confondre avec : // t2 est un pointeur sur un tableau de 10 char 3.2.4 Chaînes de caractères En C++, les chaînes de caractères sont strictement définies comme des tableaux de caractères terminés par le caractère nul : '\0'. Toutes les remarques sur les tableaux et les équivalences tableaux-pointeurs s'appliquent donc pour les chaînes de caractères. Les chaînes ne sont pas directement manipulables car il n'y a pas d'opérateurs prédéfinis dans le langage (contrairement à Turbo Pascal). Il existe cependant de nombreuses fonctions standards (dont les prototypes sont définis dans le fichier string.h, voir plus loin bibliothèques standards) qui permettent de réaliser les principales fonctions : − strcpy (s1, s2) pour recopier une chaîne s2 dans la chaîne s1, − strcmp (s1, s2) pour comparer les deux chaînes s1 et s2 (strcmp renvoie une valeur entière négative, nulle ou positive selon que s1 est alphabétiquement plus petite, égale ou plus grande que s2), − strlen (s) pour obtenir la longueur de la chaîne s, − strcat (s1, s2) pour concaténer la chaîne s2 à la chaîne s1, etc, ... 3.2.5 Initialisation des tableaux Un tableau peut être initialisé au moment de la compilation uniquement si son attribut est externe ou statique. Le cas le plus simple est celui des chaînes de caractères initialisées pendant leur déclaration. Cependant, des précautions doivent être prises car la manipulation des chaînes de caractères est bien souvent source d'erreur en C++. Exemple 1 : static char text[9] = "POLYTECH"; déclare une chaîne de 9 caractères, les 8 premiers prenant respectivement les valeurs 'P', 'O', 'L', 'Y', 'T', 'E', 'C', 'H', et le dernier la valeur '\0'. Pour éviter au programmeur de compter les caractères du texte, le compilateur calcule automatiquement la longueur de la chaîne tableau de 9 caractères) si l'on tape : static char text[ ] = "POLYTECH"; Exemple 2 : char chaine[17]; suivi de chaine = "POLYTECH"; conduit à une erreur car chaine est un tableau, donc un pointeur constant que l'affectation tente de modifier par l'adresse de la chaîne constante "POLYTECH". Exemple 3 : char *chaine; suivi de chaine = "POLYTECH" est correct car le pointeur chaine peut être initialisé par l'adresse de la chaîne constante – Page 27 – Cours de C++ "POLYTECH". Mais attention, toute modification sur chaine entraînera la même modification sur la constante "POLYTECH" puisque chaine pointe sur cette constante. Pour initialiser un tableau de valeurs dans le cas général, ses éléments doivent être spécifiés un à un sous la forme d'une liste : Exemple 4 : static int chiffres[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; Dans le cas où la dimension est spécifiée, le nombre d'éléments de la liste d'initialisation doit être strictement égal à la dimension du tableau. 3.3 STRUCTURES, UNIONS ET ENUMERATIONS Les structures et les unions peuvent apparaître comme des extensions du type tableau, dans la mesure où ce sont des variables dont les éléments peuvent avoir des types différents. Une variable de type structure ou union est en effet la juxtaposition structurée de plusieurs variables qui constituent ses champs. Les énumérations permettent de définir des listes de constantes. 3.3.1 Structures Les structures servent donc à regrouper plusieurs variables sous un unique identificateur. Syntaxe : struct nom_de_structure { type1 nom_de_champ_1; type2 nom_de_champ_2; type3 nom_de_champ_3; ... }; Remarques : Une fois défini, le nom de la structure devient un nouveau type et s'utilise comme les types prédéfinis. Pour accéder aux champs d'une structure, on utilise l'opérateur point (.) en regroupant le nom de la variable structure et celui du champ auquel on veut accéder : variable_structure.nom_de_champ, Lorsqu'on utilise des pointeurs sur des structures, la notation -> permet de simplifier l'écriture : (*variable_structure).nom_de_champ ≡ variable_structure->nom_de_champ, On peut considérer les structures comme des classes simplifiées (sans fonctions membres). Mais comme il n'existe pas de fonction membre pour accéder aux champs, on perd le bénéfice de l'abstraction des données. Exemple : struct date { int jour; char mois[10]; int annee; }; // déclaration d'une structure date date // déclaration d'une variable d de type date d, – Page 28 – Pointeurs et fonctions *pd; d.annee = 1995; pd->jour = 19; // déclaration d'un pointeur pd de type date // initialisation du champ année de d // équivalent à (*pd).jour = 19; 3.3.2 Unions Le type union est une extension du type structure qui permet de manipuler une même zone mémoire selon différentes interprétations. Comme les structures, les unions contiennent des champs. Mais les zones mémoires de ces champs ont la même adresse, le compilateur réservant suffisamment d'espace mémoire pour stocker le plus grand des champs de l'union. Syntaxe : union nom_d'union { type1 nom_de_champ_1; type2 nom_de_champ_2; type3 nom_de_champ_3; ... }; Remarque : On ne peut accéder simultanément qu'à un seul champ d'une union. Exemple : union valeur { int entier; double reel; }; // déclaration de l'union valeur valeur v; // définition d'une variable de type valeur v.entier = 4; v.reel = 3.1415; // v est utilisée comme un entier // v est utilisée comme un réel 3.3.3 Énumérations Une liste de constantes peut se définir par énumération. Le premier identificateur de la liste a la valeur 0 par défaut, les valeurs des identificateurs suivants étant incrémentées pour chaque nouvel identificateur. Syntaxe : enum nom_d'enumeration { identificateur_1, identificateur_2, identificateur_3, ... } Remarques : Il n'y a aucune vérification de débordement. Il est possible de modifier les valeurs par défaut des identificateurs : enum couleur {JAUNE=-2, ROUGE, VERT=4, BLEU}; Exemple : enum jour {LUNDI, MARDI, MERCREDI, JEUDI, – Page 29 – Cours de C++ VENDREDI, SAMEDI, DIMANCHE}; jour j1, j2; j1 = MARDI; j2 = j1+3; // j1 vaut MARDI qui vaut 1 // j2 vaut VENDREDI qui vaut 4 3.4 DEFINITION DE TYPE : TYPEDEF Il est possible de définir de nouveaux types en C++ à l'aide du mot réservé typedef. Ceci est particulièrement utile lorsque l'on utilise des types imbriqués les uns dans les autres. La syntaxe d'utilisation de typedef, similaire à celle de déclaration des variables, est présentée dans les exemples suivants : Syntaxe : typedef type identificateur; Exemples : typedef unsigned char BYTE; // déclaration du type BYTE typedef BYTE TAB[10]; // TAB est un tableau sur 10 BYTEs typedef TAB *PTAB; // PTAB est un pointeur sur TAB 3.5 FONCTIONS Certains langages comme le Pascal distinguent deux sortes de sous-programmes : les fonctions (qui retournent une valeur) et les procédures (qui ne retournent aucune valeur). A l'inverse en C++, tous les sous-programmes sont des fonctions. Un fichier source C++ est donc une succession de fonctions. Le programme principal est lui-même une fonction, dont l'identificateur est obligatoirement main, spécifiant que cette fonction principale doit être exécutée en premier. 3.5.1 Déclaration et définition La démarche naturelle consiste à déclarer toutes les fonctions avant de les utiliser ou les définir. Ainsi, il est possible pour le compilateur de lever d'éventuelles ambiguïtés et de réaliser le maximum de vérifications. La déclaration d'une fonction permet au compilateur de connaître le prototype de celleci, c'est à dire le type des paramètres qu'elle utilise et de la valeur retournée. Ainsi, lorsque cela est possible, le compilateur est en mesure de faire les conversions de types nécessaires à une transmission correcte des paramètres et de la valeur renvoyée. La syntaxe de déclaration d'une fonction est la suivante. Le nom de la fonction (identificateur) doit être précédé du type de la valeur retournée par la fonction, et suivi (entre parenthèses) de la liste de ses paramètres. Syntaxe : type_retourné identificateur (type1, type2, type3, ...); ou type_retourné identificateur (type1 param1, type2 param2, ...); Remarques : Si une fonction ne retourne pas de valeur, elle doit être déclarée void. – Page 30 – Pointeurs et fonctions Si une fonction n'a pas de paramètre, il suffit de la déclarer avec une liste vide ( ), ou avec le type void entre parenthèses (void). Exemple : double sin (double); float moyenne (int n, float *x); void init (double); Attention : Avec certains compilateurs, l'utilisation d'une fonction non déclarée au préalable conduit à la définition d'un prototype par défaut, induit par le type des paramètres rencontrés lors du premier appel. De même, si le type de la valeur retournée par la fonction n'est pas défini, le type entier est pré-supposé. Pour lever toute ambiguïté, il est donc indispensable de déclarer les fonctions avant de les utiliser. La syntaxe de la définition des fonctions est la suivante. Vient tout d'abord l'entête de la fonction, rappel éventuel de sa déclaration mais en précisant obligatoirement le nom des paramètres et en ne mettant pas de point-virgule. Juste après, le corps de la fonction est placé entre accolades ({}) et contient les déclarations locales et les instructions. Lorsque la fonction retourne une valeur, il faut utiliser, en n'importe quel point de cette fonction, l'instruction return qui permet de sortir immédiatement. Syntaxe : type_retourné identificateur (type1 param1, type2 param2, ...) { ... } Exemple : int divisible (int a, int b) { if ( (b == 0) || ((a%b) != 0) ) return (0); else return (1); } 3.5.2 Paramètres d'entrée : transmission par valeur Par défaut, le passage des paramètres dans les fonctions se fait par valeur. Ceci signifie que ce sont les valeurs des paramètres qui sont transmises aux fonctions, et qu'aucune modification de ces valeurs dans les fonctions n'est répercutée dans la fonction d'appel (lors de l'appel d'une fonction, les paramètres sont recopiés dans une zone mémoire qui est libérée lorsqu'on en sort). Cette transmission des paramètres par valeur est utilisée pour les paramètres d'entrée, qui ne sont pas modifiés dans les fonctions. 3.5.3 Paramètres de sortie : transmission par adresse Pour modifier des valeurs de paramètres à l'intérieur d'une fonction, on peut utiliser des pointeurs. En effet, en transmettant l'adresse de variables en paramètres, il est possible, grâce à l'opérateur *, d'accéder aux valeurs de ces variables dans les fonctions. On ne transmet donc plus directement les valeurs des variables, mais leurs adresses. A partir de ces adresses, on accède aux contenus. Toute modification de la valeur est donc automatiquement répercutée dans la fonction d'appel, puisque c'est le même emplacement mémoire qui est modifié. Ce mécanisme de transmission par adresse est utilisé pour les paramètres de sortie des fonctions. – Page 31 – Cours de C++ Syntaxe : type nom_de_fonction (type1 *param1, type2 *param2, ...) Remarques : Dans tout le corps de la fonction, parami représente l'adresse du paramètre, et *parami le contenu pointé par parami. Quand la fonction est appelée, il faut prendre garde à transmettre les variables par adresse en les faisant précéder de l'opérateur & (adresse de la variable). Exemple : void swap (int *i, int *j) { int t = *i; *i = *j; *j = t; } void main (void) { int a = 10, b = 20; swap (&a, &b); } // a = 20 et b = 10 3.5.4 Paramètres de sortie : transmission par référence Le mécanisme de la transmission de paramètres par référence permet, comme l'utilisation des pointeurs, de modifier la valeur de variables dans les fonctions. La transmission par référence utilise des paramètres de type "référence". Une variable de type référence est une variable qui contient l'adresse d'une autre variable, et qui peut être utilisée comme cette variable. Une variable référence doit être initialisée lors de sa déclaration (avec l'adresse d'une autre variable) et cette affectation est définitive. Pour déclarer une variable de type référence, on fait précéder son nom du caractère &, et on le fait suivre par une affectation. Par exemple, int i, &j = i; déclare une variable référence j sur l'entier i, qui peut être utilisée indifféremment à la place de i. Ces variables références sont très utilisées pour la transmission de paramètres par adresse dans les fonctions car elle simplifie grandement l'écriture de la transmission par adresse. La syntaxe est alors : Syntaxe : type nom_de_fonction (type1 &param1, type2 &param2, ...) Remarques : Pour modifier le contenu d'un paramètre dans une fonction, il suffit de faire précéder sa déclaration par le caractère &. Lors de l'appel de la fonction, les paramètres peuvent être écrits directement. L'utilisation des variables références dans la transmission des paramètres par adresse est identique à celle des pointeurs. Cependant, elle simplifie énormément l'écriture et réduit ainsi les risques d'erreur (& ou * oublié ou en trop). La valeur de retour d'une fonction peut être aussi de type référence, ce qui autorise à mettre une fonction dans le membre de gauche d'une affectation. Exemple : void swap (int &i, int &j) { – Page 32 – Pointeurs et fonctions int t = i; i = j; j = t; } void main (void) { int a = 10, b = 20; swap (a, b); } // a = 20 et b = 10 3.5.5 Paramètres multivalués : transmission par référence constante Pour transmettre en entrée des paramètres de grosse taille sans recopie temporaire (optimisation de la mémoire et de l'exécution du programme), il faut utiliser des paramètres par référence constante. Ainsi, les paramètres, bien que transmis par référence, ne peuvent être modifiés dans la fonction. Exemple : void print (const gros_type & grosse_variable); 3.5.6 Valeurs par défaut En C++, il est possible d'omettre certains paramètres lors de l'appel d'une fonction, et d'utiliser alors des valeurs par défaut pour ces paramètres. Syntaxe : type nom (type1 param1, type2 param2=val2, type3 param3=val3, ...); Remarques : Il faut noter que dans la déclaration de la fonction, les paramètres par défaut doivent obligatoirement être les derniers de la liste de paramètres. De même, lors de l'appel de la fonction, on ne peut omettre que le ou les derniers paramètres, parmi ceux qui ont une valeur par défaut. Il est possible, dans la déclaration d'une fonction, de définir des paramètres qui n'étaient pas déjà définis. Exemple : double division (double a, double b=1) { if (b != 0) return (a/b); } double division (double a = 0, double b); void main (void) { double c = division (); } // par défaut, a=0 et b=1 // c = 0; 3.5.7 Paramètres de la ligne de commande Il est possible de récupérer dans un programme C++ les paramètres de la ligne de commande d'un programme, c'est à dire la chaîne de caractères qui a déclenché son exécution (nom du programme et paramètres éventuels). Pour cela, il suffit de déclarer deux paramètres à la fonction main : argc et argv. Le premier paramètre, entier, argc, contient le nombre de champs qui composent la ligne de commande (au minimum 1, le nom du programme). Le second paramètre, argv, est un tableau de chaînes de caractères qui permet d'accéder à chacun de ces champs. L'exemple suivant affiche tous les champs de la ligne de commande. – Page 33 – Cours de C++ Exemple : #include <iostream.h> void main (int argc, char *argv[]) { int i; for (i=0;i<argc;i++) cout << "Paramètre " << i << " : " << argv[i] << endl; } 3.6 MOTS CLEFS Allocation dynamique Déclaration Définition Déréférenciation Énumération Libération dynamique Paramètre d'entrée Paramètre de sortie Paramètre multivalué Pointeur Prototype Structure Tableau Transmission par adresse Transmission par référence Transmission par référence constante Transmission par valeur Type référence Union Variable dynamique – Page 34 – 4. CONSTRUCTEURS ET DESTRUCTEURS, SURCHARGE 4.1 CONSTRUCTEURS ET DESTRUCTEURS Comme toute variable d'un programme C++, l'instance d'une classe est créée au moment de sa déclaration, et supprimée à la fin du bloc où elle est déclarée. La création d'une instance correspond à la réservation d'emplacements mémoire pour stocker les données membres de la classe, tandis que sa destruction libère les zones allouées. Réalisées automatiquement pour des données membres non dynamiques, ces allocations et libérations de mémoire doivent être contrôlées par le programmeur lorsque les données membres sont des pointeurs sur des variables qui nécessitent une réservation dynamique de mémoire. Pour cela, on utilise deux fonctions membres spéciales appelées constructeur et destructeur. 4.1.1 Constructeurs Un constructeur est une méthode appelée systématiquement lors de la création d'une instance d'une classe, c'est à dire lors de sa déclaration ou de sa création dynamique par new, juste après la réservation de mémoire pour les données membres. Un constructeur optimise les allocations mémoires liées aux variables dynamiques, et initialise les données membres des classes. Par exemple, la création d'une instance d'une classe avec des données membres pointeurs réserve bien évidemment de la place pour les pointeurs, mais non pour les zones pointées. Le constructeur sert donc à la fois à réserver ces zones pointées et à les initialiser. Pour rendre systématique l'initialisation des données membres des classes, toute classe doit contenir un constructeur. Comme les autres fonctions membres, les constructeurs sont déclarés dans la définition des classes. Ils peuvent être inline ou non, et avoir des paramètres par défaut. Cependant, ils ne retournent pas de valeur et on ne peut pas obtenir leur adresse. L'identificateur d'un constructeur est identique à celui de la classe. Syntaxe : Remarques : class X { ... public: X (paramètres); ... } X::X (paramètres) { ... } // constructeur de la classe X Le constructeur est automatiquement appelé lors de la déclaration de l'objet, et il est interdit d'appeler des constructeurs en dehors de cette déclaration. – Page 35 – Cours de C++ Les objets qui ont des constructeurs avec des paramètres peuvent être initialisés en passant les valeurs des paramètres entre parenthèses à la suite de la déclaration de l'objet. Exemple : identificateur objet (val1, val2, val3, ...). Pour simplifier l'écriture, un constructeur avec un seul paramètre peut être appelé en utilisant simplement le signe =. Exemple : identificateur objet = 4; Les paramètres des constructeurs peuvent être initialisés par défaut. Les constructeurs peuvent initialiser les données membres de deux manières : soit en les initialisant dans le corps du constructeur, soit en les initialisant par une liste placée avant le corps du constructeur. Exemple : complx (double r, double i = 0.0) {re = r; im = i; }ou complx (double r, double i = 0.0) : re (r), im (i) { instructions; } 4.1.2 Constructeurs copies Les constructeurs copies sont utilisés pour faire une copie d'un objet d'une classe dans un objet de même type classe. Ils n'ont qu'un seul paramètre de type référence constante à leur propre classe (les paramètres de même type que la classe ne sont pas admis : seules sont autorisées les références constantes). Exemple : class X { int a, b; public: X (const X&); } X::X (const X& x) { a = x.a; b = x.b; } Remarques : Si un constructeur copie n'existe pas et est néanmoins nécessaire au cours de l'exécution d'un programme, la plupart des compilateurs en définissent un automatiquement. Un constructeur copie correspond par défaut à l'affectation de la zone de données d'un objet dans la zone de données d'un autre objet. Il est indispensable de définir explicitement un constructeur copie lorsque la zone de données contient un pointeur sur une zone mémoire qu'il faut aussi recopier (données membres pointeurs sur des variables dynamiques). Les constructeurs copies sont les seules fonctions où les données membres d'une classe sont utilisées directement. 4.1.3 Destructeurs A l'inverse d'un constructeur, un destructeur est une méthode qui est appelée systématiquement lors de la destruction d'une instance d'une classe, c'est à dire en fin de bloc ou lors de l'utilisation de l'opérateur delete. Il est appelé juste avant la suppression de la zone de – Page 36 – Constructeurs et destructeurs, surcharge données et sert à contrôler la libération des zones mémoires allouées pour les variables dynamiques. Les destructeurs sont eux-aussi déclarés dans la définition des classes. Ils peuvent être inline ou non, mais n'ont pas de paramètre et ne retournent pas de valeur. Comme les constructeurs, on ne peut obtenir leur adresse. L'identificateur d'un destructeur est identique à celui de la classe, mais il est précédé du caractère tilde (~) pour le différencier du constructeur. Syntaxe : class X { ... public: X (paramètres); ~X ( ); ... } X::~X () { ... } // constructeur de la classe X // destructeur de la classe X 4.2 POINTEUR THIS Le pointeur "this" est un pointeur spécial, automatiquement ajouté aux données membres et initialisé par le constructeur d'une classe. Dès que l'instance d'une classe est créée, le pointeur this de cette classe contient l'adresse de l'objet instancié. Il identifie ainsi toujours la classe ellemême. Ce pointeur this est passé comme paramètre caché à toutes les fonctions membres d'une classe, et ne peut être utilisé que dans les fonctions membres. Par exemple, utiliser une donnée membre a dans une fonction membre est équivalent à utiliser this->a. Pour éviter toute ambiguïté, il est interdit au programmeur de déclarer un pointeur this comme donnée membre d'une classe et de l'initialiser. Le pointeur this permet de résoudre certaines identifications équivoques. Par exemple, une fonction membre ne peut normalement pas avoir de paramètre ayant même identificateur qu'une donnée membre. Cela est néanmoins possible si partout dans la fonction la donnée membre est référencée par this->. Exemple : class X { int a; public: X (int a) {this->a = a;} ... } 4.3 SURCHARGE La surcharge consiste à donner le même identificateur à des fonctions ou des opérateurs qui ont des paramètres différents. En C++, il est possible de redéfinir les fonctions et la plupart des opérateurs standards. Généralement, fonctions et opérateurs sont surchargés pour étendre leur utilisation sur des types de données différents de ceux pour lesquels ils ont été écrits. – Page 37 – Cours de C++ 4.3.1 Surcharge de fonctions Pour surcharger une fonction, il suffit de la déclarer plusieurs fois avec le même identificateur, à condition que ces déclarations diffèrent par le type ou le nombre d'arguments de la fonction. A chaque appel, le compilateur choisit alors la fonction à utiliser en fonction du type et du nombre de paramètres de l'appel. On appelle polymorphisme [plusieurs formes] cette faculté de pouvoir redéfinir le corps d'une même fonction. La surcharge s'applique à toute fonction d'un programme C++, donc aussi, bien évidemment, aux fonctions membres des classes. Exemple : #include <iostream.h> void print (int i) { cout << "Entier : " << i << endl; } void print (double f) { cout << "Réel : " << f << endl; } void print (char *c) { cout << "Caractère : " << *c << endl; } void main (void) { print (2); // appelle print (int) print (2.718); // appelle print (double) print ("Deux"); // appelle print (char*) } Ces trois fonctions print au même identificateur réalisent la même opération sur des types de données différents. Pour le compilateur, deux fonctions sont identiques si les trois conditions suivantes sont respectées: 1. elles ont le même identificateur, 2. elles sont déclarées dans le même bloc, 3. les listes de leurs arguments sont identiques. Ainsi, pour pouvoir surcharger des fonctions sans ambiguïté, il faut remarquer que : − la valeur retournée par deux fonctions ne suffit pas à les distinguer (deux fonctions qui diffèrent seulement par la valeur retournée ne doivent pas avoir le même identificateur), − un type déjà connu mais redéfini par typedef n'est pas considéré comme un nouveau type, − pointeurs et tableaux sont considérés comme identiques. – Page 38 – Constructeurs et destructeurs, surcharge En conclusion, seuls le nombre et les types des arguments permettent de distinguer deux fonctions surchargées. 4.3.2 Surcharge de constructeurs La notion de surcharge s'applique aussi aux constructeurs. Il est ainsi possible de prévoir plusieurs constructeurs pour une même classe, ceux-ci devant se différencier par le type ou le nombre des paramètres. Cette possibilité, très utilisée, permet une grande souplesse lors de la création de l'instance d'une classe. Il est ainsi courant de définir au moins deux constructeurs par classe : un sans paramètre qui initialise automatiquement les données membres de la classe avec des valeurs par défaut, l'autre avec paramètres pour initialiser les données membres avec des valeurs spécifiées par l'utilisateur. 4.3.3 Surcharge d'opérateurs L'écriture utilisée pour travailler sur des instances d'objet est souvent lourde et peu lisible. Pour remédier à cela, les opérateurs classiques du C++ peuvent être redéfinis, amenant ainsi une programmation lisible et agréable. Par exemple, avec la déclaration complex a, b, c;, l'instruction a = b + c; est plus lisible que a.addition (b,c);. L'opérateur + est redéfini, celui-ci restant bien entendu utilisable pour l'addition de deux nombres non complexes. Remarque : l'opérateur classique + dépend lui même du type des opérandes : en effet l'addition de deux entiers n'est pas implémentée de la même manière que l'addition de deux réels. Un opérateur surchargé est appelé fonction opérateur. Sa syntaxe de déclaration est identique à celle des fonctions membres. Il est simplement déclaré avec le préfixe operator. Un opérateur surchargé est différent d'une fonction surchargée, mais comme pour ces fonctions, la sélection des opérateurs surchargés se fait sur le nombre et le type des opérandes utilisés. Les opérateurs qui peuvent être surchargés sont les suivants. Certains de ces opérateurs sont unaires (un seul opérande), d'autres binaires (deux opérandes), et certains à la fois unaires et binaires : + − * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= >> << >>= <<= == != <= >= && || ++ −− , * -> () [] new delete 4.3.4 Règles générales de surcharge des opérateurs Deux règles fondamentales régissent la surcharge des opérateurs unaires ou binaires : 1. si un opérateur est membre d'une classe, son premier opérande est toujours du type de la classe à laquelle il appartient. Le type du second opérande, s'il existe, doit correspondre au type déclaré lors de la surcharge de l'opérateur, 2. quand un opérateur n'est pas membre d'une classe, au moins un de ses opérandes doit être de type classe. Comme précédemment, le type du second opérande, s'il existe, doit correspondre au type déclaré lors de la surcharge de l'opérateur. – Page 39 – Cours de C++ Habituellement, les opérateurs surchargés sont utilisés en appliquant la syntaxe classique des opérateurs. Il est cependant possible de les utiliser de manière explicite, même si cela nuit souvent à la lisibilité des programmes. 4.3.5 Surcharge d'opérateurs unaires Les opérateurs unaires qui peuvent être redéfinis sont : ++ −− ! ~ + − * & Les quatre premiers sont essentiellement unaires tandis que les quatre derniers peuvent également être binaires. Comme ils n'ont qu'un paramètre, ces opérateurs ne peuvent être redéfinis que pour des opérandes objet (voir les règles générales de surcharge des opérateurs). Un opérateur unaire redéfini est un opérateur fonction membre (sans paramètre) ou amie (paramètre de type classe) d'une classe. Les syntaxes de déclaration et de définition restent celles des fonctions membres ou amies. Syntaxe : Utilisation : type operator ~ ( ); ou friend type operator ~ (classe); // opérateur fonction membre ~ objet ou objet.operator ~ ( ) ou ~ objet ou operator ~ (objet) // opérateur fonction membre // opérateur fonction amie // opérateur fonction amie Remarques : Pour les fonctions membres ou amies, les deux écritures présentées ci-dessus sont totalement équivalentes. La première est appelée notation opératoire ou implicite, la seconde notation fonctionnelle classique ou explicite. Pour les opérateurs ++ et −−, on peut définir deux sens différents comme ++x et x++. La déclaration suffixe est réalisée en déclarant deux arguments respectivement de type classe et entier pour les opérateurs surchargés hors d'une classe. Pour les opérateurs membres d'une classe, un seul paramètre de type entier suffit pour spécifier l'opérateur suffixe. Dans tous les autres cas, l'opérateur est considéré comme un opérateur préfixe. Exemple : #include <iostream.h> class complx { private: double re, // partie réelle im; // partie imaginaire public: complx (double r = 0, double i = 0) { re = r; im = i; } complx operator - ( ); // opérateur conjugué friend double operator & (const complx &); // op. module }; complx complx::operator - ( ) { – Page 40 – Constructeurs et destructeurs, surcharge complx resultat; resultat.re = re; resultat.im = - im; return resultat; } double operator & (const complx & c) { return sqrt (c.re*c.re + c.im*c.im); } void main (void) { complx x (4,4); complx y = -x; complx z = x.operator - ( ); double m = &x; double n = operator & (x); } // appel implicite // appel explicite // appel implicite // appel explicite 4.3.6 Surcharge de l'opérateur d'affectation = Lorsque les données membres d'une classe contiennent des pointeurs sur des variables dynamiques, il est indispensable de redéfinir l'opérateur d'affectation =. En effet, dans la plupart des cas, il faut non seulement recopier les données membres de la classe, mais aussi recopier les valeurs des variables dynamiques pointées, après leur avoir réservé un emplacement mémoire propre (cf constructeur copie). Lors de la surcharge de l'opérateur =, il est judicieux de passer l'unique paramètre en référence constante pour ne pas risquer de le modifier. De même, on peut donner à cet opérateur une valeur de retour du même type classe. Cela permet de "cascader" les affectations, comme c'est l'usage en C++. Exemple : class X { ... public: X operator = (const X&); } X X::operator = (const X & x) { ... return (*this); } void main (void) { X x1, x2, x3, x4; ... – Page 41 – Cours de C++ x1 = x2 = x3 = x4; } 4.3.7 Surcharge d'opérateurs binaires Les opérateurs binaires qui peuvent être redéfinis sont : + * / % ^ & | || && = < > += -= *= /= %= ^= &= |= << >> <<= <<= == != <= >= Les opérateurs binaires se définissent de manière analogue aux opérateurs unaires. Il s'agit encore, soit d'opérateurs fonctions membres, soit d'opérateurs fonctions amies de classes. Un des deux opérandes doit obligatoirement être une instance d'une classe, le deuxième pouvant être soit l'instance d'une classe (la même ou une autre), soit un autre type (voir les règles générales de surcharge des opérateurs). Les syntaxes de déclaration et de définition restent celles des fonctions membres ou amies. Syntaxe : Utilisation : retourné operator ~ (type); ou friend retourné operator ~ (classe, type); // opérateur fn membre objet ~ var ou objet ~ var objet.operator ~ (var) //opérateur fn membre operator ~ (objet, var) // opérateur fonction amie // opérateur fonction amie Remarques : Dans la syntaxe d'utilisation des opérateurs binaires ci-dessus, var représente soit un objet, soit un paramètre de type quelconque. Comme pour les opérateurs unaires, les deux écritures implicites et explicites sont complètement équivalentes. Lorsqu'une redéfinition concerne un opérateur dont le membre de gauche n'est pas une classe, cette redéfinition doit être obligatoirement réalisée par une fonction amie. En effet, le premier opérande d'un opérateur binaire redéfini dans une classe est nécessairement une instance de cette classe. Exemple : #include <iostream.h> class complx { private: double re, // partie réelle im; // partie imaginaire public: complx (double r = 0, double i = 0) { re = r; im = i; } complx operator + (const complx &); friend complx operator + (double, const complx &); }; complx complx::operator + (const complx& c) { complx resultat; resultat.re = re + c.re; – Page 42 – Constructeurs et destructeurs, surcharge resultat.im = im + c.im; return resultat; } complx operator + (double r, const complx& c) { complx resultat; resultat.re = r + c.re; resultat.im = r + c.im; return resultat; } void main (void) { complx x (4,4); complx y (6,6); complx t = x.operator + (y); complx u = x + y; complx v = operator + (3.0, x); complx w = 3.0 + x; } // appel explicite // appel implicite 4.3.8 Surcharge des opérateurs [ ] et ( ) La redéfinition de l'opérateur d'indiçage permet d'utiliser une classe tableau définie par le programmeur, tout en conservant la notation traditionnelle des tableaux. Cet opérateur [ ] admet comme un paramètre de type quelconque, ce qui permet de travailler avec des tableaux de tableaux. Une utilisation astucieuse de cet opérateur consiste à définir une classe tableau qui conduise à une véritable notion de tableau dynamique avec vérification de débordement. De même, en donnant une valeur de retour qui soit de type référence à l'opérateur [], il sera possible de mettre cet opérateur [] à gauche d'un signe d'affectation, et donc de modifier le contenu de l'élément considéré du tableau. Exemple : class CTab { private: double t[TMAX]; public: double& operator [] (int); }; double& CTab::operator [] (int i) { if ( (i<0) || (i>TMAX-1) ) cerr << "Débordement de tableau" << endl; return t[i]; } void main (void) { – Page 43 – Cours de C++ ... t[4] = t[3]; } L'opérateur ( ) peut lui aussi être redéfini. Il correspond à la fonction operator ( ). Son utilisation est tout à fait similaire à celle de l'opérateur [ ]. 4.3.9 Surcharge des opérateurs new et delete Comme pour les autres opérateurs, il est possible de redéfinir new et delete, et par ce moyen fournir une utilisation simplifiée de l’allocation dynamique d’objets. 4.3.10 Correspondance des paramètres Quand une fonction ou un opérateur surchargé est appelé dans un programme C++, le compilateur choisit la déclaration de fonction ou d'opérateur qui correspond "le mieux" à l'appel. Pour cela, le compilateur compare les paramètres passés lors de l'appel avec ceux de la déclaration. Trois cas de figure se présentent alors : 1. la correspondance est exacte, 2. il n'y a pas de correspondance, 3. il y a une correspondance ambiguë. Les deux premiers cas ne posent pas de problème. Quand au troisième, il faut absolument le bannir pour éviter tout fonctionnement incorrect ou aléatoire des programmes. 4.4 MOTS CLEFS Appel explicite Appel fonctionnel Appel implicite Appel opératoire Constructeur Constructeur copie Destructeur Fonction opérateur Opérande Opérateur binaire Opérateur unaire Pointeur this Polymorphisme Portée Redéfinition Règle de surcharge Surcharge – Page 44 – 5. HÉRITAGE Les classes peuvent être définies et utilisées de manière autonome, chaque classe constituant un ensemble homogène indépendant de toute autre classe. Cependant, il peut être très utile qu'une classe soit construite à partir d'une autre classe, en conservant les propriétés de la classe d'origine et en en acquérant de nouvelles. En C++, ce processus est possible et s'appelle héritage ou dérivation. Il conduit à la notion de hiérarchie de classe. 5.1 HERITAGE L'héritage (ou dérivation) est un mécanisme qui permet de construire des classes à partir d'autres classes, en définissant une nouvelle classe, appelée classe dérivée, comme une extension d'une classe existante, appelée classe de base. La dérivation permet à une classe dérivée d'hériter des propriétés, c'est à dire des données et fonctions membres, d'une classe de base. Ainsi, il est possible de compléter des classes, en rajoutant données ou des fonctions membres, et/ou de les personnaliser, en redéfinissant des données ou des fonctions membres. 5.1.1 Syntaxe Dans la définition d'une classe dérivée, le nom de la classe de base, séparé par le signe deux points (:), suit le nom de la nouvelle classe. Syntaxe : class base { ... }; class dérivée : base { ... }; Remarques : Quand une classe dérivée hérite d'une classe de base, les données et les fonctions membres de cette classe de base sont incorporées aux données et fonctions membres de la classe dérivée. Une classe dérivée peut redéfinir des données ou des fonctions membres d'une classe de base. Pour lever toute ambiguïté, l'opérateur de résolution de portée :: peut être utilisé. Les classes de base qui apparaissent dans la déclaration de classes dérivées doivent avoir été définies préalablement. Les classes de base sont souvent aussi appelées classes ancêtres. De même, les classes dérivées sont aussi appelées classes filles. Grâce à l'héritage, il est possible de définir des lignées d'objets, c'est à dire des successions de classes qui héritent les unes des autres. Dans ce schéma, une classe de base qui apparait explicitement dans la déclaration de sa classe dérivée est appelée classe directe. A l'inverse, une classe qui – Page 45 – Cours de C++ n'apparait pas directement dans la déclaration d'une classe dérivée (mais qui cependant appartient à la hiérarchie) est appelée classe indirecte. Exemple : #include <iostream.h> class point { double x,y; public: ... } class cercle : point { double ray; public: ... } // coordonnées (x,y) d'un point // rayon d'un cercle Dans cet exemple, la classe cercle a trois données membres : x, y et ray. 5.1.2 Affectation En programmation orientée objet, un objet de classe dérivée peut être manipulé comme un objet de classe de base. Ceci signifie par exemple qu'un objet dérivé peut être affecté à un objet de base, contrairement à la règle traditionnelle de l'affectation qui précise que les membres à gauche et à droite du signe = doivent être de même type. En fait, pour être autorisée, il suffit que l'affectation (ou projection) d'un objet dans un deuxième objet initialise tous les champs de ce deuxième objet. C'est le cas lorsqu'un objet dérivé est projeté sur un objet de base, mais pas l'inverse. Valide pour l'affectation d'objets, cette propriété est aussi très utilisée pour le passage de paramètres objets dans les fonctions et pour l'affectation de pointeurs sur les objets. Exemple : (suite de l'exemple précédent) void main ( ) { point p; cercle c; p = c; c = p; // valide, toutes les données de p sont initialisées // interdit, ray n'est pas initialisé. } 5.1.3 Constructeurs et destructeurs Lorsque la classe de base d'une classe dérivée possède un constructeur (ce qui devrait toujours être le cas), cette classe dérivée doit elle aussi obligatoirement avoir un constructeur, sauf s'il y a dans la classe de base un constructeur sans paramètre. En effet, lors de la création d'une instance d'une classe dérivée, le constructeur de la classe de base est exécuté préalablement à celui de la classe dérivée. Quand une classe dérivée possède plusieurs classes de base (hiérarchie), le constructeur appelé en premier est celui de la classe la plus ancêtre, le dernier – Page 46 – Héritage étant toujours celui de la classe dérivée. Ce mécanisme permet ainsi d'initialiser systématiquement en cascade toutes les données membres des classes d'une hiérarchie. Dans une classe dérivée, pour passer des paramètres au constructeur de la classe de base, il suffit de préciser la liste de ces paramètres à la suite de l'en-tête du constructeur de la classe dérivée (en séparant cette liste de l'en-tête par le symbole :). Syntaxe : class base { ... public: base (param_base) {corps}; }; class dérivée : base { ... public: dérivée (param_dérivée) : base (param_base) {corps}; }; Remarque : Lorsqu'une donnée membre d'une classe est un objet, le constructeur de cette classe doit passer des paramètres au constructeur de l'objet membre. Ceci est réalisé en appelant explicitement le constructeur de l'objet membre, avec ses paramètres, à la suite de l'entête du constructeur de la classe. Exemple : #include <iostream.h> class point { double x,y; // coordonnées d'un point public: point (double a=0, double b=0) { x = a; y = b; } } class cercle : point { double ray; // rayon public: cercle (double a, double b, double r=0) : point (a,b) {ray = r;} } Dans une hiérarchie, les destructeurs sont appelés dans l'ordre inverse des constructeurs. Ainsi, le destructeur appelé en premier est celui de la classe dérivée, le second celui de la classe immédiatement supérieure, et ainsi de suite jusqu'au destructeur de la classe la plus ancêtre. Ce mécanisme permet ainsi de libérer systématiquement en cascade toutes les données membres des classes d'une hiérarchie. – Page 47 – Cours de C++ 5.1.4 Accès aux membres hérités Les classes dérivées n'héritent pas automatiquement des privilèges des classes de base. Ainsi, dans une classe dérivée, les spécificateurs private, public et protected permettent de contrôler précisément l'accès aux membres des classes de base. Membres privés : Les membres privés (private) d'une classe sont inaccessibles à toute autre classe, y compris à une classe dérivée. De cette manière, le principe de l'abstraction des données est complètement préservé. Membres protégés : Les membres d'une classe qui sont déclarés protégés (protected) sont privés, sauf pour les classes dérivées dans lesquelles ils peuvent être utilisés directement. Membres publics: Les membres publics (public) d'une classe sont accessibles partout. Parallèlement à ce contrôle en amont, c'est à dire lors de la déclaration d'une classe, il est possible de contrôler l'accès des différents membres d'une classe de base en aval, c'est à dire lors de la déclaration d'une classe dérivée. Pour cela, un spécificateur d'accès peut précéder la déclaration d'une classe de base dans la déclaration d'une classe dérivée. Ceci ne modifie pas bien sûr l'accès aux membres de la classe de base dans la classe de base, mais l'accès aux membres de la classe de base dans la classe dérivée. Syntaxe : class base { ... } class dérivée1 : public base { ... } class dérivée2 : private base { ... } Remarque : Lorsqu'aucun contrôleur d'accès n'est spécifié, la classe de base est privée par défaut. Trois cas se présentent, selon que la classe de base est déclarée publique, protégée ou privée : 1. Si une classe de base est déclarée publique, les membres publics et protégés de la classe de base restent membres publics et protégés de la classe dérivée, 2. Si une classe de base est déclarée privée (accès par défaut), les membres publics et protégés de la classe de base deviennent membres privés de la classe dérivée, – Page 48 – Héritage 3. Si une classe de base est déclarée protégée, les membres publics et protégés de la classe de base deviennent membres protégés de la classe dérivée. Dans tous les cas, les membres privés d'une classe de base restent privés dans les classes dérivées. C'est le concepteur d'une classe (et lui seul) qui autorise ou non l'accès aux membres de sa classe. Il est aussi possible de restaurer localement l'accès d'un membre d'une classe de base dans une classe dérivée. Cette déclaration d'accès est permise par exemple pour les membres publics d'une classe de base déclarée privée ou protégée dans la classe dérivée. En effet, tous les membres d'une classe de base déclarée privée ou protégée deviennent privés ou protégés pour la classe dérivée. En les redéclarant publics de manière explicite dans la classe dérivée, leur accès public est restauré. Syntaxe : class base { ... public: int i; } class dérivée1 : private base // les membres de base sont privés { ... public: base::i; // restauration de i publique dans la classe dérivée } Remarque : Pour ne pas cacher le membre i de la classe de base, il faut utiliser l'opérateur de résolution de portée :: et nommer explicitement le membre dont il faut modifier l'accès. Dans le cas contraire, la donnée membre publique i déclarée dans la classe dérivée rend inaccessible la donnée membre privée de la classe de base. 5.2 HERITAGE MULTIPLE En C++, il est possible de créer des classes dérivées à partir de plusieurs classes de base. On parle alors d'héritage multiple. 5.2.1 Héritage direct La figure suivante présente un cas simple où une classe X hérite de trois classes A, B et C: – Page 49 – Cours de C++ La syntaxe de déclaration et d'utilisation d'une classe dérivée qui hérite de plusieurs classes de base est totalement identique à celle des héritages linéaires. Il suffit de faire suivre la déclaration de la classe dérivée par la liste des classes de base : Syntaxe : class A { .. }; class B { .. }; class C { .. }; class X : public A, private B, public C { ... }; Remarque : L'ordre de déclaration des classes de base ne sert qu'à déterminer l'ordre d'appel des constructeurs et des destructeurs de la hiérarchie. Comme pour une hiérarchie linéaire, les constructeurs des classes de base sont placés, avec leurs paramètres, dans l'ordre de déclaration des classes de base, à la suite de l'entête du constructeur de la classe dérivée : X::X (paramXs) : A (paramAs), B (paramBs), C (paramCs). 5.2.2 Classes de base identiques Une classe de base directe ne peut apparaître plus d'une fois dans la déclaration d'une classe dérivée. Par contre, il peut arriver qu'une classe dérivée hérite d'une classe indirecte par plusieurs classes directes. On aboutit alors au schéma suivant, où la classe X hérite de la classe A par les classes B et C : – Page 50 – Héritage Bien évidemment, un tel schéma conduit à des ambiguïtés puisque deux objets de classe A accessibles dans X existent. Il est néanmoins possible de lever ces ambiguïtés en utilisant l'opérateur de résolution de portée pour référencer explicitement B::A ou C::A. 5.2.3 Classes de base virtuelles Lorsqu'au moins deux classes d'une hiérarchie ont une classe de base commune, il est possible de déclarer cette classe commune virtuelle, de manière à forcer le partage d'une unique instance plutôt que deux copies séparées. Ainsi, dans le schéma suivant, les classes B et C ancêtres de X partagent la même classe A. Pour obliger la classe X à n'hériter que d'un objet de classe A, il suffit de préciser le mot réservé virtual devant A lors de la déclaration des classes B et C. Syntaxe : class A { .. }; class B : virtual public A { .. }; class C : virtual public A { .. }; class X : public B, public C – Page 51 – Cours de C++ { ... }; Remarque : Il est possible qu'une classe dérivée ait à la fois des classes de base virtuelles et non virtuelles. Dans une hiérarchie qui comprend une classe de base virtuelle accessible par plusieurs chemins, on accède à cette classe de base par le chemin qui procure le plus de droit d'accès (c'est à dire d'abord public, puis protected puis private). Dans des hiérarchies complexes, il faut faire très attention aux ambiguïtés (données ou fonctions membres identiques) qui peuvent survenir. 5.2.4 Types de hiérarchies Deux approches différentes peuvent être utilisées pour construire une hiérarchie. La première consiste à spécialiser les classes dérivées au fur et à mesure de la hiérarchie. On parle alors de spécialisation des classes, puisque les classes dérivées sont plus spécialisées que les classes de base. Cette spécialisation conduit souvent à des hiérarchies linéaires ou arborescentes croissantes. Mais il est aussi possible de procéder de manière complètement opposée en construisant une hiérarchie basée sur une généralisation de caractéristiques. Dans ce cas, plusieurs classes qui partagent des propriétés identiques sont regroupées dans une classe de base. Les graphes engendrés par les généralisations sont alors bien souvent arborescents décroissants. 5.3 POLYMORPHISME En programmation orientée objets, l'édition de liens dynamiques [dynamic binding ou late binding] permet d'écrire des programmes encore plus modulaires qu'en programmation structurée classique. En effet, grâce aux fonctions virtuelles, il est possible de définir dans les classes d'une hiérarchie plusieurs implémentations d'une même fonction membre, commune aux classes de cette hiérarchie. Les programmes d'application peuvent alors utiliser ces fonctions membres sans connaître le type (classe) de l'objet courant au moment de la compilation : c'est pendant l'exécution du programme que le système sélectionne la fonction membre appelée, selon le type de l'objet courant. Comme pour la surcharge des fonctions, ce processus qui permet de donner plusieurs implémentations à des fonctions identiques dans toute une hiérarchie de classes s'appelle polymorphisme. 5.3.1 Fonctions virtuelles Grâce aux règles de compatibilité entre classe de base et classe dérivée, un pointeur sur une classe dérivée est aussi un pointeur sur une classe de base (mais la réciproque n'est pas vraie). Toutefois, par défaut, le type des objets pointés est défini lors de la compilation. Ainsi, dans l'exemple suivant, class A { ... class B : public A { ... – Page 52 – Héritage public: void fct ( ); ... }; public: void fct ( ); ... }; main (void) { A *pa; B *pb; } l'affectation pa = pb est autorisée, mais quel que soit le contenu de pa (pointeur sur un objet de classe A ou B), pa->fct ( ) appelle toujours la fonction fct ( ) de la classe A. En effet, pendant la compilation, le compilateur lie le pointeur pa à la fonction A.fct ( ), et quel que soit le contenu de pa pendant l'exécution du programme, c'est toujours la fonction A.fct ( ) qui est appelée. L'utilisation de fonctions virtuelles permet de contourner ce problème. Lorsqu'une fonction est déclarée virtuelle dans une classe (il suffit pour cela de faire précéder sa déclaration du mot réservé virtual), les appels à cette fonction sont résolus pendant l'exécution du programme, et non plus à la compilation. Ainsi, pour reprendre l'exemple précédent, la fonction A.fct ( ) est appelée quand le pointeur pa contient un objet de la classe A, et la fonction B.fct ( ) est appelée lorsque le pointeur pa contient un objet de la classe B. Dans ce cas, on parle de ligature dynamique des fonctions, puisque le choix de la fonction à appeler est réalisé lors de l'exécution du programme et non plus lors de la compilation. Syntaxe : public: virtual type fonction ( ); Remarques : Pour qu'une fonction soit virtuelle dans toute une hiérarchie, il faut que le nombre ou le type de ses arguments soit rigoureusement identique dans toutes les classes dérivées. Une fonction déclarée virtuelle dans une classe de base peut ou non être redéfinie dans des classes dérivées. Le mot réservé virtual ne s'emploie qu'une fois pour une fonction donnée, impliquant que toute redéfinition de cette fonction dans des classes dérivées est virtuelle. Un constructeur ne peut pas être virtuel mais un destructeur peut l'être. Une fonction virtuelle peut être déclarée amie dans une autre classe. Lorsqu'une fonction déclarée virtuelle est explicitement référencée à l'aide de l'opérateur ::, le mécanisme d'appel virtuel n'est pas utilisé. 5.3.2 Fonctions virtuelles pures Une classe abstraite est une classe de base qui regroupe des caractéristiques communes à toute une hiérarchie de classes. Dans la mesure où ses classes dérivées la complètent, cette classe de base ne doit généralement pas être instanciée. En C++, pour interdire l'instanciation d'une classe, il suffit de donner à cette classe une fonction membre virtuelle pure, publique, de la forme suivante. Syntaxe : public: – Page 53 – Cours de C++ virtual type fonction ( ) = 0; Remarques : Une fonction virtuelle pure n'a pas de définition et ne peut être appelée. Puisqu'une fonction virtuelle pure n'est pas définie, tout appel de cette fonction est indéfini. Cependant, l'appel d'une fonction virtuelle pure ne produit pas d'erreur. Aucun objet d'une classe ayant une fonction virtuelle pure ne peut être déclaré. Si une classe de base contient une fonction virtuelle pure et que sa classe dérivée ne redéfinit cette fonction virtuelle pure, alors la classe dérivée est elle aussi une classe abstraite qui ne peut être instanciée. 5.4 MOTS CLEFS Classe abstraite Classe de base Classe dérivée Classe directe Classe indirecte Classe virtuelle Dérivation Fonction virtuelle Fonction virtuelle pure Héritage Héritage multiple Hiérarchie Membre privé Membre protégé Membre public Polymorphisme Projection Virtual – Page 54 – 6. FLUX Les fonctions d'entrées/sorties ne sont pas définies dans le langage C++, mais sont implémentées dans une libraire standard C++ appelée I/O stream [Input/Output stream ≡ flots d'entrées/sorties]. En C++, les entrées/sorties sont considérées comme des échanges de séquences de caractères ou octets, appelées flots de données. Un flux est un périphérique, un fichier ou une zone mémoire, qui reçoit (flux d'entrée) ou au contraire qui fournit (flux de sortie) un flot de données. 6.1 LIBRAIRIE I/O STREAM La libraire I/O stream contient deux classes de base, ios et streambuf, et plusieurs classes dérivées, comme indiqué sur la figure ci-dessous. La classe streambuf et ses classes dérivées définissent des tampons pour les flots, tandis que la classe ios et ses classes dérivées définissent les opérations sur ces flots. Les tampons sont des zones mémoires qui servent de relais entre les variables du programme (via les fonctions de – Page 55 – Cours de C++ la classe ios) et les périphériques, fichiers et zones mémoires (via les fonctions du système d'exploitation). Ils sont toujours utilisés car ils optimisent les échanges en réduisant le nombre des appels systèmes qui sont souvent bien longs. 6.2 ENTREES-SORTIES STANDARDS Pour lire ou écrire des données dans un flux, les deux opérateurs >> et << sont communément utilisés avec les deux instances prédéfinies cin et cout des classes istream (flux d'entrée) et ostream (flux de sortie). cin est un flux associé à l'entrée par défaut, le clavier, tandis que cout un est un flux associé à la sortie par défaut, l'écran. Il existe aussi deux autres flux prédéfinis, cerr (non bufferisé) et clog (bufferisé), qui sont reliés à la sortie erreur standard. L'opération de sortie de flot est réalisée par l'opérateur d'insertion <<, défini dans la classe ostream. A l'inverse, l'opération d'entrée de flot est réalisée par l'opérateur d'extraction >>, défini dans la classe istream. Bien évidemment, ces opérateurs ne sont définis que pour les types de base du langage, mais il est possible de les redéfinir pour les adapter à d'autres types de données. Pour cela, il faut les déclarer comme fonction opérateur amie d'une classe et utiliser les schémas suivants : ostream & operator << (ostream & sortie, const classe & objet) { // utiliser l'opérateur classique pour des types de base. Ex : sortie << ... return sortie; } ou istream & operator >> (istream & entree, const classe & objet) { // utiliser l'opérateur classique pour des types de base. Ex : entree >> ... return entree; } 6.2.1 États de formats Pour formater les sorties par défaut, on utilise des manipulateurs simples ou on appelle directement des fonctions membres de la classe de base ios. Les manipulateurs simples s'emploient sous la forme flux << manipulateur ou flux >> manipulateur et sont regroupés dans le tableau de la page ci-contre. Exemple : int i = 512; cout << oct << i << endl; cout << hex << i << endl; cout << 255 << endl; cout << dec << i << endl; – Page 56 – // affiche 1000 // affiche 200 // affiche FF // affiche 512 Flux Manipulateur Utilisation Action dec Entrée/Sortie Conversion décimale hex Entrée/Sortie Conversion hexadécimale oct Entrée/Sortie Conversion octale ws Entrée Suppression des espaces dans le tampon endl Sortie Insertion d'un caractère saut de ligne ends Sortie Insertion d'un caractère fin de chaîne \0 flush Sortie Vidage du tampon Plusieurs fonctions membres de la classe ios modifient les données membres de cette classe pour contrôler les formats d'entrée et/ou de sortie des flots. Elles s'appliquent sur les objets de la classe ios ou de ses classes dérivées. Le tableau suivant présente ces manipulateurs de manière synthétique. La notation * indique que, utilisée sans paramètre, la fonction membre retourne les valeurs courantes des données membres correspondantes. Manipulateur Rôle int precision (int)* fixe le nombre de décimales des réels (défaut : 6) int fill (char)* fixe le caractère de remplissage (défaut : espace) long flags (long)* fixe toutes les options de format (voir le tableau d'options) long setf (long) fixe plusieurs options de format (voir le tableau d'options) long setf (long, long) fixe une option de format exclusive (1er paramètre) après réinitialisation du groupe (2ème paramètre, ios::basefield, ios::floatfield ou ios::adjustfield) int skip (int) fixe l'option de format skips (voir le tableau d'options) long unsetf (long) supprime les options de format spécifiées int width (int)* spécifie la largeur de l'affichage Exemple : double pi = 3.1415926535897932385; cout << pi; // affiche 3.141592 cout.precision (8); cout << pi // affiche 3.14159265 cout.setf (ios::showpoint,ios::floatfield); // setf pour options exclusives cout << 10.0; //affiche 10.00000000 cout.setf (ios::fixed,ios::floatfield); // notation scientifique cout.width (6); // affichage sur 6 caractères cout.fill ('@'); // caractère de remplissage @ cout << 12.5; // affiche @@12.5 cout.setf (ios::showpos); // affiche + pour les nombres > 0 cout.setf (ios::left, ios::adjustfield); // justification à gauche cout << 12.5; // affiche +12.5@ – Page 57 – Cours de C++ Les fonctions membres flags, setf, skips et unsetf utilisent des options de format comme paramètres. La liste de ces options est donnée dans le tableau suivant. Les exposants 1/2/3 indiquent les options qui sont mutuellement exclusives. Utiliser simultanément des options exclusives conduit à des résultats imprévisibles. Il est possible de préciser plusieurs options simultanément en utilisant l'opérateur | (ou sur bits). Option de Format Rôle ios::skipws supprime les espaces tapés pendant la saisie de nombres ios::left1 justifie les valeurs à gauche -1.25e10xxx ios::right1 justifie les valeurs à droite xxx-1.25e10 ios::internal1 ajoute des caractères de remplissage interne -x1.25ex10 ios::dec2 conversion décimale ios::oct2 conversion octale ios::hex2 conversion hexadécimale ios::showbase conversion en constante entière ios::showpos ajoute le caractère + pour les valeurs décimales positives +3. ios::showpoint affiche toujours la virgule et complète par des zéros 2.000 ios::scientific3 affichage en notation scientifique 3.4e10 ios::fixed3 affichage en notation décimale 123.987 ios::uppercase affichage des caractères en majuscules 12F4, 10E34 ios::unitbuf réalise un flush à chaque insertion 6.2.2 États d'erreurs Les états d'erreurs permettent de garder trace des erreurs qui peuvent éventuellement survenir pendant la manipulation des flux, de la classe ios et de toutes ses classes dérivées. Il est donc fondamental de les connaître. États d'erreur Signification ios::goodbit aucun bit d'erreur n'est activé ios::eofbit marque fin de flux rencontrée pendant une extraction ios::failbit erreur d'allocation mémoire pendant l'utilisation d'un flux ios::badbit erreur fatale sur le tampon (streambuf) associé au flux ios::hardfail erreur interne à la librairie : ne jamais l'utiliser Pour préserver l'encapsulation des données, on accède aux valeurs des différents états d'erreurs au moyen de fonctions membres publiques dont la liste est donnée dans le tableau suivant. – Page 58 – Flux Fonctions membres Rôle int bad ( ) const retourne une valeur non nulle si ios::badbit est activé void clear (int) efface les états d'erreur spécifiés (l'opérateur de bit | peut spécifier plus d'un état) ou tous les états si le paramètre est nul int eof ( ) const retourne une valeur non nulle si ios::eofbit est activé int fail ( ) const retourne une valeur non nulle si ios::badbit ou ios::failbit sont activés int good ( ) const renvoie une valeur non nulle si aucun bit d'erreur est activé int rdstate ( ) const retourne toutes les valeurs courantes de l'état d'erreur operator void* ( ) operator const void* () convertit le flux courant en pointeur pour pouvoir le comparer à NULL. Retourne 0 si ios::failbit ou ios::badbit sont activés. int operator ! ( ) const ! ( ) retourne une valeur non nulle si ios::badbit ou ios::failbit sont activés streambuf* rdbuf ( ) retourne un pointeur sur l'objet streambuf associé au flux Exemple : if (!cin) { cout << "ios::failbit ou ios::badbit est activé" << endl; if (cin.bad ( )) cin.clear (ios::badbit|cin.rdstate( )); // réinitialise ios::badit } 6.3 MANIPULATION DE FLUX Avant d'être utilisé, un flux doit toujours être associé à un tampon, puisque toutes les opérations d'entrée/sortie sont réalisées dans ce tampon. C'est pourquoi les classes ios, istream, ostream et iostream reçoivent un tampon comme unique paramètre pour leurs constructeurs. Il faut alors penser, préalablement à toute utilisation de ces classes, à déclarer et initialiser correctement un objet tampon dérivé de streambuf. Afin de réduire cette contrainte, les trois classes ofstream, ifstream, et fstream (dont les entêtes se trouvent dans le fichier stream.h), dérivées respectivement de ostream, istream et iostream, permettent de manipuler très simplement des flux en créant automatiquement un tampon associé au flux. Pour cela, on utilise principalement quatre fonctions membres de ces classes. 6.3.1 Constructeur [i/o]fstream ( ) Il y a quatre versions différentes des constructeurs des classes [i/o]fstream. [i/o]fstream ( ); [i/o]fstream (int d); [i/o]fstream (const char* fname, int mode, int prot=filebuf::openprot); [i/o]fstream (int d, char *p, int len); – Page 59 – Cours de C++ La première version n'a pas de paramètre et construit un objet [i/o]fstream qui n'est pas ouvert. La seconde version reçoit un unique paramètre et construit un objet [i/o]fstream relié à un descripteur de flux d. Ce descripteur peut correspondre par exemple à un flux déjà associé à un tampon. Si d n'a pas été préalablement ouvert, ios::failbit est initialisé. La troisième version a trois arguments. Elle permet d'ouvrir un flux fname, avec le mode d'ouverture spécifié par mode et le mode de protection spécifié par prot. Le tableau suivant précise les différents modes d'ouverture possibles, qui peuvent être combinés entre eux à l'aide de l'opérateur ou (|). Mode d'ouverture Action ios::in ouverture en lecture seule (obligatoire pour la classe ifstream) ios::out ouverture en écriture seule (obligatoire pour ofstream) ios::binary ouverture en mode binaire ios::app ouverture en ajout de données (écriture en fin de flux) ios::ate déplacement en fin de flux après ouverture ios::trunc si le flux existe, son contenu est effacé (obligatoire si ios::out est activé sans ios::ate ni ios::app) ios::nocreate le flux doit exister préalablement à l'ouverture ios::noreplace le flux ne doit pas exister préalablement à l'ouverture (sauf si ios::ate ou ios::app est activé) La quatrième version reçoit elle aussi trois paramètres. Le premier est un objet associé à un descripteur de flux d. Si d n'a pas été préalablement ouvert, ios::failbit est initialisé. Ce constructeur permet de construire un objet tampon streambuf, de taille len caractères et qui commence à l'adresse p. Si p vaut NULL ou que len est égale à zéro, l'objet associé filebuf n'est pas tamponné. 6.3.2 Ouverture de flux open ( ) La fonction [i/o]fstream::open (const char* fname, int mode, int prot=filebuf::openprot); permet d'ouvrir un flux fname, avec le mode d'ouverture spécifié par mode et le mode de protection spécifié par prot. Le tableau précédent précise les différents modes d'ouverture possibles, qui peuvent être combinés entre eux à l'aide de l'opérateur ou (|). Cette fonction membre est essentiellement utilisée lorsque l'objet flux est initialisé par le constructeur sans paramètre. 6.3.3 Pointeur de tampon rdbuf ( ) La fonction [i/o]fstream::rdbuf ( ) retourne un pointeur sur le tampon attaché à l'objet de la classe [i/o]fstream. 6.3.4 Fermeture du flux close ( ) La fonction fstreambase::close ( ) ferme le tampon attaché à l'objet de la classe [i/o]fstream en supprimant la connexion entre l'objet et le descripteur de flux. – Page 60 – Flux 6.3.5 Flux en écriture La fonction ostream& ostream::put (char c) insère le caractère c dans le tampon associé à l'objet de la classe [i/o]fstream. Pour insérer plusieurs caractères simultanément, il faut utiliser la fonction ostream& ostream::write (char* cp, int n) qui insère n caractères stockés à l'adresse pointée par cp. Pour déplacer le pointeur d'élément dans le flux, il faut utiliser la fonction ostream& ostream::seekp (streamoff n, ios::seek_dir dir) qui positionne le pointeur à n octets de la position dir. dir peut prendre l'une des valeurs du tableau suivant. Position dans flux Action ios::beg Début du flux ios::cur position courante du flux ios::end fin du flux Si on tente de déplacer le pointeur d'élément vers une position invalide, seekp () bascule la valeur ios::badbit. Pour connaître la valeur courante du pointeur d'élément, on utilise la fonction streampos ostream::tellp ( ). 6.3.6 Flux en lecture La fonction istream& istream::get (char c) extrait un caractère du tampon associé à l'objet de la classe [i/o]fstream et le place dans c. Pour extraire plusieurs caractères simultanément, il faut utiliser la fonction istream& istream::read (char* cp, int n) qui extrait n caractères du tampon et les stocke à l'adresse pointée par cp. Il existe d'autres versions de la fonction get, mais elles ne sont pas souvent utilisées. Pour déplacer le pointeur d'élément dans le flux, il faut utiliser la fonction istream& istream::seekg (streamoff n, ios::seek_dir dir) qui positionne le pointeur à n octets de la position dir (les différentes valeurs de dir sont regroupées dans le tableau précédent). Pour connaître la valeur courante du pointeur d'élément, on utilise la fonction streampos istream::tellg ( ). Trois fonctions supplémentaires permettent de manipuler les flux en lecture. int istream::gcount ( ) retourne le nombre de caractères réellement extraits du tampon lors du dernier appel de get ou read. int istream::peek ( ) retourne le prochain caractère du tampon, sans l'extraire réellement (s'il n'y a pas de caractère, peek ( ) retourne eof). istream& istream::putback (char c) replace le caractère c dans le tampon (c doit obligatoirement être le dernier caractère extrait du tampon). – Page 61 – Cours de C++ Exemples : // Écriture dans un flux texte // Écriture dans un flux binaire #include <iostream.h> #include <fstream.h> #include <stdlib.h> #include <math.h> #define N 16 #include <iostream.h> #include <fstream.h> #include <stdlib.h> #include <math.h> #define N 16 void main (void) { int i; double s[N], t[N]; char * flux_name = "data.txt"; void main (void) { int i; double s[N], t[N]; char * flux_name = "data.res"; for (i=0;i<N;i++) s[i] = sin (2 * M_PI * i / (double) N); ofstream fr (flux_name, ios::out); if (!fr) { cerr << "Erreur d'ouverture de "; cerr << flux_name << endl; exit (1); } for (i=0;i<N;i++) fr << s[i]; fr.close ( ); for (i=0;i<N;i++) s[i] = sin (2 * M_PI * i / (double) N); ofstream fr (flux_name, ios::out); if (!fr) { cerr << "Erreur d'ouverture de "; cerr << flux_name << endl; exit (1); } fr.write ((char*) s, N*sizeof (double)); fr.close ( ); ifstream fd (flux_name, ios::in); if (!fd) { cerr << "Erreur d'ouverture de "; cerr << flux_name << endl; exit (1); } for (i=0;i<N;i++) fd >> t[i]; fd.close ( ); ifstream fd (flux_name, ios::in); if (!fd) { cerr << "Erreur d'ouverture de "; cerr << flux_name << endl; exit (1); } fd.read ((char*) t, N*sizeof (double)); fd.close ( ); } } – Page 62 – Flux 6.4 FICHIERS IOSTREAM.H ET FSTREAM.H Les classes présentées ci-après sont des extraits des fichiers iostream.h et fstream.h. Pour avoir de plus amples renseignements, il faut se reporter directement à ces deux fichiers. 6.4.1 Déclaration de la classe ios (iostream.h) class ios { public: enum io_state enum open_mode enum seek_dir enum static const long static const long static const long virtual long long long long long int int ostream* ostream* char char int int int int int int int int void streambuf* long & void* & static long static int static void int { goodbit=0, eofbit=1, failbit=2, badbit=4, hardfail=0200 }; { in=1, out=2, ate=4, app=010, trunc=020, nocreate=040, noreplace=0100 }; { beg=0, cur=1, end=2 }; { skipws=01,left=02,right=04, internal=010, dec=020, oct=040, hex=0100, showbase=0200, showpoint=0400, uppercase=01000, showpos=02000, scientific=04000, fixed=010000, unitbuf=020000, stdio=040000 } ; basefield; /* dec|oct|hex */ adjustfield; /* left|right|internal */ floatfield; /* scientific|fixed */ ios (streambuf*); ~ios (); flags () const { return x_flags ; } flags (long f); sef (long setbits, long field); setf (long); unsetf (long); width () const { return x_width ; }; width (int w) {int i = x_width ; x_width = w ; return i ; } tie (ostream*); tie () { return x_tie ; } fill (char); fill () const { return x_fill ; } precision (int); precision () const { return x_precision ; } rdstate () const { return state ; } operator void*() { if (state&(failbit|badbit|hardfail)) return 0 ; else return this ; } operator const void*() const { if (state&(failbit|badbit|hardfail)) return 0 ; else return this ; } operator! () const { return state&(failbit|badbit|hardfail); } eof () const { return state&eofbit; } fail () const { return state&(failbit|badbit|hardfail); } bad () const { return state&badbit ; } good () const { return state==0 ; } clear (int i =0); rdbuf () { return bp ; } iword (int); pword (int); bitalloc (); xalloc (); sync_with_stdio (); skip (int); protected: ostream* long short char short void x_tie; x_flags; x_precision; x_fill; x_width; init (streambuf*); ios (); assign_private; int /* No initialization at all. Needed by multiple inheritance versions */ /* needed by with_assgn classes */ private: void ios operator = (ios&); (ios&); }; – Page 63 – Cours de C++ 6.4.2 Déclaration de la classe istream (iostream.h) class istream : virtual public ios { public: istream virtual ~istream (streambuf*); (); ipfx seekg seekg tellg operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> operator >> get get get getline getline getline get get get get get peek ignore read read( read gcount putback sync (int noskipws=0); (streampos p); (streamoff o, ios::seek_dir d); (); (istream& (*f)(istream&)) (ios& (*f)(ios&) ); (char*); (signed char*); (unsigned char*); (char&); (signed char&); (unsigned char&); (short&); (int&); (long&); (unsigned short&); (unsigned int&); (unsigned long&); (float&); (double&); (long double&); (streambuf*); (char* , int lim, char = '\n'); (signed char*, int, char = '\n'); (unsigned char*, int, char = '\n'); (char*, int, char = '\n'); (signed char*, int, char = '\n'); (unsigned char*, int, char = '\n'); (streambuf&, char = '\n'); (char&); (signed char&); (unsigned char&); (); (); (int = 1,int = EOF); (char *, int); signed char * s, int n) (unsigned char *s, int n) (); (char); () istream (); x_gcount; xget (char *); public: int istream& istream& streampos istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& int int istream& istream& istream& istream& int istream& int { return (*f)(*this) ; } { return read((char*)s,n) ; } { return read((char*)s,n) ;} { return bp->sync() ; } protected: private: int void }; 6.4.3 Déclaration de la classe istream_with_assign (iostream.h) class istream_withassign : public istream { public: istream_withassign virtual ~istream_withassign istream_withassign& operator = istream_withassign& operator = }; (); (); (istream&); (streambuf *); – Page 64 – Flux 6.4.4 Déclaration de la classe ostream (iostream.h) class ostream : virtual public ios { public: virtual ostream ~ostream (streambuf*); (); int void ostream& ostream& ostream& streampos ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& opfx osfx flush seekp seekp tellp put operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << operator << write write write () () (); (streampos); (streamoff, ios::seek_dir); (); (char ); (char c); (signed char); (unsigned char); (const char*); (const signed char*); (const unsigned char*); (int); (long); (double); (long double); (float); (unsigned int); (unsigned long); (const void*); (streambuf*); (short i) (unsigned short i) (ostream& (*f)(ostream&)) (ios& (*f)(ios&) ); (const char *,int); (const signed char* s, int n) (const unsigned char* s, int n) ostream (); public: { if ( ospecial ) return do_opfx() ; else return 1 ; } { if ( osfx_special ) do_osfx() ; } { return *this << (int)i ; } { return *this << (int)i; } { return (*f)(*this) ; } { return write((const char*)s,n); } { return write((const char*)s,n); } protected: }; 6.4.5 Déclaration de la classe ostream_with_assign (iostream.h) class ostream_withassign : public ostream { public: ostream_withassign virtual ~ostream_withassign ostream_withassign& operator = ostream_withassign& operator = }; (); (); (ostream&); (streambuf*); 6.4.6 Déclaration de la classe iostream (iostream.h) class iostream : public istream, public ostream { public: iostream virtual ~iostream protected: iostream }; (streambuf*); (); (); – Page 65 – Cours de C++ 6.4.7 Déclaration de la classe iostream_with_assign (iostream.h) class iostream_withassign : public iostream { public: iostream_withassign virtual ~ iostream_withassign iostream_withassign& operator = iostream_withassign& operator = }; (); (); (ios&); (streambuf*); 6.4.8 Déclaration des variables globales du fichier iostream.h extern istream_withassign cin; extern ostream_withassign cout; extern ostream_withassign cerr; extern ostream_withassign clog; ios& ostream& ostream& ostream& ios& ios& istream& dec endl ends flush hex oct ws (ios&); (ostream& i); (ostream& i); (ostream&); (ios&); (ios&); (istream&); 6.4.9 Déclaration de la classe fstreambase (fstream.h) class fstreambase : virtual public ios { public: fstreambase fstreambase fstreambase fstreambase ~fstreambase void open void attach void close void setbuf filebuf* rdbuf private: filebuf buf; protected: void verify }; (); (const char*, int , int = filebuf::openprot); (int); (int fd, char*, int); (); (const char*, int, int = filebuf::openprot); (int fd); (); (char*, int); () { return &buf ; } (int); 6.4.10 Déclaration de la classe ifstream (fstream.h) class ifstream : public fstreambase, public istream { public: ifstream ifstream ifstream ifstream ~ifstream filebuf* rdbuf void open }; (); (const char*, int = ios::in, int = filebuf::openprot); (int fd); (int, char*, int); (); () { return fstreambase::rdbuf(); } (const char*, int = ios::in, int = filebuf::openprot); – Page 66 – Flux 6.4.11 Déclaration de la classe ofstream (fstream.h) class ofstream : public fstreambase, public ostream { public: ofstream ofstream ofstream ofstream ~ofstream filebuf* rdbuf void open }; (); (const char*, int = ios::out, int = filebuf::openprot); (int); (int, char*, int); (); () { return fstreambase::rdbuf(); } (const char* , int = ios::out, int = filebuf::openprot); 6.4.12 Déclaration de la classe fstream (fstream.h) class fstream : public fstreambase, public iostream { public: fstream fstream fstream fstream ~fstream filebuf* rdbuf void open }; (); (const char*, int, int = filebuf::openprot); (int); (int, char*, int); (); () { return fstreambase::rdbuf(); } (const char*, int, int = filebuf::openprot); 6.5 MOTS CLEFS Buffer cerr/cin/cout Descripteur de flux Entrées/Sorties Etat de format Etat d'erreur Flot Flux Flux de sortie Flux d'entrée Flux d'erreur fstream.h iostream.h Périphérique Pointeur d'élément Tampon – Page 67 – 7. GÉNÉRICITÉ 7.1 INTRODUCTION Un langage fortement typé comme le C++ peut parfois sembler être un obstacle à l'implémentation de fonctions que l'on écrirait naturellement par ailleurs. Par exemple, bien que l'algorithme de la fonction min () ci-dessous soit simple, il est nécessaire de définir une fonction pour chaque paire de paramètres de types différents que l'on souhaite comparer. Entiers int min (int a, int b) { return (a<b) ? a : b; } Réels simples Caractères float min (float a, float b) { return (a<b) ? a : b; } char min (char a, char b) { return (a<b) ? a : b; } Une alternative envisageable mais dangereuse consisterait à utiliser la directive #define du pré-processeur au lieu d'écrire explicitement chaque instance de min (). #define min(a,b) ( ( (a) < (b) ) ? (a) : (b) ) En effet, bien que cette définition fonctionne correctement pour des cas simples, elle ne se comporte pas comme on l'attend pour des appels plus complexes, puisque le pré-processeur effectue une simple substitution du texte de ses paramètres. Ainsi, le plus petit des paramètres est évalué deux fois : une première fois pendant le test (a < b), et une seconde fois pendant l'exécution de l'instruction retournée. Cas simples : fonctionnement correct min (10, 20); // retourne 10 min (10.0, 20.0); // retourne 10.0 min ('d', 'f'); // retourne 'd' Cas plus complexes : fonctionnement anormal int i=10; min (i++,20); // retourne 11, et i vaut 12 (i++ est réalisée 2 fois) La notion de généricité permet de tirer parti de la notation compacte de la directive #define du pré-processeur, sans perdre aucun avantage des langages fortement typés. Elle autorise le programmeur à paramétrer à volonté le type des paramètres, le type des variables locales, et le type de retour de fonctions dont le corps reste le même (instructions identiques). Cette possibilité s'appelle "généricité", puisqu'on décrit toute une famille de fonctions que le compilateur instancie à la demande. De la même manière qu'on définit des fonctions génériques, aussi appelées fonctions templates, il est aussi possible de décrire toute une famille de classes à l'aide d'une seule – Page 69 – Cours de C++ définition comportant des paramètres de type et/ou des paramètres expression. On aboutit alors à la notion de classe générique, ou classe template. 7.2 FONCTIONS GENERIQUES Ce paragraphe présente la syntaxe de déclaration ou de définition des fonctions génériques. 7.2.1 Déclaration Le mot réservé template est toujours placé au début de la définition et de la déclaration des fonctions génériques. Vient ensuite une liste constituée des types paramètres séparés par des virgules, délimitée par une paire de signes d'inégalité (< et >). Cette liste est appelée liste des paramètres formels de la fonction générique. La définition ou la déclaration de la fonction suit la liste des paramètres formels. Syntaxe : template <class t1, class t2, ...> type nom_fonction (paramètres) { } Remarques : La liste des paramètres formels ne doit pas être vide. Tous les paramètres formels doivent apparaître comme type des paramètres de la fonction. Chaque déclaration de paramètre formel comprend le mot réservé class suivi d'un identificateur (ce mot réservé class indique que le type paramètre peut éventuellement être un type construit ou défini par l'utilisateur). Une fois déclarés, les paramètres formels servent de spécificateur de type pour le reste de la définition de la fonction générique. L'identificateur d'un paramètre formel doit être unique à l'intérieur d'une liste de paramètres génériques, mais il peut être réutilisé pour d'autres déclarations, partout où un type effectif est autorisé. Excepté la présence de paramètres formels comme spécificateurs de type, la définition d'une fonction générique est la même que celle d'une fonction classique. Lors de multiples déclarations, les identificateurs des paramètres formels n'ont pas besoin d'être identiques. Une fonction générique, de la même manière qu'une fonction classique, peut être déclarée extern, inline ou static. Dans ces cas, le spécificateur doit suivre, et non précéder, la liste de paramètres formels. Exemple : template <class Type> static Type min (Type a, Type b) { return (a<b) ? a : b; } – Page 70 – Généricité 7.2.2 Instanciation A chaque fois qu'une fonction générique est employée, le compilateur utilise la définition générique pour créer (instancier) une fonction adéquate. Pour cela, il cherche à réaliser une correspondance exacte des types. Par exemple, avec les déclarations suivantes, int i, j; double x, y; min (12, 14); min (i, j); min (x,y); instancie la fonction instancie la fonction instancie la fonction int min (int, int); int min (int, int); double min (double, double); le compilateur crée deux instanciations différentes, l'une utilisant des entiers, l'autre des doubles, à partir de la fonction générique min (). Pour cela, il est nécessaire que chaque paramètre de type apparaisse au moins une fois dans l'entête de la fonction. Le type courant à donner aux paramètres formels est déterminé en évaluant les paramètres passés à la fonction. Le type de retour n'est pas pris en compte. Par exemple, double d = min (3,4) instancie la fonction int min (int, int), mais la valeur renvoyée est transtypée en double avant l'affectation à d. Une fonction générique ne peut être compilée à partir de sa seule définition, puisque c'est l'utilisation de cette fonction qui permet au compilateur d'implémenter le code conforme aux types utilisés. Mais elle doit être connue du compilateur pour qu'il puisse instancier la fonction appropriée. C'est pourquoi les définitions de fonctions génériques figurent en général dans des fichiers de déclaration d'extension .h, de façon à pouvoir les compiler dès leur utilisation. 7.2.3 Redéfinition de fonctions génériques Il est possible de définir plusieurs fonctions génériques de même identificateur, possédant des paramètres formels en nombre ou de types différents, qui puissent permettre au compilateur de distinguer sans ambiguïté les instances de ces fonctions génériques. Par ailleurs, il peut arriver que l'instanciation d'une fonction générique ne soit pas appropriée pour un type particulier de données. Il est alors possible de fournir la définition d'une ou plusieurs fonctions particulières, qui seront utilisées en lieu et place de celles instanciées à partir de la fonction générique. On parle alors de définition explicite de fonction générique. Par exemple, l'instanciation de la fonction min (T, T) pour deux paramètres de type char* ne donnera pas le résultat attendu si le programmeur souhaite que chaque paramètre soit interprété comme une chaîne de caractères et non comme un pointeur de caractère. Pour résoudre cela, il suffit de fournir une instance spécialisée de la fonction min : template <class Type> Type min (Type a, Type b) char *min (char *s1, char *s2) { { return (a<b) ? a : b; return (strcmp (s1,s2)<0?s1:s2); } } – Page 71 – Cours de C++ L'algorithme d'instanciation ou d'appel d'une fonction générique, surchargée ou non, est le suivant : 1. le compilateur recherche tout d'abord une correspondance exacte avec les fonctions ordinaires (non génériques). Des conversions triviales sont réalisées si elles conduisent à une correspondance exacte de type. S'il y a ambiguïté, la recherche échoue, 2. si aucune fonction ordinaire ne convient, le compilateur examine toutes les fonctions génériques de même identificateur. Si une seule correspondance exacte est trouvée, la fonction correspondante est instanciée. S'il y en a plusieurs, la recherche échoue. Là aussi, des conversions triviales sont réalisées si elles conduisent à une correspondance exacte de type, 3. enfin, si aucune fonction générique ne convient, le compilateur examine à nouveau toutes les fonctions ordinaires en les considérant comme des fonctions redéfinies (conversions explicites). 7.3 CLASSES GENERIQUES Comme les fonctions génériques, les classes génériques permettent de construire des classes en paramétrant des types. Ceci conduit à des descriptions générales de classes qui, selon les utilisations, conduisent à instancier des classes différentes à partir d'un masque (ou empreinte) unique. Avec les classes génériques, on obtient une génération automatique d'instances de classes, liées à un type particulier. Les classes génériques se comportent exactement comme les classes non génériques. Même si l'écriture d'une classe générique est au premier abord "intimidante", le code revêt en définitive un aspect plutôt habituel. 7.3.1 Déclaration Comme pour les fonctions génériques, le mot réservé template est toujours placé au début de la déclaration des classes génériques. Vient ensuite la liste des paramètres formels séparés par des virgules, délimitée par une paire de signes d'inégalité (< et >), et suivie de la déclaration de la classe. Si une fonction membre est définie à l'extérieur de la définition d'une classe générique (ce qui est souvent le cas), il faut donner la liste des paramètres formels précédée du mot réservé template avant l'identificateur de la classe, puis répéter cette liste de paramètres formels après l'identificateur de la classe. Syntaxe : template <class t1, class t2, ..., type v1, type v2, ...> class X { private: ... public: X (); ~X (); membre (...); } – Page 72 – Généricité template <class t1, class t2, ..., type v1, type v2, ...> X <t1, t2, ..., v1, v2, ...>::membre (...) { ... } Remarques : La liste de paramètres formels ne doit pas être vide. La liste des paramètres formels peut contenir des déclarations de paramètres classiques (par exemple int i, char *b, ...). Cette caractéristique est généralement utilisée pour définir des constantes dans la classe générique (dimension de tableaux par exemple). Chaque paramètre formel comprend le mot réservé class suivi d'un identificateur (ce mot réservé class indique que le type paramètre peut éventuellement être un type construit ou défini par l'utilisateur). Une fois déclarés, les paramètres formels servent de spécificateur de type dans toute la déclaration de la classe, partout où un type effectif est autorisé. Excepté la présence de paramètres formels comme spécificateurs de type, la définition d'une classe générique est la même que celle d'une classe ordinaire. Une classe générique, de la même manière qu'une classe ordinaire, peut être déclarée extern ou static. Dans ces cas, le spécificateur doit suivre, et non précéder, la liste de paramètres formels. L'identificateur d'une classe générique doit toujours (en dehors de la définition de la classe) être suivi de la liste des paramètres formels, délimitée par une paire de signes d'inégalité (< et >). Exemple : template <class Type, int dim> class essai { private: Type a; // donnée membre de type Type Type *pa; // donnée membre pointeur sur Type Type pa[dim]; // tableau de dim Type ... public: essai (Type); // construct. à un paramètre de type Type ~essai (); // destructeur membre (Type); // fonction membre ... } template <class Type, int dim> essai <Type, dim>::essai (Type ai) { ... } 7.3.2 Instanciation Une classe générique est instanciée en donnant une liste complète de types (appelés paramètres effectifs et associés aux paramètres formels), délimitée par une paire de signes – Page 73 – Cours de C++ d'inégalité (< et >), à la suite de l'identificateur de la classe, lors de la déclaration d'un objet. Le spécificateur de type d'une classe générique peut être utilisé partout où est utilisé habituellement un spécificateur de type ordinaire. Les objets d'une classe générique instanciée sont déclarés de la même façon que les objets d'une classe ordinaire. Par exemple : essai <double, 3> e; déclare un objet e de la classe essai. Les paramètres effectifs (types ou expressions) doivent correspondre aux paramètres figurant dans la liste. Les paramètres expressions doivent obligatoirement être des expressions constantes du même type que celui figurant dans la liste donnée lors de la déclaration de la classe. Un paramètre effectif peut lui-même être une classe générique. Une classe générique peut comporter des membres (données ou fonctions) statiques ; dans ce cas, chaque instance différente de la classe dispose de son propre jeu de membres statiques. La définition d'une classe générique n'est pas compilée tant qu'il n'existe pas d'objet de cette classe. Le compilateur n'émet donc pas d'erreur tant que cette classe n'est pas instanciée. 7.3.3 Spécialisation d'une classe générique Contrairement aux fonctions génériques, une classe générique ne peut pas être redéfinie (on ne peut pas définir deux classes de même identificateur). Par contre, on peut spécialiser une classe générique de deux manières : soit en spécialisant une fonction membre, soit en spécialisant la classe. Par exemple, on peut spécialiser la classe essai de la manière suivante : class essai <char, 10> { ... // nouvelle définition de essai } 7.3.4 Identité de classes génériques En C++, on ne peut affecter entre eux que deux objets de même type. Dans le cas d'objets de type classe générique, il y a identité de type lorsque leurs paramètres de types sont identiques et que les paramètres expressions ont même valeur. 7.3.5 Classes génériques et héritage Il y a trois manières de combiner des classes génériques dans une hiérarchie de classes : 1. classe ordinaire dérivée d'une classe générique : on obtient une seule classe dérivée. Par exemple, si A est une classe générique définie par : template <class T> A la classe : class B : public A <int> dérive de la classe A <int> et est unique. – Page 74 – Généricité 2. classe générique dérivée d'une classe ordinaire : on obtient une famille de classes génériques. Par exemple, A étant une classe ordinaire, la classe : template <class T> class B : public A définit une famille de classes de paramètre de type T. 3. classe générique dérivée d'une classe générique : on obtient là aussi des familles de classes. Par exemple, si A est une classe générique définie par : template <class T> A la classe : template <class T> class B : public A <T> engendre une famille de classes dérivées dont le nombre est identique au nombre de classes de base instanciables. Par contre, avec la définition, template <class T, class U> class B : public A <T> chaque classe de base instanciable peut engendrer une famille de classes dérivées de paramètre de type U. 7.4 DIFFERENCES ENTRE CLASSES ET FONCTIONS GENERIQUES L'identificateur d'une classe générique est toujours suivi de la liste des paramètres formels (définition de la classe) ou effectifs (déclaration d'un objet, instanciation), entourée des signes d'inégalité (< et >). Toute référence à une classe générique doit utiliser cette syntaxe complète. En C++ en effet, le compilateur impose une convention d'appel explicite stricte pour assurer la génération de classes appropriées. Exemple: template <class T, int range> class ex { ... } ex <double, 20> obj1; // valide ex <double> obj2; // erreur ex obj3; // erreur A l'inverse, l'instanciation d'une fonction générique ne diffère pas de l'appel d'une fonction ordinaire, le choix d'une implémentation particulière étant déterminé par le type des paramètres passés lors de l'appel de la fonction. 7.5 EXERCICES D'APPLICATION 1- Créer une fonction générique qui permet de calculer le carré d'une valeur de type quelconque (le résultat possèdera le même type). Ecrire un programme qui utilise cette fonction générique. 2- Soit cette définition de fonction générique : template <class T, class U> T fct (T a, U b, T c) { ... } avec les déclarations suivantes : – Page 75 – Cours de C++ int n, p, q; float x; char t[20]; char c; Quels sont les appels corrects et, dans ce cas, quels sont les prototypes des fonctions instanciées ? fct (n, p, q); // appel n°1 fct (n, x q); // appel n°2 fct (x, n, q); // appel n°3 fct (t, n, &c); // appel n°4 3- Créer une fonction générique qui permet de calculer la somme des éléments d'un tableau de type quelconque, le nombre d'éléments du tableau étant fourni en paramètre. Ecrire un petit programme d'utilisation. 4- Soit la définition suivante d'une classe générique : template <class T, int n> class essai { private: T tab [n]; public: essai (T); }; a) Donnez la définition du constructeur de la classe essai en supposant que le constructeur recopie la valeur reçue en paramètre dans chacun des éléments du tableau tab. b) Soient les déclarations : const int n = 3; int p = 5; Quelles sont les instructions correctes et les classes instanciées ? Pour chaque classe instanciée, on fournira une définition équivalente sous la forme d'une classe ordinaire. essai <int, 10> ei (3); // cas n°1 essai <float, n> ef (0.0); // cas n°2 essai <double, p> ed (2.5); // cas n°3 7.6 MOTS CLEFS Classe générique Fonction générique Généricité Générique Instanciation de classe Instanciation de fonction Paramètre de type Paramètre effectif Paramètre expression – Page 76 – Généricité Paramètre formel Template – Page 77 – 8. EXCEPTIONS Les exceptions sont des situations inhabituelles qui surviennent au moment de l'exécution d'un programme. Par exemple, une division par zéro, un dépassement arithmétique, un dépassement de tableau, un manque de mémoire, sont des exceptions. Pour gérer ces situations particulières, C++ fournit un gestionnaire d'exceptions. 8.1 GESTION DES EXCEPTIONS La gestion des exceptions permet de répondre aux anomalies d'un programme au moment de son exécution. Une syntaxe et un style standard sont proposés, qui peuvent bien évidemment être affinés et adaptés au type d'exception rencontrée. Mais le mécanisme de gestion des exceptions peut réduire de façon significative la taille et la complexité du code du programme en éliminant le besoin de tester les anomalies explicitement. C++ fournit trois mots réservés pour manipuler les exceptions : 1. throw : expression pour lancer une exception, c'est à dire suspendre l'exécution normale du programme à l'endroit où survient l'anomalie et passer la main au gestionnaire d'exceptions, 2. try : bloc qui regroupe une ou plusieurs instructions susceptibles de rencontrer des exceptions, 3. catch : bloc placé à la suite du bloc try ou d'un autre bloc catch pour exécuter les instructions particulières au traitement d'une exception. 8.1.1 Throw Lorsqu'une exception survient, l'expression throw est utilisée pour envoyer, ou "lancer", un objet au gestionnaire d'exception. Cet objet peut avoir été créé explicitement pour la gestion de l'exception, ou bien être directement l'objet cause de l'erreur. Syntaxe : throw objet; Remarques : N'importe quel objet (de type prédéfini ou non) peut être lancé à condition qu'il puisse être copié et détruit dans la fonction dans laquelle l'exception survient. Une instruction throw ressemble beaucoup à une instruction de retour de fonction, mais ce n'en est pas une. Une expression throw vide passe simplement l'exception au bloc try englobant suivant. Une expression throw vide ne peut apparaître qu'à l'intérieur d'un gestionnaire catch. Une fonction peut spécifier la série des exceptions qu'elle lèvera au moyen d'une liste throw, déclarée entre parenthèses à la suite de l'entête de la fonction. – Page 79 – Cours de C++ Exemple : class A {...} if (condition) throw A(); 8.1.2 Try Pour pouvoir être interceptées, les exceptions doivent se produire dans un bloc d'instructions appelé bloc d'essai. Ce bloc d'essai regroupe donc une ou plusieurs instructions susceptibles de rencontrer des exception. Il commence par le mot réservé try, suivi d'une séquence d'instructions comprises entre deux accolades. Syntaxe : try { ... } Remarques : Un bloc try regroupe une série d'instructions dans lesquelles des exceptions peuvent survenir. Exemple : try { func1 (); func2 (); } 8.1.3 Catch Une fois l'exception lancée, celle-ci doit être interceptée par un gestionnaire d'exception pour pouvoir être traitée de manière appropriée. Un tel gestionnaire est un bloc d'instructions appelé bloc catch. Ce bloc commence par le mot réservé catch, suivi par une déclaration (entre parenthèses) d'exceptions. Vient ensuite une séquence d'instructions entre accolades. La déclaration permet de spécifier les types des objets que le gestionnaire d'exceptions doit "attraper". Syntaxe : catch (objet identificateur) { ... } Remarques : Un bloc catch suit immédiatement un bloc try, ou un autre bloc catch. Plusieurs blocs catch peuvent se succéder les uns aux autres. La sélection du gestionnaire d'exception est alors réalisée en fonction du type de l'exception envoyée. Une adéquation entre l'exception lancée et celle attendue par un bloc catch est réalisée si : 1. les deux types sont exactement les mêmes, 2. le type du gestionnaire catch est une classe de base publique de l'objet envoyé, – Page 80 – Exceptions 3. le type de l'objet envoyé est un pointeur qui peut être converti de manière implicite en pointeur du type de celui du gestionnaire catch. Les gestionnaires catch sont évalués dans l'ordre de leur apparition à la suite du bloc try, mais une fois qu'une adéquation est réalisée, les gestionnaires catch suivant ne sont pas examinés. Si aucun bloc catch ne peut intercepter l'exception, la fonction prédéfinie terminate () est appelée. L'utilisation de trois points de suspension (...) dans la déclaration qui suit le mot réservé catch permet d'intercepter n'importe quelle exception. Exemple : catch (A) { cout << "Message d'erreur" << endl; exit (1); } 8.2 EXEMPLE 1 #include <iostream.h> #include <stdlib.h> class zero { private: ... public: zero (); ~zero (); }; void testzero (double d) { if (d==0.0) throw zero (); } void main () { double a; cout << "Entrez un nombre : " << endl ; cin >> a; try { testzero (a); cout << "L'inverse de " << a << " est : " << 1.0/a << endl; } – Page 81 – Cours de C++ catch (zero) { cout << "Il est impossible de déterminer l'inverse de zéro" << endl; exit (1); } exit (0); } 8.3 FONCTIONS SPECIALES Les exceptions lancées ne peuvent pas forcément toutes être interceptées correctement par des blocs catch. Il y a des situations où la meilleure solution pour gérer l'exception est de terminer le programme. En C++, deux fonctions spéciales permettent de traiter les exceptions non gérées par les gestionnaires : unexpected () et terminate (). 8.3.1 unexpected () Quand une fonction avec une liste d'exceptions envoie une exception qui n'est pas dans cette liste, la fonction prédéfinie unexpected () est appelée. Cette fonction appelle à son tour une fonction spécifiée à l'aide de la fonction set_unexpected (). Par défaut, unexpected () appelle la fonction terminate () qui, à son tour, appelle par défaut la fonction abort () (définie dans le fichier inclus stdlib.h), terminant ainsi le programme. 8.3.2 terminate () Dans certains cas, le mécanisme de gestion des exceptions échoue, et la fonction prédéfinie terminate () est appelée. Cet appel de terminate () se produit lorsque : 1. 2. 3. 4. la fonction terminate () est appelée explicitement, aucun gestionnaire catch ne peut être trouvé pour gérer une exception lancée, des problèmes de piles sont rencontrés lors de la gestion d'une exception, la fonction unexpected () est appelée. Par défaut, la fonction terminate () appelle la fonction abort () qui arrête l'exécution du programme. Mais cet appel à abort () peut être remplacé par un appel à une autre fonction au moyen de la fonction prédéfinie set_terminate (). 8.3.3 set_unexpected () et set_terminate () Il est possible d'utiliser les fonctions set_unexpected () et set_terminate () pour remplacer respectivement les appels aux fonctions terminate () et abort () des fonctions unexpected () et terminate (). Les déclarations de ces deux fonctions set_unexpected () et set_terminate () sont incluses dans les fichiers <unexpected.h> et <terminate.h>. Le type de retour et le paramètre de ces deux fonctions sont un pointeur sur une fonction sans paramètre et ne retournant pas de valeur. La fonction passée en paramètre remplace alors les fonctions appelées dans unexpected () et terminate (). Les deux valeurs retournées par set_unexpected () et set_terminate () sont des pointeurs sur les fonctions appelées auparavant par unexpected () et terminate (). Ainsi, en sauvant les valeurs retournées, il est possible de restaurer les fonctions d'origine. – Page 82 – Exceptions 8.4 EXEMPLE 2 L'exemple suivant présente une utilisation des fonctions spéciales dans le mécanisme de gestion des exceptions : #include <terminate.h> #include <unexpected.h> #include <iostream.h> class X {...}; class Y {...}; class Z {...}; void new_terminate () { cout << "Appel à new_terminate." << endl; } void new_unexpected () { cout << "Appel à new_unexpected." << endl; } void f () throw (X, Y) // f est autorisée à lancer des objets des classes X et Y { A obj; throw (obj); // erreur f () ne peut pas lancer d'objet de classe A } typedef void (*pfv) (); // pfv est un pointeur sur une fonction retournant void void main (void) { pfv old_terminate = set_terminate (new_terminate); pfv old_unexpected = set_unexpected (new_unexpected); try { f(); } catch (X) { ... } catch (Y) { ... } catch (...) { ... } set_unexpected (old_unexpected); – Page 83 – Cours de C++ try { f(); } catch (X) { ... } catch (Y) { ... } catch (...) { ... } } A l'exécution, le programme s'execute de la manière suivante : 1. L'appel à set_terminate () assigne à old_terminate l'adresse de la fonction passée comme paramètre lors du dernier appel de set_terminate (). 2. L'appel à set_unexpected () assigne à old_unexpected l'adresse de la fonction passée comme paramètre lors du dernier appel de set_unexpected (). 3. La fonction f () est appelée dans un bloc de test. Comme f () lance une exception incorrecte, un appel à unexpected () est réalisé. A son tour, unexpected () appelle new_unexpected (), et le message de new_unexpected () s'affiche. 4. Le second appel à set_unexpected () remplace la fonction new_unexpected () par l'adresse de la fonction d'origine (terminate (), appelée par défaut par unexpected ()). 5. Dans le second bloc de test, la fonction f () est appelée à nouveau. Comme f () lance toujours une exception incorrecte, un appel à unexpected () est réalisé. La fonction terminate () est alors appelée automatiquement, qui appelle à son tour new_terminate (). 6. Le message de new_terminate () s'affiche. 8.5 MOTS CLEFS Bloc d'essai Catch Exception Gestionnaire d'exceptions Interception Throw Try – Page 84 – TABLE DES MATIÈRES 1. ENCAPSULATION_____________________________________________________ 3 1.1 Introduction ____________________________________________________________ 3 1.2 Abstraction des données___________________________________________________ 3 1.3 Encapsulation ___________________________________________________________ 4 1.4 Classes _________________________________________________________________ 4 1.4.1 1.5 Contrôle d'accès_______________________________________________________________ 4 Données membres ________________________________________________________ 5 1.5.1 1.6 Membres statiques _____________________________________________________________ 5 Fonctions membres_______________________________________________________ 5 1.6.1 1.6.2 1.6.3 2. Fonctions « non inline »_________________________________________________________ 5 Fonctions inline _______________________________________________________________ 6 Fonctions amies (friend) ________________________________________________________ 6 1.7 Déclaration d'instances ___________________________________________________ 7 1.8 Mots clefs _______________________________________________________________ 7 ÉLÉMENTS DE SYNTAXE ______________________________________________ 9 2.1 Structure d'un programme c++ ____________________________________________ 9 2.2 Eléments de base_________________________________________________________ 9 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.2.10 2.2.11 Caractères ___________________________________________________________________ 9 Commentaires _______________________________________________________________ 10 Délimiteurs _________________________________________________________________ 10 Identificateurs _______________________________________________________________ 10 Mots réservés________________________________________________________________ 10 Types de base _______________________________________________________________ 11 Valeurs littérales _____________________________________________________________ 11 Déclaration des variables_______________________________________________________ 12 Attributs des variables _________________________________________________________ 12 Initialisation des variables____________________________________________________ 13 Constantes typées __________________________________________________________ 13 2.3 Instructions ____________________________________________________________ 13 2.4 Opérateurs_____________________________________________________________ 13 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.4.10 2.4.11 2.4.12 2.4.13 Opérateurs arithmétiques _______________________________________________________ Opérateurs de manipulation de bits _______________________________________________ Opérateurs d'affectation, d'incrémentation et de décrémentation_________________________ Opérateurs relationnels ________________________________________________________ Opérateurs logiques ___________________________________________________________ Opérateur conditionnel ________________________________________________________ Opérateur sizeof______________________________________________________________ Opérateur , __________________________________________________________________ Opérateur . __________________________________________________________________ Opérateur :: _______________________________________________________________ Opérateur ( ) : conversion de type______________________________________________ Autres opérateurs __________________________________________________________ Précédence des opérateurs ___________________________________________________ – Page 85 – 14 14 14 14 15 15 15 15 15 15 15 15 16 Cours de C++ 2.5 2.5.1 2.5.2 2.6 2.6.1 2.6.2 2.6.3 2.7 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.9 3. if ... else ...___________________________________________________________________ 16 switch ______________________________________________________________________ 16 Structures itératives _____________________________________________________ 17 Boucle while _________________________________________________________________ 17 Boucle do ... while ____________________________________________________________ 17 Boucle for ___________________________________________________________________ 18 Branchements __________________________________________________________ 18 break _______________________________________________________________________ 18 continue_____________________________________________________________________ 19 goto ________________________________________________________________________ 19 return_______________________________________________________________________ 19 Directives pour le préprocesseur ___________________________________________ 20 #define et #undef______________________________________________________________ 20 #include_____________________________________________________________________ 20 Compilation conditionnelle #if/#ifdef/#ifndef ... #else ... #endif _________________________ 20 Mots clefs ______________________________________________________________ 21 POINTEURS ET FONCTIONS___________________________________________23 3.1 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.2 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.3 3.3.1 3.3.2 3.3.3 Pointeurs et variables dynamiques _________________________________________ 23 Type pointeur ________________________________________________________________ 23 Pointeur sur void ______________________________________________________________ 23 Opérateur & _________________________________________________________________ 24 Opérateur * __________________________________________________________________ 24 Variables dynamiques, opérateurs new et delete______________________________________ 24 Pointeurs constants ____________________________________________________________ 25 Opérations sur les pointeurs _____________________________________________________ 25 Tableaux_______________________________________________________________ 25 Déclaration __________________________________________________________________ 26 Relation entre tableaux et pointeurs _______________________________________________ 26 Tableau de pointeurs___________________________________________________________ 27 Chaînes de caractères __________________________________________________________ 27 Initialisation des tableaux _______________________________________________________ 27 Structures, unions et énumérations_________________________________________ 28 Structures ___________________________________________________________________ 28 Unions______________________________________________________________________ 29 Énumérations ________________________________________________________________ 29 3.4 Définition de type : typedef _______________________________________________ 30 3.5 Fonctions ______________________________________________________________ 30 3.5.1 3.5.2 3.5.3 3.5.4 3.5.5 3.5.6 3.5.7 3.6 4. Structures conditionnelles ________________________________________________ 16 Déclaration et définition ________________________________________________________ 30 Paramètres d'entrée : transmission par valeur ________________________________________ 31 Paramètres de sortie : transmission par adresse ______________________________________ 31 Paramètres de sortie : transmission par référence _____________________________________ 32 Paramètres multivalués : transmission par référence constante___________________________ 33 Valeurs par défaut _____________________________________________________________ 33 Paramètres de la ligne de commande ______________________________________________ 33 Mots clefs ______________________________________________________________ 34 CONSTRUCTEURS ET DESTRUCTEURS, SURCHARGE ___________________35 4.1 4.1.1 4.1.2 4.1.3 Constructeurs et destructeurs _____________________________________________ 35 Constructeurs ________________________________________________________________ 35 Constructeurs copies ___________________________________________________________ 36 Destructeurs _________________________________________________________________ 36 – Page 86 – Table des Matières 4.2 Pointeur this ___________________________________________________________ 37 4.3 Surcharge______________________________________________________________ 37 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.8 4.3.9 4.3.10 4.4 5. 38 39 39 39 40 41 42 43 44 44 Mots clefs ______________________________________________________________ 44 HÉRITAGE __________________________________________________________ 45 5.1 Héritage _______________________________________________________________ 45 5.1.1 5.1.2 5.1.3 5.1.4 5.2 Syntaxe ____________________________________________________________________ Affectation __________________________________________________________________ Constructeurs et destructeurs ____________________________________________________ Accès aux membres hérités _____________________________________________________ 5.3 Héritage direct _______________________________________________________________ Classes de base identiques ______________________________________________________ Classes de base virtuelles_______________________________________________________ Types de hiérarchies __________________________________________________________ 49 50 51 52 Polymorphisme _________________________________________________________ 52 5.3.1 5.3.2 5.4 45 46 46 48 Héritage multiple _______________________________________________________ 49 5.2.1 5.2.2 5.2.3 5.2.4 6. Surcharge de fonctions_________________________________________________________ Surcharge de constructeurs _____________________________________________________ Surcharge d'opérateurs_________________________________________________________ Règles générales de surcharge des opérateurs _______________________________________ Surcharge d'opérateurs unaires __________________________________________________ Surcharge de l'opérateur d'affectation =____________________________________________ Surcharge d'opérateurs binaires __________________________________________________ Surcharge des opérateurs [ ] et ( ) ________________________________________________ Surcharge des opérateurs new et delete ____________________________________________ Correspondance des paramètres _______________________________________________ Fonctions virtuelles ___________________________________________________________ 52 Fonctions virtuelles pures ______________________________________________________ 53 Mots clefs ______________________________________________________________ 54 FLUX _______________________________________________________________ 55 6.1 Librairie i/o stream______________________________________________________ 55 6.2 Entrées-sorties standards_________________________________________________ 56 6.2.1 6.2.2 6.3 États de formats ______________________________________________________________ 56 États d'erreurs _______________________________________________________________ 58 Manipulation de flux ____________________________________________________ 58 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.4 Constructeur [i/o]fstream ( ) ____________________________________________________ Ouverture de flux open ( ) ______________________________________________________ Pointeur de tampon rdbuf ( ) ____________________________________________________ Fermeture du flux close ( )______________________________________________________ Flux en écriture ______________________________________________________________ Flux en lecture _______________________________________________________________ 58 58 58 58 58 58 Fichiers iostream.h et fstream.h ___________________________________________ 58 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.4.7 6.4.8 6.4.9 6.4.10 Déclaration de la classe ios (iostream.h) ___________________________________________ Déclaration de la classe istream (iostream.h)________________________________________ Déclaration de la classe istream_with_assign (iostream.h) _____________________________ Déclaration de la classe ostream (iostream.h) _______________________________________ Déclaration de la classe ostream_with_assign (iostream.h) _____________________________ Déclaration de la classe iostream (iostream.h)_______________________________________ Déclaration de la classe iostream_with_assign (iostream.h) ____________________________ Déclaration des variables globales du fichier iostream.h _______________________________ Déclaration de la classe fstreambase (fstream.h) _____________________________________ Déclaration de la classe ifstream (fstream.h) _____________________________________ – Page 87 – 58 58 58 58 58 58 58 58 58 58 Cours de C++ 6.4.11 6.4.12 6.5 7. Mots clefs ______________________________________________________________ 58 GÉNÉRICITÉ_________________________________________________________58 7.1 Introduction____________________________________________________________ 58 7.2 Fonctions génériques ____________________________________________________ 58 7.2.1 7.2.2 7.2.3 7.3 7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 8. Déclaration de la classe ofstream (fstream.h)______________________________________ 58 Déclaration de la classe fstream (fstream.h)_______________________________________ 58 Déclaration __________________________________________________________________ 58 Instanciation _________________________________________________________________ 58 Redéfinition de fonctions génériques ______________________________________________ 58 Classes génériques_______________________________________________________ 58 Déclaration __________________________________________________________________ 58 Instanciation _________________________________________________________________ 58 Spécialisation d'une classe générique ______________________________________________ 58 Identité de classes génériques ____________________________________________________ 58 Classes génériques et héritage____________________________________________________ 58 7.4 Différences entre classes et fonctions génériques______________________________ 58 7.5 Exercices d'application___________________________________________________ 58 7.6 Mots clefs ______________________________________________________________ 58 EXCEPTIONS ________________________________________________________58 8.1 8.1.1 8.1.2 8.1.3 Gestion des exceptions ___________________________________________________ 58 Throw ______________________________________________________________________ 58 Try ________________________________________________________________________ 58 Catch _______________________________________________________________________ 58 8.2 Exemple 1______________________________________________________________ 58 8.3 Fonctions spéciales ______________________________________________________ 58 8.3.1 8.3.2 8.3.3 unexpected ()_________________________________________________________________ 58 terminate () __________________________________________________________________ 58 set_unexpected () et set_terminate () ______________________________________________ 58 8.4 Exemple 2______________________________________________________________ 58 8.5 Mots clefs ______________________________________________________________ 58 – Page 88 – Christophe LÉGER Université d'Orléans Polytech’Orléans / LESI 12, rue de Blois BP 6744 45067 ORLÉANS cedex 2 Tél : 02 38 49 45 63 Fax : 02 38 41 72 45 E-mail : Christophe.Leger@univ-orleans.fr – Page 89 –