C++ - PDF
Document Sample


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