184
ECOLE NATIONALE DINGÉNIEURS DE SOUSSE Notes de cours Programmation Orientée Objet (C++) Par Karim Kalti Version 3.7

CoursPOOC++

Embed Size (px)

Citation preview

Page 1: CoursPOOC++

ECOLE NATIONALE D’INGÉNIEURS DE SOUSSE

Notes de cours

Programmation Orientée Objet

(C++)

Par Karim Kalti

Version 3.7

Page 2: CoursPOOC++

SOMMAIRE

Partie I :

- Les bases du langage (règles d'écriture, types, variables, opérateurs, structures de contrôle, …) - Les entrées /sorties en C++. - Les tableaux. - Les pointeurs et les références. - La gestion dynamique de la mémoire. - Les fonctions. - Les chaînes de caractères - Les structures et les énumérations.

Partie II :

- Introduction à la programmation orientée objet. - Les classes (attributs, méthodes, droits d'accès, instanciation,...) - Constructeurs et destructeur. - Espaces de noms. - Membres statiques et membres constants. - Fonctions amies. - Héritage et polymorphisme. - La surcharge des opérateurs. - Les modèles. - La gestion des exceptions.

Annexe : - Les fichiers.

Page 3: CoursPOOC++

Les règles d’écriture Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 1

Les règles d'écriture Premier programme

#include <iosrteam.h> void main() { cout<<"bonjour"; }

• Ce programme affiche le mot Bonjour à l'écran. Cet affichage est réalisé à travers l'opérateur d'extraction appliqué à l'objet cout. cout est un objet défini dans la bibliothèque iostream.h. Cette bibliothèque doit être incluse dans le programme.

• En général les déclarations des fonctions standards du langage C++ qui sont susceptibles d'apparaître dans le programme se trouvent dans des fichiers d'entête (header) ayant l'extension .h. Ces fichiers doivent être inclus au début de chaque programme avec la directive du préprocesseur include.

• La fonction main est la fonction principale du programme. C'est elle qui contient le corps du programme. • Les accolades jouent le rôle de "begin" et "end" du Pascal. Elles indiquent le début et la fin d'un bloc de code.

Remarque :

Ce petit programme donne une première idée sur la structure d'un programme C++. D'autres éléments du langage peuvent encore prendre place. Leurs définitions ainsi que leur emplacements seront décrits dans les parties suivantes du cours.

Structure d'un programme C++

D'une manière générale et d'un point de vue structurel, un programme C++ se décompose en deux grandes parties : • Les déclarations : qui comprennent :

o Les directives de compilation pour l'inclusion des fichiers d'entêtes. o Les déclarations des données ou des fonctions partagées avec d'autres fichiers. o Les déclarations des types propres au fichier. o Les déclarations des données globales du fichier.

• Les définitions : il s'agit des définitions des fonctions du fichier et des classes.

Les mots clés

Les mots clés sont réservés par le langage à un usage bien défini et ne peuvent se voir changer de signification, ni être utilisés comme identificateurs. Les mots clés du C++ sont :

Mots clés communs avec le C :

auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static

struct swich typedef union unisgned void volatile while

Mots clés spécifiques au C++ :

bool catch class delete friend inline new operator private protected public template this throw try virtual

Page 4: CoursPOOC++

Les règles d’écriture Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 2

De plus le pré-processeur utilise les mots clés suivants :

#define #elif #else #endif #error #if #ifdef #ifndef #include #inline #pragma #undef

Remarque :

Certains compilateurs peuvent ajouter d'autres mots clés qui lui sont propres.

Les identificateurs

Les identificateurs servent à désigner les différents "objets" manipulés par le programme (variables, fonctions, etc.). Ils se présentent sous la forme de chaînes de caractères alphanumériques (combinaison de caractères alphabétiques et de chiffres), de taille quelconque, dans les limites acceptées par le compilateur.

En C++, les identificateurs doivent respecter les règles suivantes : • Un identificateur doit toujours commencer par une lettre ou par le caractère underscore _. • Les caractères accentués (é, è, ê, à, â, ç,…) ne sont pas acceptés par le compilateur. • Un identificateur doit être différent des mots clés du langage. • Les majuscules et les minuscules sont considérées comme des lettres distinctes. • En générale la taille d'un identificateur ne doit pas dépasser les 32 caractères. Ce nombre varie suivant les

compilateurs. Par exemple Borland C++ Version 5.0 utilise 250 caractères comme caractères significatifs. Les caractères situés au delà du nombre significatif ne sont pas pris en considération.

• Les identificateurs servent à donner des noms à divers éléments du langage : les variables, les fonctions, les constantes, les énumérations, les structures…

Exemples d'identificateurs valides :

Nabs n1234_ abc table chaise

Exemple d'identificateurs non valides :

2abc n'est pas un identificateur valide.

Exemples d'identificateurs différents :

Somme ≠ somme ≠ sOmme

Les commentaires

Les commentaires sont fondamentaux pour la compréhension des programmes et donc pour leur maintenance et réutilisation. C'est pourquoi, il est très conseillé de les utiliser autant que c'est possible. Ils se présentent comme des portions de texte qui ne sont pas pris en compte lors de la compilation. Le C++ supporte deux types de commentaires : • Le commentaire multi-lignes : Il est introduit par /* et se termine par */ • Le commentaire ligne : le commentaire peut s'étendre sur une ligne seulement. Il est introduit par //. Sa fin est

indiquée par un retour à la ligne.

Exemple :

/* Ceci est un commentaire sur plusieurs lignes */

// Ceci est un commentaire sur une seule ligne.

Le format libre

Le C++ autorise une mise en page libre. Ainsi une instruction peut s'étendre sur un nombre quelconque de lignes pourvu qu'elle se termine par un point virgule. De même une ligne peut comprendre autant d'instructions que voulues.

Page 5: CoursPOOC++

Types de base et déclaration des variables Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 3

Types de base et déclaration des variables

Toute variable utilisée dans un programme C/C++ doit être déclarée. Une variable possède un nom

(identificateur) et un type. Elle contient en général une information bien précise. Lors de l'exécution du programme, une zone mémoire ayant la taille du type de la variable lui sera réservée. Cette zone contiendra cette information.

Les types de données en C++

Les types de base

Les types de base en C++ sont :

Type Description int Pour les entiers standards.

float Pour les nombres réels standards. double Pour les nombres réels en double précision. char Pour les caractères bool Pour les variables booléennes

Les types dérivés

A ces types de base s'ajoutent d'autres variantes obtenues en plaçant une spécification supplémentaire devant le type de base. Les mots clés permettant de construire les types dérivés sont :

Mot-clé Signification

long Pour définir des entiers ou des réels de grande taille. long s'applique aux types de base int et double. Lorsque ce mot est utilisé tout seul il désigne alors par défaut un entier long.

short Permet de manipuler les entiers de petite taille. Il s'utilise avec int ou tout seul (même signification).

unsigned Il se place devant les types entiers ou caractères qui doivent être considérés comme étant non signés. Il peut s'employer tout seul. Il désigne alors un entier non signé (unsigned int).

La liste complète des types en C++ est alors :

Type Taille Plage de valeurs

Caractères char 1 -128 …127

unsigned char 1 0 … 255

Entiers

short int 2 -32768 … 32768 unsigned short 2 0 … 65535

long int 4 -2 147 483 648 … 2 147 483 647 unsigned long int 4 0 … 4 294 967 295

int 2 ou 4 Comme short ou long unsigned int 2 ou 4 Comme unsigned short ou unsigned long

Réels

float 4 ±1.175 10-38 … ±3.402 10+38

double 8 ±2.225 10-308 … ±1.797 10+308

long double 10 ±3.4 10-4932 … ±1.1 10+4932

Booléen bool 1 true, false

Remarque 1 :

• Le type int possède toujours la taille d'un mot machine. Par conséquent l'espace qu'il occupe en mémoire dépend de la machine sur laquelle s'exécute le programme. Cette taille est par exemple de 2 octets (identique à celle du type short) pour les µp d'intel 8086 et 80286. Elle est de 4 octets (identique à celle du type long) pour les µp PENTIUM. Pour cela, et pour assurer la portabilité et le bon fonctionnement des programmes sur n'importe quelle machine, il est préférable d'utiliser pour les entiers les types short et long.

Page 6: CoursPOOC++

Types de base et déclaration des variables Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 4

• Tous les autres types correspondent à des tailles fixes et ne dépendent pas des machines.

Remarque 2 :

• La représentation des réels utilise le format suivant : Signe Mantisse 10Exposant.

Remarque 3 :

Le type char peut servir en C/C++ pour le stockage des entiers qui sont compris entre –128 et 127. Les caractères sont d'ailleurs représentés sous forme d'entiers. Ces entiers correspondent aux codes ASCII.

Les variables

Déclaration

TYPE NomVariable;

Où : • Type : désigne le type des données que peut stocker la variable. • NomVariable : désigne le nom de la variable. La nomination d'une variable doit respecter les régles qui

régissent les identificateurs en C++.

Exemple :

float tension; int resistance; float Moyenne;

• Plusieurs variables peuvent être déclarées simultanément si elles ont le même type. Elles sont alors séparées les unes des autres par des virgules.

Exemple :

int a,b,c; float note, moyenne;

• Une variable est caractérisée par son adresse et par la taille qu'elle occupe en mémoire. L'adresse est automatiquement attribuée par le système alors que la taille dépend du type de la variable.

Lieu de déclaration d'une variable

• Une variable doit être déclarée avant d'être utilisée. • En C, les variables qui sont utilisées à l'intérieur d'un bloc doivent être déclarées au début de ce dernier. Cette

restriction a été éliminée en C++. Il est ainsi possible de déclarer une variable n'importe où dans le bloc pourvu que cette déclaration soit faite avant la première utilisation.

Exemple :

// Code correct en C/C++

{ … … … double i,j, somme, moyenne; i=5.5; j=6.2; somme = i+j; moyenne = somme/2; … … … }

// Code correct en C++ // mais incorrect en C

{ … … … double i,j; i=5.5; j=6.2; double somme; somme = i+j; double moyenne; moyenne = somme/2; … … … }

Page 7: CoursPOOC++

Types de base et déclaration des variables Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 5

Initialisation des variables

• Une valeur initiale peut être affectée à une variable dès sa déclaration. int i=4; char C ='a'; bool b1 = true; bool b2 = false;

• La valeur d'initialisation d'une variable peut être le résultat d'un calcul portant sur des constantes. int i = 4+5; double j=2.5*3.2;

• Le contenu d'une variable non initialisée est indéterminé, sauf pour les variables globales et statiques qui sont automatiquement initialisées à 0.

Affectation d'un contenu à une variable

• L'opérateur qui permet de faire l'affectation d'un contenu à une variable est (=). Il est appelé opérateur d'affectation ou d'assignation. Dans une affectation (exemple : i=5), la constante ou le résultat d'une opération arithmétique ou logique se trouvant à droite de l'opérateur est copié dans la variable se trouvant à sa gauche.

• Il est possible de faire plusieurs affectations en même temps et ce en enchaînant plusieurs fois l'opérateur (=) de la manière suivante :

int i,j; i = j = 2;

Conversions lors d'assignation

Les assignations entre variables de types différents sont autorisées. Elles engendrent alors des conversions implicites (automatiques) des types de données. Ces conversions sont régies par les règles présentées dans le tableau suivant :

• Les conversions présentées dans le tableau ci-dessus peuvent engendrer des pertes de données. C'est pourquoi les assignations entre variables de types différents doivent être effectuées avec précaution.

La notion de bloc

Un bloc est une suite d'instructions délimitée par une accolade ouvrante et une accolade fermante.

Exemple :

{ instruction_1; instruction_2; … … … … instruction_n; }

Portée des variables :

• La portée d'une variable est définie par les zones du programme où la variable peut être utilisée ou en d'autres mots les zones où la variable est visible.

• Une variable n'est visible qu'à l'intérieur du bloc dans lequel elle est déclarée.

Conversion Commentaire

char → int

Aucune modification de valeur. - Pour le type char on a expansion du bit de signe. - Pour le type unsigned char il n'y a pas d'expansion.

int → char Perte des octets les plus significatifs de l'entier. short → int (4 octets) Pas de modification de valeur int (4 octets)→ short Résultat tronqué : perte des octets les plus significatifs.

unsigned → short Perte des octets les plus significatifs de l'entier. long → unsigned Les deux octets les plus significatifs du long prennent la valeur 0.

int → float Ajout d'une partie décimale nulle (exemple : 15 →15.0f) float → int Perte de la partie décimale (exemple :2.5f→2)

float → double Pas de problème double → float Perte de précision

Page 8: CoursPOOC++

Types de base et déclaration des variables Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 6

Exemple 1 :

#include <stdio.h> void main() { int i=5; { int i=1, j=3; printf("i du bloc :%d\n", i); printf("j du bloc :%d\n", j);

} printf("i du main : %d\n", i); }

Résultat :

i du bloc : 1 j du bloc : 3 i du main : 5

Exemple 2 :

#include <stdio.h> void main() { int i=5; { int i=1, j=3; printf("i du bloc :%d\n", i); } printf("j du bloc :%d\n", j); // ERROR car j n'est pas visible à ce niveau printf("i du main : %d\n", i); }

• Les variables i et j du bloc ne sont vues qu'à l'intérieur du bloc et perdent par conséquent leur signification à la sortie de ce dernier. Elles sont dites locales au bloc.

• La variable i du bloc cache à l'intérieur de ce dernier le i du main.

Variable globale

• Des variables peuvent être déclarées en dehors de tous les blocs et fonctions. Elles sont dites globales et peuvent être utilisés dans tout le programme.

Exemple :

#include <stdio.h> int i; void main() { … … … … … … … … printf("i vaut : %d\n", i); // i vaut 0 }

La variable i dans ce cas de figure est considérée comme une variable globale puisqu'elle ne fait partie d'aucun bloc. Elle est automatiquement initialisée à 0.

Les constantes

• Une constante est une donnée qui ne peut pas être modifiée. Cette donnée peut être un entier, un réel, un caractère ou une chaîne de caractères.

• C distingue les constantes entières, les constantes à virgules flottantes, les constantes de type caractère et les constantes de type chaîne. C++ introduit en plus les constantes booléennes.

Les constantes booléennes

Elles peuvent avoir deux valeurs true ou false.

Page 9: CoursPOOC++

Types de base et déclaration des variables Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 7

Les constantes entières

Elles peuvent être définies dans les différentes bases du codage. • Format décimal : (0,1,2, …, 9). • Format octal : (1,2,…7) : Ce format se distingue par l'adjonction du chiffre 0 devant la valeur ou par le

préfixe \. • Format hexadécimal : (0,1,2, …,9,A,BC,D,E,F) : ce format est caractérisé par l'ajout du préfixe 0x ou 0X ou

\x devant la valeur.

De plus quand il s'agit : • D'une constante représentant une valeur négative, il faut faire précéder la valeur de l'opérateur de signe -. • D'une valeur affectée à un entier long, il faut adjoindre à la fin de la valeur la lettre l ou L.

Par rapport au C, le C++ introduit les suffixes u et U pour spécifier qu'une constante est un entier non signé.

Exemples :

Constante Signification

12 Constante entière décimale 12L Constante entière de type long 12U Constante entière non signée

12LU Constante entière non signée de type long 014 Constante entière octale 0xC Constante entière hexadécimale (12 dans la base décimale)

0xCLU Constante entière hexadécimale (12) affectée à un entier long non signé

Les constantes flottantes

Deux notations sont possibles pour les constantes flottantes : • Notation littérale avec virgule flottante seule. Cette notation doit comporter obligatoirement un point (qui

correspond à la virgule). La partie entière ou la partie décimale peut être omise (seule une des deux mais pas les deux en même temps).

Exemples :

12.75, -0.58, -.58 , 4. , 0.27, 4.00

• La notation scientifique avec virgule flottante et exposant noté e ou E représentant l'exposant en base 10. (le point peut être absent dans cette notation).

Exemples :

5.69E4 5.69E+4 56.9e3 48e13 48.e13 48.E13 57.3e-5

Remarque :

Par défaut, toutes les constantes flottantes sont codées par le compilateur comme étant de type double. Il est toutefois possible de leur imposer d'être de type :• float : en les faisant suivre de la lettre f ou F. • long double : en les faisant suivre de la lettre l ou L.

Exemple :

12.34 Nombre flottant de double précision (double) 12.34f Nombre flottant de simple précision (float) 12.34L Nombre flottant de très grande précision (long double).

Page 10: CoursPOOC++

Types de base et déclaration des variables Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 8

Les constantes caractères

Les constantes de type caractères peuvent être représentées de deux manières : • Pour les caractères imprimables : par la valeur du caractère placée entre deux apostrophes [simples quottes]. • Pour les caractères imprimables ou non (d'une manière générale) : une constante caractère peut être définie

par son code ASCII octal ou hexadécimal précédé d'un antislash le tout placé entre deux quottes. Il est à noter que la représentation à l'aide du code hexadécimal doit être préfixé en plus d'un x.

Exemple :

Le caractère (a) peut être représenté de plusieurs manières : Base décimale : 'a' Base octale : '\101' Base hexadécimale : '\x41'

Les caractères spéciaux

Par ailleurs, certains caractères non imprimables possèdent une représentation spéciale utilisant l'antislash. Le tableau suivant présente ces caractères et leur signification.

Notation Code ASCII Signification

\n 0A Génère une nouvelle ligne (saut de ligne). \t 09 Tabulation horizontale \v 0B Tabulation verticale \b 08 Retour d'un caractère en arrière (backspace)\r 0D Retour chariot \f 0C Saut de page \a 07 Bip sonore \' 2C ' \" 22 " \? 3F ? \\ 34 \

Les constantes chaînes de caractères

Une chaîne est une séquence de plusieurs caractères. Une constante de ce type peut être définie en délimitant cette séquence par des guillemets.

Exemple :

"Ceci est une constante chaîne de caractères"

Les constantes symboliques

Il est possible d'associer à une valeur particulière un nom identificateur qui permet de faire référence à cette valeur sous forme symbolique en utilisant le mot-clé const. La déclaration d'une constante symbolique se fait comme suit : const Type NomConstante = Valeur;

Exemple :

const int moyenne = 10;

Remarque :

• En C++, une constante symbolique est évaluée au moment de la compilation. Ce n'est pas le cas pour les constantes symboliques en langage C.

• De ce fait, les constantes symboliques peuvent être utilisées en C++ dans la déclaration des tableaux.

Exemple :

Le code suivant est correct en C++ alors qu'il ne l'est pas en C : … … const int MAX = 1000; char tab[MAX]; / … …

En langage C il faut plutôt utiliser le code suivant : #define MAX 1000 … … char tab[MAX]

Page 11: CoursPOOC++

Les opérateurs Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 9

Les opérateurs Les opérateurs arithmétiques

Opérateur Signification

+ Addition - Soustraction * Multiplication / Division entière ou réelle.

% Reste de la division entière

Remarques :

• Les opérateurs "+, - , *, / " peuvent effectuer des opérations entre entiers ou entre réels. • L'opérateur "/" effectue en fonction du type des opérandes soit une division entière soit une division

réelle. • L'opérateur " % " ne s'applique pas aux réels (aussi bien float que double). Il n'est défini que pour des

opérandes de type entier.

Exemples :

7/3=2 7%3=1 7/-3= -2 7%-3=1 car (7=-3*-2+1)

7/-3= 2 -7%-3= -1 car (-7= -3*2-1)

Dépassement de capacité des entiers :

Soit l'instruction suivante : short n = 10000*100; La valeur placée dans n se situe en dehors de la capacité du type short. Le contenu de n sera alors erroné mais il n'y aura pas d'indication d'erreur ni au moment de la compilation ni au moment de l'exécution.

Dépassement de capacité des réels :

Pour les réels ou pour la division par zéro, le dépassement est immédiatement signalé par le message "floating

point error : overflow".

Combinaison d'opérandes de types différents

Une opération arithmétique peut faire intervenir des opérandes de types différents. Dans ce cas le compilateur effectue des conversions temporaires et automatiques de types. Ces conversions obéissent aux règles suivantes :

Règles

R0 char → int

Si un des deux types est alors l'autre est converti en R1 long double long double R2 double double R3 float float R4 unsigned long unsigned long R5 long long R6 unsigned int unsigned int

Ces règles possèdent une priorité descendante. (R0 est prioritaire par rapport à R1, R1 est prioritaire par rapport à R2 et ainsi de suite).

Forçage de type (casting)

Il est possible d'imposer d'une manière explicite une conversion de type (forçage ou casting) en préfixant l'élément à convertir du type de destination placé entre parenthèses. La syntaxe du forçage de type se présente comme suit : (TypeDestination) var

Page 12: CoursPOOC++

Les opérateurs Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 10

Exemple :

int i=5,j=2; double x; x=i/j; x=(double)i/j; // x= 2.5

Les opérateurs d'affectation

Les opérateurs d'incrémentation et de décrémentation

Les opérateurs logiques

Ce sont les opérateurs qui effectuent des comparaisons de valeurs ou des opérations logiques usuelles. Ils renvoient une valeur booléenne (true, false).

Opérateur Description

< Le test inférieur entre deux expressions arithmétiques (entières ou flottantes). Cet opérateur renvoie true si la valeur de l'opérande gauche est inférieur à celle de l'opérande de droite et false si non.

> Le test supérieur. Cet opérateur renvoie true si la valeur de l'opérande gauche est supérieur à celle de l'opérande de droite et false si non.

<= Le test inférieur ou égal. Cet opérateur renvoie true si la valeur de l'opérande gauche est inférieur ou égale à celle de l'opérande de droite et false si non.

>= Le test supérieur ou égal. Cet opérateur renvoie true si la valeur de l'opérande gauche est supérieur ou égale à celle de l'opérande de droite et false si non.

== Le test d'égalité. Renvoie true si l'opérande de gauche est égale à l'opérande de droite et false sinon. && ET logique : renvoie true si les deux opérandes sont évalués à true. || OU logique : renvoie true si au moins un des deux opérandes est évalué à true. ! NON logique : renvoie true si l'opérande est évalué à false et false dans le cas contraire.

Remarque :

• Les valeurs (entières, flottantes, caractères, …) non nulles sont évaluées à true. • Les valeurs (entières, flottantes, caractères, …) nulles sont évaluées à false.

Exemple :

int i=0, j=5; bool b1=i<j; // b1 vaut true bool b2= i==j && i<j // b2 vaut false

Opérateur Signification = i=5 ; permet d'affecter le contenu du membre de droite au membre de gauche+= i+=5 ⇔ i=i+5

-= i-=5 ⇔ i=i-5

*= i*=5 ⇔ i=i*5

/= i/=5 ⇔ i=i/5

Opérateur Signification ++ i++ ⇔ i=i+1 opérateur d'incrémentation-- i-- ⇔ i=i-1 opérateur de décrémentation

Page 13: CoursPOOC++

Les opérateurs Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 11

Priorité des opérateurs

La priorité des opérateurs permet de définir l'ordre dans lequel sont évalués les opérateurs dans une expression lorsque cette dernière en fait intervenir plusieurs. Le tableau suivant donne la priorité des opérateurs les plus utilisés :

Opérateur :: opérateur de résolution de portée () [] -> (casting) sizeof & * ! ++ -- new delete * / % + - < <= > >= == != && || ? : = += -= *= /= %=

Remarque :

• Les opérateurs présentés dans le tableau ci-dessus possèdent une priorité descendante : les opérateurs de la première ligne sont prioritaires par rapport à ceux de la deuxième ligne et ainsi de suite.

• Les opérateurs d'une même ligne possèdent la même priorité. Si une expression fait intervenir en même temps plusieurs opérateurs qui ont la même priorité alors l'opérateur situé le plus à gauche dans l'expression sera le premier évalué.

Exemple :

Expression Opérations résultat 8/4*6

8*6/4

28/(3*4)

3/4*6

3*6/4

(float)2/4

(float)(2/4)

-3+4%5/2

Opérateur conditionnel

Cet opérateur permet de tester une expression et de retourner une valeur suivant le résultat du test. Sa syntaxe est donnée comme suit :

Expression ? Valeur renvoyée si Expression vaut vrai : Valeur renvoyée sinon

Remarque :

Les valeurs renvoyées doivent être du même type.

Exemple 1 :

int i=5,j=6,k=18,m; m=i<j ? k- 2*i : i+j;

Exemple 2 :

Cet exemple montre l'utilisation de l'opérateur conditionnel dans le calcul du maximum de deux entiers : int FMAX(int a, int b) { return (a>b ? a : b); }

Page 14: CoursPOOC++

Les opérateurs Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 12

Opérateur sizeof( )

L'opérateur sizeof renvoie la taille en octets d'un type ou d'une variable. Le type ou la variable sont passés en argument.

Exemple :

unsigned i; float j; i = sizeof(short); // i vaut 2 i = sizeof(j); // i vaut 4 i = sizeof(long[12]); // i vaut 48

Page 15: CoursPOOC++

Les entrées/sorties en C++ Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 13

Les entrées / sorties en C++

• Les entrées/sorties désignent les opérations de lecture et d'écriture de données. • Les opérations de lecture se font à partir du fichier standard d'entrée (stdin en C). Par défaut ce fichier est

associé au clavier mais il peut être redirigé vers d'autres périphériques ou d'autres fichiers sur le disque. • Les opérations d'écriture se font dans le fichier standard de sortie (stdout en C). Par défaut ce fichier est

associé à l'écran mais il peut être redirigé vers d'autres périphériques tels que l'imprimante par exemple.

• Le C++ offre deux objets appelés flux (streams) pour la gestion des opérations d'E/S : o L'objet cout de type ostream associé à la sortie standard (écran). o L'objet cin de type istream associé à l'entrée standard (clavier).

• Ces deux objets sont définis dans la bibliothèque iostream.h.

Les opérations de sortie

• Les opérations de sortie des données sont effectuées à l'aide de l'objet cout auquel est associé un opérateur de redirection noté (<<) qui indique le sens de transfert des données.

• La syntaxe de l'utilisation de l'objet cout avec l'opérateur (<<) est la suivante : cout<<Donnée;

• Le paramètre Donnée désigne la donnée à afficher. Il peut être une variable, une constante ou une expression. • Le paramètre Donnée peut avoir comme type un des types prédéfinis du C++ (bool, char, int, short, long,

float, double, char*, …). • L'opérateur (<<) indique que le sens de transfert des informations se fait de Donnée vers le flux de sortie cout.

Il est également appelé opérateur d'insertion. • L'opérateur (<<) peut être surchargé afin de permettre l'affichage de données ayant des types personnalisés

(classes, structures, …). • Contrairement aux fonctions de sortie du C, l'objet cout n'utilise aucun caractère de formatage. La

reconnaissance du type des informations à afficher est automatique. • Il est possible d'enchaîner plusieurs opérateurs de redirection de la manière suivante :

cout<<Donnée1<<Donnée2<<Donnée3;

Exemple 1 : Affichage d'un texte

#include <iostream.h> … … … cout<<"Ceci est un message";

Exemple 2 : Affichage d'un nombre

int i=12; float j = 2.5f; cout<<"i="<<i<<'\n'; cout<<"j="<<j<<'\n';

Exemple 3 : Affichage d'un caractère

char c='a'; cout<<"le caractère c contient : "<<c<<'\n';

Exemple 4 : Affichage d'une chaîne de caractères

char T[10]="Bonjour"; cout<<T;

Exemple 5 : Affichage du résultat d'une expression

int i =5; int j=7; cout<<"La somme de i et de j est : "<<i+j;

Page 16: CoursPOOC++

Les entrées/sorties en C++ Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 14

Manipulateurs d'affichage en C++

Il existe un ensemble de manipulateurs en C++ qui offrent différentes possibilités concernant le formatage de l'affichage autres que celles proposées par défaut.

Manipulateur de saut de ligne :

Le manipulateur endl (end line) permet d'insérer un saut de ligne dans le flux de texte envoyé à l'écran.

… … … … cout<<"Bonjour"<<endl<<"Au revoir"<<endl; … … … …

// Résultat de l'exécution Bonjour Au revoir

Manipulateurs d'affichage des entiers :

Il est possible de modifier la base dans laquelle est affiché un entier à l'aide des manipulateurs suivants :

Manipulateur Signification dec Affichage dans la base décimale pour les entiers (affichage par défaut). hex Affichage dans la base hexadécimale pour les entiers. oct Affichage dans la base octale pour les entiers.

int i=75; cout<<"Affichage par défaut : "<<i<<endl; cout<<"Affichage hexa : "<<hex<<i<<endl; cout<<"Sans manipulateur : "<<i<<endl; cout<<"Affichage décimal :"<<dec<<i<<endl; cout<<"Sans manipulateur : "<<i<<endl;

// Résultat de l'exécution Affichage par défaut : 75 Affichage hexa : 4b Sans manipulateur : 4b Affichage décimal : 75 Sans manipulateur : 75

L'exemple précédent montre qu'un manipulateur de conversion de la base d'affichage d'un entier reste actif depuis l'endroit de son application et jusqu'à l'application d'un autre manipulateur.

Manipulateur du gabarit d'affichage

Manipulateur Signification

setw(nombre)

Définit le gabarit de la variable à afficher avec une justification à droite par défaut. Si la valeur à afficher est plus importante que le gabarit, alors ce dernier ne sera pas respecté et la variable sera affichée de façon conventionnelle. Le manipulateur setw doit être utilisé pour chacune des informations à afficher.

setfill(caractère) Définit le caractère de remplissage lorsqu'on utilise un affichage avec la gestion de gabarit. Par défaut, le caractère de remplissage est l'espace.

Les manipulateurs setw et setfill sont définis dans la bibliothèque iomanip.h.

#include <iostream.h> #include <iomanip.h> … … … … … int i=55; cout<<setw(4)<<i<<endl; cout<<setfill('0')<<setw(4)<<i<<endl; … … … … …

// Résultat de l'exécution 55 0055

Page 17: CoursPOOC++

Les entrées/sorties en C++ Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 15

Les opérations d'entrée

• Les opérations d'entrée sont effectuées à l'aide de l'objet cin auquel est associé l'opérateur de redirection (>>). • La syntaxe de l'utilisation de l'objet cin avec l'opérateur (>>) est la suivante :

cin>>var;

• Le paramètre var désigne la variable qui va stocker l'information saisie. • La variable var peut avoir comme type un des types prédéfinis du C++ (bool, char, int, short, long, float,

double, char*, …). • L'opérateur (>>) indique que le sens de transfert des informations se fait du flux d'entrée cin vers la variable

var. Cet opérateur est également appelé opérateur d'extraction. • Tout comme cout, cin n'utilise aucun caractère de formatage. La reconnaissance des types des données à saisir

est automatique.

Exemple :

#inlcude <iostream.h> void main( ) { int i; float f; char c; cout<<"donnez un entier"<<endl; cin>>i; cout<<" donnez un réel"<<endl; cin>>f; cout<<" donnez un caractère"<<endl; cin>>c; }

• Il est possible d'enchaîner plusieurs opérateurs de redirection avec cin afin de faire la saisie de plusieurs variables en même temps. Dans ce cas les valeurs à faire entrer doivent être séparées au moment de la saisie par un blanc (tabulation, espace ou retour chariot).

Exemple :

int v1,v2; cout<<"Veuillez saisir deux entiers"; cin>>v1>>v2;

Page 18: CoursPOOC++

Les structures de contrôle Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 16

Les structures de contrôle Introduction

Il existe trois catégories de structures de contrôle : • Les instructions de branchement conditionnel (alternative). • Les instructions de branchement inconditionnel. • Les instructions répétitives.

Les instructions de branchement conditionnel

Trois types de branchement conditionnel peuvent être distingués : le branchement conditionnel simple, le branchement conditionnel imbriqué et le branchement conditionnel multiple.

Le branchement conditionnel simple

Ce branchement est réalisé par le mot réservé if de la manière suivante :

• Si la condition testée est vraie alors le programme exécute le bloc d'instructions. Si cette condition est fausse alors le programme saute ce bloc et continue son exécution normalement.

• Il est possible de spécifier un autre bloc d'instructions à exécuter dans le cas où la condition est fausse par l'adjonction de l'instruction else au branchement if. Cette structure de contrôle devient alors :

Remarque :

Si le bloc (bloc 1 ou bloc 2) se réduit à une seule instruction alors les accolades deviennent facultatives.

Application 1 :

Écrire un programme qui permet à partir de la saisie d'un nombre d'afficher un message pour indiquer la possibilité ou non de l'utiliser comme diviseur.

#include<iostream.h> void main() { int i; cout<<"Donner un entier : "; cin>>i; if(i==0) cout<<"Impossible d'utiliser cet entier comme diviseur"<<endl; else cout<<"Il est possible d'utiliser cet entier comme diviseur"<<endl; }

if(Condition est vraie) { Bloc 1 d'instructions à exécuter } else { Bloc 2 d'instructions à exécuter }

if(Condition est vraie) { Bloc d'instructions à exécuter }

Page 19: CoursPOOC++

Les structures de contrôle Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 17

Application 2 :

Écrire un programme qui permet de déterminer si un nombre entier saisi au clavier est pair ou impair.

#include<iostream.h> void main() { int i; cout<<"Donner un entier : "; cin>>i; if(i%2) cout<<"L'entier saisi est impair"<<endl; else cout<<"L'entier saisi est pair"<<endl; }

Les branchements conditionnels imbriqués

En pratique, il est souvent utile de pouvoir enchaîner un ensemble de tests pour examiner plusieurs valeurs possibles. Ceci peut être réalisé en imbriquant les if et les else de la manière suivante :

Remarque :

Lors de l'imbrication de plusieurs instructions de branchement conditionnel simple, un else se rapporte toujours au dernier if rencontré auquel aucun else n'a été encore attribué.

Exercice

Écrire un programme qui demande à l'utilisateur son age et lui indique le niveau du cours qu'il doit suivre en se basant sur les critères suivants : • Si l'age est entre 7 et 15 il lui affiche "vous avez besoin du cours du premier niveau". • Si l'age est entre 16 et 20 il lui affiche "vous avez besoin du cours du deuxième niveau". • Si l'age est entre 20 et 25 il lui affiche "vous avez besoin du cours du troisième niveau". • Si l'âge est inférieur à 7 il lui affiche " vous êtes encore jeune". • Si l'âge est supérieur à 25 il lui affiche " vous êtes trop âgé".

if(test_1 est vrai) { Bloc_1 } else { if(test_2 est vrai) { Bloc_2 } else { if(test_3 est vrai) { Bloc_3 } else …… …… else{ if(test_n est vrai) { Bloc_n } else { Bloc_n+1 } } …… …… } }

if(test_1 est vrai) { Bloc_1 } else if(test_2 est vrai) { Bloc_2 } else if(test_3 est vrai) { Bloc_3 } …… …… else if(test_n est vrai) { Bloc_n } else { Bloc_n+1 }

Ce code est équivalent à celui-ci:

Page 20: CoursPOOC++

Les structures de contrôle Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 18

Branchement conditionnel multiple

Les branchements imbriqués utilisant le if et le else donnent à un programme un aspect peu lisible, en plus des risques d'erreurs qu'ils peuvent engendrer surtout lors du placement des accolades. C'est pourquoi, lorsqu'il s'agit de traiter plusieurs alternatives on leur préfère la structure switch définie de la manière suivante :

• La structure de contrôle switch compare généralement la valeur d'une variable de type entier ou assimilé (char, int, unsigned, long, …) à un ensemble de constantes. Lorsque cette valeur correspond à l'une des constantes alors le bloc d'instructions associé à cette dernière est exécuté.

• Le bloc défini par le mot-clé default est un bloc facultatif (non obligatoire) qui désigne un ensemble d'instructions qui seront exécutés par défaut.

• Le mot-clé break permet une sortie immédiate de la structure swtich et évite alors au programme de tester toutes les autres alternatives après avoir exécuté un bloc i donné. Cette instruction n'est pas obligatoire.

Exercice:

En utilisant la structure de contrôle switch, donner un programme qui demande à l'utilisateur de saisir un nombre. Si ce nombre est égale à 0, il lui affiche "vous avez saisi une valeur nulle". S'il est égale à un, il lui affiche "vous avez saisi un". Si ce nombre est différent de 1 et de 0 il lui affiche un message d'erreur "valeur

incorrecte".

#include <stdio.h> void main() { int i; printf("Donner une valeur entière 0 ou 1:"); scanf("%d",&i); switch( i ) { case 0: printf("\n vous avez saisi une valeur nulle \n"); break; case 1: printf("\n vous avez saisi un "); break; default: printf("\n valeur incorrecte "); } }

switch(variable ou expression) { case constante_1: Bloc 1 d'instructions break; case constante_2: Bloc 2 d'instructions break; …… …… case constante_n: Bloc n d'instructions break; default: bloc d'instructions par défaut }

Page 21: CoursPOOC++

Les structures de contrôle Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 19

Résultat de l'exécution :

• Si le nombre saisi est 6: le message de sortie sera → "valeur incorrecte". • Si le nombre saisi est 1: le message de sortie sera → "vous avez saisi un".

• Si on élimine le break du case : 1. o Si le nombre saisi est 6: le message de sortie sera → "valeur incorrecte". o Si le nombre saisi est 1: le message de sortie sera → "vous avez saisi un " suivi du message "valeur

incorrecte ". • Si le nombre saisi est 0 le message de sortie sera "vous avez saisi une valeur nulle".

• Si on élimine le break du case 0 et on préserve celui de case 1: o Si le nombre saisi est 6, le message de sortie sera → "valeur incorrecte". o Si le nombre saisi est 1 le message de sortie sera → "vous avez saisi un ". o Si le nombre saisi est 0 le message de sortie sera → "vous avez saisi une valeur nulle", "vous avez saisi

un".

• Si on élimine le break du case 0 et celui de case 1: o Si le nombre saisi est 6, le message de sortie sera → " valeur incorrecte ". o Si le nombre saisi est 1 le message de sortie sera → " vous avez saisi un ". o Si le nombre saisi est 0 le message de sortie sera → "vous avez saisi une valeur nulle", "vous avez saisi

un", "valeur incorrecte".

o Si on supprime les instructions break, le programme traitera tous les cas qui suivent la première correspondance entre la valeur et une des constantes de test. Pour l'exemple précèdent, si on supprime les break et on saisit 0, le programme affichera les messages suivants : → "vous avez saisi une valeur nulle", "vous avez saisi un", "valeur incorrecte".

Remarque :

L'instruction switch possède l'inconvénient de limiter les comparaisons à des valeurs constantes et ne peut pas traiter des intervalles.

Les instructions répétitives

Le langage C++ dispose de trois structures pour le contrôle répétitive: while, do … while et for. Théoriquement, ces structures sont interchangeables, c.-à-d. il serait possible de programmer toutes sortes de boucles conditionnelles en n'utilisant qu'une seule des trois structures.

La boucle while

La syntaxe de cette boucle est :

La boucle while exécute un bloc d'instructions tant que le test qui lui est associé est vrai. Si ce bloc se réduit à une seule instruction alors les accolades deviennent non obligatoires.

Exercice :

Écrire un programme qui affiche tous les multiples d'un entier de référence qui sont inférieurs à une valeur maximale. L'entier de référence et la valeur maximale seront donnés par l'utilisateur.

while(Test est vrai) { Bloc d'instructions à exécuter }

Page 22: CoursPOOC++

Les structures de contrôle Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 20

#include <stdio.h> void main( ) { unsigned long ValeurMax,NbRef; printf("Donnez l'entier de référence : "); scanf("%lu", &NbRef); printf("Donnez la valeur maximale : "); scanf("%lu", &ValeurMax); while(ValeurMax >= NbRef) { if(ValeurMax % NbRef == 0) printf("%d\t", ValeurMax); ValeurMax--; } printf("C'est fini\n"); }

La boucle do… while

Cette structure est similaire à la boucle while mais avec une petite différence qui réside dans le fait qu'elle assure l'exécution au moins une fois des instructions du bloc.

En pratique, l'utilisation de la structure do - while n'est pas aussi fréquente que while; mais dans certains cas, elle fournit une solution plus élégante. Une application typique de do - while est la saisie contrôlée de données.

Exemple 1 :

Donner un programme qui permet de contrôler la saisie d'un entier compris entre 1 et 10. … … … int N; do { printf("Introduisez un nombre entre 1 et 10 :"); scanf("%d", &N); } while (N<1 || N>10); … … …

Exemple 2 :

On veut écrire un programme qui demande à l'utilisateur s'il veut continuer ou arrêter une tâche donnée. • Si l'utilisateur tape le caractère o alors le programme quitte et termine la tâche. • S'il tape le caractère n alors le programme continue l'exécution de la tâche. • S'il tape tout autre caractère le programme repose la même question à l'utilisateur.

#include <stdio.h> void main( ) { char c; do { printf("voulez vous terminer et quitter ?"); scanf("%c",&c); fflush(stdin); }while (c!='o' && c!='n'); if(c=='o') printf("la tâche se termine tout de suite\n"); else printf("OK on continue\n"); }

do { Bloc d'instructions à exécuter }while(Test est vrai);

Page 23: CoursPOOC++

Les structures de contrôle Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 21

La boucle for

La structure de la boucle for se présente comme suit :

• <expr1> est évaluée une seule fois et ce avant la première itération de la boucle. Elle est utilisée pour initialiser les données de la boucle.

• <expr2> est évaluée avant chaque itération de la boucle. Elle représente généralement une condition qui est utilisée pour décider si la boucle fera une itération supplémentaire ou non.

• <expr3> est évaluée à la fin de chaque itération de la boucle. Elle est utilisée pour réinitialiser les données de la boucle.

Remarque :

• La boucle for constitue une alternative syntaxique à la boucle while dans laquelle tous les éléments relatifs au contrôle de la boucle sont regroupés ensemble d'une manière lisible dans l'entête. En effet, les trois expressions de contrôle présentées ci-dessus peuvent être placées dans la boucle while de la manière suivante :

• Le plus souvent, la boucle for est utilisée comme boucle de comptage :

for(initialisation ; condition de continuité ; compteur) { <bloc d'instructions> }

Exercice :

Écrire un programme qui affiche les nombres entiers de 0 jusqu'à 100.

#include <stdio.h> void main( ) { int i; for(i=0;i<101;i++) printf("%d\t",i); }

Remarques :

• Chacune des trois instructions de la boucle for est facultative. Ainsi la boucle de l'exercice précèdent peut être écrite de la manière suivante:

• Lorsque <expr2> est absente, alors la condition de continuation sera considérée comme étant toujours vraie et la boucle for sera une boucle infinie.

for (<expr1>;<expr2>;<expr3>) { <bloc d'instructions> }

i=0; for( ;i<101;i++) printf("%d\t",i);

i=0; for( ;i<101; ) { printf("%d\t",i); i++; }

ou également:

<expr1>; while ( <expr2> ) { <bloc d'instructions> <expr3>; }

Page 24: CoursPOOC++

Les structures de contrôle Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 22

Le branchement inconditionnel

Le langage C++ dispose de trois instructions pour le branchement inconditionnel : break, continue et goto.

L'instruction break

• En plus de son utilisation en association avec switch, l'instruction break peut être utilisée dans n'importe quelle boucle. Cette instruction provoque alors une interruption et une sortie immédiate de la boucle.

• L'exécution du programme continue alors au niveau de l'instruction située tout juste après la boucle.

Exemple:

void main() { int i; for(i=1;i<=10;i++) { printf("Début de l'itération %d\n",i); printf("Bonjour\n"); if(i==3) break; printf("Fin de l'itération %d\n",i); } printf("Après la boucle"); }

Remarque :

En cas de boucles imbriquées, l'instruction break fait sortir seulement de la boucle la plus interne.

L'instruction continue

L'instruction continue intervient également pour interrompre l'exécution des boucles. Mais contrairement à break, elle ne provoque pas la sortie complète de la boucle mais plutôt l'interruption de l'itération courante et le passage à l'itération suivante de cette boucle.

Exemple 1:

#include <stdio.h> void main( ) { int i; for(i=1;i<=4;i++) { printf("Début itération %d\n",i); if(i < 3) continue; printf("bonjour\n"); } }

Exemple 2 :

Le programme ci-dessous demande à l'utilisateur de saisir un entier positif et lui affiche son carré. Si l'entier est négatif alors le programme redemande à l'utilisateur de saisir un autre entier. L'exécution s'arrête lorsque'un entier nul est saisi. #include <stdio.h> void main( ) {int n; do { printf("\n donnez un entier n > 0: "); scanf("%d",&n); if(n<0) { printf("\n donnez un n positif\n"); continue ; } printf(" le carré de n est : %d\n",n*n); }while(n) ; }

Remarque :

En cas de boucles imbriquées l'instruction continue ne concerne que la boucle la plus interne.

Exécution:

Début de l'itération 1 Bonjour Fin de l'itération 1 Début de l'itération 2 Bonjour Fin de l'itération 2 Début de l'itération 3 Bonjour Après la boucle

Exécution:

Début itération 1 Début itération 2 Début itération 3 bonjour Début itération 4 bonjour

Page 25: CoursPOOC++

Les structures de contrôle Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 23

L'instruction goto

L'instruction goto provoque un branchement immédiat du programme à un endroit prédéfini. Les boucles, les tests sont interrompues. L'endroit où reprend le programme est défini par une étiquette suivie du symbole : La syntaxe globale de cette instruction est : goto NomEtiquette; ……… NomEtiquette:

Exemple:

#include <stdio.h> void main( ) { int i; for(i=1;i<=10;i++) { printf("Début de l'itération %d\n",i); printf("Bonjour\n"); if(i==3) goto Sortie; printf("Fin de l'itération %d\n",i); } Sortie: printf("Après la boucle"); }

Exécution:

Début de l'itération 1 Bonjour Fin de l'itération 1 Début de l'itération 2 Bonjour Fin de l'itération 2 Début de l'itération 3 Bonjour Après la boucle

Page 26: CoursPOOC++

Les tableaux Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 24

Les tableaux

Introduction

• Un tableau est un type particulier de variables qui permet d'associer sous le même nom, un ensemble fini de valeurs de même nature (type).

• Tous les types de base et tous les types personalisés construits en C/C++ peuvent servir à définir des tableaux.

Déclaration d'un tableau

La syntaxe permettant la déclaration d'un tableau est la suivante :

Type NomTableau[Taille];

Où : • Type désigne le type des éléments du tableau, • NomTableau désigne son nom, • Taille désigne le nombre de ses éléments.

Exemples:

Déclaration:

int TableauEntiers[15]; double TableauFlottants[40];

• La taille maximale d'un tableau dépend de la configuration de l'environnement de travail (compilateur). • La taille d'un tableau doit être une constante ou une expression constante, cependant elle ne peut pas être une

variable.

#define N 10 int M =10; int Tab1[10]; // Déclaration correcte int Tab2[N]; // Déclaration correcte int Tab3[M]; // Déclaration incorrecte float Tab4[2*N+1]; // Déclaration correcte

• La dimension peut également être une constante symbolique (Ceci est possible en C++ mais pas en C). const int N=10; int Tab[N]; // Déclaration correcte en C++ mais incorrecte en C

Accès aux éléments d'un tableau

• L'accès à la valeur d'un élément du tableau se fait à travers son numéro d'ordre ou indice de la manière suivante:

NomTableau[Indice]

• Un tableau est toujours numéroté à partie de 0. Ainsi dans un tableau de N éléments, le premier a pour indice 0 et le dernier a pour indice N-1.

• La référence d'un élément du tableau qui n'existe pas (par exemple T[N+2] pour un tableau de taille N) n'est signalée comme erreur ni à la compilation ni à l'exécution. En effet, le programme fait comme si cet élément existait. La seule manifestation de cette erreur de référence réside dans l'obtention de résultats imprévisibles.

Page 27: CoursPOOC++

Les tableaux Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 25

Tableaux à plusieurs dimensions

En plus des tableaux à une dimension (vecteur), le C++ autorise la déclaration des tableaux à plusieurs dimensions. Cette déclaration se fait pour un tableau de N dimensions de la manière suivante :

Type NomTableau[TailleD_1][TailleD_2]…[TailleD_N];

Où TailleD_i désigne la taille de la ième dimension.

A ce titre, une matrice est déclarée d'une manière générale comme suit :

Type NomTableau[NombreLigne][NombreColonne];

Exemples: int T[2][3];

L'arrangement en mémoire des cases est comme suit :

float TabF[3][6]; char TabCh[4][5][6];

Initialisation des tableaux

Tableaux à une dimension Type NomTableau[N] = {Valeur1, Valeur2,…, Valeur_N-1};

Exemple 1:

int Tab[3] = {1,7,4};

• Le nombre des valeurs d'initialisation ne peut pas dépasser la taille du tableau mais il peut être inférieur. Dans ce cas les éléments non explicitement initialisés seront automatiquement initialisés à 0.

Exemple 2:

int Tableau[5] = { , ,5, ,3};

• Lors de l'initialisation d'un tableau, la spécification de sa taille est facultative. • Dans le cas où elle est absente, la taille est automatiquement déterminée d'après le nombre des valeurs

d'initialisation.

Exemple 3 :

int Tableau[ ] = {1,7,4} // La taille est du tableau est égale à 3

Tableaux à deux dimensions

Exemples :

int Tab[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; int Tab[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12}};

Il est également possible d'omettre quelques valeurs qui seront automatiquement initialisées à 0. int Tab[3][4] = { {1,2, ,4}, , {9, ,11,12}};

T[0][0] T[0][1] T[0][2] T[1][0] T[1][1] T[1][2]

Page 28: CoursPOOC++

Les tableaux Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 26

Affectation des valeurs aux éléments d'un tableau

Exemple :

int Tab[10]; Tab[0]=500; // affecte au premier élément 500 Tab[5]=100; // affecte au sixième élément 100 Tab[9]=50; // affecte au dernier élément 50

char T[5][3]; T[0][2] = 'a';

Quelques règles d'écriture

• Les éléments d'un tableau peuvent être incrémentés ou décrémentés: Tab[4] = 5; Tab[4]++ // tab[4] vaut 6.

• Les indices peuvent être des expressions arithmétiques: T[2*i-1] ou Tab[i-3][j+k]

• Il n'est pas possible d'affecter d'une manière globale un tableau à un autre : int T1[5]; int T2[5]; T1 = T2; // instruction incorrecte.

Exercice :

Donner un programme qui fait la copie des éléments d'un tableau d'entiers initialisé, dans un deuxième tableau de même taille.

#include<stdio.h> void main() { int i; int T1[3] = {5,6,23}; int T2[3]; for(i=0;i<3;i++) T2[i]=T1[i]; }

Saisie et affichage des éléments d'un tableau

La saisie et l'affichage d'un élément d'un tableau se fait de la même manière que pour n'importe quelle variable possédant le même type que celui du tableau.

Exercice :

Donnez un programme qui fait la saisie des éléments d'un tableau de caractères de taille 5 et qui les affiche ensuite.

#include<stdio.h> void main() { int i; char T[5];

/* Saisie */ for(i=0;i<5;i++) { printf("\n Donner le caractère numéro %d: ", i+1); scanf("%c", &T[i]); }

/* Affichage */ for(i=0;i<5;i++) printf("%c \t", T[i]); }

Page 29: CoursPOOC++

Les tableaux Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 27

Exercice :

Écrire un programme qui fait la saisie des éléments d'un tableau d'entiers à deux dimensions 3x4 et qui les affiches sur 3 lignes et 4 colonnes.

#include<stdio.h> void main() { int i,j; int T1[3][4], T2[3][4]; for(i=0;i<3;i++) for(j=0;j<4;j++) scanf("%d",&tab[i][j]); /* Affichage */ for(i=0;i<3;i++) { for(j=0;j<4;j++) printf("%d\t", tab[i][j]); printf("\n"); } }

Page 30: CoursPOOC++

Les pointeurs et les références Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 28

Les pointeurs et les références

1 - Introduction

• Un pointeur est une donnée (constante ou variable) qui représente l'adresse d'une variable. • Pour que l'adresse stockée dans un pointeur soit exploitable, il faut connaître le type de l'information qui se

situe au niveau de cette adresse afin de pouvoir l'interpréter convenablement. C'est pour cette raison qu'un pointeur doit être toujours associé à un type donné.

• Les pointeurs, tout comme les tableaux, les références et les structures sont considérés comme des outils de construction de types étendus (types construits sur la base d'autres types).

• Un pointeur est dit qu'il pointe (ou renvoie) vers la variable dont il stocke l'adresse.

2 - Déclaration

Formellement la syntaxe de déclaration d'un pointeur est la suivante :

Type * NomPointeur ;

NomPointeur désigne le nom d'une variable de type pointeur.

Exemples :

char *pc; // définit un pointeur vers une donnée de type caractère. int * pi; // définit un pointeur vers une donnée de type int double * pdr; // définit un pointeur vers une donnée de type réel double unsigned long * pli; // définit un pointeur vers une donnée de type unsigned long

Remarque :

Les pointeurs occupent tous la même taille en mémoire indépendamment de la taille du type de l'objet pointé. Cela signifie par exemple qu'une variable de type pointeur sur un long double possède la même taille en mémoire qu'une variable de type pointeur sur un caractère.

Déclaration multiple int *p1, *p2; // p1 et p2 sont deux pointeurs sur des entiers. int *p1, p2; // p1 est un pointeur sur un entier alors que p2 est une variable entière. int p1, *p2 // p1 est une variable entière alors que p2 est un pointeur sur un entier.

3 - Initialisation des pointeurs

• Comme pour les autres variables, il est possible d'initialiser les variables de type pointeur. La valeur initiale est dans ce cas, l'adresse d'une donnée possédant comme type, le type vers lequel pointe le pointeur en question.

• L'initialisation d'un pointeur ne peut s'effectuer qu'en lui affectant comme valeur l'adresse d'une variable déjà existante.

Exemple :

short s; short *ps1 = &s;

short *ps2 = ps1; // ⇔ short *ps2=&s;

Page 31: CoursPOOC++

Les pointeurs et les références Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 29

Remarque :

Si l'on indique comme valeur d'initialisation pour un pointeur l'adresse d'une variable ayant un autre type de données que celui vers lequel le pointeur peut renvoyer, le compilateur affiche alors une erreur lors de la compilation.

Exemple :

long l, *pl=&l; unsigned long *pul=&l; // erreur: pul n'est un pointeur vers un long

4 - Affectation des pointeurs et conversion

Une variable de type pointeur peut obtenir sa valeur non seulement par une initialisation, mais également par une opération d'affectation.

Exemples :

float f1; float *pf1,*pf2; pf1 =&f1; // pf1 contient l'adresse de f1. pf2=pf1; // pf2 contient l'adresse de f1.

Conversion

Il n'existe aucune conversion implicite d'un type pointeur vers un autre type pointeur. Le seul moyen pour faire des conversions entre types pointeurs est le casting.

int i,*pi=&i; unsigned int *pui; pui = pi; // erreur pui=&i; // erreur pui=(unsigned int *)pi; // OK

5 - Accès indirect aux variables

• Il est possible d'accéder à une variable à travers un pointeur qui pointe vers cette variable. Il suffit d'utiliser pour cela l'opérateur * de la manière suivante :

Type Variable, *NomPointeur=&Variable;

Variable ⇔⇔⇔⇔ * NomPointeur

• L'opérateur * est appelé dans ce cas opérateur d'indirection car il permet d'accéder d'une manière indirecte au contenu de la variable (à travers le pointeur).

Exemple 1 :

int i; int *pi; i =1234; pi= &i // pi contient l'adresse de i // *pi désigne d'une manière indirecte le contenu de i cout<<i; cout<<*pi;

Exemple 2 :

Cet exemple montre la saisie et l'affichage de la valeur d'une variable à travers un pointeur : int i,*pi=&i; scanf("%d",pi); printf("%d",*pi);

Ou également en utilisant les fonctions d'E/S du C++ int i,*pi=&i; cin>>*pi; cout<<*pi;

Page 32: CoursPOOC++

Les pointeurs et les références Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 30

Exemple 3 :

#include<stdio.h> void main() { int i=125; int *pi=&i; printf("la valeur de i est: %d",i); //125 printf("la valeur du *pi est %d",*pi); //125 i++; printf("la valeur de i incrémentée est: %d",i); //126 printf("la valeur du *pi est %d",*pi); //126 (*pi)++; printf("la valeur de i est: %d",i); //127 printf("la valeur du *pi est %d",*pi); //127 *pi=2*(*pi); printf("la valeur de i est: %d",i); //254 printf("la valeur du *pi est %d",*pi); //254 }

6 - Incrémentation de pointeurs et addition

• L'incrémentation d'un pointeur donne l'adresse située à sizeof(TYPE) octets à partir de la valeur courante du pointeur. TYPE étant le type de la variable pointée par le pointeur.

• L'addition entre un pointeur et un entier N donne l'adresse située à N*sizeof(TYPE) à partir de la valeur courante du pointeur.

Exemple 1:

int i; int *p=&i; p++; // incrémente p de 4 octets en décimal.

Exemple 2:

int * i,* j; int k; i=&k; j=i+10; j++;

/* Si i contient par exemple 1600 alors j vaut 1600+10*sizeof(int)=1640. j++ donne 1644 */

Remarque :

Pour les pointeurs, l'addition n'est définie qu'entre un pointeur et un entier. Elle n'est pas définie entre deux pointeurs ni entre un pointeur et un nombre en virgule flottante.

7 - Décrémentation de pointeurs et soustraction

• Contrairement à l'addition, on peut soustraire d'un pointeur non seulement un nombre entier mais aussi un autre pointeur de même type.

• La soustraction d'un nombre entier à un pointeur fonctionne d'une manière analogue à l'addition. De même la décrémentation fonctionne d'une manière analogue à l'incrémentation.

• La soustraction entre deux pointeurs (de même type) fournit le nombre d'éléments, du type en question, situés entre les deux adresses correspondantes (Ce n'est pas le nombre d'octets).

Exemple:

int *a, *b; int tab[6]; a= &tab[5]; b=&tab[1]; cout<<a-b; //donne 4 cout<<b-a; //donne -4

Page 33: CoursPOOC++

Les pointeurs et les références Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 31

8 - Comparaison entre pointeurs

La comparaison de pointeurs est possible mais seulement entre pointeurs de même type.

Exemple 1 :

int *pa, *pb; … … … if(pa = = pb) cout<<"les deux pointeurs pointent vers la même donnée"; else cout<<"les deux pointeurs pointent vers des données différentes"; … … …

Exemple 2 :

if(pa < pb) cout<<"le pointeur pb contient une adresse plus grande que le pointeur pa";

9 - Pointeur nul

• Il existe en C/C++ un pointeur particulier qui pointe vers l'adresse 0, appelé le pointeur nul. En C/C++ cette adresse ne contient aucune donnée, par conséquent le pointeur nul ne pourra en aucun cas pointer vers une donnée.

• Chaque pointeur vers n'importe quel type peut être comparé au pointeur nul.

Exemple :

int *pi; if(pi==0) cout<<"error";

• La constante symbolique NULL peut être utilisée à la place de la constante numérique 0. Elle est prédéfinie dans le fichier stdio.h.

• NULL peut être également définie par la directive define comme suit : #define NULL 0 ou également #define NULL 0L selon que les adresses sur la machine sont représentées par des int ou des long.

10 - Pointeurs génériques

• Il existe en C/C++ des pointeurs particuliers appelés pointeurs génériques qui peuvent pointer vers des données de n’importe quel type.

• Les pointeurs génériques s'avèrent utiles si par exemple l'adresse d'une zone mémoire doit être enregistrée, mais qu'il n'est pas encore établi quel type de données cette zone doit accueillir ou si le programmeur veut se réserver la possibilité d'enregistrer (successivement) des types de données différents ou bien encore s'il souhaite pour d'autres raisons ne pas se fixer dans l'immédiat sur un quelconque type de données.

Déclaration

Les pointeurs génériques sont déclarés à l'aide du type : void*La déclaration se fait comme suit : void* PointeurGenerique;

Exemple:

int a; double d; void * vp; // pointeur générique vp=&a; // vp stocke l'adresse d'un int vp=&d; // vp stocke l'adresse d'un double

Page 34: CoursPOOC++

Les pointeurs et les références Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 32

Restrictions dans l'utilisation des pointeurs génériques

• Un pointeur générique ne peut pas servir pour faire des accès indirects aux contenus des variables.

Exemple :

double d; void* pg=&d;

Ainsi *vp=1.234 génère une erreur. En effet, même si vp stocke l'adresse d'un double, le type void* ne donne aucune infromation sur la taille mémoire qu'occupe la variable pointée. Pour résoudre ce problème, il faut explicitement convertir vp comme suit : *(double * )vp=1.234;

• Par ailleurs, vp++ ou toute autre opération arithmétique sur les pointeurs génériques génère une erreur car le compilateur ne peut pas savoir de combien d'octets se déplacer.

Affectation d'un pointeur générique à un pointeur d'un autre type (type* ←←←← void*)

• En C++, l'affectation du contenu d'un pointeur générique (void*) à un pointeur (type*) doit obligatoirement passer par le casting.

• En C, le casting n'est pas obligatoire pour faire ce genre d'opérations même s'il reste conseillé.

Exemple :

int i=5; int *pi1=&i, *pi2; void* vp; vp=pi1; // Ok C et C++ pi2=vp; // Ok en C erreur en C++ pi2=(int*)vp // Ok C et C++ cout<<*pi2; // opération possible car le type de pi2 est connu int* pi2++; // opération possible car le type de pi2 est connu int*

Remarque

La conversion implicite entre pointeur générique et un pointeur type se fait se fait donc dans les cas suivants : T* vers void* // légale en C et C++ void* vers T* // légale en C seulement en C++ le casting est obligatoire

11 - Pointeurs et tableaux

• En C/C++, le nom d'un tableau est considéré comme une constante d'adresse définissant l'emplacement de début à partir duquel vont se succéder les éléments du tableau.

• Ainsi, lorsqu'il est employé seul, l'identificateur d'un tableau, est considéré comme un pointeur constant.

Notation d'accès aux éléments d'un tableau

Si on considère la déclaration suivante : int T[10];

Alors on a les équivalences de notations suivantes : T ⇔ &T[0], T+1 ⇔ &T[1], T+i ⇔ &T[i]

*T ⇔ T[0], *(T+1) ⇔ T[1], *(T+i) ⇔ T[i]

Exemple 1 :

Cet exemple montre un programme de remplissage des éléments d'un tableau d'entiers avec des 1.

int T[10],i; for(i=0;i<10;i++) T[i]=1;

int T[10],i; for(i=0;i<10;i++) *(T+i) = 1;

int T[10],i, *p; for(p=T,i=0; i<10 ; i++,p++) *p=1;

Page 35: CoursPOOC++

Les pointeurs et les références Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 33

Le nom du tableau ne peut pas être utilisé pour faire le parcours des éléments (l'incrémentation T++ est incorrecte) car T est dans ce cas considéré comme un pointeur constant. Ceci explique l'utilisation d'une variable de type pointeur dans l'exemple 3 pour faire ce parcours.

Exemple 2 :

L'accès aux éléments peut se faire à l'aide d'indices négatifs si ces éléments sont situés à une adresse précédant l'adresse contenue dans le pointeur permettant de faire l'accès. int T[5]; int *p=&T[4];

P[-1] ⇔ T[3], P[-2] ⇔ T[2], …, P[-4] ⇔ T[0].

Cas d'un tableau à deux dimensions:

Toute déclaration d'un tableau à N dimensions est considérée comme une déclaration d'un pointeur constant. Cependant, la référence des éléments à l'aide des pointeurs diffère de celle des tableaux à une dimension. En effet si on considère le cas particulier où N=2, la déclaration int T[3][4] est interprétée comme étant un tableau de 3 éléments, chaque élément de ce tableau étant lui-même un tableau de 4 entiers.

Exemple :

typedef int QuatreEntiers[4]; QuatreEntier Tab[3];

Le tableau tab est équivalent au tableau T déclaré d'une manière classique comme suit : int T[3][4]Les éléments de T étant des int[4]. De ce qui précède T+1 correspond à l'adresse de T+4*sizeof(int) octets (car l'élément du tableau correspond à 4 entiers et non pas à un seul). De même les écritures suivantes sont équivalentes :T ⇔ &T[0][0] ⇔ T[0]

T+1 ⇔ &T[1][0] ⇔ T[1]

Pointeurs et constantes

Pointeur en lecture seule

Un pointeur en lecture seule peut pointer sur n'importe quelle variable. Il ne permet toutefois que l'accès en lecture à la variable pointée. Toute tentative de modification (accès en écriture) est signalée comme erreur.

Déclaration

const Type* NomPointeur;

Exemple :

int i=5,j=10; const int* p = &i; // pointeur sur un entier cout<<"Valeur pointée :"<<*p<<endl; p = &j; // ok cout<<"Valeur pointée :"<<*p<<endl; *p = 8; // erreur (tentative de modification) cout<<"Valeur pointée :"<<*p<<endl;

Remarque :

On ne peut pas affecter l'adresse d'un objet constant à un pointeur sur un type non constant, car une telle affectation pourrait permettre de modifier cet objet par l'intermédiaire du pointeur.

Exemple :

const char espace=' ' ; const char *p = &espace ; // ok ; char *q = & espace ;; //erreur

Page 36: CoursPOOC++

Les pointeurs et les références Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 34

Pointeur constant

Un pointeur constant est un pointeur qui ne peut pointer que sur une seule variable. Le contenu de la variable pointée n'est pas nécessairement constant et peut par conséquent être modifié.

Déclaration

Type* const NomPointeur;

Exemple :

int i=5,j=10; int* const p = &i; cout<<"Valeur pointée :"<<*p<<endl; i=8; cout<<"Valeur pointée :"<<*p<<endl; p = &j; // erreur cout<<"Valeur pointée :"<<*p<<endl;

Les références

• Le C++ introduit un nouvel outil de construction de types dérivés appelé référence. Une valeur de type référence est une adresse unique (qui ne change pas) et qui désigne une variable bien déterminée.

• Une référence doit obligatoirement être initialisée par une variable. Elle joue dès lors le rôle d'alias de cette variable (cette variable peut être manipulée à travers sa référence).

• Les références sont définies selon la syntaxe suivante :

Type &NomReference = VariableInitialisation;

• Les références sont à la fois similaires aux pointeurs du fait qu'ils stockent des adresses en mémoire et différents de ces derniers du fait que le contenu d'un pointeur peut être variable alors que celui d'une référence est constant.

• Les références peuvent être considérées comme des "pointeurs constants" qui sont manipulés, d'un point de vue syntaxique comme des variables ordinaires.

Exemple :

int i; int &r1 = i; // r1 est une référence de i. Elle le restera pour tout le programme. r1 peut // désormais être utilisée en tant qu'autre nom de i. int &r2; // Erreur, une référence doit être initialisée

Déclaration multiple de références int a=1; int &r1=a, &r2=a; // r1 et r2 référencent a.

Remarques :

• Il n'est pas possible de créer une référence générique. Ainsi void& n'est pas valide. • Il n'est pas possible de créer des pointeurs vers des références, ni des références de références. int a=1; int &r =a; int & *ptr; // Erreur int &&rr; // Erreur

• Il n'est pas possible de déclarer des tableaux de références. • Il est possible de déclarer des références de pointeurs. int i,*p=&i; int* &r=p; // r est une référence d'un pointeur sur entier i=5; cout<<*r; // Affiche le contenu de i à savoir 5

Les références de constantes

En plus des références de variables, il est également possible de créer des références de constantes en ajoutant const à la déclaration.

Page 37: CoursPOOC++

Les pointeurs et les références Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 35

Exemple :

const int c=2; const int &rc=c; //rc est une référence de c. Elle peut la remplacer

Opérations sur les références

Une fois qu'une référence a été déclarée et initialisée, toutes les opérations effectuées par la suite sur cette référence se rapporteront exclusivement à "l'objet" référencé et non à la référence elle-même. (En effet, le contenu d'une référence ne peut pas être modifié après son initialisation vu qu'il est constant).

Exemples :

int x; int *p; int &r=x; r=1; // x reçoit la valeur 1 et r reste inchangé et contient toujours l'adresse de x. r++; // incrémente x de 1.

L'exemple ci-dessus montre que les références sont bien manipulées comme des variables sans utilisation de l'opérateur d'indirection).

Dans les expressions d'adressage x peut également être remplacée par r, ainsi :

p=&r; // affecte l'adresse de x à p. *p=r; // copie le contenu de x dans l'emplacement pointé par p; cout<<&x<<'\t'<<p<<'\t'<<&r; //Affiche 3 fois la même valeur qui est l'adresse de x

Initialisation des références

• Une référence non constante ne peut être initialisée qu'avec une lvalue de même type.

unsigned char uc; double d1,d2; int &ri =1024; // error : not lvalue char &rc =uc // error inexact types double &dr=d1+d2; // error not an lvalue

• Une référence sur un objet constant peut être initialisée aussi bien avec une rvalue qu'avec une lvalue.

const unsigned char uc; const double d1; const double d2; const int &ri=1024; // OK : 1024 c'est l'adresse const unsigned char &rc = uc; // OK const double &rd = d1+d2; // OK

Page 38: CoursPOOC++

Les chaînes de caractères Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti36

Les chaînes de caractères

• Une chaîne de caractères est un type particulier de données qui se présente comme une série de caractères alphanumériques.

• En C/C++, les chaînes de caractères sont considérées comme des cas particuliers des tableaux de caractères et sont d’ailleurs déclarées de la même manière que ces derniers.

• Toutefois, les chaînes se distinguent par rapport aux simples tableaux de caractères par le fait qu'elles se terminent toujours par un caractère spécial codé sur un octet ayant la valeur ‘\0’ appelé le zéro de fin de chaînes. Ce dernier ne doit pas être confondu avec le caractère 0 dont le code ASCII est 48.

Remarque : La bibliothèque standard (STL) fournie avec le langage C++ dispose d'une classe string qui offre tous les outils nécessaires pour la manipulation des chaînes en termes de copie, de concaténation, etc.

Déclaration d'une chaîne

• La déclaration d'une chaîne de caractères est faite comme suit : char NomChaine[Taille];

• La taille d’un tableau de caractères doit être suffisante pour contenir le zéro de fin de chaîne. Ainsi une chaîne déclarée avec une taille de 10 ne peut contenir au plus que 9 caractères utiles. [Le compilateur ne procède à aucune vérification].

char message[10] ; // chaîne de 9 caractères

Initialisation d'une chaîne

• Une chaîne de caractères peut être initialisée comme suit : char ch1={'A','B','C','\0'} ; ou également char ch2[]= "abc";

• L'affectation globale d'une constante chaîne de caractère à une variable de type chaîne n'est permise que lors d'une initialisation. Ainsi l'instruction suivante provoque une erreur.

char ch3[5]; ch3="abc"; /* donne une erreur */

• La spécification de la taille d'une chaîne lors de sa déclaration n'est pas nécessaire si cette déclaration est suivie d'une initialisation.

char msg []= "Bonjour" ; char phrase[]= "ceci est une phrase" ;

• La taille de msg n’est pas fixée mais dépend de la taille de la chaîne d’initialisation. Dans le cas présent, ce tableau occupe 8 octets, le premier contenant ‘B’ et le dernier est le zéro de fin de chaîne.

Initialisation d’un tableau de caractères à deux dimensions.

char buf[3][10]={" Ali ", " Mohamed ","Salah"};

La première dimension : 3, est facultative. Chaque ligne occupe 10 caractères.

Page 39: CoursPOOC++

Les chaînes de caractères Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti37

Saisie et affichage d'une chaîne de caractères

La saisie

La saisie d'une chaîne peut se faire à l’aide de la fonction scanf en utilisant le caractère de formatage %s ou également à l’aide de la fonction gets dont la syntaxe est la suivante :

char* gets(char *NomChaîne); (stdio.h)

• La fonction gets retourne la chaîne saisie en cas de succès et NULL en cas d'échec. • La fonction gets arrête la saisie des caractères lorsque l'utilisateur tape le caractère \n indiquant un retour à la

ligne. Elle remplace alors ce caractère par \0.

L'affichage

L'affichage d'une chaîne peut se faire à l’aide de la fonction printf en utilisant le caractère de formatage %s ou également à l’aide de la fonction puts dont la syntaxe est la suivante :

int puts(const char* NomChaîne); (stdio.h)

Cette fonction retourne une valeur non négative en cas de succès. En cas d'échec elle retourne EOF. Elle remplace le caractère \0 de la chaîne par \n.

Exemple 1

# include<stdio.h> void main( ) {

char ligne[81] ; printf(" donnez une chaîne ") ; scanf(" %s ", ligne) ; printf(" la ligne saisie est : %s\n ",ligne) ; }

Exemple 2

#include <stdio.h> void main( ) { char tab[]= "Bonjour"; printf("Affichage du contenu d’un tableau de caractères : \n " ) ; printf(" %s ",tab) ; }

Exemple 3

#include <stdio.h> void main( ) { char tab[]= "Bonjour” ; /* puts réalise un retour automatique à la ligne */ puts(" Affichage du contenu d’un tableau de caractères : " ) ; puts(tab) ; }

Affectation entre chaînes de caractères

L’affectation directe entre chaînes n'est pas permise en C/C++ car il n'est pas possible de faire une affectation globale entre tableaux. Pour cela, il est nécessaire de passer par la fonction strcpy.

Prototype : char *strcpy( char *Destination, const char *Source );

Bibliothèque: (string.h) Valeur de retour: Destination. Action: Elle copie la chaîne Source dans la chaîne Destination. Aucune valeur de retour n'a été prévue pour indiquer une erreur.

Page 40: CoursPOOC++

Les chaînes de caractères Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti38

Exemple :

# include <string.h> # include <stdio.h> void main( ) { char string1[10], string2[10]=”abcdef”; strcpy(string1,string2); printf(" %s\n”,string1); }

Longueur d'une chaîne

La fonction strlen permet de déterminer le nombre de caractères présents dans une chaîne sans tenir compte du zéro de fin de chaîne.

Prototype: size_t strlen( const char *string ); Bibliothèque: string.h Valeur de retour: nombre de caractères

Exemple :

#include<string.h> #include<stdio.h> void main( ) { int nb; char ch[20]; strcpy(ch,”Bonjour”); nb=strlen(ch); printf(" le nombre de caractères est %d ",nb) ; }

Comparaison de deux chaînes de caractères

Ordre lexicographique des chaînes

On appelle ordre lexicographique, l'ordre dans lequel les mots sont rangés dans un dictionnaire. Il est défini mathématiquement de la façon suivante : Soit A et B deux mots que l'on peut considérer comme des valeurs de variables de type chaîne.

Exemples :

"ARBRE" < "ARBRES" "ARBRE" < "ARBRE " "ARBRE" > "ARBORESENCE" "ARBRE" < "ARBUSTE"

Remarque :

En C/C++, les opérateurs = =, < ,> ,<= et >= ne permettent pas de comparer le contenu de deux chaînes. Il faut dans ce cas passer par la fonction strcmp qui effectue une comparaison sur la base de l'ordre lexicographique susmentionné. Prototype : int strcmp( const char *Chaîne1, const char * Chaîne2 );

A � B si et seulement si

Long(A) � Long(B) et quel que soit i � Long(A), A[ i ] = B[ i ] .

Ou s'il existe: i, 1� i � min{ Long(A), Long(B) }, tel que :

A[ i ] < B[ i ] et que quel que soit j, 1� j � i A[ j ] = B[ j ]

Page 41: CoursPOOC++

Les chaînes de caractères Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti39

Bibliothèque : string.h Valeur renvoyée : une valeur entière qui est : • < 0 si Chaîne1 est inférieure à Chaîne2 • = = 0 si Chaîne1 es égale à Chaîne2 • 0 si Chaîne1 est supérieur à Chaîne2.

Exemple 1 :

# include <string.h> # include <stdio.h>

void main( ) { int resultat; char ch1[]=”ARBUSTE ”; char ch2[]=”ARBRE ”; char ch3[]=”ARBORESENCE ”; resultat = strcmp(ch1,ch2); if(resultat>0) printf("ch1 est supérieure à ch2\n ") ; else printf("ch1 est inférieure à ch2\n ") ;

resultat = strcmp (ch2,ch3) ; if( resultat > 0 ) printf("ch2 est supérieure à ch3\n ") ; else printf("ch2 est inférieure à ch3\n ") ; }

Exemple 2 :

# include <string.h> # include <stdio.h>

void main( ) { char ch1[10], ch2[10]; int resultat; strcpy(ch1,"Bonjour"); printf("contenu de ch : %s\n",ch1); printf(" deuxième chaîne ? ") ; scanf(" %s ",ch2) ; resultat = strcmp(ch1,ch2); if(resultat ==0) printf("les deux chaînes sont identiques ") ; else printf("les deux chaînes sont différentes ") ; }

Concaténation de deux chaînes

Concaténer deux chaînes consiste à les unir pour obtenir une seule en copiant la deuxième à la fin de la première. La concaténation en C/c++ est réalisée avec la fonction strcat dont le prototype se présente comme suit :

Prototype: char* strcat(char *Chaîne1, const char * Chaîne2); Bibliothèque: string.h Valeur de retour: La chaîne chaîne1 qui contient le résultat de la concaténation de chaîne1 et chaîne2.

Exemple :

# include <string.h> # include <stdio.h> void main( ) { char Destination[30]; char Ch1[10]= "Turbo " , Ch2[2]= " " , Ch3[5]= "C "; strcpy(destination,Ch1) ; strcat(destination, Ch2); strcat(destination, Ch3); puts(destination); }

Page 42: CoursPOOC++

Les chaînes de caractères Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti40

Recherche d’un caractère dans une chaîne

La fonction strchr cherche la première occurrence d'un caractère dans une chaîne. Prototype: char*strchr(const char *Str, char car ); Bibliothèque : string.h Valeur de retour : un pointeur sur la première occurrence du caractère car dans la chaîne Str et NULL si le caractère considéré ne figure pas dans la chaîne.

Exemple :

#include<string.h> #include<stdio.h> void main() { char String[15], *ptr, c=’r’; strcpy(String,”Voici une chaine”); ptr=strchr(String,c) ; if(ptr) printf(“Le caractère %c est dans la position: %d\n”,c,ptr-String+1) ; else printf(« caractère non détecté.\n » ) ; }

Page 43: CoursPOOC++

La gestion dynamique de la mémoire Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti41

La gestion dynamique de la mémoire Il arrive fréquemment en programmation que les besoins effectifs en mémoire ne soient pas connus au moment de l'écriture du programme. Par exemple, le nombre d'éléments à utiliser dans un tableau peut être inconnu au moment de la compilation et sera spécifié au moment de l'exécution par l'utilisateur. Une fixation arbitraire de ces besoins en mémoire peut s'avérer parfois insuffisante et mener vers le blocage du système. Elle peut s'avérer également excessive et conduire alors vers un gaspillage de la mémoire. Pour remédier à ce problème, il est souvent fait usage des outils de la gestion dynamique de la mémoire.

Outils de gestion dynamique de la mémoire issus du C

Le langage C offre la possibilité de la gestion dynamique de la mémoire à travers un ensemble de fonctions dont les principales sont : malloc, calloc, realloc et free. L'usage de ces fonctions est tout à fait possible en C++.

La fonction malloc

Prototype : void* malloc(size_t size);

Cette fonction alloue une zone mémoire de la taille de size. Elle retourne un pointeur générique donnant l'adresse de début du bloc alloué ou un pointeur NULL si l'allocation a échoué pour une insuffisance d'espace.

Bibliothèques :

malloc est définie dans les bibliothèques stdlib.h et alloc.h.

La fonction free

Prototype : void free(void* PtrZone)

La fonction free permet de libérer un espace préalablement alloué. Le paramètre PtrZone désigne un pointeur qui pointe sur la zone à libérer.

Bibliothèques :

free est définie dans les bibliothèques stdlib.h et alloc.h.

Exemple :

#include <stdio.h> #include <alloc.h> void main() { int * adr, i; adr= (int *) malloc(10*sizeof(int)); // adr= malloc(10*sizeof(int)); for(i=0;i<10;i++) *(adr+i)=1;

for(i=0;i<10;i++) printf("%d\t", *(adr+i)); free(adr); }

// Utilisation de la valeur de retour // de malloc #include <stdio.h> #include <alloc.h> void main() { int * adr, i; adr= (int *) malloc(10*sizeof(int)); // adr= malloc(10*sizeof(int)); if(!adr) printf("Echec de l'allocation"); else { for(i=0;i<10;i++) *(adr+i)=1;

for(i=0;i<10;i++) printf("%d\t", *(adr+i)); free(adr); } }

Page 44: CoursPOOC++

La gestion dynamique de la mémoire Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti42

La fonction calloc

Prototype : (void*) calloc(size_t nb_blocs, size_t taille_bloc);

• Cette fonction joue le même rôle que celui de malloc. Ainsi, elle alloue l'emplacement nécessaire à nb_blocs

consécutifs, occupant chacun en mémoire taille_bloc octets. • Contrairement à ce qui se passe avec malloc, où le contenu de l'espace mémoire alloué est aléatoire, calloc

remet à zéro chacun des octets de la zone allouée. • calloc retourne un pointeur sur l'espace alloué si l'allocation s'est bien déroulée, sinon elle retourne NULL.

Bibliothèques : stdlib.h et alloc.h

Exemple :

#include<stdio.h> #include<alloc.h> void main ( ) { long* buffer; buffer = (long*) calloc(40, sizeof(long)); if(buffer!=NULL) printf("Espace alloué pour 40 longs"); else printf("Impossible d'allouer de l'espace"); free (buffer); }

La fonction realloc

Prototype : void* realloc(void* PtrBloc, size_t taille)

Bibliothèques : stdlib.h et alloc.h

• Cette fonction permet de modifier la taille d'une zone préalablement allouée (par malloc, calloc ou realloc). • Le paramètre PtrBloc désigne l'adresse de début de la zone dont on veut modifier la taille, quant au paramètre

taille, il désigne la nouvelle taille souhaitée. • En cas de succès, cette fonction retourne un pointeur sur la zone réalloué. • Elle retourne NULL si la taille à réallouer est 0 et PtrBloc n'est pas NULL ou s'il n'y a pas assez d'espace

contigu pour étendre la mémoire à la taille demandée. Dans le premier cas, le bloc d'origine est libéré, dans le second cas, le bloc d'origine reste inchangé.

• Dans le cas où le paramètre PtrBloc vaut NULL alors la fonction realloc se comporte comme malloc. • Lorsque la nouvelle taille demandée est supérieure à l'ancienne, le contenu de l'ancienne zone est conservé. • Dans le cas où la nouvelle taille est inférieure à l'ancienne, le début de l'ancienne zone verra son contenu

inchangé.

La fonction _msize

Prototype : size_t _msize( void * PtrBloc );

Bibliothèques : alloc.h

Cette fonction retourne la taille en octets d'une zone mémoire allouée par malloc, calloc ou realloc. Le type size_t est assimilé au type unsigned int. Il est utilisé pour les variables qui sont sensées stocker la taille en octets des blocs mémoires.

Page 45: CoursPOOC++

La gestion dynamique de la mémoire Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti43

Exemple : (realloc et _msize)

#include<stdio.h> #include<stdlib.h> #include<malloc.h> void main(void) { long *buffer; size_t size;

if((buffer=(long*)malloc(100*sizeof(long)))==NULL) exit(1); // ERROR size= _msize(buffer); printf("la taille du bloc de 100 long après malloc est %u \n", size); // 400

if((buffer= realloc(buffer, size + (200 * sizeof(long))))==NULL) exit(1); // ERROR size= _msize(buffer); printf("la taille du bloc après reallocation de 200 autres long est %u \n", size); // 1200 free(buffer); exit(0); // sortie normale }

L'adresse du buffer peut rester la même comme elle peut changer. Cela dépend de l'espace mémoire contigu disponible au moment de l'exécution.

Outils de gestion de la mémoire spécifiques au C++

• Le C++ réalise la gestion dynamique de la mémoire grâce à des opérateurs et non des fonctions comme c'est le cas pour le C. Ces opérateurs, appelés new et delete font partie de la liste des mots réservés du C++ et ne nécessitent par conséquent aucune bibliothèque à inclure pour pouvoir être utilisés. Par ailleurs ces opérateurs offrent une syntaxe de gestion de la mémoire qui est beaucoup plus simple que celle offerte par le C.

• Il est à noter que les fonctions de gestion de la mémoire du C restent toujours valables en C++.

L'opérateur New

• L'opérateur new alloue de l'espace mémoire pour des objets de type élémentaire ou étendu. Il fournit l'adresse mémoire de la zone réservée à un pointeur qui servira pour faire les opérations d'accès à cette zone.

• Si l'allocation échoue, l'opérateur new retourne NULL. • Le contenu de la zone allouée n'est pas initialisé (Il est aléatoire).

Allocation dynamique d'objets isolés

Pointeur = new type_de_donnees;

Exemple 1 :

int *p; // définit un pointeur vers int p = new int; //alloue de l'espace pour un entier et affecte l'adresse de cet espace //à p. int *x = new int; *x=1; // Accès à la zone allouée (seulement à travers x car cette zone ne possède // pas de nom).

Exemple 2 : Allocation et initialisation

double* d= new double (3.14); // allocation et initialisation

Page 46: CoursPOOC++

La gestion dynamique de la mémoire Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti44

Allocation des tableaux

Tableau à une dimension

Pointeur = new type_de_donnees[Nb d'élements];

Exemple :

int* p = new[10]; ou également int* p; p = new int[10];

• L'accès aux éléments du tableau peut se faire par p[i] ou par *(p+i). • L'initialisation d'un tableau dynamique lors de l'allocation n'est pas permise.

int* p = new int[5] (1,2,3,4,5); // ERREUR

Tableau à plusieurs dimensions

Pointeur = new type_de_donnees[n][Cst1][Cst2]…

Seule la première dimension peut être variable, les dimensions restantes doivent être constantes, donc connues.

Exemple :

int (*Z)[4] = new int [3][4] ⇔ int Z[3][4];

L'opérateur delete

L'opérateur delete libère un espace mémoire déjà alloué par new. Il possède une syntaxe double qui différencie les objets isolés des tableaux.

Libération d'un objet isolé : delete pointeur;

Exemple :

#include <iostream.h> void main() { long* L = new long; cout<<"Donnez un nombre entier"; cin>>*L;

cout<<"Le nombre saisi est:"<<*L; delete L; }

Libération des tableaux dynamiques : delete[] pointeur;

Remarque :

Pour les tableaux de type de base les crochets peuvent être omis. Ce n'est pas le cas par contre pour les tableaux d'objets.

Exemple :

#include <iostream.h> void main() { int i, *p; p = new int [10]; for(i=0;i<10;i++) *(p+i) = i;

for(i=0;i<10;i++) cout<<*(p+i);

delete[] p; }

Page 47: CoursPOOC++

La gestion dynamique de la mémoire Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti45

Allocation dynamique d'un tableau à deux dimensions

malloc et new ne permettent de créer que des blocs à une dimension (des vecteurs). Alors pour créer une matrice de L lignes et C colonnes l'idée consiste à créer L vecteurs comportant chacun C éléments.

Le schéma suivant montre la représentation en mémoire de la structure dynamique à créer.

Allocation de la matrice

Dans le code suivant T désigne un type quelconque, prédéfini ou personnalisé :

Accès à un élément de la matrice

L'accès à un élément d'indice (i,j) se fait comme suit : *(*(M+i)+j) � M[i][j]

Libération de la matrice

Le code suivant permet de libérer la matrice :

Il faut toujours commencer par libérer les lignes puis on libère la table qui stocke l'adresse de ces lignes.

Variable auto vs variable dynamique

• Une variable auto est une variable dont la durée de vie est gérée d'une manière automatique par le système. Une telle variable est allouée sur la pile (empilée) suite à l'exécution de l'instruction qui la déclare. Elle sera ensuite automatiquement libérée (dépilée) lorsque l'exécution du programme atteint la fin du bloc d'instructions dans lequel elle est déclarée.

• Une variable dynamique est une variable dont la durée de vie est gérée par le programmeur. Une telle variable est allouée sur le tas (et non sur la pile) par un appel explicite à new (ou malloc). Elle ne sera libérée de la mémoire qu'à travers un appel explicite à delete (ou free) ou suite au redémarrage du système.

• Les variables dynamiques ne possèdent généralement pas de noms. Le seul moyen permettant de les manipuler et leur adresse en mémoire (renvoyée par new ou malloc). Cette dernière doit alors être préservée dans un pointeur approprié.

T** M

T*

T*

T*

T*

C éléments de type T

L lignes

// Version C++ M= new(T*)[L]; for(int i=0; i<L;i++) *(M+i)=new T[C];

/* Version C */ M= (T**)malloc(L*sizeof(T*)); for(int i=0; i<L;i++) *(M+i)= (T*)malloc(L*sizeof(T));

// Version C++ for(int i=0; i<L;i++) delete[] *(M+i); delete[] M;

/* Version C */ for(int i=0; i<L;i++) free(*(M+i)); free(M);

Page 48: CoursPOOC++

La gestion dynamique de la mémoire Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti46

Utilité de l'allocation dynamique de la mémoire

• Les espaces mémoire dynamiques sont utiles : o pour créer des variables dont la taille est inconnue au moment de la compilation. C'est le cas par

exemple lorsqu'il s'agit de créer un tableau dont le nombre d'éléments sera déterminé au moment de l'exécution par l'utilisateur.

o Pour créer des variables de très grande taille qui dépasse ce que peut accepter la pile d'exécution (c'est le cas des tableaux de grande taille par exemple). L'allocation des espaces dynamiques s'effectue sur le tas. Ceci fait que la seule contrainte concernant leur taille soit la disponibilité de la mémoire vive physique (taille de la RAM disponible). La taille de la pile d'exécution quant à elle reste limitée et dépend généralement des compilateurs. Elle peut être paramétrée dans les options de ces derniers.

Problème de fuite de la mémoire

Une zone mémoire dynamiquement créée mais non explicitement libérée va persister dans la RAM même après la fin de l'exécution du programme. Ce phénomène est communément appelé : fuite de mémoire. Le programme suivant donne une illustration de ce phénomène. 01- void main() 02- { 03- int i; 04- int* p; 05- p= new int; 06- i=5; 07- *p=6; 08- cout<<"i :"<<i<<" et *p :"<<*p; 10- }

Ce programme se compile avec succès. Il s'exécute également d'une manière normale. Toutefois à sa sortie il engendre une fuite de mémoire. Pour détecter cette fuite nous allons analyser l'état de la mémoire qu'il utilise à différents niveaux d'exécution.

Etat de la mémoire suite à l'exécution de l'instruction de la ligne 4

i et p sont deux variables de type "auto". Elles sont allouées sur la pile d'exécution.

Etat de la mémoire suite à l'exécution de l'instruction de la ligne 5

new int va engendrer la création d'une variable dynamique dont l'adresse est stockée dans p.

Etat de la mémoire suite à l'exécution de l'instruction de la ligne 7

Le 5 est placé dans i et le 6 dans la zone dynamique pointée par p.

Pile

FF0EA5Ci

p

Espace mémoire dynamiquement créé

Pile

5

FF0EA5C

i

p 6

i

p

Pile

Page 49: CoursPOOC++

La gestion dynamique de la mémoire Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti47

Etat de la mémoire suite à l'exécution de l'instruction de la ligne 10

La ligne 10 désigne la fin du programme. A ce niveau toutes les variables de type auto seront automatiquement libérées (i et p dans ce cas car leur portée est limitée au bloc du main) mais pas la zone dynamique pointée par p.

Pour remédier à ce problème il faut libérer la zone pointée avant de quitter le programme.

01- void main() 02- { 03- int i; 04- int* p; 05- p= new int; 06- i=5; 07- *p=6; 08- cout<<"i :"<<i<<" et *p :"<<*p; 10- delete p; 11- }

Il est à noter que l'instruction delete p ne libère pas p (p étant de type auto) mais libère la zone mémoire dont l'adresse est stockée dans p.

Remarque :

La fuite de mémoire même si elle ne cause aucune perturbation directe du fonctionnement intrinsèque d'un programme peut être très nocive pour l'environnement dans lequel tourne ce dernier. En effet, l'espace mémoire non libérée va occuper inutilement une partie des ressources (RAM) de l'environnement. Ce problème est encore plus grave si le programme en question est sensé tourner sur un serveur en mode client/serveur. Dans ce cas, chaque client qui lance une exécution du programme va engendrer après sa sortie une occupation inutile d'une portion de la RAM du serveur. Au bout d'un certain nombre de connexions de clients, une grande partie de cette RAM se trouvera occupée sans être réellement utilisée. Ceci va conduire vers une chute des performances du serveur voire même son plantage.

6

Page 50: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti48

Les Fonctions

Introduction

• Une fonction est une portion de code regroupant un ensemble d'instructions délimité par deux accolades et à laquelle est associé un nom qui permet de l'appeler.

• Une fonction peut être déclarée dans un programme ou dans une autre fonction, et peut être exécutée suite à son appel par le programme englobant.

• Une fonction est dite paramétrée si elle demande des données en entrée pour pouvoir s'exécuter. Ces données sont appelées arguments ou paramètres.

• Généralement, une fonction retourne une valeur qui est le résultat de l'exécution de son code. Cependant, il est possible en C/C++ de définir des fonctions qui ne retournent aucune valeur. De telles fonctions sont équivalentes aux procédures définies en algorithmique ou dans d'autres langages de programmation tel que le PASCAL.

Déclaration d'une fonction

La déclaration d'une fonction se fait comme suit :

Type NomFonction(TypeArg1 NomArg1,…, TypeArg_n NomArg_n); Où : • Type désigne le type de la valeur retournée par la fonction. • TypeArg_i NomArg_i désignent respectivement le type et le nom du ième argument de la fonction.

Exemple:

int f(int i,int j);

Déclaration du prototype d'une fonction

La déclaration d'une fonction peut se réduire seulement à son prototype. Ce dernier donne une idée sur le modèle de la fonction en termes du nombre et des types des paramètres qu'elle prend, ainsi que du type de sa valeur de retour. Il est déclaré de la manière suivante:

Type NomFonction(TypeArg1, TypeArg2,…, TypeArg_n);

Exemple:

int f(int,int);

Définition d'une fonction

• La partie définition d'une fonction correspond au corps de la fonction. Il s'agit donc de la spécification de l'ensemble des instructions dont l'exécution réalise la tâche assurée par la fonction.

• La définition d'une fonction se fait comme suit :

Type NomFonction(TypeArg1 NomArg1, …, TypeArg_n NomArg_n) { Déclarations des variables locales; instruction_1; … instruction_n; return valeur; }

Page 51: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti49

• En C, les premières instructions du corps de la fonction doivent correspondre aux déclarations des variables qui seront utilisées à l'intérieur de la fonction. Ce n'est pas le cas en C++ où ces déclarations peuvent être faites n'importe où dans le bloc définissant le corps de la fonction.

• En C/C++, Ces variables sont dites des variables locales à la fonction. Elles ne peuvent par suite être utilisées qu'à l'intérieur de cette dernière. Elles ne sont pas visibles à l'extérieur.

• Les dernières instructions d'une fonction servent généralement à renvoyer une valeur, appelée valeur de retour, à l'extérieur de la fonction. Ce renvoi est effectué à l'aide du mot-clé return.

• En C, lorsqu'une fonction ne prend aucun argument, le mot-clé void doit être placé entre les parenthèses à la place de la liste des paramètres. Ceci n'est pas nécessaire en C++ où les parenthèses peuvent rester vide.

Type d'une fonction et valeur de retour

• Le type de la fonction indique le type de sa valeur de retour. • Si aucun type n'est spécifié pour une fonction alors cette dernière est considérée par défaut comme étant de

type int. (Elle retourne un entier de type int). • L'instruction return provoque un arrêt immédiat de l'exécution des instructions du bloc associé à la fonction et

renvoie une valeur ou le résultat d'une expression qu'elle prend comme paramètre de la manière suivante

return (valeur);

L'utilisation des parenthèses avec return est facultative.

• Il est possible de spécifier que la fonction ne retourne aucune valeur d'aucun type en la déclarant comme étant de type void.

• Si l’instruction return est absente du corps d'une fonction, alors le programme continue son exécution des instructions jusqu'à l'accolade fermante.

• La valeur renvoyée par return doit être du même type que celui de la fonction. Elle peut être exploitée comme n'importe qu'elle autre valeur du même type. Par exemple, elle peut être récupérée et stockée dans une variable de la manière suivante :

int var; … var = NomFonction(arg1, arg2, arg3…); …

• Suite à une exécution une fonction ne peut retourner q'une seule valeur et pas plus.

Exemple 1 :

Le programme suivant permet de calculer le montant de l'achat d'un nombre donné d'articles possédant chacun un prix unitaires PU.

/* 1 */ #include <stdio.h> /* 2 */ double Montant(int NbArticles, double PU); /* 3 */ void main( ) /* 4 */ { /* 5 */ int nb; /* 6 */ double val; /* 7 */ printf("\n donnez le nombre d'articles puis leur prix unitaire: "); /* 8 */ scanf("%f %lf", &nb, & val); /* 9 */ resultat = Montant(nb,val); /* 10*/ printf("\n Le Montant est %f", resultat);/* 11*/ } /* 12*/ double Montant(int NbArticles, double PU) /* 13*/ { /* 14*/ double total; /* 15*/ total = NbArticles * PU; /* 16*/ return total; /* 17*/ }

Page 52: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti50

Commentaires

• La ligne 2 représente la déclaration de la fonction Montant. On aurait pu se limiter au prototype en omettant les noms des variables.

• La ligne 9 est un appel à la fonction Montant. • La définition de la fonction Montant s'étend de la ligne 12 jusqu'à la ligne 17. • La variable total (14) est une variable locale à la fonction Montant, elle ne peut être utilisée qu'à l'intérieur de

cette dernière. • Dans cet exemple on peut vérifier que la déclaration de la fonction s'est faite avant sa première utilisation. La

définition peut suivre par la suite. Une fonction doit être déclarée avant son utilisation.

Exemple 2 :

• Il est possible dans un programme de se limiter à la définition d'une fonction sans faire une déclaration du prototype. Dans ce cas, cette définition doit précéder le premier appel de la fonction.

#include <stdio.h>

double Montant(int NbArticles, double PU) { double total; total = NbArticles * PU; return total; }

void main() { int nb; double val; printf("\n donnez le nombre d'articles puis leur prix unitaire: "); scanf("%f %lf", &nb, & val); resultat = Montant(nb,val); printf("\n Le Montant est %f", resultat); }

Exemple 3 :

Il s'agit d'une autre version de l'exemple 2 mais avec moins de variables. #include <stdio.h>

double Montant(int NbArticles, double PU) { return NbArticles * PU;; }

void main() { int nb; double val; printf("\n donnez le nombre d'articles puis leur prix unitaire: "); scanf("%f %lf", &nb, & val); printf("\n Le Montant est %f", Montant(nb,val)); }

Exemple 4 : (fonction sans valeur de retour: procédure)

Il s'agit de la définition d'une fonction qui prend comme paramètre un nombre et qui affiche un message indiquant si ce nombre est strictement positif, strictement négatif ou nul.

#include <stdio.h> void Signe(int Nb); { if(Nb < 0) printf("le nombre est négatif \n"); else if (Nb >0) printf("le nombre est positif \n"); else printf("le nombre est nul \n"); // return; (on aurait pu ajouter cette ligne pour indiquer que la fonction ne retourne rien) }

Page 53: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti51

void main( ) { int Nombre; printf("Donnez le nombre à tester"); scanf("%d",&Nombre); Signe(Nombre); }

Exemple 5 :

La dernière instruction de la fonction 1 (return res2) ne peut jamais être atteinte et par suite exécutée parce que le premier return (return res1) va engendrer une sortie immédiate de la fonction avec comme valeur de retour res1. Ce n'est pas le cas pour la fonction 2 où chacune des instructions return peut être atteinte selon que le i est inférieur à j ou non. Il est à rappeler que pour une exécution donnée de la fonction 2 c'est un seul return parmi les deux qui sera exécuté.

Fonction locale et fonction globale

• Tout comme les variables, une fonction peut être déclarée à l'intérieur d'un bloc (éventuellement une fonction) ou à l'extérieur. Si la déclaration est faite à l'intérieur alors la fonction est considérée comme locale à ce bloc et ne peut être appelée en dehors de celui-ci.

• Une fonction est dite globale, si elle est déclarée en dehors de tout bloc. Elle peut être dans ce cas appelée de n'importe qu'elle endroit du programme.

Exemple 1 :

#include <stdio.h>

void main() { double Montant(int NbArticles, double PU) { return NbArticles * PU;; }

int nb; double val; printf("\n donnez le nombre d'articles puis leur prix unitaire: "); scanf("%f %lf", &nb, & val); printf("\n Le Montant est %f", Montant(nb,val)); }

Dans cet exemple la fonction Montant a été définie à l'intérieur du main. Par conséquent, elle ne peut être appelée que de l'intérieur de la fonction main.

Exemple 2 :

On veut écrire un programme qui demande la matricule et les notes d'un étudiant et qui l'invite à la fête de fin d'année s'il possède une moyenne supérieure ou égale à 10.

# include <stdio.h>

void main( ) { int Mat; float n1,n2; float Moyenne(float, float); // déclaration locale void Invitation(int, float, float ); // déclaration locale

// Fonction 1 int f1(int i, int j) { int res1, res2; res1 =i+j; res2 = i-j; return res1; return res2; //unreachable code }

// Fonction 2 int f2(int i, int j) { if(i<j) return i; return j; }

Page 54: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti52

printf("Donnez la matricule de l'étudiant"); scanf("%d",&Mat); printf("Donnez ses deux notes "); scanf("%f %f",&n1,&n2); Invitation(Mat, n1, n2); }

void Invitation(int Matricule, float note1, float note2) { float MoyenneLoc; MoyenneLoc = Moyenne(note1,note2); if( MoyenneLoc <10 ) printf(" L'étudiant possédant la matricule %d n'est pas invité \n", Matricule); else printf(" L'étudiant possédant la matricule %d est invité \n", Matricule); }

float Moyenne ( float note1, float note2) { return (note1+note2)/2; }

• Conformément à la norme ANSI1, et puisque la fonction Moyenne a été déclarée à l'intérieur du main son utilisation dans le bloc de la fonction Invitation devient incorrecte. En effet, dans ce cas cette fonction est considérée comme locale à la fonction main et elle ne pourra pas être appelée par suite en dehors du bloc de cette dernière.

• Il est possible pour éviter ce problème de déclarer la fonction Moyenne à l'intérieure de la fonction Invitation. Ainsi, elle sera reconnue par cette dernière. Une autre alternative consiste à faire une déclaration globale de la fonction Moyenne en dehors de tout bloc.

Remarque :

• La norme ANSI concernant la déclaration locale des fonctions est étendue par certains compilateurs qui considèrent qu'une fonction est reconnue dans tout le reste du fichier source depuis l'endroit où elle a été déclarée peu importe que la déclaration soit interne à un bloc ou globale. De ce fait, l'écriture précédente devient correcte avec certains compilateurs. Cependant et pour des raisons de portabilité elle est à éviter.

Les catégories des paramètres des fonctions

Paramètres formels et paramètres effectifs

• Deux types de paramètres de fonctions peuvent être distingués : les paramètres formels et les paramètres effectifs. o Les paramètres formels sont ceux qui figurent dans la définition de la fonction. Ils sont utilisés dans les

instructions faisant partie du bloc de la fonction et là seulement. Ces paramètres sont considérés comme des variables locales à la fonction.

o Les paramètres effectifs sont ceux qui figurent dans l'instruction d'appel de la fonction. Ils sont substitués aux paramètres formels au moment de cet appel.

• Les paramètres formels et effectifs doivent s'accorder en nombre, ordre et type. (les types peuvent être compatibles seulement et pas nécessairement identiques).

Différents types des paramètres formels

Suivant le rôle qu'ils assurent dans la fonction les paramètres formels peuvent être classés en trois catégories: les paramètres données, les paramètres résultats et les paramètres données-résultats.

• Un paramètre "donnée" est une donnée nécessaire pour réaliser la tâche associée à la fonction. Il ne subit aucune modification au cours de l'exécution de la fonction.

1 ANSI : Acronyme de American National Standards Institute, institution qui définit les normes d'un ensemble de systèmes et de

langages de programmation.

Page 55: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti53

Exemple :

Les paramètres NbArticles et PU dans la fonction Montant sont des paramètres de type données. C'est le également le cas des paramètres note1 et note2 dans la fonction Moyenne.

• Un paramètre "résultat" est une variable qui est destinée à contenir le résultat de l'action de la fonction. Sa valeur n'est pas significative avant le début.

Exemple:

int CalculerTriple( int x, int triple) { triple = 3 *x; return triple; }

Dans cet exemple x est un paramètre "donnée" alors que triple est un paramètre "résultat". Généralement un paramètre "résultat" n'a pas besoin d'être déclaré comme argument de la fonction mais plutôt comme une variable locale.

• Un paramètre "donnée-résultat" est à la fois une donnée et un résultat. c'est à dire qu'il sert à passer une donnée à la fonction et que cette dernière modifie sa valeur.

Exemple :

Dans une fonction qui prend deux paramètres entiers et qui permute leurs valeurs. Ces entiers sont des paramètres "données-resultats" puisqu'ils fournissent les données en entrée (les valeurs à permuter) et stockent normalement le résultat de la fonction (les valeurs permutées).

Modes de passage des paramètres

Passage par valeur

• C'est le mode de passage d'arguments le plus courant en C/C++ et le plus simple également. Dans ce mode, les valeurs des arguments au moment de l'appel (paramètres effectifs) sont recopiées dans les éléments de données locaux correspondants à la fonction (paramètres formels). Après la sortie de la fonction, les valeurs des paramètres effectifs restent identiques à celles qu'ils avaient avant l'exécution de la fonction.

• Ce mode de passage est adapté aux paramètres de type "données". Pour les paramètres de type "résultat" ou "donnée-résultat", ce mode ne permet pas de récupérer les éventuels modifications effectuées en interne par la fonction à l'extérieur de cette dernière.

• La déclaration d'un passage par valeur s'effectue de la manière suivante :

TypeFonction NomFonction(……, Type ParamValeur,…);

ParamValeur étant le paramètre passé par valeur et Type étant son type.

Exemple 1 :

Les deux notes sont passées à la fonction Moyenne par valeur.

Exemple 2 :

#include <stdio.h> void CalculerTriple( int x, int triple) { triple = 3 *x; }

void main(void) { int ent = 5; int res; CalculerTriple(ent,res); printf("le résultat est %d", res); // donne un résultat incorrect }

Le mode de passage par valeur est adapté pour le paramètre x (paramètre "donnée") mais ne l'est pas pour le paramètre triple (paramètre "résultat"). En effet la modification effectuée sur ce paramètre à l'intérieur de la

Page 56: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti54

fonction CalculerTriple n'est pas récupérée à l'extérieur de cette dernière (dans le main dans ce cas de cet exemple).

Exemple 3 :

#include <stdio.h> void Permuter (int val1,int val2) { int temp; temp =val1; val1= val2; val2= temp; }

void main( ) { int x= 5, y=3; Permuter(x,y); printf("x contient %d et y contient %d \n", x,y); // 5 et 3 }

Les paramètres val1 et val2 sont de type "donnée-résultat". En les faisant passer par valeur comme c’est le cas dans cet exemple, le résultat de la permutation n'est pas récupéré à l'extérieur de la fonction et le résultat affiché est par conséquent incorrect. • Une des solutions permettant de remédier au problème du passage par valeur des paramètres "résultat" et

"donnée-résultat" consiste à éliminer ces derniers de la liste des arguments de la fonction et à les déclarer comme des variables globales. Toutefois cette solution présente l'inconvénient de rendre la fonction difficilement réutilisable.

• Une deuxième solution plus intéressante consiste à utiliser un autre mode de passage de paramètres qui permet de récupérer ces modifications : c'est le cas du passage par adresse (C/C++) et du passage par référence (C++ seulement).

Passage par adresse

• Dans un passage par adresse d'un argument on impose à la fonction de travailler non plus sur une copie locale du paramètre effectif mais plutôt directement sur ce dernier. (Pas de copie du paramètre effectif dans le paramètre formel, le travail se fait directement sur le paramètre effectif). De cette façon, toute modification effectuée à l'intérieur de la fonction sera en réalité réalisée sur le paramètre effectif et par conséquent visible à l'extérieur de la fonction.

• Ce mode de passage de paramètre est effectué en faisant passé à la fonction l'adresse du paramètre effectif. Toute référence à ce dernier de l'intérieur de la fonction doit se faire à l'aide de l'opérateur d'indirection (*).

Déclaration d'un passage par adresse

TypeFonction NomFonction(……, Type* ParamAdresse,…);

ParamAdresse étant le paramètre passé par adresse et Type étant son type.

Référence à l'intérieur de la fonction d'un paramètre passé par adresse

TypeFonction NomFonction(……, Type* ParamAdresse,…) { … … … … *ParamAdresse; }

Passage du paramètre par adresse au moment de l'appel de la fonction

NomFonction(……, &ParamAdresse,…)

• Les paramètres concernés par le mode de passage par adresse sont ceux de type "résultat" ou "donnée-résultat". Les paramètres "donnée" peuvent toujours être passés par valeur.

Page 57: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti55

Exemple 1 :

#include <stdio.h> void CalculerTriple(int x, int* triple) { *triple = 3*x; }

void main(void) { int ent = 5; int res; CalculerTriple(ent, &res); printf("le résultat est %d", res); // donne un résultat correct 15 }

Exemple 2 :

#include <stdio.h> void Permuter (int* val1,int* val2) { int temp; temp =*val1; *val1= *val2; *val2= temp; }

void main( ) { int x= 5, y=3; Permuter(&x,&y); printf("x contient %d et y contient %d \n", x,y); // 3 et 5 }

Passage par référence (C++ seulement)

• Par rapport au C, le C++ introduit un nouveau mode de passage de paramètres appelé le passage par

référence. Il est équivalent de point de vue effet au passage par variable du pascal ou au passage par adresse du C.

• Dans le passage par référence, toute modification effectuée sur le paramètre formel de la fonction est répercutée directement sur le paramètre effectif.

• La déclaration d'un passage par référence s'effectue en adjoignant au type du paramètre à passer le symbole & de la manière suivante :

Déclaration d'un passage par référence

TypeFonction NomFonction(……, Type& ParamRéférence,…);

ParamRéférence étant le paramètre passé par référence et Type étant son type.

Référence à l'intérieur de la fonction d'un paramètre passé par référence

TypeFonction NomFonction(……, Type& ParamRéférence,…) { … … … … ParamAdresse; }

Passage du paramètre par adresse au moment de l'appel de la fonction

NomFonction(……,ParamRéférence,…)

Exemple

#include <iostream.h> void Permuter (int& val1,int& val2) { int temp; temp =val1; val1= val2; val2= temp; }

Page 58: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti56

void main( ) { int x= 5, y=3; Permuter(x,y); cout<<" x contient : "<<x<<"\n y contient : "<<y<<endl; }

Transmission des tableaux comme arguments d'une fonction

Si un tableau figure comme argument d'une fonction alors son passage se fait toujours par adresse. Par conséquent, toutes les opérations et modifications appliquées à ses éléments seront visibles à l'extérieur de la fonction.

Exemple1:

Le programme suivant remplace les éléments d'un tableau par leurs valeurs absolues sans qu'il soit nécessaire de spécifier que le passage se fait par adresse.

#include <stdio.h> void ValAbsTab( float[], int); void main( ) { float Tableau[5]; int i; for(i=0;i<5;i++) { printf("Donnez un nombre: "); scanf("%f", &Tableau[i]); } ValAbsTab(Tableau, 5);

for(i=0;i<5;i++) printf("%f", Tableau[i]); }

void ValAbsTab( float TabVal[], int Taille) { int i; for(i=0;i<Taille;i++) if(TabVal<0) TabVal[ i ]= - TabVal[ i ]; }

Remarque :

Les deux prototypes suivants sont également valables pour faire passer un tableau comme paramètre de la fonction ValAbsTab. void ValAbsTab( float* TabVal, int Taille) void ValAbsTab( float TabVal[Taille], int Taille)

Exemple2: (cas d'un tableau à deux dimensions)

#include<stdio.h> void SaisieMatrice( int[5][10], int, int); void main( ) { int M[5][10]; SaisieMatrice(M,5,10); }

void SaisieMatrice( int Matrice[5][10], int L, int C) { int i,j; for(i=0;i<L;i++) for(j=0;j<C;j++) { printf("Donnez Matrice[%d][%d]",i,j); scanf("%d", &Matrice[i][j]); } }

Page 59: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti57

Remarque :

Les deux prototypes suivants sont également valables pour faire passer un tableau comme paramètre de la fonction SaisirMatrice. void SaisieMatrice( int Matrice[ ][10], int L, int C) void SaisieMatrice( int** Matrice, int L, int C)

Les fonctions en ligne

• Une fonction en ligne est une fonction expansée à chaque appel. En d'autres mots, le corps de la fonction est inséré dans le module appelant à l'endroit de l'appel.

• Les fonctions en ligne permettent d'accélérer l'exécution (en évitant les allées-retours entre la fonction et le module appelant).

Déclaration : inline Type NomFonction(Type1 arg1, Type2 arg2, … , TypeN argN);

Remarques :

• La définition d'une fonction en ligne doit obligatoirement précédée son appel. La déclaration seule ne suffit pas.

• inline est une recommandation au compilateur. Ce dernier peut l'ignorer si : o La fonction contient des boucles ou si elle est récursive. o La fonction est trop grande pour que le gain en temps d'exécution soit significatif.

• Les fonctions en ligne sont généralement de petite taille (1 à 3 instructions).

Exemple :

inline int abs(int x){return x>0?x:-x;} inline int max(int x, int y){return x>y?x:y;}

Surcharge de fonctions (surdéfinition)

• La surcharge de fonctions consiste à proposer plusieurs définitions d'une même fonction au sein d'un même programme. Ces définitions doivent se distinguer par leurs signatures.

• Il est à rappeler que la signature d'une fonction est définie par le nombre, le type et l'ordre de ses paramètres. La valeur de retour n'entre pas en considération dans ce cas.

• On surcharge généralement les fonctions qui font le même traitement mais sur des données de types différents.

Exemple 1:

///////////////////////////////////////////// int MaxTab(int* T, int n) { int i, max; for(i=0, max=T[0];i<n;i++) if(T[i]> max) max = T[i]; return max; }

///////////////////////////////////////////// double MaxTab(double* T, int n) { int i; double max; for(i=0, max=T[0];i<n;i++) if(T[i]> max) max = T[i]; return max; }

Page 60: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti58

///////////////////////////////////////////// char MaxTab(char* T) { char max = T[0]; while(*T++) if(*T> max) max = *T; return max; }

Exemple 2:

Arguments par défaut d'une fonction

Des valeurs par défaut peuvent être affectées aux arguments d'une fonction. Ces arguments ont généralement des valeurs habituelles qu'il n'est pas nécessaire de spécifier à chaque utilisation.

Déclaration Type NomFonction(Type1 arg1, Type2 arg2, Type3 Arg3 = Val);

Exemple :

int f(int a, int b=0);

Remarque 1:

Seuls les derniers arguments, de la droite vers la gauche peuvent avoir des valeurs par défaut.

Exemple :

void g(int a = 3, int b, int c=5); // Error void g(int a, int b=6, int c =8); // OK

Remarque 2:

• Si la valeur d'un argument par défaut a été spécifiée dans la déclaration de la fonction, alors elle ne doit pas être mentionnée à nouveau dans la définition.

• Si une fonction est directement définie, alors la valeur par défaut doit être spécifiée dans la définition.

Exemple :

• Au moment de l'appel de la fonction, la spécification de l'argument par défaut est optionnelle.

Exemple :

// Déclaration void Print(int Valeur, int Base = 10); // Appel Print(31) � 31 Print(31,10) � 31

• Il est également possible de donner une nouvelle valeur pour l'argument par défaut. Print(31,16) � 1f Print(31,2) � 11111

void f(int a = 0) // OK {Corps de la fonction}

void f(int a=0); // ou void f(int=0); void f(int a=0) // Error { Corps de la fonction }

// Surcharge incorrecte int f(char, int); char f(char, int);

// Surcharge correcte int f(int, char); int f(char, int);

Page 61: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti59

Les Pointeurs sur les fonctions

Les fonctions en C/C++ ne sont pas des entités variables. Néanmoins, il est possible de placer leurs noms dans des variables. En effet, le nom d'une fonction utilisé tout seul désigne l'adresse de l'emplacement mémoire où est chargé le code de la fonction (les instructions). Cette adresse peut être par conséquent stokée dans une variable de type pointeur. Elle peut également être passée comme paramètre à une autre fonction.

Déclaration d'un pointeur sur une fonction

La déclaration d'un pointeur sur une fonction se fait de la manière suivante :

Type(*NomPointeur)(Type1 P1, Type2 P2, …,TypeN PN);

• NomPointeur désigne le nom du pointeur à déclarer. • Type désigne le type de la valeur retournée par la fonction pointée par le pointeur. • Type1,… TypeN désignent la liste des paramètres de la fonction pointée par le pointeur. • Les parenthèses autour de (*pointeur) sont très importantes car autrement avec Type* pointeur(…) et en

raison de la priorité de l'opérateur ( ) par rapport à * on déclarerait non plus un pointeur sur une fonction mais une fonction qui retourne une valeur de Type*.

Exemple :

int(*pf)(double,int);

Cette déclaration spécifie que pf est un pointeur qui peut pointer sur des fonctions prenant comme argument un double et un int et retournant un int. Soit la fonction int f(double, int) Si pf=f alors int k=f(5.4,6); ⇔ int k=(*pf)(5.4,6);

Exemple :

On veut écrire un programme qui affiche, suivant le choix de l'utilisateur le sinus, le cosinus ou la tangente d'un angle fourni par l'utilisateur.

#include <iostream.h> #include<math.h> void main( ) { double Deg, Rad; int NumF; double(*Tpf[3])(double)={sin,cos,tan}; cout<<"Donnez un angle en degree : "; cin>>Deg; cout<<"Tapez le numéro de la fonction que vous voulez utiliser\n"; cout<<" 1- sinus \n 2- cosinus\n 3- Tangente\n"; cin>>NumF; Rad=3.1416*Deg/180; cout<<"Le résultat est : "<<(*Tpf[NumF-1])(Rad)<<endl; }

Remarque : Différence entre le C et le C++ concernant les pointeurs sur des fonctions sans paramètres

• En langage C, la liste des paramètres spécifiée lors de la déclaration d'un pointeur sur une fonction n'est pas obligatoire. Son absence ne signifie pas forcément que la fonction référencée par le pointeur ne possède pas de paramètres. En effet, si rien n'est indiqué concernant les paramètres, le compilateur n'effectue aucune vérification en ce qui concerne la correspondance entre paramètres effectifs et paramètres formels (cette propriété existe déjà pour les fonctions). Ainsi la définition d'un pointeur de fonctions ayant une liste de paramètres vide peut présenter un avantage puisqu'un tel pointeur peut servir à mémoriser les adresses des fonctions admettant toutes sortes de paramètres.

Exemple :

int (*pf)();

pf=f1; ou pf = f2; avec f1 et f2 deux fonctions. La seule contrainte ici pour que ces deux affectations soient correctes est que f1 et f2 retournent une valeur de type int. Les deux fonctions suivantes sont dans ce cas valables.

Page 62: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti60

• Contrairement au C, le compilateur C++ effectue toujours une vérification concernant la correspondance entre paramètres effectifs et paramètres formels même si ces derniers ne sont pas mentionnés. Par conséquent un pointeur de fonction déclarée sans paramètres ne peut pointer que sur une fonction n'ayant pas de paramètres. Le pointeur pf de l'exemple précédent ne peut pointer alors que sur f1.

Exemple :

L'exemple suivant est correct en C mais incorrect en C++.

int(*pf)() = printf;

printf("Test des pointeurs de fonctions");⇔(*pf)("Test des pointeurs de fonctions");

Passage d'une fonction comme argument d'une autre fonction

Du fait que le nom d'une fonction soit considéré comme une adresse, rien n'empêche de le passer en tant qu'argument à une autre fonction. Cette possibilité peut être utile dans certains cas même si elle reste rare d'utilisation.

Exemple :

On veut écrire deux fonctions qui font la saisie et l'affichage des éléments d'un tableau. Ces éléments peuvent être de type entier, réel ou caractère. #include<stdio.h> void SaisirChar(char* Tab, int l) { int i; for(i=0;i<l;i++) { scanf("%c", &Tab[i]); fflush(stdin); } }

void SaisirInt(int* Tab, int l) { int i; for(i=0;i<l;i++) scanf("%d", &Tab[i]); }

void SaisirFloat(float* Tab, int l) { int i; for(i=0;i<l;i++) scanf("%f", &Tab[i]); }

void AfficherChar(char* Tab, int l) { int i; for(i=0;i<l;i++) printf("%c, ",Tab[i]); }

void AfficherInt(int* Tab, int l) { int i; for(i=0;i<l;i++) printf("%d, ",Tab[i]); }

int f1() { /* intsructions*/ return 0; }

int f2(int a, int b, int c) { /* intsructions*/ return C; }

Page 63: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti61

void AfficherFloat(float* Tab, int l) { int i; for(i=0;i<l;i++) printf("%f, ",Tab[i]); }

void AfficherTableau( void* Tab, int l, void(*pf)(void*, int)) { (*pf)(Tab,l); } void SaisirTableau( void* Tab, int l, void(*pf)(void*, int)) { (*pf)(Tab,l); } void main() { char C[5]; int I[5], l =5,n; float F[5]; printf("Tapez le numéro 1 du type des éléments que vous voulez saisir : \n 1 - caractère\n 2 - entier\n 3 - réel\n"); scanf("%d", &n); fflush(stdin); printf("****** Début de la saisie : *******\n"); switch(n) { case 1: SaisirTableau(C,l,SaisirChar); break; case 2: SaisirTableau(I,l,SaisirInt); break; case 3: SaisirTableau(F,l,SaisirFloat); break; }

printf("****** Affichage du résultat de la saisie: *******\n"); switch(n) { case 1: AfficherTableau(C,l,AfficherChar); break; case 2: AfficherTableau(I,l, AfficherInt); break; case 3: AfficherTableau(F,l, AfficherFloat); break; } }

Fonctions avec un nombre variable de paramètres

• En programmation, il existe souvent des opérations pour lesquelles il n'est pas possible de préciser à l'avance le nombre de paramètres à traiter. L'utilisation des fonctions avec un nombre fixe de paramètres ne convient pas dans ce cas. Il faut plutôt passer par des fonctions possédant un nombre variable de paramètres.

• Les fonctions printf et scanf du C/C++ constituent deux exemples de ce genre de fonctions. • Le concept de "fonction avec un nombre variable de paramètres" signifie que la fonction est définie avec un

nombre quelconque de paramètres fixes (au moins un) et qu'elle accepte lorsqu'on l'appelle un nombre variables de paramètres effectifs supplémentaires sans qu'il y ait besoin que ces derniers soient explicitement définis à l'aide de paramètres formels.

Déclaration

La déclaration d'une fonction de la sorte se fait de la manière suivante : Type NomFonction(Liste des paramètres fixes, …);

Exemple :

int f(char a, int b,…);

Page 64: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti62

Evaluation des paramètres optionnels

• L'évaluation des paramètres effectifs optionnels doit passer par l'utilisation des trois macros suivantes va_start, va_arg et va_end du header stdarg.h ainsi que par un pointeur de type va_list (défini comme void*

ou char*) et qui va permettre de pointer vers ces arguments optionnels (Un argument est un paramètre effectif).

La macro va_start

void va_start( va_list arg_ptr, prev_param ); (ANSI version) void va_start( va_list arg_ptr ); (UNIX version)

va_start prend deux paramètres, le premier est un pointeur de type va_liste vers les paramètres optionnels de la fonction alors que le second est le nom du dernier paramètre fixe. va_start initialise arg_ptr au premier argument optionnel de la liste des arguments passée à la fonction.

La macro va_arg

TypeArg va_arg( va_list arg_ptr, TypeArg ); La macro va_arg récupère une valeur de type TypeArg à partir de l'endroit donné par arg_ptr. En d'autres mots, elle permet de récupérer la valeur de l'argument courant. Elle incrémente aussi arg_ptr pour pointer sur l'argument suivant de la liste en utilisant la taille du type pour déterminer l'adresse de début du prochain argument.

La macro va_end

void va_end( va_list arg_ptr );

Après évaluation de la liste des paramètres, le pointeur d'arguments va_end réinitialise arg_ptr à NULL.

Remarque :

Dans la majorité des cas, Il est nécessaire de faire communiquer à la fonction acceptant un nombre variable de paramètres le nombre effectif de paramètres optionnels à traiter. Ce nombre est généralement placé dans un des paramètres fixes.

Exemple :

L'exemple suivant montre la définition d'une fonction Somme qui peut calculer la somme d'un nombre variable d'entiers.

#include "iostream.h" #include "stdarg.h"

long Somme(int L,...) { int i; va_list arg_ptr; long resultat; va_start(arg_ptr,L); i=1; resultat=0; while (i<=L) { resultat+=(long)va_arg(arg_ptr,int); i++; } va_end(arg_ptr); return resultat; }

void main() { cout<<"le résultat de la somme est :"<<f(3,3,5,10)<<endl; }

Page 65: CoursPOOC++

Les fonctions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti63

Paramètres sur la ligne de commande

Considérons le programme suivant : #include <stdio.h> void main() { int a,b,c; printf("donnez trois entiers"); scanf("%d %d %d ",&a,&b,&c); printf("%d",a+b+c); }

• Supposons que ce programme soit sauvegardé dans un fichier nommé Somme.cpp alors la compilation et l'édition des liens de ce fichier va conduire vers la création d'un fichier exécutable nommé Somme.exe. Pour lancer cet exécutable il suffit de taper son nom après le prompt du système d'exploitation puis de valider avec <entrée>. Le programme va demander alors à l'utilisateur de saisir trois nombres et lui affiche comme résultat leur somme.

• Il est également possible de faire fonctionner le programme Somme à la manière d'une commande qui prend trois arguments. Pour ce faire, ces arguments doivent être passés à la fonction principale main.

Passage d'arguments à la fonction main

La fonction main peut prendre normalement deux paramètres formels optionnels communément appelés argc et argv.

main(int argc, char* argv[])

• argc est un argument de type int et désigne le nombre de paramètres spécifiés sur la ligne de commande y compris le nom du programme qui est considéré aussi comme paramètre.

• argv est un tableau de chaînes de caractères qui stocke les arguments passés en ligne de commande. La taille de ce tableau dépend du nombre d'arguments passés. Par convention arg[0] renvoie sur la commande avec laquelle le programme est invoqué, arg[1] représente le premier argument passé réellement au programme, arg[2] représente l'argument suivant, etc. Le dernier élément de argv est toujours argv[argc] et contient le pointeur nul.

• Le premier argument passé est argv[1] et le dernier est argv[argc-1]. • La reconnaissance du nombre de paramètres est automatique. Ces derniers doivent cependant être séparés par

des espaces. Si un des paramètres lui même est un espace alors il doit être placé entre deux guillemets.

Exemple :

#include <stdio.h> #include <stdlib.h>

void main(int argc, char* argv[]) { int a,b,c; a=atoi(argv[1]); b=atoi(argv[2]); c=atoi(argv[3]); printf("%d",a+b+c); }

Si on enregistre ce code dans un fichier portant le nom Somme.cpp alors l'exécution en ligne de commande devra se faire de la manière suivante : C:\ Somme 4 5 2

C:\ 11

Remarque :

La fonction main peut prendre un troisième paramètre communément appelé envp. Ce paramètre pointe sur les rubriques de l'environnement du programme (les chemins implicites, l'allure du prompt,…) il est utilisé sous dos et unix.

void main(int argc, char* argv[],char* envp[])

Page 66: CoursPOOC++

Structures et énumérations Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti64

Structures et énumérations

Les types de base du langage C (int, float, double, char, …) ne permettent pas de couvrir tous les besoins de programmation en types de données. Les programmeurs ont souvent recours à la définition de types personnalisés qui répondent à leurs besoins spécifiques. Le langage C met à la disposition de ces derniers un ensemble d'outils de construction de types personnalisés parmi lesquels figurent les structures et les énumérations.

Définition des structures

Une structure est un ensemble d'éléments (variables), ayant des types pouvant être différents et regroupés sous un même nom global. Chaque élément de cette structure, appelé aussi membre possède un nom qui lui est propre permettant de le distinguer des autres. Il possède également un type qui peut être un type de base ou un type personnalisé. La structure elle même est considérée comme un type personnalisé. Une structure est définie à l'aide du mot-clé struct selon la syntaxe suivante :

Exemple

struct FicheSignaletique { int age; float taille; float poids; char nom[20]; };

Déclaration des variables de type structure

Déclaration en langage C

• En langage C la déclaration d'une variable de type structure se fait comme suit :

struct NomStructure Var;

Exemple

struct FicheSignaletique Personne;

• Il est également possible (bien que peu recommandé) de regrouper la définition du modèle structure et la déclaration des variables dans une seule instruction.

Exemple

struct FicheSignaletique { int age; float taille; float poids; char nom[20]; }P1, P2;

• Il est possible dans ce cas d'omettre le nom du type. struct { int age; float taille; float poids; char nom[20]; } P1;

struct NomStructure { TypeMembre_1 NomMembre_1; TypeMembre_2 NomMembre_2; … TypeMembre_n NomMembre_n; };

Page 67: CoursPOOC++

Structures et énumérations Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti65

Déclaration en langage C++

En langage C++, il n'est pas nécessaire d'utiliser struct pour la déclaration des variables et le nom de la structure peut être par conséquent utilisé seul pour désigner le type. Cette déclaration se fait alors comme suit :

NomStructure Var;

Exemple

struct FicheSignaletique { int age; float taille; float poids; char nom[20]; } … … … FicheSignaletique F;

Pour des raisons de compatibilité la syntaxe de déclaration du C est également acceptée par les compilateurs C++ et peut encore être utilisée.

Manipulation des membres d'une structure

Les membres d'une structure peuvent être manipulés individuellement. Ceci est réalisé en spécifiant le nom de la structure suivi de l'opérateur de champ ( . ) suivi du membre considéré.

NomVariableStructure.NomMembre

Exemple:

FicheSignaletique Per; Per.Taille = 1.80;

Affichage du contenu du membre : printf("%d", Per.taille);

Saisie de la valeur du membre : scanf("%d",&Per.taille);

Remarque :

Les types des membres d'une structure peuvent être aussi bien des types prédéfinis que personnalisés y compris les structures.

Utilisation globale d'une structure

Il est possible d'affecter à une structure le contenu d'une autre structure définie à partir du même modèle. Par exemple si P1 et P2 sont deux variables de type FicheSignaletique

FicheSignaletique P1,P2; alors, il est possible d'écrire : P1 = P2;

Remarques:

• L'affectation globale n'est pas possible entre tableaux. Elle l'est par contre entre structures. Paradoxalement, il devient de ce fait possible en créant une structure contenant un seul champ de type tableau de réaliser une affectation globale entre tableaux.

• Deux structures ne peuvent pas être comparées d'une manière globale. Ainsi:

FicheSignaletique P1,P2; P1==P2 et P1>P2 sont deux instructions incorrectes.

Page 68: CoursPOOC++

Structures et énumérations Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti66

Initialisation des structures

L'initialisation d'une variable de type structure se fait à travers l'initialisation de ses membres. Cette initialisation s'effectue de la même manière que pour les tableaux.

Exemple :

struct FicheEtudiant {float taille, poids; char Nom[20]; }; FicheEtudiant Personne1 = {1.70, 80,"Ammar"}; FicheEtudiant Personne2 = {1.85, ,"Salah"};

• S'il y a moins de valeurs d'initialisation que de champs, les variables qui manquent seront automatiquement initialisées à 0. S'il y a plus de valeurs d'initialisation que de champs, le compilateur signale une erreur.

• Il est strictement interdit de définir une initialisation systématique d'un membre d'une structure (il faut séparer déclaration et initialisation).

Exemple :

struct FicheEtudiant {float taille; float poids =75; //error char Nom[20]; };

• Une constante symbolique peut avoir un type qui est défini à l'aide du mot réservé struct.

Exemple :

const FicheSignaletique P1={23,1.78,75,"Ali"};

Imbrication de structures

• Un membre d'une structure peut lui même être de type structure. Dans ce cas on parle de structures imbriquées.

Exemple:

struct Date { int jour; int mois; int annee; }; struct personne { char Nom[20]; char Prenom[20]; Date DateNaissance; };

ou également :

struct FicheEtudiant { float taille, poids; Date DateNaissance; char Nom[20]; };

• L'accès à la valeur du jour de naissance d'un étudiant est donnée par : FicheEtudiant Etu; Etu.DateNaissance.jour;

Initialisation d'une structure comportant une autre structure

• L'initialisation de la structure interne se fait en spécifiant les valeurs de ses membres sous forme d'un sous ensemble de la liste des valeurs des membres de la structure externe.

Page 69: CoursPOOC++

Structures et énumérations Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti67

Exemple :

FicheEtudiant P1 = {1.70, 70,{1,4,1985},"Ammar"};

• Il est possible d'omettre des valeurs pour certains membres. Ces derniers seront automatiquement initialisés à 0.

Exemple :

FicheEtudiant P2 = {1.85, 90,{18,6, }, };

Tableau de structures

Déclaration

Les éléments d'un tableau peuvent être de type structure. La déclaration de ce tableau se fait comme suit :

NomStructure NomTableau[Taille];

Accès au tableau

• L'accès au ième élément du tableau se fait comme suit : NomTableau[i];

• L'accès à un membre du ième élément du tableau se fait comme suit : NomTableau[i].NomMembre;

Exemple :

struct Point { int x; int y; };

Point Courbe[50];

• L'accès à l'abscisse du ième point de la courbe: Courbe[i].x • L'accès à l'ordonné du ième point de la courbe: Courbe[i].y

Utilisation de typedef avec les structures

• Le mot-clé typedef permet d'associer à un type particulier un nouveau nom plus explicite au sein d'un programme. Il ne définit en aucun cas un nouveau type.

• Formellement la syntaxe de définition d'un nouveau nom pour un type est :

Typedef NomTypeConnu NouveauNomType;

• La déclaration de variables de ce nouveau type s'effectue comme les déclarations usuelles.

Exemple 1 :

typedef int Chaise; Chaise Numero; // la variable numéro a pour type chaise. Son type de base est int.

typedef int vecteur[3]; Les deux déclarations suivantes sont équivalentes :int v1[3]; vecteur v2; // v1 et v2 sont deux tableaux de trois entiers

Exemple 2 : typedef struct FicheEtudiant { float taille, poids; Date DateNaissance; }; struct FicheEtudiant P;

typedef struct { float taille, poids; Date DateNaissance; } FicheEtudiant; FicheEtudiant P;

ou également

Page 70: CoursPOOC++

Structures et énumérations Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti68

Exemple 3 :

struct Date{int jour, mois, annee;}; typedef Date MatriceDate[5][5]; MatriceDate M; // M est une matrice 5X5 de type Date. M[1][2].jour=5;

Pointeurs sur des structures

Déclaration

Il est possible de déclarer des pointeurs sur des structures. Cette déclaration se fait comme suit : NomStructure* NomPointeur;

Exemple :

FicheEtudiant Etu; FicheEtudiant *pEtu; pEtu=&Etu;

Accès aux membres

L'accès aux membres de la structure à partir d'un pointeur peut se faire de deux manières :

(*NomPointeur).NomMembre ou également NomPointeur->NomMembre

Exemple :

Struct Point {int x; int y;}; Point P1; Point* PP; P1.x=10; P1.y=5; PP=&P1 printf(" l'abscisse de P1 est %d", (*pp).x); printf("l'ordonné de P1 est %d", pp->y);

Les énumérations

• Les énumérations représentent un outil supplémentaire pour la construction de type. • Une énumération est constituée d'un ensemble de constantes symboliques associées chacune à un numéro

entier.

Définition d'une énumération

• La définition d'une énumération se fait à l'aide du mot-clé enum de la manière suivante :

enum NomEnumération {NomValeur1, NomValeur2,…, NomValeurN};

• Chaque valeur de l'énumération est associée à son numéro d'ordre dans la liste. La première ayant la valeur entière 0, la deuxième la valeur 1 et la nième la valeur N-1.

Exemple 1 :

enum Couleur {blanc, noir, gris};

Exemple 2 :

enum Bool {false, true}; // construction du type booléen en C.

Page 71: CoursPOOC++

Structures et énumérations Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti69

Remarques :

• Il est possible de spécifier des valeurs à affecter aux différents éléments de l'énumération. Ces valeurs doivent respecter l'ordre spécifié dans la déclaration et donc être croissantes du premier élément de l'énumération au dernier.

enum Couleur{blanc=3, noir= 255, gris=600};

enum jours{lundi,mardi=7,mercredi, jeudi, vendredi = 25, samedi = 30, dimanche}; Lundi vaut 0, mardi vaut 7, mercredi vaut 8, jeudi vaut 9, vendredi vaut 25, samedi vaut 30, et dimanche vaut 31.

Déclaration d'une variable de type énumération en langage C

La déclaration d'une variable de type énumération se fait de la manière suivante :

enum NomEnumération NomVariable;

Exemple 1 :

enum Couleur {blanc, noir, gris}; enum Couleur cl=noir; //cl est une variable initialisée à noir

ou également : enum Couleur{blanc, noir,gris} cl; cl=noir;

Exemple 2 :

… … … enum Couleur{rouge, orange, vert}; enum Couleur Feu; // Déclaration d'une variable Feuif(Feu==rouge) printf("il faut stopper"); else if (Feu==vert) printf("vous pouvez passer"); else if (Feu==orange) printf("il faut commencer à freiner"); … … …

Déclaration d'une variable de type énumération en langage C++

En C++, il n'est pas nécessaire d'utiliser le mot réservé enum lors de la spécification du type de la variable. La déclaration de cette dernière se fait alors comme suit :

NomEnumération NomVariable;

Exemple:

… … … enum Couleur{rouge, orange, vert}; Couleur Feu; // Déclaration d'une variable Feu en C++.

Page 72: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 70

Les classes Introduction

Les entités du monde réel sont généralement assez complexes et ne peuvent être convenablement représentées dans les programmes à l'aide des types scalaires. Il faut leur construire de nouveaux types qui leurs sont adaptés. C++ met à la disposition des programmeurs un ensemble d'outils de construction de tels types. Comme exemple de ces outils, il est possible de citer :

• Les tableaux pour la représentation des ensembles d'éléments de même type.

• Les structures pour la représentation des entités complexes comportant plusieurs champs.

Considérons l'exemple suivant :

#include <iostream.h> struct Date {int jour, mois, annee;};

Date Saisir() { Date D; cout<<"Donnez le jour :"; cin>>D.jour; cout<<"Donnez le mois :"; cin>>D.mois; cout<<"Donnez l'année :"; cin>>D.annee; return D; }

void Afficher(Date D) { cout<<endl; cout<<D.jour<<'/'<<D.mois<<'/'<<D.annee; cout<<endl; }

Date DateRecente(Date D1, Date D2) { if( D1.annee<D2.annee) return D2; else if(D1.annee>D2.annee) return D1; else { if( D1.mois<D2.mois) return D2; else if(D1.mois>D2.mois) return D1; else { if(D1.jour<D2.jour) return D2; else return D1; } } }

Page 73: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 71

int main() { Date D1,D2; D1 = Saisir(); D2 = Saisir(); cout<<"la date la plus récente est"; Afficher(DateRecente(D1,D2)); Return 0 ; }

Les classes : un nouvel outil de construction de type en C++

Les deux fonctions utilisées dans le programme précédent ainsi que la structure Date

constituent des modules explicitement distincts même si sur le plan sémantique ils sont implicitement liés. En effet les deux fonctions Saisir et Afficher s'appliquent essentiellement à une structure de type Date. Il sera donc intéressant de définir une structure de données qui les regroupe en une seule entité.

C++ introduit un nouvel outil de construction de type de données, appelé classe, qui étend les possibilités des structures et qui offre entre autres cette possibilité de regroupement.

Une classe est un type réunissant :

• Une collection de données membres appelées attributs. Les attributs définissent la partie statique de la classe.

• Une collection de fonctions membres appelées méthodes et servant à faire des traitements sur les données membres. Les méthodes définissent la partie dynamique de la classe. On dit également qu'elles définissent le comportement de la classe.

Déclaration d’une classe

La déclaration d’une classe se fait comme suit : class NomClasse ;

Exemple :

class Date ;

class Personne ;

Attribut_1 Attribut_2 … … … Attribut_n

Méthode_1() Méthode_2() … … … Méthode_m()

CLASSE

Page 74: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 72

Définition d'une classe

La définition d'une classe désigne la spécification des attributs et des méthodes de la classe.

Règles syntaxiques de la définition :

• La définition commence par le mot-clé class.

• Ensuite, il y a lieu de déclarer les collections d'attributs et de méthodes. Ces déclarations doivent être placées entre deux accolades.

• La définition d'une classe se termine enfin par un point virgule.

Exemple :

class Date

{ int jour, mois, annee;

void Saisir();

void Afficher();

};

Instanciation d’une classe

• Une classe définit un nouveau type. De ce fait, il est possible de déclarer des variables ou des constantes ayant un type classe.

• Ces variables et constantes sont appelées des objets ou des instances de la classe.

• Le processus de création d’objets est appelé instanciation.

Exemple :

Date D; // D est un objet ayant le type classe Date.

Remarque 1 :

• Il n'est pas possible d'instancier une classe déclarée mais non encore définie. • Il est possible par contre de déclarer un pointeur vers une classe déclarée mais non encore

définie.

Remarque 2 : Déclaration des attributs d'une classe

Les attributs d'une classe peuvent être des variables ou des constantes. D'ailleurs leur déclaration se fait suivant la même syntaxe que les variables et les constantes classiques. Toutefois par rapport à ces dernières, les attributs se distinguent par le fait qu'au moment de leur déclaration dans la définition de la classe aucun espace mémoire ne leur est réservé. En effet la définition d'une classe constitue un processus de définition de type et non un processus de déclaration d'objet. De ce fait, il devient impossible d'initialiser un attribut au moment de la définition d'une classe.

Exemple :

class A

{ ………

int i = 5; // Erreur

};

Page 75: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 73

Remarque 3 : Type des attributs

Les attributs peuvent être de n'importe quel type (scalaire ou personnalisé y compris de type classe). Toutefois, un attribut ne peut pas être du type de la classe à laquelle il appartient (À cause de la récursivité dans la réservation de l'espace mémoire que cela engendre lors de l'instanciation de la classe).

Il est toutefois possible de déclarer un attribut de type pointeur sur la classe à laquelle appartient cet attribut.

Exemple :

Classe anonyme

Une classe anonyme est une classe qui ne porte pas de nom (sans nom). Elle sert essentiellement à faire des déclarations directes d'objets (dans la même instruction qui définit la classe).

Exemple :

class {……………} D; // D est un objet de type classe sans nom

Remarque :

Une instance d'une classe anonyme ne peut pas figurer dans la liste d'arguments d'une fonction (Il est impossible en effet de spécifier le type du paramètre formel puisqu'il est sans nom).

Fonctions membres : méthodes

Déclaration d'une méthode

La déclaration des méthodes se fait suivant la même syntaxe que la déclaration des fonctions classiques. Toutefois une méthode doit être obligatoirement déclarée à l'intérieur d'une classe.

Définition d'une méthode

Il existe deux façons pour définir une méthode de classe :

Première façon :

Elle consiste à définir la méthode au sein de la classe même.

Exemple :

class Date

{ int jour, mois, annee;

void Saisir() {…………………}

void Afficher(){…………………}

};

class A {………… A pa; // Erreur ………… };

class A {………… A *pa; // OK ………… };

Page 76: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 74

Deuxième façon :

Elle consiste à :

• déclarer la fonction à l’intérieur de la classe,

• la définir ensuite à l’extérieur de la classe. Il faut pour cela indiquer la classe d’origine de la fonction. Ceci se fait comme suit :

Syntaxe :

TypeRetour NomClasse::Methode(type1 arg1,…, typeN argN);

• :: est l’opérateur de résolution de portée.• NomClasse::Methode est appelé le nom qualifié de la méthode.

Exemple :

class Date

{

int jour, mois, annee;

void Saisir();

void Afficher();

};

void Date::Afficher(){…………………} // Définition de Afficher

void Date::Saisir(){…………………} // Définition de Saisir

Remarque :

• Une méthode définie à l’intérieur d’une classe est considérée par défaut comme étant inline.

• Une fonction définie à l’extérieur de la classe n’est pas considérée comme inline. Pour la rendre ainsi, il faut ajouter explicitement la spécification inline devant sa définition.

Exemple :

inline void Date :: Afficher()

{………………………… }

Encapsulation

• L'encapsulation est le mécanisme qui permet de regrouper les données et les méthodes au sein d'une même structure (classe). Ce mécanisme permet également à l'objet de définir le niveau de visibilité de ses membres.

• Un membre visible est un membre qui est accessible à partir de l'objet : o Si le membre est un attribut alors l'accès concerne une opération de lecture ou

d'écriture de la valeur de cet attribut. o Si le membre est une méthode alors l'opération d'accès consiste en un appel de cette

méthode.

Page 77: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 75

• Généralement un objet ne doit exposer (rendre visible) que les membres qu'il juge utiles pour ses utilisateurs. Il doit cacher tous les détails de son implémentation (corps des méthodes ainsi que les attributs et méthodes qui sont utilisés en interne).

Spécificateurs des droits d'accès aux membres

En C++, la spécification des droits d'accès aux membres d'une classe se fait à l'aide des trois mots-clés suivants : public, private et protected.

• public : ce spécificateur rend les membres d’une classe X (attributs ou méthodes) accessibles en dehors de la classe, généralement par les utilisateurs de cette dernière.

• private : ce spécificateur restreint l’accès aux membres de la classe (attributs et méthodes) aux méthodes de la classe seulement, ainsi qu’aux fonctions déclarées amies de la classe.

• protected : ce spécificateur a un effet similaire à private mais qui est toutefois moins sévère. En effet protected restreint l’accès aux membres d'une classe X aux méthodes de X, aux fonctions amies de X et aux méthodes des classes basées sur X.

Remarque :

Il est possible d’utiliser au sein d’une même classe différents spécificateurs d’accès. Ainsi, il est possible de rendre une partie de la classe publique et de cacher une autre partie en la qualifiant de privée.

Portée d’un spécificateur d’accès :

La portée d’un spécificateur d’accès s'étend depuis l'endroit de sa définition jusqu'à la rencontre de la définition d'un autre spécificateur.

Remarque : Spécificateur d'accès par défaut

Si aucun spécificateur d’accès n’a été défini dans une classe alors les membres de cette dernière sont qualifiés par défaut comme étant privés.

Exemple :

Tous les membres de la classe X sont considérés par défaut comme étant privés.

class X { int a1; char a2; void f1(); int f2(double k); };

class X { private : int a1; //privé char a2; //privé public : void f1(); //publique int f2(double k); //publique };

class X { public : int a1; //publique void f1(); //publique private : char a2; //privé public : int f2(double k); //publique };

Page 78: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 76

Accès aux membres d'une classe

Signification de l’accès

• Pour un attribut, l’accès consiste à faire une opération de lecture ou d’écriture dans cet attribut.

• Pour une méthode, l’accès consiste à faire un appel à cette méthode.

Accès de l'intérieur de la classe

• Les membres d'une classe sont locaux à cette dernière. Leur portée est donc définie par la classe dans laquelle ils sont déclarés.

• Tout membre d'une classe est visible par les autres membres de cette classe sans aucune considération des spécificateurs d'accès (public, private, protected).

• Les attributs d’une classe sont considérés comme des variables globales par rapport aux méthodes de cette classe. Tout attribut peut par conséquent être utilisé par n'importe quelle méthode de sa classe.

• Une méthode d'une classe peut être appelée par toute autre méthode de sa classe indépendamment de l'ordre de déclaration de ces dernières dans la classe.

• Les membres peuvent être manipulés directement par leurs noms sans avoir besoin d'aucune qualification.

Exemple :

class Date { int jour, mois, annee; public: void Saisir(); void Afficher(); };

void Date::Saisir() { cout<<"Donnez dans l’ordre le jour, le mois et l’année"; // Accès direct aux attributs par la méthode Saisir cin>>jour>>mois>>annee; }

void Date::Afficher() { cout<<endl; cout<<jour<<'/'<<mois<<'/'<<annee; cout<<endl; }

Le fait que les attributs jour, mois, annee soient directement accessibles par les méthodes Saisir et Afficher de la classe Date évite à ces dernières de prendre un paramètre de type Date

comme c’est le cas lorsqu’il s’agit de fonctions indépendantes (Cf. exemple avec les

structures).

Page 79: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 77

Accès de l'extérieur de la classe

• L'utilisation d'un membre d'une classe en dehors de cette dernière (par exemple par une fonction non membre de la classe) se fait généralement à partir d'une instance (objet).

• Le membre doit être associé à l'objet auquel il appartient. Cette association se fait à l'aide de l'opérateur dyadique (prend deux opérandes) point de la manière suivante :

o Pour les attributs : Objet.Attribut;

o Pour les méthodes : Objet.Methode(Paramètres effectifs); • Seuls les membres publiques peuvent être accédés de l'extérieur d'une classe. Les autres

membres (private et protected) ne sont pas accessibles de l'extérieur.

Exemple :

class Date

{ int jour, mois;

public:

int annee ;

void Saisir();

void Afficher();

};

Portée des classes et des objets

Portée d'une classe

Un type "classe" n’est visible que dans le bloc dans lequel il est déclaré. Ainsi :

• Toute classe déclarée dans une fonction n’est instanciable que dans cette fonction.

• Toute classe X déclarée à l’intérieur d’une classe Y n’est instanciable que dans Y.

• Une classe déclarée en dehors de tout bloc est une classe globale. Elle peut être instanciée n’importe où dans le programme.

Portée d’un objet

Les règles définissant la portée d’un objet sont les mêmes que celles définissant la portée des variables en C++. Ainsi un objet ne peut exister qu'à l'intérieur du bloc dans lequel il est déclaré ou dans les sous blocs de ce bloc.

void main() { Date D; D.jour = 5; // Erreur D.Mois = 10; // Erreur D.anne = 2004; // OK cout<<D.jour; // Erreur cout<<D.mois; // Erreur cout<<D.annee; // OK }

void main() { Date D; D.Saisir(); // OK D.Afficher(); // OK }

Page 80: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 78

Imbrication de classes

Une classe X peut avoir un membre de type une autre classe Y. Les deux classes sont dites dans ce cas des classes imbriquées.

Exemple :

class Personne

{ char Nom[20];

char Prenom[20];

Date Dn;

Public :

void Saisir();

void Afficher();

};

Date et Personne sont dans ce cas deux classes imbriquées.

Manipulation des attributs et des méthodes des classes imbriquées

On considère les classes Personne et Date définies ci-dessus. Nous allons discuter dans ce qui suit de la possibilité d'accéder aux champs de Date à partir de la classe Personne et ce en utilisant différents spécificateurs d'accès.

- Dn est un champ publique et ses champs sont publiques. Personne P;

P.Dn.Jour; // OK

- Dn est un champ privé et ses champs sont publiques. Personne P;

P.Dn.Jour; // Erreur

- Dn est un champ publique et ses champs sont privés. Personne P;

P.Dn.Jour; // Erreur

Les tableaux d'objets

Déclaration : TypeClasse NomTableau[NbElements] ;

Accès aux attributs d’un élément du tableau: NomTableau[indice].NomAttribut

Accès aux méthodes d’un élément du tableau: NomTableau[indice].NomMéthode(<args>)

Exemple :

… … … … … Date TD[5]; // Saisie de 5 dates for(int i=0;i<5;i++) T[i].Saisir(); // Affichage des dates for(int i=0;i<N;i++) T[i].Afficher(); … … … … …

Page 81: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 79

Pointeur sur un objet

Il est possible de déclarer un pointeur vers un objet. Ce pointeur doit être obligatoirement initialisé pour être utilisable. L'accès aux membres d'une classe à partir d'un pointeur se fait à l'aide de l'opérateur ->

Déclaration : TypeClasse* PtrObj;

Initialisation : PtrObj = new TypeClasse();

Accès aux attributs : PtrOpj->Atribut ou également : (*PtrOpj).Atribut

Accès aux méthodes : PtrObj->Methode(liste d'arguments)

ou également : *(PtrObj).Methode(liste d'arguments)

Le pointeur this

this est un pointeur particulier en C++ qui pointe toujours sur l’objet en cours d’utilisation. Ce pointeur possède plusieurs champs d'applications.

Utilisation implicite de this

Chaque objet d'une classe possède sa propre copie des attributs (sa propre zone en mémoire). Cette copie définit l'identité même de l'objet qui permet de le distinguer des autres. Toutefois, tous les objets se partagent la même copie des méthodes (tous les objets ont le même comportement).

Cette organisation en mémoire des objets peut engendrer un problème lors de l'appel des méthodes. En effet, le problème qui peut se poser est la suivant : en cas de présence de plusieurs objets d'une même classe, comment une méthode peut-elle connaître la copie des attributs sur laquelle elle doit travailler (car rappelons-le on peut manipuler à l'intérieur d'une méthode les attributs directement par leurs noms sans aucune qualification).

La résolution de ce problème passe par l'utilisation du pointeur this. En effet, toute méthode d'une classe prend d'une manière implicite un argument caché de type pointeur sur la classe à laquelle elle appartient. Ce pointeur est utilisé implicitement pour référencer les attributs à l'intérieur de la méthode. Il suffit alors d'affecter le pointeur this à cet argument au moment de l'appel de la méthode. Le référencement implicite des attributs devient alors : this->Attribut. Cette syntaxe élimine toute ambiguïté concernant la copie de l'attribut sur laquelle on doit travailler puisqu'elle désigne bien l'attribut de l'objet en cours d'utilisation.

Exemple :

La méthode Saisir définie comme suit : void Date::Saisir()

{ cout<<"Donnez dans l'ordre le jour, le mois et l'année";

cin>>jour>> mois>>annee;

}

est en réalité implicitement définie comme suit : void Date::Saisir(Date const* this)

{ cout<<"Donnez dans l'ordre le jour, le mois et l'année";

cin>>this->jour>>this->mois>>this->annee;

}

Page 82: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 80

Remarque :

Le pointeur this est en lecture seule. C'est le système qui lui affecte sa valeur pour un objet donné. Dans un programme, il n'est donc possible que de lire cette valeur mais pas la modifier. D'ailleurs ceci explique le fait que le this est déclaré comme un pointeur constant (Type const* this).

Exemple d'utilisation explicite du pointeur thisUn des problèmes qui peuvent être résolus par l'utilisation du pointeur this est celui qui se pose lors de l'utilisation d'une méthode qui possède des paramètres portant les mêmes noms que les attributs. Une ambiguïté de qualification se pose dans ce cas. Pour lever cette ambiguïté il suffit d'appeler les attributs de la classe à travers le pointeur this.

Exemple :

class A {

int x, y; public :

void f(int x, int y) { this->x=x; this->y=y; } };

Opérations applicables aux objets

Lecture de l’adresse et de la taille d'un objet

• Il est possible de récupérer l'adresse mémoire d'un objet et ce à l'aide de l'opérateur &.

• Il est possible de récupérer la taille en mémoire d'un objet et ce à l'aide de l'opérateur sizeof. Cette taille est égale à la somme des tailles des attributs de l'objet.

Exemple :

class CX {public :

int a1; int a2; void Affiche() {cout<<" a1: "<<a1<<" et a2: "<<a2;}

};

void main() { CX x;

cout<<"\n Adresse de x :"<<&x; cout<<"\n Taille de x : "<<sizeof(x);

}

Page 83: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 81

Affectation d’objets

Il est possible d’affecter d’une manière globale un objet O1 à un autre objet O2 de la même classe. La valeur de chaque attribut de O1 est alors copiée dans l’attribut qui lui correspond dans O2.

Exemple :

void main() { CX x1,x2; x1.a1=5; x1.a2=8; x2=x1; x2.Affiche(); x1.a1=18; x1.Affiche(); x2.Affiche(); }

Affectation entre pointeurs sur des objets

Il est possible de faire une affectation entre deux pointeurs sur des objets de même type. Toutefois l’utilisation de ces pointeurs doit se faire avec précaution.

Exemple :

void main() { CX *px1,*px2; px1=new CX(); // Objet sans nom px1->a1=5; px1->a2=8; px2=px1; // px1 et px2 pointent sur le même objet px2->Affiche(); px1->a1=18; px1->Affiche(); px2->Affiche(); delete px1; delete px2; // Erreur car l'objet a été déjà libéré }

Opérations interdites sur les objets

Il n’est pas possible d’appliquer aux objets, les opérateurs logiques et arithmétiques tels qu'ils sont définis par défaut en C++.

Exemple :

CX x1,x2 ; … x1+x2 ; // Erreur x1<x2 ; // Erreur

Page 84: CoursPOOC++

Les classes Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 82

Il n’est pas possible d’utiliser cin>> et cout<< pour faire la saisie et l’affichage d'une manière globale du contenu d’un objet.

Exemple :

CX x ; cin>> x ; // Erreur cout<< x ; // Erreur

Page 85: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 83

Constructeurs et destructeur

Initialisation d'objets

Il est possible d'attribuer des valeurs initiales aux attributs d'un objet à travers une liste de valeurs spécifiées entre deux accolades et séparées par des virgules (comme pour les tableaux).

Exemple :

class Point2D {public: int x; int y; };

class Point3D {public: int x; int y; int z; };

Point2D P1={5,6}; Point3D P2={5,6,9}; Point3D P3={4,,9} // le deuxième attribut est initialisé à 0. Point2D T[3] = {1,5,6,8,9,10}; Point2D T[3] = {{1,5},{6,8},{9,10}};

Limites de l'initialisation avec les listes de valeurs

• Il faut que tous les membres de l'objet à initialiser soient publiques. • Il faut que l'objet soit d'une classe de base et qu'il n'ait pas de membres virtuels. • Certaines opérations d'initialisation sont complexes et nécessitent plusieurs étapes comme

le montre l'exemple suivant :

Exemple : class TabEntiers { public : int Nb; int* T; };

Pour pouvoir initialiser le membre T, ce dernier doit auparavant être alloué.

Solution 1 : Passage par une méthode d'initialisation

Avantages :

• Possibilité de réaliser les opérations d'allocation pour les membres dynamiques. • Possibilité d'accéder aux membres privés et protégés.

Page 86: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 84

Exemple :

class TabEntiers { int Nb; int* T; public : void Init(int Ni) { Nb=Ni; T=new int [Nb]; } void Init(int* Ti,int Ni) { Nb=Ni; T=new int [Nb]; for(int k=0;k<Nb;k++) T[k]=Ti[k]; } } void main() { TabEntiers TE; TE.init(5); }

Inconvénients :

• Pour pouvoir utiliser l'objet, il est nécessaire d'appeler à chaque fois, d'une manière explicite, la méthode Init. Cet appel explicite peut engendrer des erreurs surtout dans les programmes de grande taille où le risque d'oubli devient élevé.

• L'utilisation de Init ne peut pas être considérée comme étant une initialisation au vrai sens technique du terme car une initialisation se fait généralement dans la même instruction que la déclaration.

Solution 2 : Utilisation des constructeurs du C++ Le C++ offre une solution de création et d'initialisation d'objets qui permet de remédier à tous les problèmes susmentionnés (initialisation au vrai sens du terme, appel implicite, initialisation des membres dynamiques avec allocation de mémoire,…).

Les constructeurs

Définition

Un constructeur est une méthode particulière d'une classe qui s'exécute automatiquement d'une manière implicite, lors de la création d'un objet de cette classe.

Utilisation des constructeurs Rôle de base : (implicite, transparent par rapport aux programmeurs)

• Un constructeur d'une classe assure la réservation de l'espace mémoire nécessaire à la création de tout objet de cette classe.

• L'espace dont on parle ici désigne l'espace de base nécessaire à la création de l'objet. Il ne comprend pas les espaces dynamiques associés aux membres dynamiques éventuels de la classe.

Page 87: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 85

Exemple :

Pour la classe TabEntiers définie précédemment, l'espace de base nécessaire à la création d'un objet est égale à la somme des tailles de Nb et du pointeur T. Il ne comprend pas la taille de la zone mémoire pointée par T (sizeof(Nb)+sizeof(T)).

Exploitation usuelle des constructeurs : (explicite, effectuée par les programmeurs)

Les constructeurs sont généralement exploités par les programmeurs pour réaliser les opérations suivantes :

• Initialisation des attributs de la classe (publiques, privés ou protégés).

• Allocation des espaces mémoires nécessaires aux membres dynamiques de la classe.

Déclaration et définition d'un constructeur

• Un constructeur est une méthode de la classe.

• Un constructeur doit porter le même nom que celui de sa classe.

• Un constructeur peut admettre 0, un ou plusieurs paramètres.

• Un constructeur ne retourne aucune valeur. (le fait de ne pas mettre un type de retour devant le constructeur ne signifie pas que ce dernier retourne par défaut un entier (int) comme c'est le cas pour les méthodes classiques).

• Une classe peut comporter un ou plusieurs constructeurs. Dans le cas d'existence de plusieurs constructeurs, ces derniers doivent respecter les règles de surcharge des méthodes.

• Comme toute méthode un constructeur peut être défini à l'intérieur de la classe ou à l'extérieur.

Syntaxe de déclaration d'un constructeur : NomClasse (<paramètres>);

Exemple :

class Time { int Hour; int Minute; int Second; public : Time(int H,int M, int S) { Hour = H; Minute = M; Second = S; } void Afficher() { cout<<"L'heure est : "; cout<<Hour<<':'<<Minute<<':'<<Second<<endl; } };

Page 88: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 86

Droits d'accès aux constructeurs

• Les spécificateurs d'accès public, private et protected s'appliquent aux constructeurs de la même manière que pour n'importe quelle méthode de classe.

• Toutefois, pour qu'un utilisateur d'une classe puisse appeler un constructeur et par suite créer un objet, ce constructeur doit être obligatoirement publique.

Appel d'un constructeur

Toute construction d'un objet d'une classe doit être accompagnée obligatoirement par un appel à son constructeur.

Cas d'un objet de type auto

La création d'un objet de type auto peut se faire à travers un appel explicite ou implicite du constructeur.

Appel explicite :

Le constructeur est appelé par son nom comme une méthode classique.

NomClasse NomObjet = NomClasse(<paramètres effectifs>);

Time T = Time(16,30,25);

Appel implicite :

NomClasse NomObjet(<paramètres effectifs>);

Time T(16,30,25);

Cas d'un objet dynamique Pour la création dynamique d'objets, seul l'appel explicite du constructeur est possible.

NomClasse* ptrObjet = new NomClasse(<paramètres effectifs>);

Time* T = new Time(16,30,25);

Exemple :

Time T1(16,30,25); Time* T2 = new Time(17,44,59); T1.Affiche(); T2->Affiche();

Page 89: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 87

La surcharge des constructeurs

• Il est possible de définir plusieurs constructeurs pour une même classe. Ces constructeurs sont alors dits surchargés.

• Les constructeurs d'une même classe doivent respecter les règles de surcharge des fonctions en C++. En d'autres mots, ils doivent avoir des signatures différentes.

Exemple :

class Time { int Hour; int Minute; int Second; public : Time(int H,int M, int S) { Hour = H; Minute = M; Second = S; }

Time() { }

void Saisir() { cout<<"Donner l'heure : " cin>>Hour>>Minute>>Second; }

void Afficher() { cout<<"L'heure est : "; cout<<Hour<<':'<<Minute<<':'<<Second<<endl; } }; void main() { Time T1; T1.Saisir(); Time T2(16,15,30); T1.Afficher(); T2.Afficher(); }

La classe Time dispose de deux constructeurs : un paramétré et un autre qui ne l'ai pas. Ces deux constructeurs respectent la règle de surcharge de méthodes (ils ont deux signatures différentes).

Remarque 1 :

Si une classe dispose de plusieurs constructeurs alors au moment de la création d'un objet à partir de cette classe il y aura appel à un parmi ces constructeurs. Dans l'exemple ci-dessus, la création de l'objet T1 est faite à l'aide du constructeur sans arguments. Cet objet est non initialisé. Son contenu sera par la suite saisi avec la méthode Saisir. La création de l'objet T2 est faite à l'aide du constructeur paramétré. Son contenu est initialisé à 16h15mn30s.

Page 90: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 88

Remarque 2 :

• Tout objet ne peut être créé qu'à travers un appel à un constructeur. Par conséquent, ce sont les constructeurs définis dans une classe qui déterminent la manière avec laquelle peuvent être instanciés les objets à partir de cette classe.

• Dans l'exemple ci-dessus la classe Time dispose de deux constructeurs et offre donc deux façons pour instancier les objets (celles utilisées respectivement par T1 et T2). A titre d'exemple, Si le constructeur sans arguments Time() n'était pas défini dans la classe Time

alors l'instanciation Time T1; serait syntaxiquement incorrecte (erreur de compilation).

Remarque 3 :

• Il est toujours recommandé de définir plusieurs constructeurs dans une classe et ce pour offrir aux utilisateurs de cette dernière plus de scénarios possibles et une plus grande souplesse concernant l'instanciation des objets.

• Par exemple la classe Time, qui constitue un type personnalisé, a été conçue de façon à offrir les mêmes scénarii d'instanciation que ceux offerts par les types standards du langage (int, char, float, …) concernant la création des variables. Ainsi pour le type int par exemple il est possible de créer une variable initialisée ou une variable non initialisée et laisser la main à l'utilisateur du programme pour lui affecter une valeur. Ces deux scénarios sont également possibles pour le type Time grâce aux constructeurs définis dans cette classe comme le montre le tableau suivant :

Création sans initialisation puis

saisie de valeur Création et initialisation

int

(Type standard) int i1; cin>>i1,

int i2 =5;

Time

(Type personnalisé) Time T1; T1.Saisir();

Time T2(16,15,30);

Le destructeur

Un destructeur joue le rôle symétrique du constructeur. Il est automatiquement appelé pour libérer l’espace mémoire de base nécessaire à l’existence même de l’objet (espace occupé par les attributs). Cette libération est implicite. Elle est effectuée par tout destructeur.

Il est généralement recommandé d’exploiter le destructeur pour faire :

• La libération d’éventuels espaces mémoires dynamiques associés aux attributs de l’objet. Contrairement à la libération de l’espace de base, cette libération ne s’effectue pas d’une manière automatique. Elle doit être faite d’une manière explicite par le programmeur.

• La fermeture d’éventuels fichiers utilisés par l’objet.

• L’enregistrement des données, etc.

Déclaration et définition d’un destructeur :

• Un destructeur porte le même nom que celui de la classe mais précédé d’un tilde ~ (pour le distinguer du constructeur).

• Un destructeur ne retourne aucune valeur.

Page 91: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 89

• Un destructeur ne prend jamais de paramètres.

• Un destructeur est une méthode de la classe. Toutes les règles qui s’appliquent aux méthodes (portée, qualification d’accès, déclaration, définition) s’appliquent donc également au destructeur.

• Une classe ne peut disposer que d’un seul destructeur. Il est toujours non paramétré.

Exemple 1 :

class Time { int Hour; int Minute; int Second; public : Time () { .... } Time (int H, int M, int S) { .... } Time (const char* str) { .... } ~Time() {} // Destructeur vide Afficher() { .... } } ;

La classe Time possède des attributs de type auto. Donc la libération d'un objet de cette classe ne demande aucun traitement supplémentaire particulier. Par conséquent son destructeur est défini vide.

Appel du destructeur :

Un destructeur peut être appelé d’une manière implicite ou explicite.

Appel explicite :

NomObjet.~NomClasse(); ou PointeurObjet->.~NomClasse();

Cet appel reste toutefois rare d’utilisation.

Appel implicite :

Un destructeur est implicitement appelé lorsque la durée de vie de l’objet est écoulée. Ceci est le cas :

• Pour un objet local de la classe mémoire auto, lorsque le domaine de validité de l’objet (bloc d’instructions dans lequel il est déclaré) est quitté.

• Pour un objet global ou local de la classe mémoire "static" lorsque le programme se termine.

• Pour un objet dynamique créé par new lorsque l’opérateur delete est appliqué au pointeur qui le désigne.

Page 92: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 90

Exemple :

L'exemple suivant montre les moments d'appel du constructeur et du destructeur pour les objets de type auto et dynamique.

class X { char NomObjet[20]; public : X(char* NomObj) { strcpy(this->NomObjet, NomObj); cout<<"Exécution du constructeur de l'objet : "<<this->NomObjet<<endl; } ~X() { cout<<"Exécution du destructeur de l'objet : "<<this->NomObjet<<endl;} };

void main() { X x1("x1"); X* x2 = new X("x2"); delete x2;

}

Une exécution de ce programme va engendrer la sortie suivante : Exécution du constructeur de l'objet : x1 Exécution du constructeur de l'objet : x2 Exécution du destructeur de l'objet : x2 Exécution du destructeur de l'objet : x1

Destructeur par défaut

Si aucun destructeur n’a été explicitement défini pour une classe, alors le compilateur en génère un par défaut, public. Ce destructeur réalise par défaut la libération de l’espace mémoire de base (des attributs). Il est par conséquent toujours défini vide de la manière suivante : ~NomClasse(){}

Remarque :

Le destructeur généré par défaut convient généralement aux classes simples (Exemple : la classe Time). Toutefois, les classes complexes qui demandent des traitements supplémentaires lors de leurs libérations nécessitent une définition explicite et personnalisée de leur destructeur comme le montre le paragraphe suivant.

Application : intérêt de la personnalisation des constructeurs et du destructeur

Les constructeurs et le destructeur sont des outils offerts par la POO qui, lorsqu'ils sont bien exploités, peuvent automatiser le déroulement de certaines opérations complexes d'initialisation et de libération d'objets et engendrer par conséquent un code d'utilisation des classes à la fois simple et sûre. L'exemple suivant qui a pour objectif de définir une classe représentant un tableau dynamique d'entiers (TabEntiers) illustre ce cas de figure.

Page 93: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 91

Première version de la classe TabEntiers On considère la première définition de la classe TabEntiers.

class TabEntiers { int Nb; int* T; public : void Saisir() { for(int k=0;k<Nb;k++) { cou<<"Donner l'élément d'indice "<<k; cin>>T[k]; } } void Afficher() { for(int k=0;k<Nb;k++) { cout<<T[k]<<' '; } } };

La classe TabEntiers ne dispose d'aucun constructeur ou destructeur explicitement définis. Ces derniers seront alors automatiquement générés par le compilateur. Ils ont respectivement les définitions suivantes : TabEntiers(){} ~TabEntiers(){}

Avec de telles définitions, la fonction principale suivante engendrera une erreur au moment de l'exécution.

void main() { TabEntiers TE; // Appel implicite du constructeur par défaut TE.Saisir(); TE.Afficher(); }

L'erreur survient exactement lors de l'exécution de la méthode Saisir et plus précisément au niveau de l'instruction cin>>T[k]. Elle est due à l'absence d'allocation dynamique de l'attribut T de l'objet TE.

Puisque tout objet de TabEntiers nécessite obligatoirement une allocation dynamique de l'espace mémoire pour l'attribut T alors il est possible d'automatiser cette opération en plaçant le code qui la réalise dans le constructeur de la classe. La deuxième version de la classe TabEntiers met en œuvre cette recommandation.

Page 94: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 92

Deuxième version de la classe TabEntiers class TabEntiers { int Nb; int* T; public : TabEntiers(int Ni) { Nb=Ni; T=new int [Nb]; }

TabEntiers(int* Ti,int Ni) { Nb=Ni; T=new int [Nb]; for(int k=0; k<Nb; k++) { T[k]=Ti[k]; } } void Saisir() { for(int k=0;k<Nb;k++) { cou<<"Donner l'élément d'indice "<<k; cin>>T[k]; } }

void Afficher() { for(int k=0;k<Nb;k++) { cout<<T[k]<<' '; } } };

Cette version de la classe TabEntiers dispose de deux constructeurs explicitement définis : le premier permet de créer un objet convenablement alloué en mémoire mais non initialisé. Son contenu peut être saisi avec la méthode Saisir. Le deuxième constructeur permet de créer un objet directement initialisé avec des entiers stockés dans un tableau passé comme argument. Les deux fonctions principales suivantes illustrent l'utilisation de ces deux constructeurs.

Problème de fuite de mémoire :

L'exécution de chacune des deux fonctions principales en utilisant la classe TabEntiers telle qu'elle est définie dans sa deuxième version engendre une fuite de mémoire qui concerne le tableau pointé par l'attributs T de l'objet TE. En effet cet espace étant dynamiquement créé par new ne sera pas libéré à travers l'appel (implicite dans ce cas) du destructeur de la classe (destructeur par défaut en l'absence d'une définition explicite de ce dernier).

void main() { TabEntiers TE(2); TE.Saisir(); TE.Afficher(); }

void main() { int Ti[3]={21,3,45}; TabEntiers TE(Ti,3); TE.Afficher(); }

Page 95: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 93

Cet espace ne peut en effet être libéré de la mémoire qu'à travers un appel explicite à delete de la manière suivante: delete TE.T;

Toutefois un tel appel n'est possible que si T est un attribut publique ce qui n'est pas le cas ici. Même si T était publique, il serait déconseillé de laisser la tâche de libération de la mémoire à l'utilisateur de la classe à cause du risque d'oubli. Pour remédier à ce problème, il serait plus astucieux de placer le code de libération dans le destructeur ce qui conduit vers la version finale de la classe TabEntiers.

Version finale de la classe TabEntiers

La classe TabEntiers utilise en plus de l'espace mémoire nécessaire pour les attributs un espace mémoire dynamique (tableau pointé par T) qui est alloué à chaque création d'un objet. Pour automatiser la libération de cet espace il suffit de définir explicitement le destructeur et d'y placer les instructions qui réalisent cette opération comme suit : ~TabEntiers(){delete[] T;} //Destructeur

La version finale de la classe TabEntiers sera alors comme suit :

class TabEntiers { int Nb; int* T; public :

TabEntiers(int Ni) { Nb=Ni; T=new int [Nb]; }

TabEntiers(int* Ti,int Ni) { Nb=Ni; T=new int [Nb]; for(int k=0; k<Nb; k++) { T[k]=Ti[k]; } } ~TabEntiers() { delete[]T; }

void Saisir() { for(int k=0;k<Nb;k++) { cou<<"Donner l'élément d'indice "<<k; cin>>T[k]; } }

3

FFE4D8A

int Nb

int* T 3 4521

TE

3 4521

Etat de la mémoire durant le main Etat de la mémoire suite à l'appel du destructeur par défaut après la fin du main

Page 96: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 94

void Afficher() { for(int k=0;k<Nb;k++) { cout<<T[k]<<' '; } } }; void main() { TabEntiers TE(2); TE.Saisir(); TE.Afficher(); }

L'instanciation de l'objet TE fait appel au premier constructeur de la classe TabEntiers. Ce dernier va allouer de l'espace mémoire pour les deux attributs ainsi que le tableau dynamique. TE étant un objet de type auto, son destructeur (explicitement défini dans ce cas) sera automatiquement appelé à la fin du main. Ce dernier va commencer par libérer l'espace mémoire dynamique puis les attributs.

Intérêt d'une bonne exploitation du constructeur et du destructeur

• Le fait de placer les instructions d'allocation et de libération dynamiques de la mémoire pour l'attribut T respectivement dans le constructeur et le destructeur de la classe TabEntiers permet d'automatiser le déroulement de ces opérations pour tout objet de cette classe.

• Une telle conception de la classe permet donc d'obtenir un code d'utilisation simple (voir le main) dans lequel toutes les opérations complexes (relatives à la gestion dynamique de la mémoire dans ce cas) sont définies une seule fois au moment de la construction de la classe et se déroulent d'une manière transparente (automatique et cachée) pour chaque objet instancié à partir de cette classe.

Constructeurs particuliers Il existe en C++ trois types de constructeurs qui sont un peu particuliers. Leur particularité provient du fait qu'ils peuvent être appelés automatiquement d'une manière parfois cachée par le compilateur. Ces constructeurs sont : • le constructeur par défaut, • le constructeur de recopie, • le constructeur de transtypage.

Constructeur par défaut

Définition

On appelle constructeur par défaut tout constructeur qui peut être appelé sans lui passer d'arguments.

Page 97: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 95

Exemple :

#include <iostream.h> #include <stdlib.h> #include <iomanip.h>

class Time { int Hour; int Minute; int Second;

public : Time() // Constructeur qui initialise l'heure par défaut à Midi {Hour = 12; Minute = 0; Second = 0; } Time(int H, int M, int S) {Hour = H; Minute = M; Second = S;} void Affiche() { cout<<"L'heure est : "; cout<<setw(2)<<setfill('0')<<Hour<<':'

<<setw(2)<<setfill('0')<<Minute<<':' <<setw(2)<<setfill('0')<<Second<<endl; } }; … Time T; // T contient Hour=12, Minute=0, Second=0;

Remarque :

La définition donnée au début de ce paragraphe permet de considérer comme étant un constructeur par défaut :

• un constructeur qui ne possède aucun paramètre,

• un constructeur qui possède des paramètres ayant tous des valeurs par défaut. En effet, un tel constructeur peut être appelé sans qu'il nécessite aucun passage explicite de paramètres (cf. chapitre les fonctions en C++). Ce type de constructeurs doit être utilisé avec précaution car il peut poser dans certains cas une ambiguïté au moment de l'instanciation des objets comme le montre l'exemple suivant.

Exemple :

Considérons la définition suivante de la classe Time :

class Time { int Hour; int Minute; int Second; public : // Constructeur qui initialise l'heure par défaut à Midi Time() { Hour = 12; Minute = 0; Second = 0; }

Page 98: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 96

// Constructeur ayant des paramètres qui ont tous des valeurs par // défaut Time(int H=12,int M=0, int S=0) { Hour = H; Minute = M; Second = S; } };

Au moment de la définition de la classe aucun problème d'ambiguïté ne se pose car les deux constructeurs ont des signatures différentes. Toutefois l'ambiguïté peut être rencontrée au moment de l'instanciation. Considérons par exemple l'instanciation suivante : Time T;

Suite à cette instruction, le compilateur signale une erreur car il ne peut pas déterminer quel constructeur appeler pour la création et l'initialisation de T. En effet, tout comme le constructeur sans paramètres, le constructeur paramètré ayant des valeurs par défaut peut être appelé sans aucun argument.

Constructeur par défaut implicite

• Si aucun constructeur n'a été explicitement défini pour une classe, alors le compilateur génère automatiquement un, publique.

• Ce constructeur est un constructeur par défaut (sans paramètres).

• Ce constructeur assure seulement la création d'objets. Il ne fait aucune initialisation. De ce fait si un objet créé à l'aide de ce constructeur est :

o Local : alors son contenu sera aléatoire.

o statique ou global : alors tous ses attributs seront initialisés à 0.

Exemple : class Time { int Hour; int Minute; int Second; public : Affiche(){ .... } }; Time T1; // objet global void main() { static Time T2; // objet static Time T3; // objet local de type auto T1.Affiche(); // 00:00:00 T2.Affiche(); // 00:00:00 T3.Affiche(); // -2145455875:-1245652381:-2896574215 }

Remarque importante :

Le compilateur ne génère aucun constructeur par défaut pour les classes qui possèdent au moins un constructeur explicitement défini, peu importe que ce constructeur soit de type par défaut ou non.

Page 99: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 97

Exemple :

class Time { int Hour; int Minute; int Second; public : Time (int H, int M, int S){ .... } Affiche(){ .... } }; void main() { .... Time T; // Erreur pas de constructeur par défaut .... }

Constructeur de recopie (par copie)

Définition

• Un constructeur de recopie est un constructeur qui permet de créer un objet et de l'initialiser avec le contenu d'un autre objet de la même classe.

• L'utilisation des constructeurs de recopie revête d'une grande importance surtout avec les objets qui possèdent des attributs dynamiques.

Caractéristiques syntaxiques

• Le premier paramètre formel du constructeur de recopie est une référence à un objet de la classe du constructeur.

• Un constructeur de recopie peut avoir d'autres paramètres supplémentaires. Ces derniers doivent obligatoirement avoir des valeurs par défaut.

• Un constructeur de recopie est toujours appelé avec un seul argument (le premier argument). L'appel des arguments supplémentaires éventuels se fait d'une manière implicite.

Syntaxe générale :

Exemple :

class Time { ... public : Time(const Time& T){...} // Constructeur de recopie ... };

Remarque 1 : Pourquoi un premier paramètre de type référence ?

La question à laquelle répond cette remarque est la suivante : pourquoi passe-t-on au constructeur de recopie une référence à l'objet à copier et non une copie de cet objet ? En d'autres mots, pourquoi utilise-t-on un passage par référence là où le passage par valeur semble suffisant?

NomClasse(<const> NomClasse& Param1<,Type2 Param2=DefVal2,..,Type_n Param_n=DefVal_n>);

Page 100: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 98

Justification du passage par référence :

Considérons la classe Time définie précédemment. On ajoute à cette classe un constructeur de recopie qui prend un paramètre Tm de type Time.

class Time { ... public : Time(){...} Time(Time Tm){...} // On suppose que c'est possible ... };

Soient f une fonction qui prend un paramètre Tf de type Time et X une instance de Time : void f(Time Tf){...} Time X;

L'appel de f avec le paramètre effectif X engendre la copie de Xdans le paramètre formel Tf. Cette copie est réalisée avec le constructeur de recopie de Time.

f(X) // Tf=X ⇔ Tf(X) Appel au constructeur de recopie de Time

et par suite :

f(X) ⇔ f(Time(X)) // instanciation du paramètre formel à l'aide du // constructeur de recopie.

Le passage de X au constructeur Time se fait par valeur. Ce constructeur va travailler donc sur une copie de X. Cette copie sera créée par un autre appel au constructeur de recopie. Ce dernier travaillant lui-même sur une copie de X, il va faire lui aussi un appel à lui-même. Cet appel récursif du constructeur de recopie va se poursuivre à l'infini.

Time(X) // Tm=X ⇔ Tm(X) et par suite Time(X) devient Time(Time(X)) et f(X) devient f(Time(Time(Time ………)))

Pour résoudre ce problème il suffit d'éviter l'appel implicite du constructeur de recopie qui est engendré par le passage par valeur et de remplacer ce dernier par un passage par référence. Ainsi avec Time(Time& Tm), f(X)se réduit seulement à f(Time(X)) car l'affectation du paramètre effectif au paramètre formel (Time& Tm=Time(X) ) représente une copie de références et non une copie d'objets.

Remarque 2 : Pourquoi la recommandation const

L'utilisation d'une référence constante comme premier paramètre dans les constructeurs de recopie est une recommandation. L'intérêt de cette recommandation réside dans la protection de l'objet passé comme argument contre toute modification qui peut survenir par erreur dans le constructeur surtout que son passage se fait par référence (on ne travaille pas sur une copie de l'objet mais directement sur ce dernier).

Constructeur de recopie par défaut

Considérons l'exemple suivant :

Exemple :

class Time { int Hour, Minute, Second;

Page 101: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 99

public : Time(){...} Time(int H,int M, int S){...} void Afficher(){...} };

void main() { Time T1(10,12,15); Time T2(T1); T2.Afficher(); }

L'instruction Time T2(T1); représente un appel du constructeur de recopie de la classe Time. Cette instruction passe avec succès la phase de compilation malgré l'absence d'une définition explicite d'un tel constructeur dans la classe. Par ailleurs, l’exécution de ce programme donne 10 :12 :15. Ce résultat montre bien que l’objet T2 a été initialisé avec le contenu de T1.

Explication :

• En cas d'absence d'un constructeur de recopie explicitement défini, le compilateur en génère automatiquement un par défaut.

• Ce constructeur effectue une copie champ par champ du contenu des deux objets.

Limites du constructeur de recopie par défaut

Exemple 1 :

class TabEntiers {public :

int Nb; int* T; TabEntiers(int Ni){...} TabEntiers(int* Ti,int Ni){...} void Saisir(){...} void Afficher(){...} }; void main() { int Ti[4]={5,2,8,1,3}; TabEntiers TE1(Ti,4); TabEntiers TE2(TE1); TE2.Afficher(); }

Commentaires :

La classe TabEntiers ne dispose pas d'un constructeur de recopie explicitement défini. Par suite c'est le constructeur de recopie par défaut qui est utilisé dans l'instruction : TabEntiers TE2(TE1);

De par sa définition, ce constructeur effectue une copie champ par champ du contenu de TE1

dans TE2. De ce fait, pour le membre dynamique T, c'est une copie d'adresses qui est effectuée entre TE1.T et TE2.T. Ces deux membres font alors référence à la même zone en mémoire et par conséquent toute modification de la ième case du tableau TE1.T est effectuée également sur la ième case du tableau TE2.T (à cause de la copie incomplète d'objets).

Page 102: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 100

Pour remédier à cela, il faut que le constructeur de recopie soit défini de façon à ce que chaque objet ait sa propre copie du tableau.

L'exemple précédent montre bien que le constructeur de recopie par défaut n’est pas adapté aux objets possédant des membres dynamiques. Pour ce genre d'objets il faut définir explicitement un constructeur qui tient compte de cette particularité.

Exemple 2 :

Soit la fonction f suivante : void f(TabEntiers TabE); Tout appel de la fonction f avec un paramètre effectif TE va engendrer la création d'un objet local TabE par recopie par défaut à partir de TE. La fin de l'exécution de f va engendrer la suppression de l'objet local TabE par appel à son destructeur. Cet appel va conduire vers la libération du tableau d'entiers pointé dans ce cas à la fois par TabE.T et par TE.T. Par conséquent, toute référence à un élément du tableau T à partir de TE après l'appel de f va engendrer un erreur d'exécution.

Solution : Il faut que la copie se fasse d'une manière totale : copie d'attributs mais également copie des éventuels espaces mémoires dynamiques pointés par les attributs de type pointeur. Pour remédier à ce problème de copie dans la classe TabEntiers, il faut définir explicitement le constructeur de recopie de la manière suivante :

TabEntiers::TabEntiers(const TabEntiers& Tc) { Nb=Tc.Nb; T=new int [Nb]; for(int k=0;k<Nb;k++) T[k]=Tc.T[k]; }

5

FFE4D8A

int Nb

int* T

2 1 8 5 3

TE15

FFE4D8A

TE2int Nb

int* Tcopie

5

FFE4D8A

int Nb

int* T

2 1 8 5 3

TE5

FFE4D8A

TabEint Nb

int* Tcopie

5

FFE4D8A

int Nb

int* T

TE

Etat de la mémoire après la copie du paramètre effectif dans le paramètre formel

Etat de la mémoire après la fin de l'appel de f. Le paramètre formel est détruit. Il libère également le tableau.

TE.T ne pointe plus sur aucune zone mémoire allouée.

5

FFE4D8A

int Nb

int* T

2 1 8 5 3

TE15

FFF5E33

TE2int Nb

int* T

Copie

Chaque objet possède sa propre copie du tableau dynamique d'entiers.

2 1 8 5 3 Copie des éléments

Page 103: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 101

Lieux d’appel d’un constructeur de recopie d’objet

Un constructeur de recopie est appelé :

• Explicitement par un utilisateur de la classe lors de la création d’un objet à l’aide d’un autre objet de la même classe.

• Implicitement par le compilateur :

o lors de la transmission de la valeur d’un paramètre effectif (de type objet) à un paramètre formel suite à l’appel d’une fonction.

o lors du retour d’une valeur de type objet par une fonction. En effet la fonction crée un objet temporaire sans nom à l’aide du constructeur de recopie et ce, à partir de l’objet passé à return et c’est cet objet temporaire qui est affecté à la variable recevant la valeur de retour de la fonction.

Exemple :

class NoData { public : NoData(){cout<<"constructeur par défaut";} NoData(const NoData& ND){cout<<"constructeur de recopie";} };

void f1(NoData D){}

void f2(NoData& D){}

NoData f3() { NoData D; ... return D; };

void main() { NoData X; // Constructeur par défaut f1(X); // Constructeur de recopie (en raison du passage par //valeur) f2(X); // Rien n'est affiché X=f3(); // Constructeur par défaut (pour l'objet local D) // Constructeur de recopie (pour l'objet renvoyé) }

Question :

Le prototype suivant pour f3 : NoData& f3() permet-il d’aboutir à un résultat correct même en l'absence d'un constructeur de recopie explicite ?

Le constructeur de recopie n'est pas appelé mais on obtient une référence sur un objet qui n'existe plus en mémoire puisqu'il est local à la fonction.

Page 104: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 102

Les constructeurs de transtypage

Un constructeur de transtypage, appelé également constructeur de conversion, est un constructeur qui permet de créer et d'initialiser un objet d'un type T1 à partir d'une donnée (objet ou non) d'un autre type T2 différent de T1.

Caractéristiques du constructeur de transtypage

• Un constructeur de transtypage ne peut être appelé qu'avec un seul paramètre : comme c'est le cas du constructeur de recopie, les paramètres qui suivent éventuellement le premier paramètre formel doivent avoir obligatoirement des valeurs par défaut.

• Le premier paramètre du constructeur de transtypage contient la donnée à convertir. Ce paramètre peut être de n'importe quel type (préfini, struct, class, etc.) sauf le type de la classe du constructeur.

Syntaxe générale :

Exemple :

class Time { int Hour; int Minute; int Second; public : Time(){Hour = 12; Minute = 0; Second = 0;} Time(int H,int M, int S){Hour = H; Minute = M;Second = S;}

Time(const char* str) {cout<<"Appel du constructeur de transtypage"; Hour = 10*(*str-'0') + (*(str+1)-'0'); Minute = 10*(*(str+3)-'0') + (*(str+4)-'0'); Second = 10*(*(str+6)-'0') + (*(str+7)-'0'); } void Afficher(){...} };

Le constructeur Time(const char* str) peut être considéré comme étant un constructeur de transtypage puisqu'il répond aux deux caractéristiques de ce type de constructeur à savoir un appel à l'aide d'un seul paramètre en plus le paramètre possède un type différent de Time(const char*).

Champs d'utilisation des constructeurs de transtypage

Utilisation explicite :

Un constructeur de transtypage peut être explicitement appelé par le programmeur à chaque fois que ce dernier a besoin de travailler avec un objet d'un certain type et qu'il dispose d'une donnée d'un autre type.

Exemple

Time X = Time("18:30:12"); ou également Time X("18:30:12");

NomClasse(<const> Type1 Param1<,Type2 Param2=DefVal2,..,Type_n Param_n=DefVa_n>);

Page 105: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 103

Utilisation implicite :

Un constructeur de transtypage d'un type T1 (objet ou non) vers un type objet T2 différent de T1 est implicitement appelé par le compilateur :

• Lors de la transmission d'un paramètre effectif de type T1 à une fonction ayant un paramètre formel de type T2.

• Dans une opération d'affectation ayant un membre de droite (objet, variable ou expression) de type T1 et un membre de gauche de type objet T2.

Remarque :

L'appel implicite du constructeur de transtypage par le compilateur engendre la création d'un objet temporaire sans non ayant le type de la classe du constructeur. C'est cet objet qui est alors transmis s'il s'agit d'un appel de fonction ou affecté s'il s'agit d'une opération d'affectation.

Exemple 1 :

Soit f une fonction ayant le prototype : void f(Time Tf);

Considérons la portion de code suivante :

… … … char T[9] ="18:30:00"; f(T); … … …

A première vue, cet appel peut sembler incorrect car f prend un argument de type Time et Test de type chaîne de caractères. Cependant et grâce au constructeur de transtypage cet appel est tout à fait correct. En effet, au moment de la transmission des paramètres le compilateur effectue un appel implicite au constructeur de transtypage qui va créer à partir de T un objet temporaire sans non de type Time. Par la suite c'est cet objet qui sera transmis réellement à f. Par ailleurs, et puisque le passage de l'argument de f se fait par valeur, alors le compilateur effectue un autre appel implicite mais cette fois au constructeur de recopie. Le but de cet appel est de faire la copie de l'objet temporaire précédemment créé dans le paramètre formel de f.

L'instanciation du paramètre formel Tf à partir du paramètre effectif se traduit réellement comme suit :

Tf(Time(Time(T))); // instanciation du paramètre formel de f.

Exemple 2 :

Considérons la portion de code suivante : Time X; char T[9] ="18:30:00"; X=T;

En considérant la définition de la classe Time donnée au début de ce paragraphe l'affectation entre T et X est tout à fait possible. En effet, au moment de l'affectation, le compilateur appelle implicitement le constructeur de transtypage pour créer à partir de T un objet temporaire de type Time. C'est cet objet qui est ensuite copié dans X.

Page 106: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 104

Remarque :

Une classe peut comporter plusieurs constructeurs de transtypage pourvu que ces derniers respectent les règles de surcharge de méthodes.

Les listes d'initialisation des constructeurs

Une liste d'initialisation de constructeur représente une alternative syntaxique pour l'initialisation des attributs d'une classe. Ainsi au lieu de faire cette initialisation dans le corps du constructeur, il devient possible grâce à cette alternative de la faire tout juste devant l'entête du constructeur selon la syntaxe suivante :

NomClasse(arguments) : A1(V1), A2(V2), …, An(Vn)

Où :

• NomClasse(arguments) représente un appel au constructeur,

• Ai : représente les attributs de NomClasse à initialiser

• Vi : valeur initiale affectée à l'attribut Ai.

Exemple 1 :

// Appel du constructeur de la classe Time Time():Hour(12),Minute(30),Second(45) { }

Exemple 2 :

// Appel du constructeur de la classe TabEntiers TabEntiers(int N, int* Tab) : Nb(N),T(new int[N]) { for(int i = 0;i<Nb;i++) T[i] = Tab[i]; }

Remarques :

• Il n'est pas obligatoire de mentionner les attributs dans une liste d'initialisation dans le même ordre que celui de leur déclaration dans la classe.

• L'ordre de l'initialisation effective des attributs suit toujours l'ordre de déclaration de ces derniers dans la définition de la classe même si cet ordre n'est pas respecté dans la liste d'initialisation.

• Une liste d'initialisation peut se contenter d'initialiser seulement une partie des attributs d'une classe.

Initialisation des attributs constants et références

Pour les attributs constants et références, les listes d'initialisation ne constituent pas une simple alternative d'écriture de code mais représentent plutôt le seul moyen permettant de leurs affecter leurs valeurs initiales.

Considérons la classe ConstRef qui comporte les trois attributs suivants : x (entier), r

(référence à un entier) et y (constante entière).

Page 107: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 105

class ConstRef { int x; int& r = x ; // ERROR const int y = 5; // ERROR … … … };

Le fait que r et y soient respectivement une référence et une constante rend leurs initialisations obligatoires. Toutefois leurs initialisations telles que réalisées dans la classe ConstRef sont interdites. En effet, la définition d'une classe constitue une définition de type sans aucune instanciation en mémoire. Par conséquent au moment de la définition de ConstRef aucun attribut n'a une existence physique en mémoire. Par suite, initialiser r avec la référence d'une variable x qui n'existe pas ou également affecter 5 à une constante y qui n'existe pas en mémoire représentent des opérations interdites.

L'utilisation d'un constructeur défini comme suit est également interdite : ConstRef::ConstRef( ){

x =3; // OK

r = x; // ERROR

y = 5; // ERROR

}

En effet, l'instruction r=x; représente une affectation de la valeur 3 à la variable référencée par r. Or r ne référence aucune variable car elle n'a pas été initialisée. Par ailleurs, l'instruction y=5; représente une affectation de la valeur 5 à la constante y et non une initialisation. Or les opérations d'affectations ne peuvent pas être appliquées aux constantes. Le seul moyen en C++ permettant d'initialiser de tels membres consiste dans l'utilisation des listes d'initialisation.

Exemple :

class ConstRef

{

int x;

int& r ;

const int y ;

ConstRef( ) : r(x),y(5)

{x=3;}

};

Initialisation d'objets membres

• Les listes d'initialisation sont également utilisées pour initialiser les membres qui sont de type classe (membres objet) essentiellement lorsque ces derniers possèdent des constructeurs paramètrés.

• La liste d'initialisation constitue le seul moyen permettant de créer et d'initialiser un objet membre si la classe de ce dernier ne dispose pas de constructeur par défaut (implicite ou explicite).

Page 108: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 106

Exemple :

class X { int a; public : X(int i) { a=i; cout<<"Constructeur de X\n"; } }; class Y { int b; X x; public : Y():b(5),x(55) {cout<<"Constructeur de Y\n";} };

L'initialisation du membre x par la liste comme suit : x(55) est possible parce qu'un constructeur ayant le prototype X(int i) est défini pour la classe X.

Remarque 1:

En tenant compte de la définition de la classe X, la définition suivante du constructeur de Y n'est pas correcte : Y() { b=5; x=X(55); cout<<"Constructeur de Y"; }

En effet, l'instruction x=X(55); ne constitue pas syntaxiquement une initialisation au vrai sens du terme. Elle est plutôt considérée comme étant une première affectation à x du contenu de l'objet temporaire X(55). Cela suppose que x a été créé auparavant à l'aide d'un constructeur par défaut chose qui n'est pas possible faute de ce type de constructeur dans la définition de la classe X.

Constructeur et destructeur des classes anonymes

Les classes anonymes ne peuvent pas avoir des constructeurs et un destructeur explicitement définis. Seuls les constructeurs et le destructeur implicitement synthétisés par le compilateur sont utilisés par ce genre de classes.

Les tableaux d'objets

• La création d'un tableau d'objets d'une classe nécessite un appel du constructeur de la classe pour chaque élément du tableau.

• Si le constructeur de la classe demande des paramètres d'initialisation, alors ces paramètres doivent être passés, pour tout le tableau, à travers une liste comprenant des enregistrements de valeurs, placés entre deux accolades et séparés par des virgules. Dans cette liste la spécification des valeurs d'initialisation doit respecter un certain nombre de règles :

Page 109: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 107

o Si la classe possède un constructeur qui demande des paramètres, alors ce constructeur doit être explicitement appelé dans la liste d'initialisation.

o Si la classe dispose d'un constructeur qui prend un seul paramètre, alors ce paramètre peut être directement spécifié dans la liste d'initialisation sans avoir besoin de faire un appel explicite du constructeur.

o Si la classe possède plusieurs constructeurs, il n'est pas obligatoire d'appeler le même constructeur pour initialiser tous les objets : différents constructeurs peuvent en effet être appelés pour les différents éléments du tableau.

o Si un constructeur par défaut est défini pour la classe, alors la liste d'initialisation peut être omise. Dans ce cas le constructeur par défaut sera automatiquement appelé pour tous les éléments du tableau.

Exemple :

class Time {int Hour; int Minute; int Second; public : Time(){Hour = 12; Minute = 0; Second = 0;} Time(int H,int M, int S){Hour = H; Minute = M; Second = S;} Time(const char* str) { Hour = 10*(*str-'0') + (*(str+1)-'0'); Minute = 10*(*(str+3)-'0') + (*(str+4)-'0'); Second = 10*(*(str+6)-'0') + (*(str+7)-'0'); } void Afficher(){...} }; void AfficherTab(Time* Tab, int N) { for(int i=0;i<N;i++) { (*(Tab+i)).Afficher(); cout<<'\n'; } } void main() { Time Tab1[3]; AfficherTab(Tab1,3); Time Tab2[3]={Time(), Time(), Time()}; AfficherTab(Tab2,3); Time Tab3[4]={"10:15:30", Time("15:30:45"), Time(14,20,30), Time()}; AfficherTab(Tab3,4); Time Tab4[4]={"10:15:30", Time(14,20,30)}; AfficherTab(Tab4,4); }

Remarque :

• L'appel explicite du constructeur peut être omis seulement pour les derniers éléments du tableau. Dans ce cas c'est le constructeur par défaut qui est implicitement appelé (Exemple Tab4).

• Un élément du tableau, pour lequel l'appel du constructeur se fait d'une manière implicite ne peut pas avoir à sa droite un élément pour lequel l'appel du constructeur se fait d'une manière explicite.

Page 110: CoursPOOC++

Constructeurs et destructeur Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 108

Exemple :

Time Tab[4]={"10:15:30", , Time(14,20,30), Time()}; //ERREUR

Cas des tableaux dynamiques

Il n’est pas possible d'utiliser une liste d'initialisation pour créer dynamiquement un tableau d’objets.

Exemple :

Time* Tab=new Time[2] (Time(10,04,2000),Time(05,04,1999));// ERREUR

Pour créer d’une manière dynamique un tableau d’objets, la classe des objets doit avoir obligatoirement un constructeur par défaut (implicite ou explicite). Seul ce constructeur peut être utilisé dans ce cas.

Exemple :

Time* Tab=new Time[2]//OK:Appel implicite du constructeur par défaut

La forme canonique d'une classe • Les versions par défaut du constructeur de recopie, du destructeur et de l'opérateur

d'affectation conviennent seulement aux classes "simples" : ne comportant pas d'attributs dynamiques. En cas de présence de ce genre d'attributs alors la classe sera dans l'obligation de définir explicitement ces méthodes afin de tenir compte des espaces pointés par ces derniers.

• La forme canonique d'une classe est une sorte de modèle de définition de base que doit respecter toute classe non triviale (comportant des attributs dynamiques) pour garantir un bon usage pour ses utilisateurs.

• Cette forme canonique comporte : o La définition explicite d'un constructeur par défaut pour permettre entre autres la

création de tableaux dynamiques d'objets de cette classe. o La définition explicite d'un constructeur de recopie pour assurer un passage par valeur

et un renvoi correctes des objets de cette classe par les fonctions des utilisateurs. o La définition explicite d'un destructeur pour la libération adéquate des ressources

(espaces pointés). o La surcharge de l'opérateur d'affectation pour garantir une copie correcte et convenable

entre objets de cette classe.

D'une manière concrète une classe T sous forme canonique doit se présenter selon le modèle minimal suivant :

class T { ... ... ... public : T(); // Constructeur par défaut T(const T&); // Constructeur de recopie ~T(); // Destructeur T& T::operator = (const T&); // Surcharge de l'opérateur = ... ... ... };

Page 111: CoursPOOC++

Le espaces de noms Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 109

Les espaces de noms • L’intérêt derrière la POO est le développement de projets sous formes de modules. Chaque

module étant représenté par une classe spécialisée dans une tâche donnée. Un grand projet peut comporter plusieurs modules. Ces derniers peuvent être développés par l’auteur même de l’application ou par des tiers. Le grand nombre de modules fait naître le plus souvent un besoin d’organisation des classes. Une organisation qui concerne le regroupement et l’attribution des identificateurs à ces classes.

• Le C++ offre un outil pour assurer cette organisation. Cet outil est celui des espaces de noms.

• Un espace de noms est un paquetage (bibliothèque) qui regroupe des classes ou des fonctions dans une même entité. Le regroupement des classes peut se faire selon plusieurs critères tels que par thème des classes, par auteur, etc.

• Outre le regroupement, les espaces de noms permettent de résoudre les problèmes de conflit des identificateurs des classes qui peuvent survenir notamment dans les projets faisant intervenir des bibliothèques diverses et comportant des classes différentes mais portant le même nom.

Définition d’un espace de noms

La définition d’un espace de noms se fait à l’aide de mot-clé namespace de la manière suivante :

Syntaxe :

namespace Identificateur { Déclaration des classes et des fonctions de l’espace de noms }

• Identificateur désigne le nom de l’espace de noms en cours de définition. • La définition d’un espace de noms ne se termine pas par un point-virgule et ce

contrairement à la définition d’une classe.

Exemple :

namespace MesClasses { X(){... ...}; }

Accès à une classe définie dans un espace de noms

Un espace de noms cache les classes qu’il contient. Ces dernières ne seront alors accessibles qu’à travers cet espace de noms et ce à l’aide de l’opérateur de résolution de portée (::).

Syntaxe :

NomEspaceDeNoms::NomClasse;

Page 112: CoursPOOC++

Le espaces de noms Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 110

Exemple :

namespace MesClasses { X(){... ...}; } int main() { MesClasses::X x1(); //OK X x2(); //Erreur return 0; }

Résolution des conflits de noms

Il est possible grâce aux espaces de noms d’utiliser deux classes qui portent le même nom sans engendrer un conflit.

Exemple :

namespace MesClasses { X(){... ...}; }

namespace MesAutresClasses { X(){... ...}; } int main() { MesClasses::X x1(); MesAutresClasses::X x2(); return 0; }

Dans cet exemple x1 représente un objet de la classe X de MesClasses et x2 est un objet de la classe X de MesAutresClasses.

La déclaration using Afin d’améliorer la lisibilité du code, il est possible d’indiquer dès le départ en une seule fois la provenance des classes qui seront utilisées. Cette indication se fait pour chaque classe à l’aide du mot réservé using de la manière suivante :

Syntaxe :

using EspaceDeNoms::NomClasse;

La classe NomClasse peut alors être désignée directement par son nom dans l’espace de portée dans lequel a été faite la déclaration using.

Exemple :

namespace MesClasses { X(){... ...}; Y(){... ...}; }

Page 113: CoursPOOC++

Le espaces de noms Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 111

using MesClasses::X; int main() { X x(); //OK la classe X est connue dans tout le fichier Y y1(); //Erreur MesClasses::Y y2(); //OK return 0; }

La directive using

En l’absence de conflits, il est possible de rendre tous les éléments d’un espace de noms accessibles et éviter de les spécifier ainsi un par un. Cette spécification globale se fait à l’aide de la directive using de la manière suivante :

Syntaxe :

using namespace EspaceDeNoms;

Exemple :

namespace MesClasses { X(){... ...}; Y(){... ...}; } using namespace MesClasses; int main() { X x(); // OK Y y(); // OK return 0; }

Espace de noms anonyme • Il est possible de définir en C++ un espace de noms anonyme. Un tel espace ne comporte

pas de noms et ne peut être par conséquent invoqué de n’importe quel autre endroit. • Les espaces anonymes peuvent être utiles pour la protection du code puisque toute entité

déclarée à l’intérieur d’un tel espace ne pourra plus être invoquée de l’extérieur et sera par conséquent privée à cet espace.

Espace de nom standard « std ».

• La bibliothèque standard de classes fournie avec tout système C++ qui respecte la norme ISO/ANSI définit tous ses symboles dans l’espace de noms appelé std.

• Pour éviter de mentionner cet espace lors de la manipulation des éléments du C++, il suffit d’utiliser la directive using.

Exemple :

#include <iostream> std::cout<<”Ceci est un message”;

ou également #include <iostream> using namespace std; cout<<”Ceci est un message”;

Page 114: CoursPOOC++

Membres statiques et membres constants d'une classe Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 112

Membres statiques et membres constants d'une classe

Attributs statiques

L'une des caractéristiques les plus importantes des classes est que chaque instance (objet) possède sa propre copie d'attributs (sa propre zone mémoire). Cette copie constitue l'identité propre à l'objet. La durée de vie des attributs est égale dans ce cas à la durée de vie de l'objet. Toutefois, dans certains cas, il est intéressant de créer des attributs qui sont indépendants des objets et qui sont plutôt liés à la classe ou en d'autres mots au modèle que définit la classe. De tels attributs peuvent être partagées par toutes les instances de la classe et sont appelés des attributs statiques.

Déclaration d'un attribut statique La déclaration d'un attribut statique se fait à l'intérieur de la définition de la classe à l'aide du mot-clé static comme suit :

Définition d'un attribut statique Un attribut statique doit être obligatoirement défini. Cette définition se fait d'une manière globale en dehors de la classe sans l'utilisation du mot-clé static. Toutefois le nom de l'attribut doit être complètement qualifié comme suit :

Type NomClasse::NomAttributStatique = Valeur;

L'affectation d'une valeur initiale à l'attribut statique lors de sa définition n'est pas obligatoire. Un attribut statique non explicitement initialisé sera automatiquement initialisé par le système à 0.

Exemple 1 :

Supposons que l'on souhaite créer par programmation une scène qui représente une chute de neige. Cette neige sera représentée par un ensemble de cercles de même rayon remplis par la couleur blanche. Le programme devra également donner la possibilité à l'utilisateur de modifier la taille des grains de neige. Cette modification affectera l'attribut Rayon de tous les cercles de manière uniforme. Une première possibilité pour représenter les cercles consiste à utiliser la classe suivante.

class Cercle { int x; int y; int Rayon;

class NomClasse{ … … … static Type NomAttributStatique; … … … };

Page 115: CoursPOOC++

Membres statiques et membres constants d'une classe Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 113

public : Cercle(int xi, int yi){x=xi; y=yi;} Cercle(){x=0; y=0;} void Deplacer(int dx, int dy){x+=dx; y+=dy;}

void SetRayon(int R){Rayon = R;} void Dessiner(){...}

};

Avec cette spécification chaque cercle possède sa propre copie de l'attribut Rayon. Or puisque tous les cercles possèdent la même taille, il devient plus intéressent de faire partager l'attribut Rayon par toutes les instances de cette classe en le déclarant comme statique.

class Cercle { int x; int y; static int Rayon; public : Cercle(int xi, int yi){x=xi; y=yi;} Cercle(){x=0; y=0;} void Deplacer(int dx, int dy){x+=dx; y+=dy;}

void SetRayon(int R){Rayon = R;} void Dessiner(){...}

};

// Définition de l'attribut statique int Cercle::Rayon = 2;

Un tel partage possède deux avantages : • Il permet un gain en espace mémoire. En effet, une seule zone mémoire sera utilisée par

toutes les instances. • Il permet de diminuer la quantité de code nécessaire à la modification d'une caractéristique

commune à toutes les instances. Par exemple, pour modifier la taille de la neige, il suffit de modifier la valeur de l'attribut Rayon une seule fois. Cette modification affectera toutes les instances.

Caractéristiques d'un attribut statique :

• Un attribut statique est partagé par toutes les instances d'une classe. Sa durée de vie ne dépend pas de celles des objets. Elle s'étend depuis l'endroit de définition de l'attribut jusqu'à la fin du programme. De ce point de vue, un tel attribut se comporte comme une variable globale. Toutefois, et à la différence de cette dernière, un attribut statique n'est visible qu'à l'intérieur de sa classe (sa visibilité est liée à la classe) alors q'une variable globale est visible dans tout le programme.

• Un attribut statique existe en mémoire même si aucun objet de la classe n’a encore été créé.

• Un attribut statique est souvent appelé attribut de classe puisque son existence dépend de l'existence de la classe et non de l'existence des objets. Un attribut non statique est appelé attribut d'instance puisque son existence est liée à l'instanciation de la classe.

• Un attribut statique peut être manipulé par les méthodes de sa classe comme n'importe quel autre attribut non statique.

• Un attribut statique peut être qualifié de privé, de publique ou de protégé.

Page 116: CoursPOOC++

Membres statiques et membres constants d'une classe Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 114

• Un attribut statique publique peut être appelé de l'extérieur de la classe à travers son nom qualifié. Cette qualification peut être faite à l'aide : o Du nom de la classe : NomClasse::NomAttributStatic; o Du nom d'un objet : NomObjet.NomAttributStatic; o Du nom d'un pointeur sur un objet : NomPtrObjet->NomAttributStatic;

Exercice d'application : Supposons que l'on souhaite calculer l'espace mémoire occupé par toutes les instances d'une classe dans un programme en cours d'exécution. Ce calcul passe par la détermination du nombre d'objets instanciés. Une solution possible à ce problème consiste à utiliser un attribut statique qui s'incrémente à chaque instanciation d'un nouvel objet et qui se décrémente à chaque destruction d'un objet. Proposer une implémentation de cette solution avec la classe Point qui représente un point dans un repère à deux dimensions.

Solution

class Point { int x; int y; public : static int Memory; Point():x(0),y(0) { Memory+=sizeof(Point); } Point(int a, int b):x(a),y(b) { Memory+=sizeof(Point); } ~Point() { Memory-=sizeof(Point); } };

int Point::Memory =0;

void main() { cout<<" Mémoire occupée :"<<Point::Memory<<endl; Point P1(10,10); cout<<" Mémoire occupée :"<<Point::Memory<<endl; Point *P2= new Point(5,5); cout<<" Mémoire occupée :"<<Point::Memory<<endl; delete P2; cout<<" Mémoire occupée :"<<Point::Memory<<endl; }

Méthodes statiques

• Tout comme les attributs, il est possible de déclarer les méthodes comme étant statiques. Une méthode statique est généralement une méthode qui réalise une tâche qui est indépendante des instances et qui est plutôt liée au modèle défini par la classe.

Page 117: CoursPOOC++

Membres statiques et membres constants d'une classe Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 115

• Une méthode statique est appelée une méthode de classe alors qu'une méthode non statique est appelée une méthode d'instance.

Déclaration d'une méthode statique Une méthode statique est déclarée dans la définition d'une classe à l'aide du mot-clé static

comme suit :

Définition d'une méthode statique Comme les méthodes classiques, une méthode statique peut être définie à l'intérieur ou à l'extérieur de la classe.

Définition à l'intérieur de la classe

class NomClasse { ... ... ... static Type NomMethodeStatique(<Liste des paramètres>) { ... } ... ... ... };

Définition à l'extérieur de la classe

La définition d'une méthode statique à l'extérieur de la classe n'utilise pas le mot-clé static. Elle doit être précédée par la déclaration de la méthode à l'intérieur de la classe :

Type NomClasse::NomMethodeStatique(<Liste des paramètres>) { ... }

Appel d'une méthode statique

Appel de l'intérieur de la classe

Une méthode statique peut être appelée de l'intérieur de sa classe comme une méthode classique et ce directement par son nom (sans considération des droits d'accès et sans qualification).

Appel de l'extérieur de la classe

Une méthode statique publique peut être appelée de l'extérieur de la classe à partir : • Du nom de la classe : NomClasse::NomMethodeStatique(…); • D'un objet de la classe : NomObjet.NomMethodeStatique(…); • D'un pointeur sur un objet : NomPtrObjet->NomMethodeStatique(…);

Remarques :

• Une méthode statique :

class NomClasse{ ... ... ... static Type NomMethodeStatique(<Liste des paramètres>); ... ... ... };

Page 118: CoursPOOC++

Membres statiques et membres constants d'une classe Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 116

o Peut appeler les méthodes statiques de sa classe. o Ne peut pas appeler les méthodes non statiques de sa classe. o Peut appeler les méthodes statiques publiques des autres classes. o Peut appeler les méthodes d'instances publiques des objets qui sont déclarés en local à

l'intérieur de cette méthode. • Une méthode non statique (méthode d'instance) peut appeler :

o Les méthodes statiques et non statiques de sa classe. o Les méthodes statiques publiques des autres classes. o Les méthodes d'instances publiques des objets qui lui sont locaux.

Accès aux attributs par une méthode statique

• Une méthode statique ne peut accéder qu'aux attributs statiques de sa classe. Elle ne peut pas accéder aux attributs non statiques. En effet, une méthode statique peut être appelée à partir du nom de la classe en dehors de toute création d'objets. Dans ce genre d'appel, aucun attribut non statique ne peut exister en mémoire car de tels attributs sont liés aux instances.

• Une méthode statique ne possède pas de paramètre caché this pour référencer les attributs vu que les seuls attributs qu'elle peut utiliser sont statiques et sont par conséquent uniques et non liés aux instances.

• Une méthode non statique peut accéder à la fois aux champs statiques et non statiques de sa classe.

Exemple :

class X { int a; static int b; static int c; int f(){return a+b;} static int g(){return a-b;} static int h()({return a*b;} };

Etant donnés la classe X définie ci-dessus et obj un objet convenablement instancié de X. Indiquer si les appels suivants sont possibles ou non. … … … int res; res=obj.f(); res=obj.g(); res=obj.h(); X.f(); X.g(); X.h(); … … …

Remarque :

De par sa définition un constructeur doit être capable d'accéder à tous les attributs de sa classe (pour faire la création et l'initialisation de l'objet). Or cette capacité n'est pas à la portée des méthodes statiques. C'est pourquoi un constructeur ne peut pas être déclaré comme étant statique. De même un destructeur ne peut pas être déclaré comme étant statique.

Page 119: CoursPOOC++

Membres statiques et membres constants d'une classe Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 117

Exemple :

class Cercle { int x; int y; static int Rayon; static int Nb; public : Cercle(int xi, int yi){x=xi; y=yi; Nb++;} Cercle(){x=0; y=0; Nb++;} ~Cercle(){Nb--;} void Deplacer(int dx, int dy){x+=dx; y+=dy;} static void SetRayon(int R){Rayon = R;} static int GetNb(){ return Nb;} void Dessiner(){...} }; int Cercle::Rayon =5; int Cercle::Nb=0; void main() { ... Cercle c1(2,7);Cercle c2(2,17);Cercle c3(2,27); int Nombre = Cercle::GetNb(); ... }

Remarque :

Une méthode statique peut être appelée à la manière des fonctions du langage C (sans nécessiter une création d'instance). C'est pourquoi, dans les nouveaux langages orientés objet on tend à regrouper les fonctions d'une même bibliothèque dans une même classe sous forme de méthodes statiques pour faciliter leur appel. A titre d’exemple, la classe Math en C# représente la bibliothèque mathématique et ne possède que des méthodes statiques qui peuvent être appelés facilement sans nécessiter la création d'une instance au préalable. Déclaration : public static double Cos(double d )

Appel direct : double i = Math.Cos(0) ;

Exercice d'application On considère une classe possédant un attribut qui stocke une valeur entière. Compléter la définition de cette classe pour que l'on puisse calculer à tout moment la moyenne des valeurs stockées dans toutes les instances de la classe.

Solution

class Entier { int Val; static int Nb; static float Somme; public : Entier() : Val(0) {Nb++;} Entier(int V):Val(V) { Nb++; Somme+=Val; }

Page 120: CoursPOOC++

Membres statiques et membres constants d'une classe Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 118

~Entier() { Nb--; Somme-=Val; } static float Moyenne() {return Somme/Nb;} }; int Entier::Nb=0; float Entier::Somme=0.f;

void main() { Entier E1(10); Entier E2(30); cout<<"La moyenne est :"<<Entier::Moyenne()<<endl; Entier *E3=new Entier(50); cout<<"La moyenne est :"<<Entier::Moyenne()<<endl; delete E3; cout<<"La moyenne est :"<<Entier::Moyenne()<<endl; }

Les objets constants

• Il est possible de déclarer constantes des données membres. Mais il est également possible de déclarer constants les objets eux-mêmes. La déclaration d'un objet constant se fait à l'aide du mot-clé const comme suit :

Syntaxe :

const NomClasse NomObjet(liste des arguments);

• Toutes les données membres d'un objet constant sont considérées comme des constantes. Elles ne peuvent être initialisées qu'à travers le constructeur. Une fois initialisées, ces données ne peuvent plus changer de valeurs (même si elles ne sont pas explicitement déclarées comme constantes).

• L'accès en écriture aux attributs d'un objet constant est interdit.

• Les constructeurs et le destructeur représentent les seules méthodes qui peuvent être appelées par un objet constant. L'appel des autres méthodes de la classe par un tel objet étant interdit.

Exemple

Considérons la définition suivante de la classe Time : class Time { public : int Hour; int Minute; int Second; Time(int H,int M, int S) : Hour(H),Minute(M),Second(S) { } void Affiche() {

Page 121: CoursPOOC++

Membres statiques et membres constants d'une classe Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 119

cout<<"L'heure est : "; cout<<setw(2)<<setfill('0')<<Hour<<':' <<setw(2)<<setfill('0')<<Minute<<':' <<setw(2)<<setfill('0')<<Second<<endl; } };

Soit T1 un objet de type Time et T2 un objet constant de type Time :

Time T1(10,25,55); const Time T2(12,10,30); T1.Hour = 11; // OK car Hour est public et T1 n'est pas constant. T2.Hour = 11; // ERREUR car T2 est un objet constant. … … … T1.Affiche() // OK car T1 n'est pas constant T2.Affiche() // ERREUR car T2 est constant

Remarque :

L'appel d'une méthode qui manipule les attributs de la classe est interdit à partir d'un objet constant même si cette méthode ne modifie pas le contenu de ces attributs comme c'est le cas pour la méthode Afficher. Ceci est dû au fait que la compilation des appels des fonctions se base essentiellement sur la déclaration de ces dernières (analyse de la signature). Le corps de la fonction appelée peut être déjà défini et pré-compilé dans un module externe au programme (c'est le cas des bibliothèques de fonctions liées d'une manière statique ".lib" ou dynamique ".dll"). Le compilateur ne peut pas par conséquent déterminer si des fonctions de ce genre effectuent en interne un accès en écriture aux attributs puisqu'il ne va pas les recompiler mais tout simplement vérifier la justesse de leur appel.

Les méthodes constantes

• Pour qu'une méthode puisse être appelée à partir d'un objet constant, elle doit être déclarée et définie comme étant constante.

• Seules les méthodes qui ne font pas des accès en écriture aux attributs peuvent être déclarées et définies comme constantes.

• Le caractère constant d'une fonction est indiqué par le mot-clé const. const doit figurer aussi bien dans la déclaration que dans la définition de la classe comme suit :

Déclaration :

Type NomMethodes(Liste des arguments) const;

Définition :

Type NomClasse:: NomMethodes(Liste des arguments) const { … … …}

Exemple :

class Time { public :

Page 122: CoursPOOC++

Membres statiques et membres constants d'une classe Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 120

int Hour; int Minute; int Second; Time(int H,int M, int S) : Hour(H),Minute(M),Second(S) { }

void Affiche() const { cout<<"L'heure est : "; cout<<setw(2)<<setfill('0')<<Hour<<':' <<setw(2)<<setfill('0')<<Second<<endl; } }; void main() { const Time T(12,10,30); T.Hour = 11; // ERREUR car T est un objet constant. T.Affiche(); // OK car Affiche est une méthode constante }

Remarque :

• Le spécificateur const dans ce cas constitue une garantie pour le compilateur que la méthode en question n'effectue aucune modification des attributs de la classe, même si la classe est déjà précompilée (sous forme de dll par exemple). Un utilisateur de cette classe peut dans ce cas appeler une telle méthode pour des instances constantes de cette classe.

• Toute instruction d'accès en écriture à un attribut dans le corps d'une méthode constante est signalée comme une erreur au moment de la compilation.

• Tout passage par référence ou par adresse d'un attribut d'une instance constante à une fonction appelée en interne dans une méthode est signalé comme erreur (risque de modification de l'attribut par la fonction).

Page 123: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 121

Les fonctions amies

Introduction La POO pure impose l'encapsulation des données. Ce principe d'encapsulation fait qu'une classe interdit à ses utilisateurs externes l'accès à ses membres privés et ne leur permet d'appeler que ses méthodes publiques.

Contraintes imposées par l'encapsulation Parfois il existe des circonstances où une telle protection peut s'avérer contraignante comme c'est le cas pour l'exemple suivant : On suppose que l'on dispose de deux classes Vecteur et Matrice et que l'on cherche à définir une méthode qui calcule le produit d'un vecteur et d'une matrice. En appliquant le principe d'encapsulation d’une manière stricte (protection des attributs de chaque classe), il ne serait possible de définir cette fonction ni comme étant membre de la classe Vecteur, ni comme membre de la classe Matrice ni encore moins comme une fonction indépendante. Comme solution à ce problème il est possible de : • Déclarer les attributs comme publiques chose qui engendre en contre partie la perte du

bénéfice de leur protection. • Utiliser des méthodes publiques d'accès aux données privées mais ceci reste toutefois

pénalisant en terme de temps d'exécution.

Le C++ dispose d'un outil qui permet de contourner le principe d'encapsulation dans certaines circonstances et pour certains utilisateurs (certaines fonctions). Cet outil est celui des fonctions amies.

La notion d'amitié • L'amitié peut exister entre une fonction et une classe ou entre deux classes. Dans ce cadre :

o Une fonction amie d'une classe est autorisée à accéder aux membres privés de cette dernière comme n'importe quel autre membre de cette classe.

o Les méthodes d'une classe A amie d'une deuxième classe B sont autorisées à accéder à tous les membres privés de cette classe B.

• La notion d'amitié est déclarée à l'aide du mot-clé friend.

Fonction amie d'une classe

Une fonction peut être définie comme étant amie d'une classe. Cette fonction peut être une fonction indépendante ou une fonction membre d'une autre classe.

Fonction indépendante amie d'une classe

La déclaration d'une fonction F comme amie d'une classe A se fait à l'intérieur de cette dernière de la manière suivante : class A { … … … friend TypeRetour F(Liste des paramètres); … … … };

Page 124: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 122

L'emplacement de la déclaration de l'amitié de F au sein de la classe A peut se faire n'importe où dans cette dernière. D'ailleurs cet emplacement peut figurer indifféremment dans la partie publique ou privée de la classe.

Exemple :

L'exemple suivant montre la définition d'une fonction indépendante appelée Somme qui calcule la somme de deux complexes. Pour pouvoir accéder aux membres privés cette fonction a été déclarée comme amie de la classe Complexe.

class Complexe { double Reel; double Imaginaire; public : Complexe(double r, double i) : Reel(r), Imaginaire(i){} Complexe(){Reel = 0; Imaginaire = 0;} // Déclaration de la fonction Somme comme amie friend Complexe Somme(Complexe C1, Complexe C2); void Afficher() { cout<<"Partie réelle : "<<Reel; cout<<"Partie imaginaire : "<<Imaginaire; } }; // Définition de la fonction Somme Complexe Somme(Complexe C1, Complexe C2) { Complexe res(C1.Reel+C2.Reel, C1.Imaginaire+C2.Imaginaire); return res; } int main() { Complexe C1(3,6), C2(4,2); Somme(C1,C2).Afficher(); return 0; }

Fonction membre amie d'une classe La fonction à définir comme étant amie d'une classe peut ne pas être indépendante mais plutôt une fonction membre d'une autre classe. Dans ce cas la déclaration de l'amitié se fait de la manière suivante : class B { … … … TypeRetour F(Liste des paramètres); … … … };

class A { … … … friend TypeRetour B::F(Liste des paramètres); … … … };

Page 125: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 123

Si la classe A figure dans le prototype de F (comme paramètre ou comme type de retour) alors pour que la compilation réussisse il faut que cette classe soit déclarée avant la classe B mais pas nécessairement définie et ce pour qu'elle soit connue au niveau de la compilation de la définition de la classe B. Voici un schéma complet de la déclaration d'amitié pour ce cas de figure : // Déclaration de A pour les besoins de compilationclass A; // Définition de B class B { … … … TypeRetour F(A); … … … }; // Définition de A class A { … … … friend TypeRetour B::F(Liste des paramètres); … … … };

Fonction amie de plusieurs classes

Une fonction peut avoir besoin d'accéder aux membres privés de plusieurs classes et nécessite par conséquent d'être déclarée amie de toutes ces classes en même temps. La déclaration d'amitié doit se faire dans ce cas dans toutes ces classes.

Exemple :

On considère une fonction indépendante F qui doit accéder aux membres privés de deux classes A et B. Le prototype de F est le suivant : void F(A, B); F doit être déclarée amie à la fois à A et B :

Syntaxe :

class B; // Définition de A

class A { … … … friend void F(A, B); … … … };

// Définition de B class B { … … … friend void F(A, B); … … … };

Page 126: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 124

void F(A a, B b) { … … … }

La déclaration suivante class B; est nécessaire pour les besoins de compilation vu que la classe A utilise B (dans F) et que B est définie après A.

Classe amie d'une autre classe

Ce cas de figure est une généralisation du cas de l'amitié d'une fonction membre à une classe. Il s'agit en effet de déclarer toutes les fonctions d'une classe A comme amies aux fonctions membres d'une autre classe B. Dans ce cas, une solution consiste à définir autant d'amitié qu'il y a de fonctions concernées. Mais, le C++ offre une alternative plus simple qui consiste à effectuer une déclaration d'amitié globale entre classes. Ainsi, il suffit de déclarer la classe A comme étant amie de la classe B.

Syntaxe :

class A { … … … … … … }; class B { … … … friend class A; … … … };

Dans ce cas, toutes les méthodes de A seront ainsi amies de B.

Page 127: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 125

Héritage et polymorphisme

Définition de l'héritage

• L'héritage est le fait de définir une nouvelle classe en se basant sur la définition d'une classe déjà existante. La nouvelle classe hérite alors les attributs et les méthodes de la classe existante et ce en plus de ses propres membres (ses attributs et/ou méthodes spécifiques).

• L'héritage est un concept propre à la POO.

Terminologie utilisée par le concept d'héritage

• La nouvelle classe définie par héritage est appelée classe dérivée ou classe fille. • La classe à partir de laquelle se fait l'héritage est appelée classe de base, super classe ou

également classe mère. • L'héritage est également appelé dérivation de classes. • La relation d'héritage est schématisée graphiquement par une flèche qui part de la classe

dérivée vers la classe de base. • Cette relation peut s'exprimer par la phrase "est un".

Exemple :

Un herbivore est un animal, un cheval est un herbivore, etc.

Remarque :

• La classe dérivée constitue une spécialisation de la classe de base. • La classe de base constitue une généralisation de la classe dérivée.

Intérêt de l'héritage L'intérêt de l'héritage réside dans : • La réutilisation des modules déjà existants. • La factorisation des propriétés communes à un certain nombre de classes � Rendre les

programmes moins complexes et plus facilement maintenables.

ANIMAL

HERBIVORE CARNIVORE

VACHE LION CHEVAL

Page 128: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 126

Exemple :

Prenons par exemple le cas de la gestion d'une bibliothèque de documents. Ces documents peuvent être de deux catégories : des livres et des périodiques (magazines, journaux, revues scientifiques, etc.). Les spécifications des classes représentant ces deux catégories de documents sont données comme suit :

D'après ces spécifications il est facile de remarquer que les deux classes possèdent un certain nombre de caractéristiques en commun qui se répètent. Afin d'éviter cette répétition, il est possible de factoriser ces caractéristiques communes et ce à l'aide d'une classe généraliste qui portera le nom Document. Les classes Livre et Periodique constitueront alors des spécialisations de cette classe.

La nouvelle spécification des classes devient alors comme suit :

Reference Titre Auteur Editeur

GetReference() GetTitre() GetAuteur() GetEditeur()

LIVRE

Reference Titre DirecteurRedaction Numero

GetReference() GetTitre() GetDirecteurRedaction() GetNuméro()

PERIODIQUE

Reference Titre

GetReference() GetTitre()

DOCUMENT

Auteur Editeur

GetAuteur() GetEditeur()

LIVRE

DirecteurRedaction Numero

GetDirecteurRedaction() GetNuméro()

PERIODIQUE

Page 129: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 127

Remarque :

Il ne faut pas confondre l'héritage avec l'agrégation des classes (appelée également composition) déjà traitée dans le chapitre précédent. Il est à rappeler que dans l'agrégation, la nouvelle classe est composée en partie par une classe déjà existante. Cette dernière s'intègre généralement comme un attribut de la nouvelle classe.

Syntaxe de dérivation en C++

La syntaxe permettant de faire dériver une classe à partir d'une autre déjà existante est comme suit :

class NomClasseDérivée : [Mode_Dérivation] NomClasseDeBase { Liste des nouveaux membres de la classe dérivée };

• NomClasseDérivée : désigne la nouvelle classe créée par dérivation.• NomClasseDeBase : désigne le nom de la classe à partir de laquelle se fait la dérivation. • Mode_Dérivation : sert à indiquer le mode avec lequel sera faite la dérivation. Ce mode

détermine les droits d'accès qui seront attribués aux membres de la classe de base dans la classe dérivée. Les modes qui sont définis en C++ sont : public, protected et private.

Exemple :

class Document { char Reference[10]; char Titre[100]; void Afficher(); public :

char* GetReference(); char* GetTitre(); };

class Livre : public Document { char Auteur[10]; char Editeur[100]; public : char* GetAuteur(); char* GetEditeur(); };

Accessibilité des membres d'une classe de base dans la classe dérivée

L'accessibilité des membres d'une classe de base depuis une classe dérivée dépend : • des droits d'accès de ces membres dans la classe de base, • du mode de dérivation utilisé.

Page 130: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 128

Le tableau suivant dresse tous les cas de figures possibles concernant cette accessibilité :

Exemple :

L'exemple suivant donne une illustration de quelques situations de contrôle d'accès pouvant être engendrées par une dérivation privée.

class A { int a1; protected : int a2; public : int a3; }; class B : private A { private : int b1; int b2; protected : void f() { b1 = a1*2; //Erreur car a1 est inaccessible dans B } void g() { b2 = a2*a3; //OK car a2 et a3 sont accessibles dans B } }; void main() { A Obj1; B Obj2; Obj1.a2 = 6; // Erreur car a2 est protégé dans A Obj2.a3 = 8; // Erreur car a3 est privé dans B }

Mode de

dérivation

Statut du membre

dans la classe de

base

Statut du membre

dans la classe

dérivée

Accessibilité du

membre dans la classe

dérivée

Accessibilité du membre

par les utilisateurs de la

classe dérivée

public

public public Accessible Accessible protected protected Accessible Inaccessible private private Inaccessible Inaccessible

protected

public protected Accessible Inaccessible protected protected Accessible Inaccessible private private Inaccessible Inaccessible

private

public private Accessible Inaccessible protected private Accessible Inaccessible private private Inaccessible Inaccessible

Page 131: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 129

Remarques :

• Le mode de dérivation par défaut (si rien n'est mentionné) est le mode "private". Toutefois le mode le plus utilisé est le mode "public". Ce mode permet aux membres hérités de préserver le même statut que dans la classe de base.

• A travers les mots-clés : public, protected et private, le C++ fournit aux concepteurs des classes des outils de contrôle de l'accès aux membres. Ce contrôle est effectué par rapport aux utilisateurs des classes. Ces utilisateurs peuvent en effet être : o des objets instanciés directement à partir de ces classes, o des classes dérivées à partir de ces classes, o des instances de classes dérivées à partir de ces classes.

Manipulation des membres de la classe de base dans la classe dérivée

• Les membres d'une classe de base qui sont accessibles dans la classe dérivée sont manipulés dans cette dernière directement à travers leurs noms à la manière des membres de la classe dérivée.

Redéfinition des méthodes

• La redéfinition des méthodes est le fait de proposer une nouvelle définition dans la classe dérivée d'une méthode déjà existante dans la classe de base.

• Généralement la méthode de base et celle redéfinie assurent la même fonctionnalité. Toutefois la redéfinition est souvent faite dans un souci de prise en compte des spécificités de la classe dérivée.

Exemple :

Considérons les deux classes A et B définies comme suit :

class A {public : int att_A; A(int p){att_A = p;} void Afficher() {cout<<"La valeur de att_A est : "<<att_A<<endl;} }; class B : public A {public : int att_B; B(int p1, int p2) : A(p1) { att_B = p2;} }; void main() { B Obj(5,9); Obj.Afficher(); } La méthode Afficher appelée à partir de Obj va engendrer la sortie suivante : La valeur de att_A est : 5

Page 132: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 130

Le contenu du champ att_B n'est pas affiché. Il est clair que cette méthode telle qu'elle est définie dans A ne convient pas aux objets de la classe B (afffichage partielle du contenu de Obj). Pour cette raison et pour que l'affichage puisse toucher tous les attributs de B il faut redéfinir cette méthode dans la classe dérivée pour qu'elle tienne compte de l'attibut supplémentaire.

Nouvelle définition de B :class B : public A {public : int att_B; B(int p1, int p2) : A(p1) { att_B = p2;}

void Afficher() { cout<<"La valeur de att_A est : "<<att_A<<endl; cout<<"La valeur de att_B est : "<<att_B<<endl; } };

Avec cette définition de B le code suivant :

B Obj(5,9); Obj.Afficher();

va engendrer l'affichage suivant :

La valeur de att_A est : 5 La valeur de att_B est : 9

Dans ce cas c'est la méthode Afficher de B qui est appelée et non celle de A.

Remarque: (masquage des méthodes de base)

Lorsqu'une méthode d'une classe de base est redéfinie dans une classe dérivée alors la redéfinition masque la méthode de la classe de base pour les objets de la classe dérivée. Pour l'exemple précédent, la méthode Afficher de A est masquée par la méthode Afficher de Bpour les instances de cette dernière.

Appel des membres d'une classe de base redéfinis dans la classe dérivée

• Les membres d'une classe de base qui sont accessibles dans la classe dérivée sont manipulés dans cette dernière directement à travers leurs noms à la manière des membres de la classe dérivée. Toutefois dans le cas où un membre (attribut ou méthode) est redéfini, ce dernier sera caché dans la classe dérivée par la redéfinition. Pour pouvoir l'appeler quand même dans la classe dérivée, le nom de ce membre doit être associé à celui de la classe de base et ce à l'aide de l'opérateur de résolution de portée selon la syntaxe suivante :

NomClasseBase::NomMembre;

• Si le membre en question est publique dans la classe dérivée alors il peut être manipulé à partir des objets de cette dernière selon la syntaxe suivante :

NomObjet.NomClasseBase::NomMembre;

Page 133: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 131

Remarque :

Dans l'exemple précédent, il est plus intéressant d'appeler la méthode Afficher de la classe A

dans la définition de Afficher de B pour assurer l'affichage de att_A (afin d'éviter de réécrire un code déjà écrit). Toutefois la définition suivante de la méthode Afficher de B :

void Afficher() { Afficher(); cout<<"La valeur de att_B est : "<<att_B<<endl; } sera interprétée comme un appel récursif de Afficher de B. Pour obtenir l'effet souhaité la redéfinition de Afficher doit se faire comme suit :

class B : public A {public : int att_B; B(int p1, int p2) : A(p1) { att_B = p2;} void Afficher() { A::Afficher(); cout<<"La valeur de att_B est : "<<att_B<<endl; } };

Généralisation de la redéfinition La redéfinition peut s'appliquer aux membres d'une classe d'une manière générale. Elle peut donc concerner aussi bien les méthodes que les attributs. Toutefois la redéfinition des attributs reste très rare d'utilisation mais tout à fait possible.

Exemple : class X { public : int att; }; class Y : public X { public : int att; }; … … … Y obj; obj.att=5; // att désigne l'attribut de Y obj.X::att = 8; // att désigne ici l'attribut de X

Différence entre redéfinition et surdéfinition Il ne faut pas confondre surdéfinition et redéfinition. Dans la redéfinition, la méthode de base et celle redéfinie doivent avoir exactement la même signature. Ce n'est pas le cas dans la surdéfinition.

Page 134: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 132

Compatibilité entre classe de base et classe dérivée

D'une manière générale en POO, il y a une certaine compatibilité entre un objet d'une classe de base et un objet d'une classe dérivée. Cette compatibilité va dans le sens suivant (Objet dérivée � Objet de base). Elle est interprétée comme suit : tout objet d'une classe dérivée

peut être considéré comme un objet de la classe de base.

Exemple :

Tout livre peut être considéré comme un document. Mais tout document n'est pas nécessairement un livre.

Remarque :

• Tout objet d'une classe dérivée comporte tous les membres d'un objet de la classe de base. Il peut par conséquent le remplacer là ou l'objet de la classe de base est demandé (le remplacer syntaxiquement mais cela ne garantit pas l'obtention d'un résultat toujours satisfaisant).

• L'opération inverse n'est pas vraie. En d'autres mots, un objet d'une classe de base ne peut pas remplacer un objet d'une classe dérivée.

Compatibilité entre objet de base et objet dérivé en C++ En langage C++, la compatibilité entre objet dérivé et objet de base est limitée seulement au cas d'une dérivation publique. Elle peut se manifester dans ce cas concrètement par une conversion implicite :

o D'un objet d'une classe dérivée vers un objet d'une classe de base. o D'un pointeur (ou une référence) vers un objet d'une classe dérivée en un pointeur (ou

une référence) vers un objet d'une classe de base.

Exemple :

Etant données les deux classes A et B définies comme suit :

class A { public : int att_A;

void Afficher(){cout<<" att_A :"<<att_A<<endl} };

class B : public A { public : int att_B; void Afficher()

{ cout<<" att_A :"<<att_A<<endl; cout<<" att_B :"<<att_B<<endl; }

};

… … … A a, *pa; B b, *pb; … … …

Page 135: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 133

// Compatibilité entre objetsa = b; // OK Cette instruction est correcte car tout objet de type B est également de type A. Dans ce cas le contenu des attributs de b provenant de la classe A sera copié dans les attributs correspondants de l'objet a (att_A de b dans att_A de a).

b = a; // Erreur Cette instruction est incorrecte car l'objet ne comporte pas tous les attributs de b (exemple : att_B ne peut pas avoir de valeur dans ce cas).

// Compatibilité entre pointeurs pa = &b; // OK Un pointeur sur une classe de base peut pointer sans problème sur un objet d'une classe dérivée. pa->att_A; // OK Cette instruction désigne l'attribut att_A de l'objet b.

pa->att_B; // Erreur Cette instruction va engendrer une erreur malgré le fait que l'attribut att_B existe en mémoire. En effet, syntaxiquement pa ne reconnaît pas le nom du champ pointé parce qu'il est de type A* et non B*.

pa->Afficher(); Cette instruction va engendrer l'appel de la fonction Afficher définie dans A. En effet, dans ce cas, le compilateur se base sur le type du pointeur et non sur le type de l'objet pointé pour décider de la version de la méthode à appeler.

Remarque : Conversion explicite d'un pointeur (Base* vers Dérivée*)

• La conversion implicite d'un pointeur d'une classe de base vers un pointeur d'une classe dérivée n'est pas implicitement acceptée par le compilateur.

pb = &a; // Erreur pb est un pointeur sur un objet d'une classe dérivée. Il ne peut pas recevoir l'adresse d'un objet d'une classe de base.

• Toutefois le compilateur peut accepter cette conversion si elle est faite d'une manière explicite à travers un casting. Cette conversion explicite même sil elle est possible reste dangereuse si elle est mal utilisée car elle peut mener vers : o des tentatives d'accès à des attributs qui ne figurent pas dans la structure de l'objet

pointé. o A la violation des protections pour la classe de base.

Exemple 1 : tentative d'accès à un attribut non existant physiquement pb = (A*)&a; // OK car conversion explicite pb->att_B; // Erreur Cette instruction va engendrer une erreur car l'objet pointé ne comporte pas un champ att_B.

Page 136: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 134

Exemple 2 : violation des protections de la classe de base On considère les deux classes suivantes : class A {public : int att_A;

void fa(); }; class C : private A {public : int att_C; };

Soient les déclarations suivantes : A ObjA; C ObjC; A* pa;

Analysons les instructions suivantes :

ObjA.att_A; Cette instruction est correcte car att_A est public dans A.

ObjC.att_A; Cette instruction est incorrecte car att_A est privé dans C.

pa = (A*)&ObjC; // Conversion explicite (cast) pa->att_A; // OK car att_A est public dans A. Ces deux instructions sont syntaxiquement correctes et possibles. La deuxième instruction va engendrer donc un accès à l'attribut att_A d'un objet d'une classe C. Ceci constitue une violation des droits d'accès car att_A est normalement privé pour les objets de la classe C et ne doit pas être par conséquent accessible.

Construction d'objets dérivés

• L'instanciation d'un objet d'une classe dérivée engendre nécessairement l'instanciation de la partie de ses attributs provenant de la classe de base. Ce processus d'instanciation s'effectue en C++ en quatre étapes : o Allocation de l'espace mémoire des attributs, o Appel du constructeur de la classe dérivée. o Appel et exécution du constructeur de la classe de base (initialisation des attributs issus

de la classe de base). o Exécution du constructeur de la classe dérivée (initialisation des attributs dérivés).

att_A

att_B

EF0A78 ObjC

A* pa

Page 137: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 135

Ordre d'exécution des constructeurs D'une manière générale l'instanciation d'un objet d'une classe dérivée engendre une exécution hiérarchique de constructeurs qui se fait dans l'ordre suivant :

o Constructeur de la (des) classe(s) de base. o Constructeurs des éventuels objets membres de la classe dérivés. o Constructeur de la classe dérivée.

Syntaxe d'appel du constructeur de la classe de base dans celui de la classe dérivée L'appel du constructeur de la classe de base peut se faire d'une manière explicite ou implicite selon que la classe de base ou la classe dérivée disposent de constructeurs explicitement définis ou non et que ces constructeurs soient paramétrés ou non.

I - Cas où la classe dérivée dispose d'un constructeur paramétré :

I.1 – Cas où la classe de base possède un constructeur paramétré :

• Dans ce cas, le constructeur de la classe de base est appelé par le constructeur de la classe dérivée dans la liste d'initialisation de ce dernier comme suit :

ConstructeurClasseDérivée(Arguments):ConstructeurClasseBase(Arguments) {… … …}

• La liste d'initialisation du constructeur d'une classe dérivée peut comporter généralement : o Un appel au constructeur de la classe de base. o Des valeurs d'initialisation des membres spécifiques de la classe dérivées.

Exemple :

class A {public : int att_A; A(int p) : att_A(p) { cout<<"Constructeur de A"<<endl; } void Afficher() { cout<<" att_A :"<<att_A<<endl; } };

class B : public A {public : int att_B; B(int p1,int p2) : A(p1),att_B(p2) { cout<<"Constructeur de B"<<endl; } void Afficher() { cout<<" att_A :"<<att_A<<" et att_B :"<<att_B<<endl; } }; … … … … B b(5,7); // OK

Page 138: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 136

I.2- Cas où la classe de base dispose d'un constructeur par défaut Ce constructeur peut être implicite ou explicite. Dans ce cas, l'appel explicite du constructeur de la classe de base dans la classe dérivée n'est pas nécessaire. En effet cet appel peut se faire d'une manière implicite.

Exemple :

class A {public : int att_A; }; class B : public A {public : int att_B; B(int p) : att_B(p) { cout<<"Constructeur de B"<<endl; } }; … … B b(7);

• Ce constructeur de la classe B fait un appel implicite au constructeur par défaut de la classe A.

• Avec ce constructeur de B, att_B sera créé et initialisé mais att_A sera seulement créé.

II - Cas où la classe dérivée ne dispose pas de constructeurs paramétrés

II.1 - Si la classe de base ne dispose que de constructeurs paramétrés Dans ce cas l'instanciation de l'objet dérivé ne sera pas possible. En effet, l'instanciation de cet objet se fera avec le constructeur par défaut généré par le compilateur. Or ce dernier ne peut pas faire un appel automatique au constructeur paramétré de la classe de base car il ne saura pas quels arguments faut-il lui passer. Cette situation engendre une erreur de compilation.

Exemple : class A {public : int att_A; A(int p) : att_A(p) { cout<<"Constructeur de A"<<endl; } };

class B : public A {public : int att_B; }; … … B b; // Erreur

Il est clair qu'avec cette définition il n y a pas moyen d'appeler et de passer des arguments au constructeur de A lors de la création d'un objet de type B.

Page 139: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 137

II.1- Si la classe de base dispose d'un constructeur par défaut Dans ce cas, c'est ce dernier qui sera automatiquement appelé par le constructeur par défaut de la classe dérivée.

Exemple :

class A {public : int att_A; };

class B : public A {public : int att_B; }; … … B b; // appel du constructeur par défaut de B qui fait appel à son // tour au constructeur par défaut de A.

Définition et appel du constructeur de recopie d'une classe dérivée

• Le constructeur de recopie d'une classe est appelé : o lors de la création d'un objet à partir d'un autre objet. o lors de la transmission de la valeur d'un objet en argument ou en retour d'une fonction.

Déroulement de la recopie d'un objet dérivé : La recopie d'un objet d'une classe dérivée nécessite également la recopie de la partie correspondant à l'objet de la classe de base. Ceci passe par un appel hiérarchique des constructeurs de recopie. Le déroulement de cet appel dépend des cas de figures qui peuvent se présenter :

Cas 1 : La classe dérivée n'a pas de constructeur de recopie explicite Dans ce cas, c'est le constructeur de recopie par défaut qui sera appelé. Ce dernier engendrera l'appel automatique du constructeur de recopie de la classe de base. Si ce dernier est explicitement défini alors il sera appelé, sinon c'est le constructeur de recopie par défaut qui sera appelé.

Cas 2 : La classe dérivée a un constructeur de recopie explicite

• En C++, l'appel du constructeur de recopie explicitement défini n'engendre aucun appel automatique du constructeur de recopie de la classe de base (même si ce dernier est explicitement défini).

• Dans ce cas, le constructeur de recopie explicite doit prendre en charge la recopie de la totalité de l'objet dérivé. Cette recopie peut se faire par : o Appel explicite de tout autre constructeur paramétré de la classe de base. o Appel explicite du constructeur de recopie (implicite ou explicite) de la classe de base.

Page 140: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 138

Problème de l'appel explicite du constructeur de recopie de la classe de base par son homologue de la classe dérivée Lors de l'appel explicite du constructeur de recopie de la classe de base par le constructeur de recopie de la classe dérivée le problème suivant se pose : comment déduire l'objet de la classe de base à passer comme argument au constructeur de recopie de cette dernière à partir de l'objet de la classe dérivée. L'idée permettant de résoudre ce problème consiste tout simplement à faire passer l'objet de la classe dérivée au constructeur de recopie de la classe de base. La conversion "objet dérivée" vers "objet de base" sera dans ce cas automatiquement faite par le compilateur.

Exemple 1 :

L'exemple suivant montre comment appeler le constructeur de recopie d'une classe de base dans le constructeur de recopie d'une classe dérivée.

class A {public : int att_A; A(int p) : att_A(p) { cout<<"Constructeur de A"<<endl; } void Afficher() { cout<<" att_A :"<<att_A<<endl; } };

class B : public A {public : int att_B; B(int p1,int p2) : A(p1),att_B(p2) {cout<<"Constructeur de B"<<endl;} B(const B& x):A(x) { att_B = x.att_B; } void Afficher() { cout<<" att_A :"<<att_A<<" et att_B :"<<att_B<<endl; } }; L'objet x passé au constructeur de recopie de A dans l'appel suivant : B(const B& x):A(x) est de type B mais il sera automatiquement converti vers un objet de type A par le compilateur grâce à la compatibilité entre "objet dérivée" et "objet de base".

Exemple 2 :

L'exemple suivant propose une autre définition du constructeur de recopie d'une classe dérivée qui fait appel non pas au constructeur du recopie de la classe de base mais à son constructeur paramétré. … … … B(const B& x):A(x.att_A) { att_B = x.att_B; } … … … Dans cette version le constructeur de recopie de B fait appel au constructeur paramétré de Adéfini comme suit : A(int p) : att_A(p){ }

Page 141: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 139

Remarque :

Les membres issus de la classe de base ne peuvent pas être initialisés directement par le constructeur de la classe dérivée. Leur initialisation doit nécessairement passer par l'appel du constructeur de la classe de base. Ainsi une définition comme la suivante du constructeur paramétré de B sera incorrecte.

B(int p1,int p2) : att_A(p1),att_B(p2) {

cout<<"Constructeur de B"<<endl; } ou également : B(int p1,int p2) {

cout<<"Constructeur de B"<<endl; att_A=p1; att_B=p2;

}

Dans ce cas le compilateur signalera que la classe B ne possède pas un attribut att_A. En effet lorsqu'il s'agit de construction et d'initialisation chaque classe (de base ou dérivée) doit s'occuper de la partie qui la concerne. Ce n'est pas le cas lorsqu'il s'agit d'utilisation de l'objet dérivée.

Destruction d'objets dérivés • L'appel du destructeur d'un objet d'une classe dérivée engendre l'appel automatique du

destructeur de la classe de base. • L'ordre d'exécution de ces destructeurs est inverse à l'ordre d'exécution des constructeurs :

Exécution du destructeur de la classe dérivée puis exécution du destructeur de la classe de base

• D'une manière plus détaillée, la libération se fait comme suit : o Appel et exécution du destructeur de la classe dérivée. o Appel et exécution du destructeur de la classe de base. o Libération de la mémoire utilisé par les attributs (Il ne s'agit pas des espaces mémoires

dynamiques pointés par les éventuels attributs de type pointeurs mais des espaces occupés par les attributs eux mêmes).

Exemple : class A { public : … … … ~A(){ cout<<"Destructeur de A"<<endl;} … … … };

class B : public A { public : … … … ~B(){ cout<<"Destructeur de B"<<endl;} … … … };

Page 142: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 140

La destruction d'un objet déclaré comme suit : B obj(5,7); va engendrer la sortie suivante … … … Destructeur de B Destructeur de A

Polymorphisme • Le mot polymorphisme désigne le fait de pouvoir prendre plusieurs formes. • En programmation orientée objet le polymorphisme est un concept qui se manifeste par la

capacité d'une méthode à s'exécuter de différentes manières selon la nature de l'objet qui l'appelle (objet de base ou objet dérivé).

• Le polymorphisme concerne essentiellement des objets liés par une relation d'héritage.

Limite de la liaison statique des méthodes

Exemple : Rappel de la liaison statique class A {public : int att_A; A(int p) : att_A(p) { cout<<"Constructeur de A"<<endl; } void Afficher() { cout<<" att_A :"<<att_A<<endl; } };

class B : public A {public : int att_B; B(int p1,int p2) : A(p1),att_B(p2) { cout<<"Constructeur de B"<<endl; } void Afficher() { cout<<" att_A :"<<att_A<<" et att_B :"<<att_B<<endl; } }; void main() { A* pa; B objB(3,5), *pb; pb=&objB; pb->Afficher(); pa=pb; pa->Afficher(); }

pb->Afficher(); engendre l'affichage suivant :att_A : 3 et att_B : 5

pa->Afficher(); engendre l'affichage suivant : att_A : 3

Page 143: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 141

• L'affichage incomplet à partir du pointeur pa provient du fait que le compilateur se base sur le type de pa au moment de la compilation pour décider de la version de la méthode à appeler : pa étant de type A* alors c'est la méthode Afficher de la classe A qui sera appelée dans ce cas.

• Le compilateur effectue dans ce cas une liaison statique. Cette dernière ne tient pas compte du véritable type de l'objet pointé et ne permet pas par conséquent d'appeler les versions appropriées des méthodes à utiliser.

Liaison dynamique et polymorphisme

• Pour remédier au problème de la liaison statique rencontré lors de la conversion d'un objet dérivé vers un objet de base il faut être capable d'appeler la méthode qui correspond au type de l'objet pointé et non au type du pointeur. Pour ce faire il faut effectuer ce que l'on appelle une liaison dynamique entre l'appel et le corps de la méthode. Cette liaison doit être faite au moment de l'exécution et non au moment de la compilation.

• La liaison dynamique dote les méthodes de la capacité de se comporter de différentes manières selon l'objet appelant. Ces méthodes sont alors dites polymorphes.

Mise en œuvre du polymorphisme

• Pour rendre une méthode polymorphe, cette dernière doit être déclarée virtuelle dans la classe de base à l'aide du mot-clé virtual selon la syntaxe suivante :

virtual Type NomMethodePolymorphe(Paramètres) { … … … … } Exemple 1: class A {public : int att_A; A(int p) : att_A(p){}; virtual void Afficher() { cout<<" att_A :"<<att_A<<endl } }; class B : public A {public : int att_B; B(int p1,int p2) : A(p1),att_B(p2){} void Afficher() { cout<<" att_A :"<<att_A<<" et att_B :"<<att_B<<endl; } }; void main() { A* pa; B objB(3,5), *pb; pb=&objB; pb->Afficher(); pa=pb; pa->Afficher(); }

Page 144: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 142

pa->Afficher(); va engendrer dans ce cas l'affichage suivant : att_A : 3 et att_B : 5

Remarque :

La liaison dynamique d'une méthode n'est pas limitée au cas où cette dernière est appelée explicitement à partir d'un objet mais s'applique également au cas de l'appel de la méthode à l'intérieur de la classe par d'autres méthodes. L'exemple suivant illustre cette situation.

Exemple :

class A {public : int att_A; A(int p) : att_A(p){}; virtual void Contenu() { cout<<" att_A :"<<att_A<<endl; } void Afficher() { cout<<"Le contenu de l'objet est : "<<endl; Contenu(); } }; class B : public A {public : int att_B; B(int p1,int p2) : A(p1),att_B(p2){} void Contenu() { cout<<" att_A :"<<att_A<<" et att_B :"<<att_B<<endl; } };

Considérations relatives à la déclaration et l'usage des fonctions virtuelles

• La spécification du mot-clé virtual n'est pas nécessaire pour les redéfinitions de la méthode virtuelle. Cette spécification peut se limiter à la classe de base.

• A partir du moment où une fonction a été déclarée virtuelle dans une classe de base, alors elle sera soumise à la liaison dynamique dans cette classe et dans toutes les classes dérivées. Il est à noter que l'effet de virtual ne se limite pas aux classes obtenues par dérivation directe mais s'étend à toute la hiérarchie des classes obtenues par dérivation successives.

• La redéfinition d'une fonction virtuelle n'est pas obligatoire dans toutes les classes dérivées. Toutefois une classe dérivée ne peut appeler une méthode virtuelle que si cette dernière possède au moins une définition dans sa hiérarchie. Dans ce cas c'est la dernière redéfinition qui sera utilisée.

• Si une méthode a été déjà définie dans une classe de base puis redéfinie comme virtuelle mais dans une classe dérivée, alors la nouvelle redéfinition sera soumise à la liaison dynamique. Toute autre redéfinition de cette méthode dans une classe dérivée sera également soumise à la liaison dynamique.

Page 145: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 143

• La surdéfinition (et non la redéfinition) dans une classe dérivée d'une méthode déclarée virtuelle dans une classe de base est interprétée comme une nouvelle définition de la méthode (une définition possédant une signature et une syntaxe d'appel différentes). Elle ne sera pas par conséquent soumise à la liaison dynamique mais plutôt à la liaison statique.

Objets auto, pointeurs, références et possibilité de typage dynamique

• Le typage dynamique n'est possible qu'à partir des pointeurs et des références. Il ne l'est pas dans le cas des objets auto.

Cas des objets de type auto :

• La copie d'un objet auto d'une classe dérivée dans un objet auto de la classe de base engendre la copie membre par membre des attributs correspondant à la classe de base depuis l'objet dérivé vers l'objet de base.

• L'appel d'une méthode virtuelle depuis l'objet de base ainsi copié ne peut invoquer que la définition associée à la classe de base. En effet, il n'est pas concevable d'appeler la version redéfinie dans la classe dérivée car l'objet copié ne dispose dans sa copie d'aucune information sur les données dérivées.

Exemple : Etant données les classes A et B dérivée de A. A ObjA; B ObjB(5,7); ObjA = ObjB;

ObjA.Afficher(); ne peut invoquer que la version de base de Afficher. Il n'est pas possible d'utiliser la version redéfinie car ObjA n'a aucune information sur les attributs dérivés (att_B dans ce cas).

Cas des pointeurs :

Lorsqu'il s'agit de pointeur la liaison dynamique est envisageable. En effet, dans le cas où ce pointeur pointe sur un objet dérivé, l'appel de la redéfinition d'une méthode virtuelle depuis ce pointeur ne risque pas de poser de problème puisque toutes les informations nécessaires au bon fonctionnement de la redéfinition sont nécessairement disponibles dans l'objet pointé.

Exemple : Etant données les classes A et B dérivée de A. B ObjB(5,7); A* pa = (A*)&ObjB; pa->Afficher();

Il est envisageable en faisant une liaison dynamique de faire appel à la redéfinition de la méthode Afficher (version définie dans B). En effet, l'objet pointé dispose dans ce cas de toutes les informations nécessaires au bon fonctionnement de cette redéfinition.

att_A = 5

att_B = 7

att_A = 5

ObjB ObjA

Copie

att_A=5

att_B=7

EF0A78 ObjB

A* pa

Page 146: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 144

Cas des références :

Il est possible d'envisager une liaison dynamique avec les références pour les mêmes raisons que celles évoqués pour les pointeurs.

Exemple : Etant données les classes A et B dérivée de A. B ObjB(5,7); A& r = ObjB; r.Afficher();

Il est envisageable en faisant une liaison dynamique de faire appel à la redéfinition de la méthode Afficher (version définie dans B). En effet, l'objet référencé (ObjB) dispose dans ce cas de toutes les informations nécessaires au bon fonctionnement de cette redéfinition.

Restriction du polymorphisme Les constructeurs ne peuvent pas être virtuels. Tout objet ne peut être construit que par le constructeur qui a été défini pour sa classe.

Polymorphisme des destructeurs Les destructeurs peuvent être déclarés virtuels. Considérons le cas suivant : class A {public : … … … ~A(){cout<<"Destructeur de A";} }; class B : public A {public : … … … ~B(){cout<<"Destructeur de B";} };

A* pa = new B(5,7); pa->Afficher(); delete pa;

• Si le destructeur de A n'a pas été déclaré comme virtuel dans A. delete pa; va engendrer dans ce cas un appel au destructeur de A (liaison statique car pa

est un pointeur sur A et ~A n'est pas virtuel. Or ce destructeur n'est pas approprié pour détruire l'objet pointé puisque ce dernier est de type B.

Pour résoudre ce problème il faut modifier la classe A comme suit : class A {public : … … … virtual ~A(){ … … … } };

delete pa; va engendrer dans ce cas un appel du destructeur de B (liaison dynamique). Cet appel convient donc au type de l'objet pointé.

att_A=5

att_B=7

EF0A78 ObjB

A& r

att_A=5

att_B=7

EF0A78

A* p

Page 147: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 145

Méthodes virtuelles pures et classes abstraites

Méthodes virtuelles pures

• Il y a des situations en héritage où une classe de base se trouve devant l'incapacité de proposer des définitions à certaines méthodes. Ceci est généralement dû à un manque d'informations nécessaires à la proposition de ces définitions. Ces informations seront le plus souvent fournies au niveau des classes dérivées.

• Une méthode qui est déclarée mais non définie dans une classe de base et qui est redéfinie dans les classes dérivées est appelée une méthode virtuelle pure.

• Une méthode virtuelle pure est déclarée comme suit : virtual Type NomMethode(Paramétres) = 0;

Exemple :

On veut écrire un programme de dessin de formes. Ces formes peuvent être de deux catégories : cercles et carrés. Les classes qui seront utilisées par ce programme sont en nombre de trois :

- Une classe de base appelée Forme pour représenter les formes d'une manière générale. - Deux classes Carre et Cercle dérivées toutes les deux de Forme.

class Forme { int x; int y; virtual int Surface() = 0; ... ... ... }; class Carre : public Forme { int Cote; int Surface() { return Cote*Cote; } ... ... ... };

class Cercle : public Forme { int Rayon; int Surface() { return 3.14 * Rayon * Rayon; } ... ... ... };

Toute forme possède une surface. C'est pourquoi une méthode de calcul de surface a été intuitivement associée à la classe Forme. Toutefois cette dernière est incapable de définir la manière avec laquelle sera calculée cette surface. Ce calcul nécessitant en effet la connaissance de la nature exacte de la forme.

Classes abstraites

• Une classe qui possède au moins une méthode virtuelle pure est appelée une classe abstraite.

• Une classe abstraite ne peut pas être instanciée.

Page 148: CoursPOOC++

Héritage et polymorphisme Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 146

• Une méthode virtuelle pure peut être redéfinie dans la classe dérivée ou laissée encore virtuelle pure (Il faut la déclarer explicitement virtuelle pure dans la classe dérivée pour les versions 2.0 et antérieures du C++. Ceci n'est plus nécessaire depuis la version 3.0).

• Une classe dérivée qui ne définit pas toutes les méthodes virtuelles pures de ses classes ascendantes est considérée comme une classe abstraite. Elle ne peut pas par conséquent être instanciée.

• Une classe dérivée (directement ou indirectement) d'une classe abstraite n'est instanciable que si elle définit toutes les méthodes virtuelles pures qu'elle hérite.

Utilité des classes abstraites Les classes abstraites servent généralement à établir une sorte de cahier des charges qui doit être rempli par les classes qui en dérivent. Elles permettent également de factoriser les propriétés communes à ces classes.

Page 149: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 147

La surcharge des opérateurs

Introduction • Les opérateurs utilisés en C++ sont essentiellement définis pour des opérandes ayant des

types prédéfinis : int, char, float, etc. • Ces opérateurs sont par défaut inapplicables lorsqu'il s'agit d'opérandes ayant des types

personnalisés comme ceux construits à l'aide des classes par exemple.

Exemple :

On considère le type défini par la classe Complexe : class Complexe { double Reel; double Imaginaire; public : Complexe(){Reel = 0; Imaginaire = 0;} Complexe(double r, double i) : Reel(r), Imaginaire(i) {} … … … };

C1 et C2 sont deux objets de type Complexe. L'expression suivante C1 + C2 ne sera pas acceptée par le compilateur car l'opérateur + n'est pas défini par défaut pour des opérandes de type Complexe.

La surcharge des opérateurs

Le C++ offre aux programmeurs un outil permettant de proposer des définitions supplémentaires à un opérateur qui soient adaptées aux types personnalisés qu'ils construisent. Cet outil est appelé la surcharge d'opérateurs ou également la surdéfinition des opérateurs.

Remarque :

La surcharge des opérateurs permet de manipuler d'une manière plus simple et plus intuitive des objets de types personnalisés. En effet si on considère l'exemple précédent, il est tout à fait possible de définir une méthode qui effectue la somme de deux complexes. Complexe Complexe::Somme(Complexe C);

La somme sera effectuée comme suit : C3 = C1.Somme(C2);

Cette expression reste toutefois moins pratique que : C3=C1 + C2;

La fonction opérateur Tout opérateur du C++ est en réalité défini comme une fonction classique qui porte comme nom l'opérateur en question précédé du mot-clé operator comme suit :

TypeRetour operator SymboleOpérateur(paramètres);

Page 150: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 148

Exemple :

int a, b, c;

L'expression a = b + c est en réalité interprétée par le compilateur comme un appel de la fonction opérateur + comme suit : a = operator+(b,c);

Syntaxe de la surcharge d'un opérateur La surcharge d'un opérateur n'est autre que la surcharge de sa fonction présentée dans le paragraphe précédent. Elle obéit de ce fait aux règles régissant la surcharge des fonctions d'une manière générale. Il s'agit donc de proposer une nouvelle définition à la fonction suivante :

TypeRetour operator SymboleOpérateur(paramètres) { Nouvelle définition }

Cette considération permet d'ailleurs de protéger les définitions par défaut prédéfinies par le langage. En effet, une nouvelle surcharge nécessite l'utilisation d'une nouvelle signature donc des paramètres différents de ceux déjà utilisés dans les définitions par défaut.

Formes de surcharge d'un opérateur Les formes possibles de surcharge d'un opérateur dépendent de la nature des opérandes manipulés. En effet : • Si la surcharge est effectuée pour des opérandes de type classe alors la nouvelle définition

de l'opérateur peut être réalisée sous forme d'une fonction membre de la classe ou sous forme d'une fonction globale.

• Si aucun des opérandes n'est de type classe (opérande de type énumération par exemple) alors la seule manière d'effectuer la surcharge est sous forme d'une fonction globale.

Surcharge d'un opérateur sous forme d'une fonction globale

L'exemple suivant montre la surcharge de l'opérateur + comme une fonction globale. L'opérateur + étant un opérateur dyadique sa surcharge doit prendre deux arguments représentant les opérandes à additionner.

Complexe operator+(const Complexe& C1, const Complexe& C2) { Complexe Resultat; Resultat.Reel = C1.Reel + C2.Reel; Resultat.Imaginaire = C1.Imaginaire + C2.Imaginaire; return Resultat; }

• Pour que cette définition soit correcte, il faut que les deux attributs Reel et Imaginairesoient accessibles par la fonction globale operator+. Pour cela ces deux attributs doivent être publiques.

• Pour préserver le principe d'encapsulation (la classe doit cacher son implémentation et la laisser privée), il est possible de procéder autrement en laissant ces deux attributs privés et en déclarant la fonction operator+ comme une fonction amie de la classe Complexe

comme suit :

Page 151: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 149

class Complexe { double Reel; double Imaginaire; public : Complexe(){Reel = 0; Imaginaire = 0;} Complexe(double r, double i) : Reel(r), Imaginaire(i){} friend Complexe operator+(const Complexe& C1, const Complexe& C2); };

Remarque :

L'utilisation des références pour les paramètres complexes permet d'éviter de copier des objets qui peuvent être de grande taille et d'optimiser par conséquent le code. L'utilisation de const

permet par ailleurs de préserver ces paramètres contre toute éventuelle modification par la fonction.

Surcharge d'un opérateur sous forme d'une fonction membre

La deuxième possibilité consiste à réaliser la surcharge sous forme d'une fonction membre. Cette forme de surcharge n'est possible que si le premier opérande est de type classe ce qui est le cas dans l'exemple du nombre complexe. class Complexe {

… … … public : Complexe operator+(const Complexe& C) { Complexe Resultat; Resultat.Reel = this->Reel + C.Reel; Resltat.Imaginaire = this->Imaginaire + C.Imaginaire; return Resultat; }

… … … };

La fonction operator+ prend dans ce cas un seul paramètre qui représente le deuxième opérande. Le premier opérande étant l'objet qui va invoquer la méthode operator+.

En effet : C3 = C1 + C2; est interprétée par le compilateur dans ce cas comme suit : C3=C1.operator+(C2);

• Le problème d'accessibilité des attributs Reel et Imaginaire ne se pose pas dans ce cas car la fonction operator+ est membre de la classe Complexe.

Quelques exemples de surcharge d'opérateurs.

Surcharge de l'opérateur d'affectation =

• Tout comme pour le constructeur de recopie, le compilateur génère automatiquement une surcharge pour l'opérateur d'affectation d'une classe. Cette surcharge sous forme d'une fonction membre publique et non statique est possible à condition que la classe ne dispose pas de membre statique, constant ou de type référence.

Page 152: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 150

• La surcharge par défaut réalise la copie champ par champ de l'opérande de droite dans l'opérande de gauche et reste par conséquent inappropriée pour l'affectation d'objets ayant des membres dynamiques alloués sur le tas.

Exemple :

L'exemple suivant montre la limite de la surcharge par défaut de l'opérateur d'affectation class TabEntiers { int Nb; int* T; public :

TabEntiers(int Ni) { Nb=Ni; T=new int [Nb]; } ~TabEntiers() { delete[]T; } void Saisir() { for(int k=0;k<Nb;k++) { cou<<"Donner l'élément d'indice "<<k; cin>>T[k]; } } void Afficher() { for(int k=0;k<Nb;k++) { cout<<T[k]<<' '; } } }; void main() { TabEntiers TE1(5), TE2(5); TE1.Saisir(); TE2 = TE1; … … … … … … … … }

• La copie réalisée par l'opérateur d'affectation reste donc partielle et incomplète. Dans ce cas il faut modifier le comportement par défaut de cet opérateur pour réaliser une copie complète d'objets. Cette modification doit procéder selon les étapes suivantes :

o Vérifier le cas de l'auto-affectation (Affectation d'un objet à lui-même)

o Libérer l'ancien contenu de l'opérande de gauche.

o Allouer un nouvel espace pour l'opérande de gauche qui correspond aux données à copier depuis l'opérande droit.

o Copier les données de l'opérande droit vers l'opérande gauche.

5

FFE4D8A

int Nb

int* T

2 1 8 5 3

TE15

FFE4D8A

TE2int Nb

int* Tcopie

Etat de la mémoire après l'affectation de TE2=TE1

Page 153: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 151

• Le code équivalent se présente alors comme suit : TabEntiers& TabEntiers::operator=(const TabEntiers& TE) { // Vérification s'il s'agit d'une auto affectation if(this != TE) { // Libération de l'ancien contenu delete[] T; // Allocation de l'espace pour le nouveau contenu T= new int [TE.Nb]; // Copie du nouveau contenu Nb = TE.Nb; for(int k=0;k<Nb;k++) T[k]=TE.T[k]; } return *this; }

Remarques :

• Le C++ impose que la surcharge de l'opérateur d'affectation se fasse comme une fonction membre non statique. Une telle contrainte oblige l'opérande de gauche d'être un objet de classe et garantit par conséquent de pouvoir le manipuler en tant qu'une lvalue dans laquelle on peut placer un contenu.

• Le fait d'utiliser un paramètre de type const permet de traiter convenablement le cas de l'affectation d'un objet constant et ce en plus des objets non constants comme opérande de droite.

• La valeur de retour de la surcharge aurait pu être void auquel cas l'affectation se limiterait à la simple expression TE2 = TE1.

• Le fait de renvoyer un objet TabEntiers offre la possibilité de pouvoir effectuer des affectations multiples du genre : TE3 = TE2 = TE1;

• TE3 = TE2 = TE1; est traduite comme TE3.operator=(TE2.operator=(TE1));

Nota bene : Renvoi d'une référence

Le renvoi d'une référence par une fonction doit être employé avec précaution. En effet, il faut éviter de renvoyer la référence d'un objet locale à la fonction parce que cet objet sera automatiquement détruit après l'appel de la fonction. Ce problème ne se pose pas dans le cas de cette surcharge puisque la référence renvoyée est celle de l'objet courant qui n'est pas local à la fonction. Le fait de renvoyer une référence à un objet TabEntiers permet par ailleurs d'éviter l'appel implicite du constructeur de recopie.

Surcharge de l'opérateur d'indexation [ ] Pour les classes qui possèdent un attribut de type tableau, la surcharge de l'opérateur d'indexation offre une possibilité syntaxique qui permet d'accéder aux éléments de ce dernier directement à partir de l'objet sans passer d'une manière explicite par le tableau.

Exemple :

Par exemple pour le cas de la classe TabEntiers, la récupération de la valeur d'un élément du tableau doit passer par l'appel d'une fonction dédiée :

Page 154: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 152

int TabEntiers::GetElement(int Indice) { return T[Indice]; } La surcharge de l'opérateur [] sous forme d'une méthode publique comme suit : int& TabEntiers::operator[](int Indice) { return T[Indice]; } permet d'avoir le même effet que GetElement mais en utilisant en plus une syntaxe plus souple. En effet l'accès à un élément du tableau à partir d'un objet TE se fait comme suit :

TabEntiers TE(5); … … … TE[i]=8; // Accès à l'élément d'indice i grâce à la surcharge de []

Remarques :

• Le fait d'utiliser une valeur de retour de type référence permet de laisser envisager d'utiliser TE[i] comme un opérande gauche dans une affectation. (Exemple : TE[i]=5).

• Une valeur de retour de type int est donc possible mais restreint l'utilisation de l'indexeur aux opérations de lecture de valeur seulement.

• Tout comme l'opérateur d'affectation, le C++ impose que la surcharge de l'opérateur d'indexation se fasse comme une fonction membre non statique.

Surcharge de l'opérateur ++ L'opérateur ++ est un opérateur unaire. Il a la particularité de pouvoir être utilisé de deux manières : postfixé et préfixé.

Surcharge de la version préfixée

L'exemple suivant montre la surcharge sous forme d'une fonction membre de la version préfixée de l'opérateur ++ pour la classe Complexe : // Surcharge sous forme d'une fonction membre Complexe Complexe::operator++() { Reel++; Imaginaire++; return *this; }

Il est tout à fait possible de réaliser cette surcharge sous forme d'une fonction globale et amie. Le prototype de cette dernière prendra alors un seul paramètre qui représente l'objet à incrémenter. Cette surcharge se présente comme suit : // Surcharge sous forme d'une fonction amie Complexe operator++(const Complexe& C) { C.Reel++; C.Imaginaire++; return C; }

Page 155: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 153

Surcharge de la version postfixée

Pour pouvoir la distinguer de la version préfixée, le C++ impose que la version postfixée utilise un paramètre supplémentaire et fictif de type int. Ce paramètre permet tout simplement au compilateur de choisir la version appropriée à utiliser mais aucune valeur ne lui sera réellement transmise lors de l'appel. La surcharge sous forme d'une fonction membre de cette version se fait comme suit : Complexe operator++(int n) { Complexe C(*this); Reel++; Imaginaire++; return C; }

La surcharge sous forme d'une fonction globale et amie de cette version se fait comme suit :

Complexe operator++(const Complexe& C, int n) { Complexe R(C); C.Reel++; C.Imaginaire++; return R; // Renvoi de l'objet non incrémenté }

Considérations relatives à la surcharge des opérateurs

• Il n'est pas possible de créer de nouveaux opérateurs avec de nouveaux symboles. • Les opérateurs surchargés conservent leur ordre de priorité et leur associativité. • La commutativité n'est pas toujours conservée. Par exemple la surcharge sous forme d'une

fonction membre de la multiplication d'un Complexe et d'un réel ne peut pas servir pour réaliser la multiplication d'un réel par un complexe (on ne peut pas appeler la méthode à partir d'un réel).

• Un opérateur garde toujours sa pluralité en terme d'opérandes. Ainsi s'il est unaire (monadique) il reste unaire et s'il est binaire (dyadique) alors il reste également binaire.

• Il n'est pas possible de définir de valeur par défaut pour les paramètres d'une fonction opérateur (pour assurer la préservation de la pluralité).

• Le C++ autorise la surcharge de la majorité des opérateurs sauf quelques uns tels que les opérateurs d'accès aux membres d'une classe (:: , . , .*), l'opérateur sizeof ainsi que l'opérateur ternaire ( ? : ).

Considération relative à la signification de la surcharge des opérateurs Le C++ laisse le libre choix quant à la signification à attribuer à la surcharge d'un opérateur. Toutefois, il est toujours conseillé que cette signification reste guidée par le bon sens et ne s'éloigne pas trop de celle utilisée par défaut. Ainsi, il est déconseillé par exemple d'utiliser le symbole + pour réaliser la multiplication de deux objets de la classe Complexe même si ceci reste tout à fait possible et permis par le langage.

Page 156: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 154

Surcharge de l’opérateur de cast

Grâce au constructeurs de transtypage, le C++ met à la disposition des programmeurs un outil permettant de faire des conversions implicites d’une donnée d’un type S (de base ou personnalisé) vers un type D qu’ils construisent à l’aide des classes (Cf. chapitre

Constructeurs et destructeur). Cela dit, les constructeurs de transtypage ne permettent pas la conversion inverse (du type D vers le type S) surtout si D est un type de base (int, char, etc.) ne disposant pas de mécanisme de construction. Même si D était un type classe, sa définition pourrait être inaccessible pour pouvoir la modifier ou lui ajouter un constructeur de transtypage symétrique (Conversion de D vers S) surtout si cette classe a été développée par un tiers. Pour palier à ce manque, il est possible d’ajouter à la définition d’une classe un opérateur de transtypage qui assure la conversion des objets de cette classe vers un type de destination souhaité. L’ajout de l’opérateur de cast à une classe se fait à travers la surcharge de l’opérateur cast. Cet opérateur est défini comme une fonction membre non statique, sans paramètres et sans indication préalable du type de retour. Elle se présente selon le modèle suivant :

Syntaxe :

NomClasse::operator TypeDestination() { … … … … return (Valeur) ; }

Dans ce modèle :

• NomClasse désigne la classe de l’objet à convertir.

• TypeDestination désigne le type de destination vers lequel sera converti l’objet. Ce type désigne également d’une manière implicite le type de retour de la fonction operator qui n’est pas explicitement spécifié dans ce cas.

• Valeur désigne le résultat de la conversion. Valeur doit être de type TypeDestination.

Exemple :

L’exemple suivant montre la surcharge de l’opérateur cast pour la classe complexe. Pour cette surcharge on considère que la conversion renvoie la partie réelle seulement du complexe.

class Complexe { … … … public : operator double(); … … … };

Complexe::operator double(); { return Reel; }

Page 157: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 155

Grâce à cette surcharge de l’opérateur cast, il devient possible d’utiliser l’expression suivante lors de la manipulation des objets de la classe Complexe :

Complexe C(2.5,3.8); double d = C ; // Conversion implicite de C vers le type double // grâce à l’appel de l’opérateur cast.

Il est également possible d’écrire l’expression suivante : double d2 = 5.2 + C ; // Conversion implicite de C vers le type // double grâce à l’appel de l’opérateur cast.

L’addition est par défaut définie entre deux réels mais elle ne l’est pas entre un réel et un complexe. Pour réaliser cette addition le compilateur convertit implicitement l’objet C vers le type double puis réalise une addition entre deux réels et le résultat est par conséquent un réel.

Conversions implicites définies par l’utilisateur

Grâce au constructeur de transtypage et à la surcharge de l’opérateur cast, il devient possible au compilateur d’effectuer des conversions implicites de type. Ces conversions définies par l’utilisateur et qui sont réalisées d’une manière automatique par le compilateur peuvent parfois conduire ce dernier vers des situations d’ambiguïté ne pouvant être résolues que par des appels explicites des outils de conversion.

Exemple :

Si on considère la définition suivante de la classe Complexe comprenant à la fois un constructeur de transtypage et une surcharge de l’opérateur +. class Complexe { double Reel; double Imaginaire; public : Complexe(double r, double i) : Reel(r), Imaginaire(i) {} Complexe(){Reel = 0; Imaginaire = 0;} // Constructeur de transtypage Complexe(double val){Reel = val;} // Surcharge de l’opérateur + Complexe operator+(const Complexe& C) { Complexe Resultat; Resultat.Reel = this->Reel + C.Reel; Resultat.Imaginaire = this->Imaginaire + C.Imaginaire; return Resultat; } // Surcharge de l’opérateur cast operator double() { return Reel; } void Afficher() { cout<<"Partie réelle : "<<Reel; cout<<"Partie imaginaire : "<<Imaginaire; } } ;

Page 158: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 156

Ambiguïté d’interprétation des expressions On considère l’expression suivante :

Complexe C(4.6, 3.8) ; 5.2 + C ;

L’interprétation de l’expression 5.2 + C; par le compilateur va conduire ce dernier vers une ambiguïté. En effet deux interprétations sont possibles dans ce cas :

Première interprétation :

o Conversion de 5.2 vers complexe grâce au constructeur de transtypage. o Faire une addition entre deux complexes vu que cette opération est surdéfinie dans ce

cas pour les complexes, chose qui va donner un résultat complexe.

Deuxième interprétation :

o Conversion du C vers le type double grâce à l’opérateur cast. o Faire une addition entre deux réels, chose qui va donner un résultat réel.

Utilisation des conversions explicites Pour éviter cette ambiguïté, il est nécessaire d’utiliser des conversions explicites dans cette expression. Cette dernière pourra alors s’écrire de deux manières :

Première possibilité : 5.2 + (double)C ;

Le compilateur commence par faire un appel à l’opérateur cast pour la conversion de l’objet C vers le type double. Le résultat est ensuite ajouté au premier double (5.2). Il s’agit dans ce cas d’une addition entre doubles et le résultat final est par conséquent un double.

Deuxième possibilité : Complexe(5.2) + C ;

L’appel explicite du constructeur de transtypage engendre la conversion du double 5.2 vers un objet Complexe. Ce dernier sera additionné par la suite à l’objet C en utilisant la surcharge de l’opérateur + définie dans la classe Complexe. Le résultat est donc dans ce cas un objet de type Complexe.

Interdiction des conversions implicites L’appel implicite du constructeur de transtypage peut conduire parfois vers des situations gênantes. En effet le compilateur considère tout constructeur pouvant être appelé avec un seul paramètre (de type différent que celui de la classe en cours) comme un constructeur de transtypage alors qu’un tel constructeur peut figurer dans une classe sans être destiné à l’origine par le concepteur de cette dernière à faire des conversions. C’est le cas par exemple du constructeur suivant de la classe TabEntiers qui est sensé tout simplement allouer l’espace mémoire nécessaire à l’objet.

Page 159: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 157

class TabEntiers { int Nb; int* T; public : … … … TabEntiers(int Ni) { Nb=Ni; T=new int[Nb]; } … … … } ;

• Pour pallier à ce problème, le C++ offre un outil permettant de contrôler l’appel implicite des constructeurs particuliers. Ce contrôle est réalisé avec le mot-clé explicit. Ce dernier utilisé comme préfixe de la déclaration d’un constructeur interdit à ce dernier d’être appelé implicitement par le compilateur et seul les appels explicites lui seront alors autorisés.

• La signature du constructeur de TabEntiers serait alors :

explicit TabEntiers(int Ni);

Remarque :

explicit ne peut être appliqué qu’aux constructeurs.

Page 160: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 158

Les modèles

Les modèles, appelés également patrons, permettent de créer des fonctions ou des classes génériques qui peuvent être paramétrées de point de vue types des données qu'elles manipulent. Ces modèles permettent ainsi d'avoir une écriture plus concise du code puisqu'un modèle particulier peut remplacer plusieurs définitions de fonctions ou de classes.

Les modèles de fonctions

• Un modèle de fonction, appelé également patron de fonction ou fonction générique est une fonction dont la définition utilise un ou plusieurs types paramétrables. Le type effectif qui sera utilisé est déterminé au moment de l'appel de la fonction générique.

• Par exemple si une fonction va effectuer les mêmes traitements (même algorithme) mais sur des données de types différents alors au lieu de surcharger une telle fonction, il est possible de la définir en tant que modèle puis instancier le type exact au moment de l'appel de cette dernière.

Déclaration du paramètre de type

La déclaration d'un paramètre représentant un type T , appelé également paramètre de type se fait de la manière suivante :

template <class T>

• template indique que l'on est en train de créer un modèle. • class T est une déclaration du nom du paramètre qui va désigner le type. Le nom de ce

paramètre est dans ce cas T.

Définition d'une fonction générique

La définition d'une fonction générique qui utilise des paramètres de type se fait de la manière suivante :

template <class T> TypeRetour NomFonction(Paramètres) { // Corps }

Exemple :

On considère la fonction Max qui renvoie le maximum entre deux éléments. Ces deux éléments peuvent être d'un type quelconque : int, char, float, double, etc. Au lieu de proposer une définition de la fonction Max pour chacun de ces types, il est possible de définir une seule version qui servira dans ce cas de modèle.

Page 161: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 159

template <class T> T Max(T a, T b) { if(a>b) return a; return b; }

Appel d'une fonction générique

La syntaxe de l’appel d’une fonction générique se présente comme suit :

NomFonction<TypeEffectif>(arguments);

Exemple :

L’appel suivant renvoie le maximum de deux entiers : int c ;

c = Max<int>(5,19) ;

Toutefois, le compilateur est capable en analysant la signature de l’appel de connaître implicitement le ou les types effectifs utilisés. De ce fait, l'appel de la fonction générique peut se faire comme pour les fonctions classiques en lui passant des paramètres effectifs sans spécifier explicitement l’argument du type. NomFonction(arguments);

Exemple :

template <class T> T Max(T a, T b) { if(a>b) return a; return b; }

int main() { int a=5, b=9, c; c=Max(a,b); double m=3.5, n=2.9, k; k=Max(m,n); cout<<"Le max des entiers est : "<<c; cout<<"Le max des réels est : "<<k; return 0; }

Remarque :

Au moment de l’appel, le compilateur va générer une nouvelle instance de la fonction générique dans laquelle il remplace le type générique T par le type effectif qui lui correspond dans l'appel. Cette nouvelle instance représente la fonction qui sera véritablement exécutée au moment de l'appel. Dans l’exemple précédent le compilateur génère deux instances de la fonction Max qui correspondent aux prototypes suivants :

• int Max(int; int) pour le premier appel.

• double Max(double, double) pour le second appel.

Page 162: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 160

Remarque :

• Le compilateur génère autant d'instances à partir d'un modèle qu'il y a d'appels avec des types effectifs différents.

• La fonction générique va servir essentiellement pour la synthèse des fonctions qui seront véritablement appelées. C'est le code binaire de ces dernières qui va figurer dans l'exécutable généré. Dans cet exécutable le code générique n'a aucune existence.

Utilisation de plusieurs paramètres de type

Il est possible d'utiliser plus qu'un paramètre de type dans une fonction générique. Dans ce cas, ces paramètres doivent être déclarés chacun avec le mot réservé class et séparés par des virgules. L'exemple suivant montre le prototype d'une fonction générique qui utilise deux paramètres de type distincts :

Exemple :

template <class T1, class T2> T2 F(T1 a, T2 b);

La fonction F prend un premier paramètre de type T1 et un deuxième paramètre de type T2. T1 et T2 sont dans ce cas génériques et seront instanciés au moment de l'appel de F.

Utilisation des paramètres classiques dans un modèle de fonctions

Il est tout à fait possible de combiner dans le prototype d'un modèle de fonctions des paramètres de type avec des paramètres classiques pour lesquels le type est connu. L'exemple suivant donne une illustration de cette situation.

Exemple :

template <class T> int CompteZero(T* Tab, int N) { int NbZeros = 0; for(int i=0;i<N; i++) if(!Tab[i]) NbZeros++; return NbZeros; }

int N est dans ce cas un paramètre classique.

Surdéfinition d'une fonction générique

Tout comme les fonctions classiques, il est tout à fait possible de surdéfinir une fonction générique. Cette surdéfinition peut être elle-même une fonction générique comme elle peut comporter des paramètres classiques.

Exemple :

L'exemple suivant montre deux surdéfinitions possibles de la fonction générique Max une générique et l'autre non : // Surdéfinition générique de la fonction Max

template <class T> T Max(T a, T b, T c)

{

return Max(Max(a,b),c);

}

Page 163: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 161

// Surdéfinition comportant des paramètres classiques (int N) template <class T> T Max(T* Tab, int N) { T MaxVal = Tab[0]; for(int i=1;i<N;i++) if(Tab[i]>MaxVal) MaxVal = Tab[i]; return MaxVal; }

Spécialisation d'une fonction générique

Il y a des situations où l'algorithme d'une fonction générique s’avère inadapté pour certains types d'arguments. Dans ce cas, il est possible de spécialiser la fonction générique en ajoutant des versions adaptées à ces types tout en conservant le modèle du prototype (il ne s'agit pas d'une surdéfinition).

Exemple :

La fonction générique Max telle qu'elle est définie précédemment n'est pas adaptée pour la comparaison des chaînes parce qu'une telle comparaison ne se fait pas à l'aide de l'opérateur > mais avec la fonction strcmp. Il est dans ce cas possible d'ajouter une spécialisation de la fonction générique pour ce cas particulier.

template <class T> T Max(T a, T b) { if(a>b) return a; return b; }

// Spécialisation de Max char* Max(char* a, char* b) { if(strcmp(a,b)>0) return a; return b; }

…………………… char Ch1[20]="Programme"; char Ch2[20]="Algorithme"; cout<<Max(Ch1,Ch2); // C’est la spécialisation qui est utilisée ……………………

Dans ce cas le compilateur va commencer sa recherche par la fonction qui correspond exactement à la signature de l'appel et va par conséquent directement utiliser la spécialisation sans avoir besoin d'analyser la fonction générique afin de synthétiser une version adaptée.

Remarque :

Il n'y a pas de moyens de limitation des types qui peuvent être utilisés avec une fonction générique. Par conséquent, c'est à celui qui fait l'appel de vérifier si le modèle est adapté aux types des arguments qu'il utilise. Par exemple la fonction générique Max peut accepter n'importe quel type y compris les types personnalisés comme les classes. Si des classes sont passées à Max alors, il faut vérifier que l'opérateur > est bien surchargé pour de telles classes.

Page 164: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 162

Les classes génériques

Tout comme c’est le cas pour les fonctions, il est possible de définir des modèles de classes. De tels modèles servent à créer des types personnalisés qui sont paramétrés par d’autres types.

Création d’un modèle de classe

Un modèle de classe est créé de la manière suivante :

template <class T1, …, class Tn> class NomClasse { // Définition de la classe };

• template sert indiquer qu’il s’agit de la création de modèle.

• < .. .. .. > désigne la zone de déclaration du ou des paramètres de type qui seront utilisés par le modèle de la classe. Dans cette zone il est également possible de mentionner directement des types concrets qui seront également utilisée par la classe.

• class Ti est une déclaration d’un paramètre de type qui porte dans le présent cas le nom Ti. un modèle peut toujours utiliser plusieurs paramètres de type. Ces derniers sont déclarés chacun avec le mot réservé class et séparés par des virgules.

• NomClasse désigne le nom de la classe à créer.

Exemple :

Cet exemple montre la création d’un modèle de classe servant à représenter des points.

template <class T> class Point { T x; T y; Point(T abs, T ord) ; void Afficher(); };

Définition des méthodes d’un modèle de classe

Les méthodes d’un modèle de classe sont des modèles de fonctions avec les mêmes paramètres de type que la classe. Ces méthodes peuvent être définies à l’intérieur ou à l’extérieur de la classe.

Définition à l’intérieur de la classe (en ligne)

La définition se fait dans ce cas d’une manière classique comme pour les méthodes usuelles.

Définition à l’extérieur de la classe

La définition à l’extérieur de la classe utilise une syntaxe un peu différente que dans le cas classique. Cette syntaxe rappelle au compilateur qu’il s’agit d’un modèle et mentionne les paramètres de types qui sont utilisés. Cette syntaxe se présente comme suit :

Page 165: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 163

template<class T> NomClasse<T>::NomMethode(Paramètres) { // Définition de la méthode }

L’exemple suivant donne une illustration de ces deux possibilités de définition.

Exemple :

template <class T> class Point { T x; T y; // Exemple de définition à l’intérieur de la classe Point(T abs, T ord) { x = abs ; y = ord ; } void Afficher(); };

// Exemple de définition à l’extérieur de la classetemplate <class T> void Point<T>::Afficher() { cout<<"Abs : "<<x; cout<<"Ord : "<<y; }

Utilisation d’un modèle de classe

Lors de l’appel d’un modèle de fonctions, le compilateur se base sur la signature pour déterminer le type effectif des arguments utilisés. Cette identification automatique par le compilateur n’est pas possible lorsqu’il s’agit d’utiliser un modèle de classe pour instancier un objet. C’est pourquoi la spécification de la classe dans ce cas doit être accompagnée explicitement du (des) types effectif(s) utilisé(s). La syntaxe d’instanciation se présente de ce fait comme suit :

NomModèleClasse <TypeEffectif> NomObjet(args du constructeur);

Exemple :

int main()

{

Point<int> P1(5,3);

Point<double> P2(2.4, 3.7);

P1.Afficher();

P2.Afficher();

return 0 ;

}

Page 166: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 164

Remarque :

Théoriquement, il est possible d’instancier la classe Point avec n’importe quel type. Toutefois, sur un plan pratique les types à utiliser doivent être compatibles avec la signification de cette classe (types numériques). Le bon usage du modèle reste alors et toujours l’affaire de l’utilisateur de ce dernier.

Classe générique avec des paramètres de type et des paramètres classiques

En plus des paramètres de type, il est tout à fait possible d’utiliser des paramètres classiques ayant des types concrets et ce lors de la création d’un modèle de classe. L’exemple suivant montre un exemple de ce type de situation.

Exemple :

La classe Tableau représente des tableaux d’éléments d’une manière générale indépendamment du type de ces éléments. Cette classe prend deux arguments, le premier représente le type des éléments à manipuler et le second représente la dimension du tableau. La définition de cette classe se présente comme suit :

template <class T, int N> class Tableau { T Elements[N]; T& operator[](int Index) { return Elements[Index]; } };

Le code suivant montre l’utilisation du modèle Tableau :

int main() { Tableau<int, 4> T1; cout<<"Saisie des éléments du tableau\n"; for(int i=0;i<4;i++) { cout<<"Donner un élément : "; cin>>T1[i]; } cout<<"Affichage des éléments du tableau\n"; for(int i=0;i<4;i++) cout<<T1[i]<<' '; system("PAUSE"); return 0; }

Remarques :

• Cet exemple montre entre autres qu’il est tout à fait possible de créer un tableau non dynamique avec une dimension paramétrée. En effet au moment de la création de la classe concrète à partir du modèle, le préprocesseur va dans le cas présent remplacer l’instruction : T Element[N]; par int Element[4]; qui est une instruction acceptée par le compilateur.

Page 167: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 165

• La dimension du tableau aurait pu être passée comme argument du constructeur ce qui éviterait ainsi l’utilisation du paramètre int dans le modèle de la classe. Mais une telle option ne permettrait pas de créer le tableau sur la pile d’exécution. Seul serait possible dans ce cas l’allocation dynamique.

Valeur par défaut des paramètres d’un patron de classe

Il est possible de spécifier des valeurs par défaut aux paramètres d’une classe générique. La règle qui régit cette spécification est semblable à celle utilisée avec les fonctions usuelles.

Exemple :

template <class T=int, int N=10> class Tableau { T Elements[N] ; T& operator[](int Index) { return Elements[Index] ; } } ;

Les instructions suivantes montrent des exemples d’utilisation du modèle Tableau ainsi déclaré.

Tableau<float, 4> ; // Tableau de 4 réels simples Tableau<Complexe> ; // Tableau de 10 objets de type Complexe. Tableau<> ; // Tableau de 10 entiers.

Remarque :

La notion de paramètre par défaut n’a pas de signification pour les modèles de fonctions.

Spécialisation d’un modèle de classe

La possibilité de spécialisation existe également avec les modèles des classes comme c’est le cas avec les fonctions. Elle peut être utile afin d’adapter le modèle à certaines situations particulières. La spécialisation peut concerner une méthode du modèle de classe comme elle peut concerner la classe en entier.

Spécialisation d’une méthode

La spécialisation d’une méthode d’un modèle de classe se fait selon la syntaxe suivante :

TypeRetour NomClasse<TypeEffectif>::NomMethode(Paramètres) { // Définition de la spécialisation de la méthode }

L’exemple suivant montre une spécialisation de la méthode Afficher du modèle Point pour qu’elle affiche convenablement les coordonnées dans le cas où le type effectif utilisé est le char (codage des entiers sur un octet).

Page 168: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 166

// Définition de la méthode Afficher pour le modèle de classe template <class T> class Point { T x; T y; Point(T abs, T ord) {x= abs ; y ord ;} void Afficher(); };

template <class T> void Point<T>::Afficher() { cout<<"Abs : "<<x<<endl; cout<<"Ord : "<<y<<endl; }

// Spécialisation de la méthode Afficher pour les caractères

void Point<char> ::Afficher()

{ cout<<"Abs : "<<(int)x<<endl; cout<<"Ord : "<<(int)y<<endl;

}

// Utilisation de la classe Point int main() { Point<int> P1(5,9); Point<char> P2('a','b'); P1.Afficher(); // Afficher générée par le compilateur P2.Afficher(); // Afficher spécialisée pour char return 0; }

Spécialisation d’une classe

Il est tout à fait possible de spécialiser la classe dans sa totalité. Dans ce cas la nouvelle spécialisation doit suivre la définition du modèle tout en indiquant le ou les types concrets qu’elle utilise. La syntaxe à adopter est la suivante :

class Nomclasse<TypeConcret> { // Nouvelle définition }

Exemple :

La spécialisation de la classe Point pour le type char doit être définie de la manière suivante :

class Point<char> { // Nouvelle spécialisation }

Page 169: CoursPOOC++

Les modèles Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 167

Remarque :

Les modèles constituent un outil puissant permettant de réduire considérablement le code. Toutefois cet outil doit être utilisé avec précaution vu l’existence de situations pouvant mener vers des ambiguïtés d’interprétation au moment de la compilation. Par ailleurs, la génération du code faite par le compilateur est réalisée d’une manière automatique et ne donne aucune garantie sur l’adaptation des traitements aux données. C’est l’utilisateur du modèle qui doit toujours veiller sur cette adaptation et l’assurer éventuellement par des spécialisations.

Page 170: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 168

La gestion des exceptions

Introduction

Les exceptions sont des anomalies qui peuvent avoir lieu au moment de l'exécution des programmes (pas au moment de la compilation) et qui conduisent généralement vers des erreurs (exemple : tentative de lecture d'un fichier qui n'existe plus ou qui est déplacé).

Ancienne technique de gestion des erreurs au moment de l’exécution Avec les langages qui ne supportent pas la gestion des exceptions (exemple C), le traitement des erreurs se fait généralement en effectuant des tests sur les valeurs de retour des fonctions. Ces dernières retournent le plus souvent des codes indiquant le type de l'erreur. Ces codes ne comportent pas des informations supplémentaires sur les causes et les paramètres des erreurs. En plus cette technique devient fastidieuse dans le cas d'appels imbriqués de fonctions (la gestion des erreurs complique le code surtout au niveau des fonctions internes).

Avantage de la gestion des exceptions Le mécanisme de gestion des exceptions permet d'éviter ces problèmes : • Il devient ainsi possible de gérer à un seul niveau les exceptions, même celles engendrées

par des fonctions internes (Possibilité de centralisation de la gestion en des points précis). • Il est également possible d'avoir plus d'informations sur les causes des erreurs. Les erreurs

ne sont plus décrites par des codes mais par des objets qui sont spécifiques chacun à un type d'erreur bien précis. Chaque objet comporte des informations qui décrivent l'erreur.

Familles d'exceptions Il existe deux grandes familles d'exceptions : • Les exceptions pouvant engendrer des erreurs systèmes comme les divisions par zéro,

l'accès à un tableau en dehors de ses bornes, l'accès à un fichier inexistant, etc. • Les exceptions pouvant engendrer des erreurs liées à la couche métier de l’application. Ces

exceptions sont généralement de type sémantique et dépendent du contexte de l'application.

Le mécanisme de gestion d’une exception Le mécanisme de la gestion des exceptions se déroule en deux phases : • La première phase concerne la détection de l’anomalie. La fonction qui détecte cette

anomalie doit alors provoquer une rupture de l’exécution de la séquence d’instructions qui la compose et lever (ou lancer) une exception pour informer son utilisateur de cet événement (utilisateur = le programme qui appelle cette fonction).

• La deuxième phase concerne la gestion de l’exception. Cette gestion doit être normalement faite par le programme appelant de la fonction ayant levé l’exception en question. Ce dernier doit alors définir le traitement adéquat de cette exception.

Ces deux phases (détection et traitement de l’anomalie) sont complètement séparées. Elles sont élaborées le plus souvent par deux utilisateurs différents (celui qui lève l'exception n'est pas nécessairement celui qui la traite). Cette séparation entre les instructions essentielles d'un programme qui gèrent son déroulement normal des instructions de gestion d'erreurs permet d'améliorer la lisibilité de ce dernier et facilite par conséquent sa maintenance.

Page 171: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 169

La levée d'une exception La levée d’une exception se fait à l’aide de l’instruction throw selon la syntaxe suivante :

Syntaxe :

throw NomException ;

• NomException désigne une expression pouvant être de n’importe quel type. Elle représente l’exception qui est lancée par le programme.

• Toutes les instructions situées après le throw (dans le module ayant levé l’exception) seront abandonnées et par conséquent jamais atteintes.

Exemple 1 :

Cet exemple illustre la levée d’une exception de type chaîne de caractères par une fonction f.

void f(int minute) { if(minute<1 || minute >=60) throw "La valeur saisie ne correspond pas à une minute valide"; // Partie jamais atteinte en cas d'exécution du throw }

Remarque :

Une exception peut être de n’importe quel type (entier, chaîne, énumération, etc.). Toutefois en POO, les exceptions sont le plus souvent codées sous forme d’objets (instances de classe). Un tel codage permet de faire accompagner l’exception d’autres informations qui peuvent s’avérer utile à celui qui va la gérer.

Interception et gestion d'une exception La gestion d'une exception se fait à l'aide d'une structure composée de deux blocs définis par les deux instructions try et catch : • Le bloc try contient les instructions qui sont susceptibles d'engendrer une erreur. try permet

donc de laisser le choix au programmeur de gérer ou non les exceptions dans son code. En effet on ne tente de gérer les exceptions que pour le code situé dans le bloc try. Les instructions placées en dehors de ce bloc ne bénéficient pas de ce mécanisme de gestion des exceptions.

• Le bloc catch assure l'interception et le traitement de l'exception. Ce bloc comporte : o Un entête indiquant le type de l'exception qu'il peut intercepter. o Une suite d'instructions définissant le code de traitement de l'exception.

Syntaxe :

try { // Bloc d'instructions du programme } catch(TypeException e) { // Bloc de gestion des erreurs }

Page 172: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 170

Remarque :

• Les accolades dans les blocs de try et de catch sont obligatoires même si ces derniers comportent une seule instruction.

Exemple :

#include <iostream.h> void f(int); int main() { int minute; try { cout<<" Donner une minute : "; cin>>minute; f(minute); } catch(const char* e) { cout<<e<<endl; } return 0; }

Remarque :

• La portée de l'identificateur e est limitée au bloc catch. Il est généralement utilisé pour récupérer les informations sur l'erreur. S'il n'est pas fait usage de e dans le bloc catch alors cet identificateur peut être omis. L'entête du catch se présente dans ce cas comme suit :

catch(TypeException) { // Bloc de gestion des erreurs }

Déroulement de l'exécution

• Le programme entre dans le bloc try. Il commence alors l'exécution séquentielle des instructions de ce bloc. Dès qu'une anomalie est détectée, une exception correspondant au type de cette anomalie est créée. Le programme abandonne alors le reste des instructions du bloc, quitte ce dernier et rentre tout aussi automatiquement dans le bloc catch.

• Après le traitement de l'exception, le programme reprend son exécution au niveau de l'instruction qui suit immédiatement le bloc catch.

• Si aucune anomalie n'a été détectée dans le bloc try alors le gestionnaire catch sera ignoré et l'exécution continue au niveau de l'instruction qui suit immédiatement ce dernier.

Exemple :

#include <iostream.h> float Div(float a, float b) { if(b==0) throw "Impossible de faire une division par zéro"; return a/b; }

Page 173: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 171

int main() { float a,b; try { cout<<" Donner a :"; cin>>a; cout<<" Donner b :"; cin>>b; cout<<"\nLe résultat de la division est :"<<Div(a,b); } catch(const char* e) { cout<<e<<endl; } cout<<"FIN DU PROGRAMME"<<endl; return 0; }

Exemple d'une division normale Exemple d'une division par zéro Donner a : 6 Donner b : 3 Le résultat de la division est : 2 FIN DU PROGRAMME

Donner a : 6 Donner b : 0 Impossible de faire une division par zéro FIN DU PROGRAMME

Interception et gestion de plusieurs exceptions

• Les instructions du bloc try peuvent parfois engendrer plusieurs anomalies de types différents. Chacune de ces anomalies peut alors être traitée par un bloc catch à part.

• La structure de gestion d'exceptions se compose dans ce cas d'un bloc try et de plusieurs gestionnaires catch successifs. Chaque gestionnaire catch sera dédié au traitement d'une exception particulière.

Syntaxe : blocs catch multiples

try { // Bloc d'instructions du programme } catch(TypeException_1 e_1) { // Bloc de gestion des erreurs } … … … … … … … … catch(TypeException_n e_n) { // Bloc de gestion des erreurs }

Page 174: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 172

Déroulement de l'exécution

• En cas d'anomalie, le contrôle passe au premier gestionnaire catch. Si cette anomalie correspond à l'argument de ce dernier alors elle sera traitée par le bloc qui lui est associé. Dans le cas contraire le catch suivant est inspecté et ainsi de suite.

• Dès qu'une anomalie est traitée par un catch les catchs suivants ne sont plus envisagés. • Dans le cas où l'anomalie ne correspond à aucun gestionnaire catch du bloc try-catch

courant, la recherche se poursuit dans le bloc try-catch de niveau supérieur (celui qui englobe le try-catch imbriqué). Ce processus de remontée de la recherche se poursuit jusqu'à ce qu'un gestionnaire catch pour l'exception courante soit trouvé.

• Dès qu'un gestionnaire catch pouvant gérer l'exception est rencontré, on entre dans son bloc et l'exécution du programme continue dans ce gestionnaire.

• Si aucun gestionnaire n'est trouvé, le programme appelle la fonction terminate() définie dans la bibliothèque standard du C++. Cette fonction propose un comportement par défaut, qui appelle notamment la fonction abort() qui elle-même indique que le programme se termine anormalement «Abnormal program termination ».

Exemple 1:

class ErreurCreation{ }; class ErreurAcces{ }; class Tableau { int* Elements; int NbElements; public : Tableau(int Taille) { if(Taille<0) throw ErreurCreation(); Elements = new int[Taille]; NbElements = Taille; for(int i=0; i<NbElements; i++) Elements[i]=0; } ~Tableau() { delete[] Elements; } int Get(int Indice) { if(Indice<0 || Indice>=NbElements) throw ErreurAcces(); return Elements[Indice]; } void Set(int Indice, int Valeur) { if(Indice<0 || Indice>=NbElements) throw ErreurAcces(); Elements[Indice] = Valeur; } void Afficher() { for(int i=0; i<NbElements; i++) cout<<Elements[i]<<' '; } };

Page 175: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 173

int main() { int Taille, Indice; try { cout<<"Donner la taille du tableau : "; cin>>Taille; Tableau Tab(Taille); cout<<"Donner l'indice de la case à mettre à 1 : "; cin>>Indice; Tab.Set(Indice,1); Tab.Afficher(); } catch(ErreurAcces) { cout<<"Erreur d'accès"; } catch(ErreurCreation) { cout<<"Erreur de création du tableau"; }

cout<<"\nFIN DU PROGRAMME"; system("PAUSE"); return 0; }

Exemple 1 :Levée d'une exception de création de tableau

Exemple 2 :Levée d'une exception d'accès au tableau

Donner la taille du tableau : -3 Erreur de création du tableau FIN DU PROGRAMME

Donner la taille du tableau : 3 Donner l'indice de la case à mettre à 1 : 5 Erreur d'accès FIN DU PROGRAMME

Déclaration des exceptions levées par une fonction

Généralement, une fonction lève des exceptions et laisse la main à ses utilisateurs pour leurs gestions (la gestion dépend du contexte de l'application). Ces utilisateurs (ceux qui appellent la fonction) ont le plus souvent accès seulement à la spécification de la fonction (prototype) mais pas à sa définition (le corps). Une fonction peut dans ce cas indiquer à ses utilisateurs les exceptions qu'elle lève en interne et ce au niveau de sa déclaration selon la syntaxe suivante :

Syntaxe :

TypeRetour NomFonction(Paramètres) throw(TypeException1,…, TypeExceptionN)

Page 176: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 174

Exemple :

class Tableau { int* Elements; int NbElements; public : Tableau(int Taille) throw (ErreurCreation); ~Tableau(); int Get(int Indice) throw (ErreurAcces); void Set(int Indice, int Valeur) throw (ErreurAcces); void Afficher() throw(); };

Remarque :

• L'instruction throw utilisée sans paramètres dans la déclaration d'une fonction de la manière suivante throw( ) signifie que cette fonction ne lève aucune exception.

• L'absence du throw dans la déclaration d'une fonction signifie que cette dernière peut lever tout type d'exceptions.

Type d'exceptions interceptées par un gestionnaire catch

Un gestionnaire catch ayant un paramètre de type T1, T1&, const T1 ou const T1& peut intercepter : • Les exceptions de type T1. • Les exceptions de type T2 dans le cas où T2 est une classe et T1 est une classe de base

accessible de T2. • Les exceptions de type T2 dans le cas où T2 est un pointeur sur une classe A et T1 est un

pointeur sur une classe de base accessible de A.

Remarque : Importance de l'ordre des gestionnaires catch

Dans une structure comportant des gestionnaires catch multiples, l'ordre de ces gestionnaires est important surtout si leurs arguments sont des objets qui dérivent les uns des autres. Par exemple si on considère les trois classes d'exceptions déclarées de la manière suivante : class A; class B : public A; class C : public A;

et les trois gestionnaires catch suivants : catch(A) {… … … … …} catch(B) // Bloc jamais exécuté {… … … … …} catch(C) // Bloc jamais exécuté {… … … … …}

Alors les deux derniers gestionnaires de cette structure ne pourront jamais être exécutés. En effet, toutes les exceptions levées de type B et C seront automatiquement interceptées par le premier gestionnaire car elles sont à la base de type A. Le gestionnaire interceptant l'exception de type A devrait normalement être placé en dernier lieu.

Page 177: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 175

Exemple :

#include <iostream.h> #include <sstream.h>

class Erreur { public : string GetMessage() { ostringstream MessageErreur; MessageErreur<<"Erreur d'exécution du programme\n"; return MessageErreur.str(); } };

class ErreurCreation : public Erreur { int TailleIncorrecte; public: ErreurCreation(int Valeur) { TailleIncorrecte = Valeur; } string GetMessage() { ostringstream MessageErreur; MessageErreur<<"Erreur de création du tableau : "<<endl; MessageErreur<<"Taille incorrecte:"<<TailleIncorrecte; return MessageErreur.str(); } };

class ErreurAcces : public Erreur { int IndiceIncorrect; int TailleMax; public: ErreurAcces(int Valeur, int Taille) { IndiceIncorrect = Valeur; TailleMax = Taille; } string GetMessage() { ostringstream MessageErreur; MessageErreur<<"Erreur d'accès au tableau : "<<endl; MessageErreur<<"Indice incorrect:"<<IndiceIncorrect; return MessageErreur.str(); } };

Page 178: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 176

class Tableau { int* Elements; int NbElements; public : Tableau(int Taille) { if(Taille<0) throw ErreurCreation(Taille); Elements = new int[Taille]; NbElements = Taille; for(int i=0; i<NbElements; i++) Elements[i]=0; } ~Tableau() { delete[] Elements; } int Get(int Indice) { if(Indice<0 || Indice>=NbElements) throw ErreurAcces(Indice, NbElements); return Elements[Indice]; } void Set(int Indice, int Valeur) { if(Indice<0 || Indice>=NbElements) throw ErreurAcces(Indice, NbElements); Elements[Indice] = Valeur; } void Afficher() { for(int i=0; i<NbElements; i++) cout<<Elements[i]<<' '; } }; int main() { int Taille, Indice; try { cout<<"Donner la taille du tableau : "; cin>>Taille; Tableau Tab(Taille); cout<<"Donner l'indice de la case à mettre à 1 : "; cin>>Indice; Tab.Set(Indice,1); Tab.Afficher(); } catch(ErreurCreation e) { cout<<e.GetMessage(); } catch(ErreurAcces e) { cout<<e.GetMessage(); } catch(Erreur e) { cout<<e.GetMessage(); }

cout<<"\nFIN DU PROGRAMME\n"; return 0; }

Page 179: CoursPOOC++

La gestion des exceptions Programmation orientée objet (C++)

_________________________________________________________________________________________________________________

Version 3.7 � Karim Kalti 177

Remarque : Exploitation du polymorphisme dans l'interception des exceptions

En définissant les méthodes GetMessage() comme étant virtuelles, il devient possible de réduire les trois gestionnaires de l'exemple précédent à un seul tout en obtenant le même effet. La définition de ce seul gestionnaire est comme suit : try { … … … } catch(Erreur& e) { cout<<e.GetMessage(); }

En effet, l'interception de la référence de l'exception permettra dans ce cas de bénéficier du polymorphisme de la méthode GetMessage().

Interception de toutes les exceptions Il existe une définition de l'entête du catch qui permet d'intercepter toute exception quelque soit son type. Cette définition se présente comme suit :

catch(…)

S'il figure dans une structure comportant plusieurs catch, ce gestionnaire doit être alors le dernier de la liste. En effet, tous les gestionnaires catch qui le succèdent ne peuvent jamais être atteints puisqu'il intercepte tout type d'exception.

Page 180: CoursPOOC++

Version 3.7 ã Karim Kalti

Annexe

Page 181: CoursPOOC++

179

Les Fichiers

Techniques d'accès à un fichier

· L'accès séquentiel : il consiste à traiter les informations "séquentiellement" c'est à dire dans l'ordre dans lequel elles apparaissent dans le fichier.

· L'accès direct : il consiste à se placer immédiatement sur l'information souhaitée sans avoir à parcourir celles qui précèdent.

Les fonctions de base pour l'ouverture et la fermeture des fichiers

La structure FILE

· FILE : est une structure prédéfinie dans la librairie stdio.h. Elle permet à un programme de manipuler les fichiers en stockant toutes les informations utiles se rapportant à ces derniers et notamment celles qui se rapportent aux tampons associés à ces derniers (adresse de début, taille en octets,…).

Ouverture d'un fichier : (fopen)

Prototype :

FILE* fopen(char* NomFich, char* ModeOuverture)

Paramètres :

· Le nom du fichier concerné fourni sous forme d'une chaîne de caractères. · Une chaîne indiquant le mode d'ouverture. Les modes possibles sont : r Lecture seulement (le fichier doit exister). Retourne NULL si le fichier n'existe pas w Écriture seulement. Si le fichier n'existe pas, il est créé. S'il existe son ancien contenu est perdu.

a Écriture en fin de fichier (appendding). Si le fichier existe déjà, il sera étendu, s'il n'existe pas, il sera créé (cas du w).

r+ Lecture et écriture. Le fichier doit exister. Le contenue n'est pas perdu. w+ Création pour lecture et écriture. Si le fichier existe, son contenu sera détruit.

a+ Lecture ou extension : si le fichier n'existe pas, il sera créé, s'il existe le pointeur sera positionné en fin de fichier. L’écriture se fait en fin de fichier.

Fermeture d'un fichier (fclose)

Prototype :

int fclose( FILE *F );

Paramètres :

F le fichier à fermer.

Valeur de retour :

0 si le fichier a été convenablement fermé.

Autre fonction de fermeture de fichier int _fcloseall( void )

fcloseall ferme tous les fichiers ouverts.

Page 182: CoursPOOC++

180

Vidage des tampons (fflush)

Prototype :

int fflush(FILE *stream ); <stdio.h>

Paramètres :

F: le tampon à vider.

Valeur de retour :

0 si succès et EOF si erreur. Autre fonction : int flushall(void); <stdio.h>

Vide tous les tampons associés aux fichiers ouverts.

Les fonctions de lecture et d'écriture binaires dans les fichiers

Écriture dans un fichier (fwrite)

Prototype :

size_t fwrite(const void* ptr,size_t sizeU, size_t count, File* F)

Paramètres :

· ptr : Adresse de début du bloc d'informations à écrire dans le fichier. · SizeU : taille en octets du bloc unitaire de données à écrire. Généralement, c'est la taille du type des données. · count : nombre de blocs unitaires de données à écrire. · F : pointeur sur le fichier dans lequel l'écriture sera faite.

Rôle :

fwrite permet de transférer un bloc de données de taille (count x sizeU) octets, situé en mémoire à l'adresse ptr dans le fichier pointé par F.

Valeur de retour :

Nombre d'unités de données (les SizeU) réellement écrites dans le fichier.

Lecture à partir d'un fichier (fread)

Prototype :

size_t fread(void* ptr, size_t sizeU, size_t count, File* F);

Paramètres :

· ptr : Adresse de début du bloc de mémoire dans lequel seront stockées les informations lues à partir du fichier. · SizeU : taille en octets du bloc unitaire de données à lire. · count : nombre de blocs unitaires de données à lire. · F : pointeur sur le fichier à partir duquel les informations sont lues.

Rôle :

fread lit (count x sizeU) octets d'informations à partir du fichier F et les place dans le bloc mémoire pointé par ptr.

Valeur de retour :

Le nombre d'unités de données (les SizeU) réellement lues à partir du fichier.

Page 183: CoursPOOC++

181

Détection de la fin d'un fichier (feof)

Prototype :

int feof(FILE* F);

Paramètre :

Fichier dont on veut détecter la fin.

Valeur de retour :

Vrai si la fin du fichier a été atteinte et faux sinon.

Remarque :

Il est à noter qu'il n'est pas suffisent d'avoir lu le dernier octet du fichier pour retourner vrai mais il faut lire au delà du dernier octet.

Exemple 1:

Le programme suivant enregistre d'une manière séquentielle une suite d'entiers dans un fichier. #include <stdio.h> void main() { char NomFichier[21]; int n; FILE* Fichier; printf("Donnez le nom du fichier à créer : "); scanf("%20s", NomFichier); Fichier=fopen(NomFichier,"w"); do { printf("Donnez un entier : "); scanf("%d",&n); if(n) fwrite (&n,sizeof(int),1,Fichier); } while(n); fclose(Fichier); }

Exemple 2 :

Le programme suivant fait un parcours séquentiel du fichier précédemment créé et liste son contenu. #include <stdio.h> void main() { char NomFichier[21]; FILE* Fichier; int n; printf("Donnez le nom de fichier à lire : "); scanf("%20s",NomFichier); Fichier=fopen(NomFichier,"r"); while(!feof(Fichier)) { if(fread(&n, sizeof(int),1,Fichier)) printf("\n%d",n); } fclose(Fichier); }

Page 184: CoursPOOC++

182

Accès direct aux fichiers

· Chaque fichier possède un pointeur interne qui indique la position courante à partir de laquelle se font les

opérations de lecture et d'écriture. Au moment de l'ouverture du fichier, cette position se trouve tout au début de ce dernier sauf pour les modes a et a+ où elle se trouve en fin de fichier. Elle s'incrémente par la suite, après toute opération d'accès, du nombre d'octets lus ou écrits.

· La position courante se situe toujours au niveau de l'octet qui succède immédiatement le dernier bloc d'informations lu ou écrit.

Positionnement du pointeur interne des fichiers

En mode d'accès direct. Il est possible d'agir sur la position courante d'un fichier de façon à la placer directement à un endroit précis sans avoir besoin de faire un déplacement séquentiel. Cette action se fait à l'aide de la fonction fseek.

Prototype :

int fseek(FILE* F, int offset, int origine);

Paramètres :

· F : Le fichier concerné par le déplacement. · Offset : la valeur avec laquelle est déplacé le pointeur du fichier. · Origine : donne l'origine à partir de laquelle est fait le déplacement. Trois origines sont distinguées. Elles

sont définies par les constantes symboliques suivantes : · SEEK_SET ou (0) : offset désigne dans ce cas un déplacement en octets par rapport au début du fichier. · SEEK_CUR ou (1) : Offset désigne un déplacement par rapport à la position courante. Offset peut être dans

ce cas une valeur positive ou négative. · SEEK_END ou (2) : Offset désigne un déplacement par rapport à la fin du fichier.

Valeur de retour

· 0 si le positionnement s'est bien déroulé. · Une valeur non nulle sinon. · Une valeur quelconque pour une tentative de positionnement en dehors du fichier.

Exemple :

#include <stdio.h> void main() { char NomFichier[21]; int n; long rang; FILE * Fichier; printf("donnez le nom du fichier à consulter : "); scanf("%s",NomFichier); printf("Donnez le rang de l'entier à consulter : "); scanf("%ld",&rang); Fichier = fopen(NomFichier, "r"); fseek(Fichier, sizeof(int)*(rang-1),SEEK_SET); fread(&n,sizeof(int),1,Fichier); printf("la valeur est : %d",n); fclose(Fichier); }

Détermination de la position courante d'un pointeur de fichier (ftell)

Prototype :

long ftell( FILE * F);

Valeur de retour :

ftell retourne la position courante du pointeur de fichier par rapport au début du fichier.