Uploaded by roocky911

Cours C++

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