Cours C
Cours 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.
– 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
{ où <déclarations> est de la forme :
private: (par défaut)
<déclarations> type1 donnée_membre1;
protected: type2 donnée_membre2;
<déclarations> ...
public: fonction_membre1 (paramètres);
<déclarations> fonction_membre2 (paramètres);
}; ...
Par exemple :
class booleen
{
short b;
public:
void init (short);
void not ();
void affiche ();
};
– 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.
– Page 5 –
Cours de C++
void booleen::not ()
{
b = !b;
}
void booleen::affiche ()
{
if (b==0)
cout << "Faux";
else
cout << "Vrai";
}
– 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.
Syntaxe : Exemple :
class identificateur_de_classe class booleen
{ {
... short b;
public: public:
fonction_membre1 (paramètres); void init (short)
fonction_membre2 (paramètres); void not ();
... void affiche ();
}; };
main () main ()
{ {
identificateur_de_classe objet; booleen fini;
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".
– Page 7 –
Cours de C++
Identificateur
Instanciation
Objet
Paramètres
Structure du code
Structure des données
– Page 8 –
2. ÉLÉMENTS DE SYNTAXE
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).
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.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.
– Page 10 –
Eléments de syntaxe
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).
Exemple : int i, j;
double t, r;
– Page 12 –
Eléments de syntaxe
Exemple :
main ()
{
int i, j=0;
double e=2.71828, a;
j = 3;
a = 5.6;
}
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++
– Page 14 –
Eléments de syntaxe
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; [Link] (); [Link] ();
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()...
– Page 15 –
Cours de C++
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.2 switch
Syntaxe : switch (expression)
{
case valeur1 : instruction;
– Page 16 –
Eléments de syntaxe
– Page 17 –
Cours de C++
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.
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.
– 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).
2.7.3 goto
L'instruction goto permet un saut inconditionnel vers l'instruction qui suit l'étiquette
spécifiée avec le goto.
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.
– 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.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>.
– Page 20 –
Eléments de syntaxe
Remarque : L'expression qui suit #if peut contenir des parenthèses, des opérateurs
unaires, binaires ou ternaire.
– Page 21 –
3. POINTEURS ET FONCTIONS
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.
– Page 23 –
Cours de C++
Syntaxe : &identificateur
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
– Page 24 –
Pointeurs et fonctions
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.
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.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 :
– Page 26 –
Pointeurs et fonctions
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++
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.1 Structures
Les structures servent donc à regrouper plusieurs variables sous un unique identificateur.
– Page 28 –
Pointeurs et fonctions
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.
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.
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.
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).
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.
– Page 31 –
Cours de C++
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.
– Page 32 –
Pointeurs et fonctions
int t = i;
i = j; j = t;
}
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.
– Page 33 –
Cours de C++
– Page 34 –
4. CONSTRUCTEURS ET
DESTRUCTEURS, SURCHARGE
4.1.1 Constructeurs
Un constructeur est une méthode appelée systématiquement lors de la création d'une
instance d'une classe, c'est à dire lors de sa déclaration ou de sa création dynamique par new,
juste après la réservation de mémoire pour les données membres. Un constructeur optimise les
allocations mémoires liées aux variables dynamiques, et initialise les données membres des
classes. Par exemple, la création d'une instance d'une classe avec des données membres
pointeurs réserve bien évidemment de la place pour les pointeurs, mais non pour les zones
pointées. Le constructeur sert donc à la fois à réserver ces zones pointées et à les initialiser. Pour
rendre systématique l'initialisation des données membres des classes, toute classe doit contenir
un constructeur.
Comme les autres fonctions membres, les constructeurs sont déclarés dans la définition des
classes. Ils peuvent être inline ou non, et avoir des paramètres par défaut. Cependant, ils ne
retournent pas de valeur et on ne peut pas obtenir leur adresse. L'identificateur d'un constructeur
est identique à celui de la classe.
Syntaxe : class X
{
...
public:
X (paramètres); // constructeur de la classe X
...
}
X::X (paramètres)
{
...
}
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; }
Exemple : class X
{
int a, b;
public:
X (const X&);
}
X::X (const X& x)
{
a = x.a;
b = x.b;
}
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); // constructeur de la classe X
~X ( ); // destructeur de la classe X
...
}
X::~X ()
{
...
}
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++
La surcharge s'applique à toute fonction d'un programme C++, donc aussi, bien
évidemment, aux fonctions membres des classes.
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.
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
– Page 39 –
Cours de C++
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.
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;
[Link] = re;
[Link] = - im;
return resultat;
}
Exemple : class X
{
...
public:
X operator = (const X&);
}
– Page 41 –
Cours de C++
x1 = x2 = x3 = x4;
}
– Page 42 –
Constructeurs et destructeurs, surcharge
[Link] = im + [Link];
return resultat;
}
– 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 [ ].
– 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.
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++
class point
{
double x,y; // coordonnées (x,y) d'un point
public:
...
}
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.
– Page 46 –
Héritage
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 :).
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++
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.
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é.
– Page 49 –
Cours de C++
Syntaxe : class A
{
..
};
class B
{
..
};
class 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).
– 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.
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
{
..
};
{
...
};
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.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.
– Page 52 –
Héritage
public: public:
void fct ( ); void fct ( );
... ...
}; };
main (void)
{
A *pa;
B *pb;
}
l'affectation pa = pb est autorisée, mais quel que soit le contenu de pa (pointeur sur un objet de
classe A ou B), pa->fct ( ) appelle toujours la fonction fct ( ) de la classe A. En effet, pendant la
compilation, le compilateur lie le pointeur pa à la fonction [Link] ( ), et quel que soit le contenu
de pa pendant l'exécution du programme, c'est toujours la fonction [Link] ( ) qui est appelée.
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é.
Syntaxe : public:
– Page 53 –
Cours de C++
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.
– 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.
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.
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;
}
– Page 56 –
Flux
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
– 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).
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
Exemple : if (!cin)
{
cout << "ios::failbit ou ios::badbit est activé" << endl;
if ([Link] ( ))
[Link] (ios::badbit|[Link]( )); // réinitialise ios::badit
}
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.
– 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é.
– Page 60 –
Flux
Si on tente de déplacer le pointeur d'élément vers une position invalide, seekp () bascule la
valeur ios::badbit.
– Page 61 –
Cours de C++
Exemples :
// Écriture dans un flux texte // Écriture dans un flux binaire
– Page 62 –
Flux
– Page 63 –
Cours de C++
– Page 64 –
Flux
– Page 65 –
Cours de C++
– Page 66 –
Flux
– 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.
int min (int a, int b) float min (float a, float b) char min (char a, char b)
{ { {
return (a<b) ? a : b; return (a<b) ? a : b; return (a<b) ? a : b;
} } }
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.
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.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)
{
}
– 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;
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,
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.
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++
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).
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 (...)
{
...
}
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.
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.
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.
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.
– Page 76 –
Généricité
Paramètre formel
Template
– Page 77 –
8. 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.
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++
8.1.2 Try
Pour pouvoir être interceptées, les exceptions doivent se produire dans un bloc
d'instructions appelé bloc d'essai.
Syntaxe : try
{
...
}
Remarques : Un bloc try regroupe une série d'instructions dans lesquelles des excep-
tions peuvent survenir.
Exemple : try
{
func1 ();
func2 ();
}
8.1.3 Catch
Une fois l'exception lancée, celle-ci doit être interceptée par un gestionnaire d'exception
pour pouvoir être traitée de manière appropriée.
Un tel gestionnaire est un bloc d'instructions appelé bloc catch. Ce bloc commence par le
mot réservé catch, suivi par une déclaration (entre parenthèses) d'exceptions. Vient ensuite une
séquence d'instructions entre accolades. La déclaration permet de spécifier les types des objets
que le gestionnaire d'exceptions doit "attraper".
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
8.2 EXEMPLE 1
#include <iostream.h>
#include <stdlib.h>
class zero
{
private:
...
public:
zero ();
~zero ();
};
void main ()
{
double 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.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 :
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 ().
– 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
try
{ f(); }
catch (X)
{ ... }
catch (Y)
{ ... }
catch (...)
{ ... }
set_unexpected (old_unexpected);
– Page 83 –
Cours de C++
try
{ f(); }
catch (X)
{ ... }
catch (Y)
{ ... }
catch (...)
{ ... }
}
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 Contrôle d'accès_______________________________________________________________ 4
1.5 Données membres ________________________________________________________ 5
1.5.1 Membres statiques _____________________________________________________________ 5
1.6 Fonctions membres_______________________________________________________ 5
1.6.1 Fonctions « non inline »_________________________________________________________ 5
1.6.2 Fonctions inline _______________________________________________________________ 6
1.6.3 Fonctions amies (friend) ________________________________________________________ 6
1.7 Déclaration d'instances ___________________________________________________ 7
1.8 Mots clefs _______________________________________________________________ 7
2. ÉLÉMENTS DE SYNTAXE ______________________________________________ 9
2.1 Structure d'un programme c++ ____________________________________________ 9
2.2 Eléments de base_________________________________________________________ 9
2.2.1 Caractères ___________________________________________________________________ 9
2.2.2 Commentaires _______________________________________________________________ 10
2.2.3 Délimiteurs _________________________________________________________________ 10
2.2.4 Identificateurs _______________________________________________________________ 10
2.2.5 Mots réservés________________________________________________________________ 10
2.2.6 Types de base _______________________________________________________________ 11
2.2.7 Valeurs littérales _____________________________________________________________ 11
2.2.8 Déclaration des variables_______________________________________________________ 12
2.2.9 Attributs des variables _________________________________________________________ 12
2.2.10 Initialisation des variables____________________________________________________ 13
2.2.11 Constantes typées __________________________________________________________ 13
2.3 Instructions ____________________________________________________________ 13
2.4 Opérateurs_____________________________________________________________ 13
2.4.1 Opérateurs arithmétiques _______________________________________________________ 14
2.4.2 Opérateurs de manipulation de bits _______________________________________________ 14
2.4.3 Opérateurs d'affectation, d'incrémentation et de décrémentation_________________________ 14
2.4.4 Opérateurs relationnels ________________________________________________________ 14
2.4.5 Opérateurs logiques ___________________________________________________________ 15
2.4.6 Opérateur conditionnel ________________________________________________________ 15
2.4.7 Opérateur sizeof______________________________________________________________ 15
2.4.8 Opérateur , __________________________________________________________________ 15
2.4.9 Opérateur . __________________________________________________________________ 15
2.4.10 Opérateur :: _______________________________________________________________ 15
2.4.11 Opérateur ( ) : conversion de type______________________________________________ 15
2.4.12 Autres opérateurs __________________________________________________________ 15
2.4.13 Précédence des opérateurs ___________________________________________________ 16
– Page 85 –
Cours de C++
– Page 86 –
Table des Matières
– Page 87 –
Cours de C++
– 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 : [Link]@[Link]
– Page 89 –