0% ont trouvé ce document utile (0 vote)
447 vues89 pages

Cours C

Ce document présente un cours de C++ axé sur l'encapsulation, un concept clé de la programmation orientée objet. Il explique la structure des classes, les membres de données et de fonctions, ainsi que les mécanismes d'accès et de déclaration d'instances. Le cours souligne l'importance de l'abstraction des données et de l'utilisation des fonctions membres pour interagir avec les objets.

Transféré par

mouandhui.3mh.technis
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
447 vues89 pages

Cours C

Ce document présente un cours de C++ axé sur l'encapsulation, un concept clé de la programmation orientée objet. Il explique la structure des classes, les membres de données et de fonctions, ainsi que les mécanismes d'accès et de déclaration d'instances. Le cours souligne l'importance de l'abstraction des données et de l'utilisation des fonctions membres pour interagir avec les objets.

Transféré par

mouandhui.3mh.technis
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

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
{ où <déclarations> est de la forme :
private: (par défaut)
<déclarations> type1 donnée_membre1;
protected: type2 donnée_membre2;
<déclarations> ...
public: fonction_membre1 (paramètres);
<déclarations> fonction_membre2 (paramètres);
}; ...

Par exemple :

class booleen
{
short b;
public:
void init (short);
void not ();
void affiche ();
};

1.4.1 Contrôle d'accès


Par défaut, les données membres et les fonctions membres des classes sont privées,
c'est à dire inaccessibles de l'extérieur de la classe, ce qui permet d'imposer l'abstraction des
données. Mais il est possible de contrôler l'accès des différents membres à l'aide des mots
réservés private, protected et public.

– 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 : Exemple :
class identificateur_de_classe class booleen
{ {
... short b;
public: public:
fonction_membre1 (paramètres); void init (short)
fonction_membre2 (paramètres); void not ();
... void affiche ();
}; };

main () main ()
{ {
identificateur_de_classe objet; booleen fini;

objet.fonction_membre1 (paramètres); [Link] (1);


objet fonction_membre2 (paramètres); [Link] ();
... [Link] ();
} }

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 TAILLE LIMITES


Signed char 1 octet -128 à 127
Unsigned char (char) 1 octet 0 à 255

TYPE : ENTIER TAILLE LIMITES


Signed short (short) 2 octets -32768 à 32767
Unsigned short 2 octets 0 à 65535
Signed int (int) 4 octets -214783648 à 214783647
Unsigned int 4 octets 0 à 4294967295
Signed long (long) 4 octets -214783648 à 214783647
Unsigned long 4 octets 0 à 4294967295

TYPE : RÉEL TAILLE LIMITE LIMITE MANTISSE


EXPOSANT
Float 4 octets -38 à 38 1.175494350822875 10-38 à
3.4028234663852886 10+38
Double 8 octets -308 à 308 2.2250738585072015 10-308 à
1.7976931348623158 10+308
Long double 8 octets -308 à 308 2.2250738585072015 10-308 à
1.7976931348623158 10+308

2.2.7 Valeurs littérales


A chaque type de base, peuvent être associées des valeurs littérales correspondantes.
En voici la liste :

Valeur caractère :
Une valeur caractère est représentée par un caractère ou une séquence d'échappement
(voir paragraphe caractères) entouré d'apostrophes. Exemple : 'X', '\'', '0', '\117'.

Valeur entière :
Pour les valeurs entières, le suffixe u ou U indique que la valeur est signée. De même,
le suffixe l ou L indique que la valeur est de type long.
décimales : Les valeurs entières décimales sont composées d'une séquence de
chiffres de 0 à 9. Elles ne doivent pas commencer par 0. Exemple : 128,
799, 128L, 32744u.
octales: Les valeurs entières octales sont composées d'une séquence de chiffres
de 0 à 7. Elles doivent obligatoirement commencer par 0. Exemple :
0732, 0899.
hexa : Les valeurs entières hexadécimale sont composées d'une séquence de
chiffres de 0 à 9 ou de lettres de A à F (majuscules ou minuscules).
Elles doivent obligatoirement commencer par 0x ou 0X. Exemple :
0x3F, 0Xe2d4.

– 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 : Les variables statiques ont un emplacement mémoire réservé au moment de la


compilation. Leur allocation est donc permanente en mémoire, et leur contenu,
qui peut évoluer, est conservé pendant toute la durée d'exécution d'un
programme. Les variables statiques sont surtout utilisées pour les variables
locales des fonctions. Elles permettent en effet de conserver la valeur d'une
variable entre deux appels d'une même fonction. Mais attention, ceci nuit
énormément à la lisibilité des programmes et ne doit être utilisé qu'en dernier
recours. Exemple : static int i;
auto : Les variables auto, propres à une fonction, sont allouées lors de l'appel de cette
fonction, puis libérées au retour de la fonction. Leur contenu est donc perdu
entre deux appels consécutifs à la fonction. Les variables locales des fonctions
sont auto par défaut. Exemple : auto int n;
register : L'attribut register permet de placer une variable dans un registre interne du
microprocesseur. Il est généralement employé avec des variables intensément

– Page 12 –
Eléments de syntaxe

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;
extern : L'utilisation de variables externes permet de référencer des variables définies
dans d'autres fichiers. Une déclaration extern ne remplace pas une définition.
Elle décrit simplement une variable définie ailleurs. Exemple : extern int i;

2.2.10 Initialisation des variables


Les variables peuvent être initialisées à l'aide de l'opérateur d'affectation =.
L'expression à droite du signe = doit être évaluable, c'est à dire représenter une valeur. Le
membre à gauche de l'affectation doit lui représenter une variable, c'est à dire un contenu.
Toutes les variables doivent être initialisées avant leur utilisation. Il est possible d'initialiser
les variables au moment de leur déclaration.

Exemple :
main ()
{
int i, j=0;
double e=2.71828, a;

j = 3;
a = 5.6;
}

2.2.11 Constantes typées


Pour déclarer une constante typée en C++, il suffit de faire précéder la déclaration
d'une variable initialisée au moment de sa déclaration par le mot réservé const. Ainsi, la valeur
de cette variable ne peut plus être modifiée. Exemple : const double rac2 = 1.41421. On verra
la déclaration et l'utilisation des constantes non typées au paragraphe sur les directives pour le
préprocesseur.

2.3 INSTRUCTIONS
Les fonctions d'un programme C++ sont constituées d'un ensemble de données et
d'instructions exécutables. Une instruction est une séquence d'opérateurs, opérandes et signes
de ponctuation. Une expression peut conduire ou non à un résultat. Le déroulement des
instructions dans le corps d'une fonction est séquentiel, sauf dans le cas de rupture de
séquence, de terminaison de boucle ou de retour de fonction. Toute instruction doit
obligatoirement se terminer par un point virgule (;) qui est un terminateur d'instruction. Une
instruction composée regroupe plusieurs instructions entourées par des accolades ({ et }). Une
instruction composée ne se termine pas par un point virgule.

2.4 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; [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()...

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++

L'expression à tester doit être de type entier.


Exemple : int i=10;
do cout << "i = " << i--;
while (i>0);

2.6.3 Boucle for


Syntaxe : for (expression1 ; expression2 ; expression3) instruction
instruction peut être une instruction composée de la forme :
{ instruction1; instruction2; ... ; }
expression1 sert d'initialisation, expression2 réalise le test de la boucle,
expression3 est exécutée à chaque itération.

Remarques : On choisira une boucle for lorsque le nombre d'itérations est connu à
l'entrée dans la boucle.
Les différentes expressions peuvent être omises (exemple : boucle sans
fin si expression2 est toujours vraie, boucle sans initialisation si
expression1 n'apparait pas).
Plusieurs expressions peuvent être regroupées pour former expression1,
expression2 ou expression3. Elles sont alors séparées pas des virgules
(exemple : for (i=0,j=N;i<N;i++,j--) instruction).
La boucle for n’est pas exécutée si la valeur de départ du compteur est
supérieure à la valeur de fin.
L'expression à tester doit être de type entier.

Exemple : for (i=0;i<N;i++) cout << i;

2.7 BRANCHEMENTS
Toutes les instructions de branchement nuisent à la clarté d'un programme. Il faut donc
s'interdire de les utiliser. Toutefois dans des cas très particuliers, on peut être amené à les utiliser
(exemple : break dans l'instruction switch).

2.7.1 break
L'instruction break permet de quitter une instruction itérative (do, for ou while), ou un
switch. L'instruction qui est exécutée après un break est la première instruction après la boucle
ou le switch.

Remarque : L'instruction break est généralement utilisée dans les instructions switch.

Exemple : for (i=0;i<5;i++)


{
if (string[i] == '\0') break;
length++;
}
Lorsque le caractère nul est rencontré dans string, l'exécution se poursuit
sur l'instruction qui suit la boucle for.

– 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 lui-
même.

2.8.1 #define et #undef


#define permet de remplacer dans un fichier toutes les occurrences d'un symbole par une
suite de caractères. Exemple : #define N 64 remplacera tous les caractères N du texte qui suit
#define par les caractères 64.
#undef annule la définition d'un symbole pour le reste d'un fichier. Exemple : #undef N.

2.8.2 #include
#include permet d'insérer, à la place de la ligne #include, le contenu d'un fichier source
courant à compiler (généralement un fichier de définitions .h). Deux syntaxes existent : #include
"nom de fichier" ou #include <nom de fichier>. Dans la première forme, le fichier appartient à
l'utilisateur et doit se trouver dans le répertoire courant, ou alors il faut spécifier son chemin
d'accès complet. Dans la seconde, le fichier est recherché dans le répertoire /usr/include.
Exemple : #include <iostream.h>.

2.8.3 Compilation conditionnelle #if/#ifdef/#ifndef ... #else ... #endif


Ces directives de compilation permettent de compiler des lignes d'un programme si une
condition est vérifiée.

Syntaxe : #if expression


séquence compilée si expression est vraie (≠0)
#else
séquence compilée si expression est fausse (=0)
#endif
poursuite de la compilation

Syntaxe : #ifdef symbole


séquence compilée si symbole a été défini par #define
#else
séquence compilée si symbole n'a pas été défini par #define
#endif
poursuite de la compilation

Syntaxe : #ifndef symbole


séquence compilée si symbole n'a pas été défini par #define
#else

– 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; // la variable i contient 4


pi = &i; // pi contient l'adresse de i : il pointe sur i
(*p)i ++; // le contenu pointé par pi (donc i) est incrémenté
j = *pi; // la valeur 5 est affectée à j

3.1.5 Variables dynamiques, opérateurs new et delete


L'opérateur new crée une variable dynamique et initialise un pointeur sur cette variable.
La zone allouée par new n'est pas initialisée automatiquement, mais pour faciliter l'initialisation,
il est possible de spécifier une valeur entre parenthèses.

Syntaxe : pointeur = new type (valeur);

Remarques : En cas de succès, new retourne l'adresse de la variable créée ; en cas


d'échec, (par exemple lorsqu'il n'y pas assez de mémoire disponible), new
retourne la valeur NULL.
Comme pour toutes les variables, c'est au programmeur de veiller à leur
initialisation. Dans le cas de variables dynamiques celui-ci doit veiller à
initialiser à la fois le pointeur sur la variable dynamique et la variable
dynamique elle-même.

Exemple : double *pi *e; // pointeurs sur un double, non initialisés


pi = new double; // réservation mémoire et initialisation du pointeur
*pi = 3.1415927; // initialisation de la variable dynamique
e = new double (2.71828); // méthode plus rapide, rationnelle et sûre

A l'inverse de new, delete libère la mémoire d'une variable dynamique.

– 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 : int *pi, *pj;


*(pi+1) = 3; // affecte 3 dans pj : utilisation très discutable

Exemple 2 : while (*s++=*t++); // copie d'une chaîne t dans une chaîne s

3.2 TABLEAUX

Un tableau est une donnée structurée dont tous les éléments sont de même type. Les
tableaux peuvent être classés en deux catégories : les tableaux unidimensionnels et les tableaux
multidimensionnels. Un tableau est caractérisé par 3 éléments : son identificateur, son type et sa
dimension. En C++, la dimension d'un tableau est une constante entière qui donne le nombre
d'éléments, le premier élément étant toujours indicé zéro.

– 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]; // tableau unidimensionnel


ou
type identificateur [dim1] [dim2] [dim3]... // multidimensionnel

Remarques : Rappel : le premier indice d'un tableau est toujours 0.


Il est possible de ne pas donner de dimension lors de la déclaration d'un
tableau. Dans ce cas, aucun espace mémoire n'est alloué. Ceci permet de
référencer un tableau dont la dimension est définie ailleurs, par exemple
avec l'opérateur new. Lors de la réservation mémoire d'un tableau,
l'adresse retournée par new est l'adresse du premier élément de ce tableau.
La réservation mémoire d'un tableau par new se fait de la manière
suivante : new type [dimension];
Les tableaux multidimensionnels sont vectorisés. Par exemple, la
déclaration double a[3][6]; conduit à l'équivalence, très utilisée en C++,
a[i][j] ≡ a[i*6+j]; (6 groupes de 3 éléments chacun).

Exemple : int ti[8]; // tableau de 8 entiers


char tc[80]; // tableau de 80 caractères
extern double td[10]; // tableau externe de 10 réels
unsigned char ima[512][512]; // tableau carré 512´512 (pixels)

for (i=0;i<10;i++) // initialisation de td


td[i] = (double) i;

for (i=0;i<512;i++) // initialisation de ima


for (j=0;j<512;j++)
ima[i][j] = 0;

3.2.2 Relation entre tableaux et pointeurs


En C++, les pointeurs et les tableaux sont des variables de même nature. Un tableau est
en fait un pointeur constant qui contient l'adresse du premier élément du tableau. La seule
différence entre pointeurs et tableaux réside dans le fait que l'adresse du premier élément du
tableau ne peut être modifiée. Par exemple, s'il est possible d'incrémenter la valeur d'un pointeur
(p++), cela est interdit pour un tableau. Pour fixer les esprits, le tableau suivant présente sous
forme synthétique certaines équivalences d'écriture entre pointeurs et tableaux.

test identique à &(t[0])


test l'adresse de t[0]
t+1 est l'adresse de t[1]
t+iest l'adresse de t[i]
*(t+i) = 1 est identique à t[i] = 1
t ayant été déclaré par int t[10];
ou t ayant été déclaré par int *t; et alloué par t = new int [10] ;

– 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]; // t1 est un tableau de 10 pointeurs sur caractère


// à ne pas confondre avec :
char (*t2)[10]; // t2 est un pointeur sur un tableau de 10 char

3.2.4 Chaînes de caractères


En C++, les chaînes de caractères sont strictement définies comme des tableaux de
caractères terminés par le caractère nul : '\0'. Toutes les remarques sur les tableaux et les
équivalences tableaux-pointeurs s'appliquent donc pour les chaînes de caractères. Les chaînes ne
sont pas directement manipulables car il n'y a pas d'opérateurs prédéfinis dans le langage
(contrairement à Turbo Pascal). Il existe cependant de nombreuses fonctions standards (dont les
prototypes sont définis dans le fichier string.h, voir plus loin bibliothèques standards) qui
permettent de réaliser les principales fonctions :
− strcpy (s1, s2) pour recopier une chaîne s2 dans la chaîne s1,
− strcmp (s1, s2) pour comparer les deux chaînes s1 et s2 (strcmp renvoie une valeur
entière négative, nulle ou positive selon que s1 est alphabétiquement plus petite, égale
ou plus grande que s2),
− strlen (s) pour obtenir la longueur de la chaîne s,
− strcat (s1, s2) pour concaténer la chaîne s2 à la chaîne s1,
etc, ...

3.2.5 Initialisation des tableaux


Un tableau peut être initialisé au moment de la compilation uniquement si son attribut est
externe ou statique.
Le cas le plus simple est celui des chaînes de caractères initialisées pendant leur
déclaration. Cependant, des précautions doivent être prises car la manipulation des chaînes de
caractères est bien souvent source d'erreur en C++.

Exemple 1 : static char text[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 // déclaration d'une structure date


{
int jour;
char mois[10];
int annee;
};

date d, // déclaration d'une variable d de type date

– Page 28 –
Pointeurs et fonctions

*pd; // déclaration d'un pointeur pd de type date


[Link] = 1995; // initialisation du champ année de d
pd->jour = 19; // équivalent à (*pd).jour = 19;

3.3.2 Unions
Le type union est une extension du type structure qui permet de manipuler une même
zone mémoire selon différentes interprétations. Comme les structures, les unions contiennent des
champs. Mais les zones mémoires de ces champs ont la même adresse, le compilateur réservant
suffisamment d'espace mémoire pour stocker le plus grand des champs de l'union.

Syntaxe : union nom_d'union


{
type1 nom_de_champ_1;
type2 nom_de_champ_2;
type3 nom_de_champ_3;
...
};

Remarque : On ne peut accéder simultanément qu'à un seul champ d'une union.

Exemple : union valeur // déclaration de l'union valeur


{
int entier;
double reel;
};

valeur v; // définition d'une variable de type valeur

[Link] = 4; // v est utilisée comme un entier


[Link] = 3.1415; // v est utilisée comme un réel

3.3.3 Énumérations
Une liste de constantes peut se définir par énumération. Le premier identificateur de la
liste a la valeur 0 par défaut, les valeurs des identificateurs suivants étant incrémentées pour
chaque nouvel identificateur.

Syntaxe : enum nom_d'enumeration


{
identificateur_1,
identificateur_2,
identificateur_3,
...
}

Remarques : Il n'y a aucune vérification de débordement.


Il est possible de modifier les valeurs par défaut des identificateurs :
enum couleur {JAUNE=-2, ROUGE, VERT=4, BLEU};

Exemple : enum jour {LUNDI, MARDI, MERCREDI, JEUDI,


– Page 29 –
Cours de C++

VENDREDI, SAMEDI, DIMANCHE};

jour j1, j2;

j1 = MARDI; // j1 vaut MARDI qui vaut 1


j2 = j1+3; // j2 vaut VENDREDI qui vaut 4

3.4 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 celle-


ci, c'est à dire le type des paramètres qu'elle utilise et de la valeur retournée. Ainsi, lorsque cela
est possible, le compilateur est en mesure de faire les conversions de types nécessaires à une
transmission correcte des paramètres et de la valeur renvoyée.

La syntaxe de déclaration d'une fonction est la suivante. Le nom de la fonction


(identificateur) doit être précédé du type de la valeur retournée par la fonction, et suivi (entre
parenthèses) de la liste de ses paramètres.

Syntaxe : type_retourné identificateur (type1, type2, type3, ...);


ou
type_retourné identificateur (type1 param1, type2 param2, ...);

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); // par défaut, a=0 et b=1

void main (void)


{
double c = division (); // c = 0;
}

3.5.7 Paramètres de la ligne de commande


Il est possible de récupérer dans un programme C++ les paramètres de la ligne de
commande d'un programme, c'est à dire la chaîne de caractères qui a déclenché son exécution
(nom du programme et paramètres éventuels). Pour cela, il suffit de déclarer deux paramètres à
la fonction main : argc et argv. Le premier paramètre, entier, argc, contient le nombre de champs
qui composent la ligne de commande (au minimum 1, le nom du programme). Le second
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 : class X
{
...
public:
X (paramètres); // constructeur de la classe X
...
}
X::X (paramètres)
{
...
}

Remarques : Le constructeur est automatiquement appelé lors de la déclaration de


l'objet, et il est interdit d'appeler des constructeurs en dehors de cette
déclaration.
– 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); // constructeur de la classe X
~X ( ); // destructeur de la classe X
...
}
X::~X ()
{
...
}

4.2 POINTEUR THIS


Le pointeur "this" est un pointeur spécial, automatiquement ajouté aux données membres
et initialisé par le constructeur d'une classe. Dès que l'instance d'une classe est créée, le pointeur
this de cette classe contient l'adresse de l'objet instancié. Il identifie ainsi toujours la classe elle-
même. Ce pointeur this est passé comme paramètre caché à toutes les fonctions membres d'une
classe, et ne peut être utilisé que dans les fonctions membres. Par exemple, utiliser une donnée
membre a dans une fonction membre est équivalent à utiliser this->a. Pour éviter toute
ambiguïté, il est interdit au programmeur de déclarer un pointeur this comme donnée membre
d'une classe et de l'initialiser.
Le pointeur this permet de résoudre certaines identifications équivoques. Par exemple,
une fonction membre ne peut normalement pas avoir de paramètre ayant même identificateur
qu'une donnée membre. Cela est néanmoins possible si partout dans la fonction la donnée
membre est référencée par this->.

Exemple : class X
{
int a;
public:
X (int a) {this->a = a;}
...
}

4.3 SURCHARGE
La surcharge consiste à donner le même identificateur à des fonctions ou des opérateurs
qui ont des paramètres différents. En C++, il est possible de redéfinir les fonctions et la plupart
des opérateurs standards. Généralement, fonctions et opérateurs sont surchargés pour étendre
leur utilisation sur des types de données différents de ceux pour lesquels ils ont été écrits.
– 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 [Link] (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 : type operator ~ ( ); // opérateur fonction membre


ou
friend type operator ~ (classe); // opérateur fonction amie

Utilisation : ~ objet ou [Link] ~ ( ) // opérateur fonction membre


ou
~ objet ou operator ~ (objet) // opérateur fonction amie

Remarques : Pour les fonctions membres ou amies, les deux écritures présentées
ci-dessus sont totalement équivalentes. La première est appelée notation
opératoire ou implicite, la seconde notation fonctionnelle classique ou
explicite.
Pour les opérateurs ++ et −−, on peut définir deux sens différents comme
++x et x++. La déclaration suffixe est réalisée en déclarant deux
arguments respectivement de type classe et entier pour les opérateurs
surchargés hors d'une classe. Pour les opérateurs membres d'une classe,
un seul paramètre de type entier suffit pour spécifier l'opérateur suffixe.
Dans tous les autres cas, l'opérateur est considéré comme un opérateur
préfixe.

Exemple : #include <iostream.h>

class complx
{
private:
double re, // partie réelle
im; // partie imaginaire
public:
complx (double r = 0, double i = 0) { re = r; im = i; }
complx operator - ( ); // opérateur conjugué
friend double operator & (const complx &); // op. module
};

complx complx::operator - ( )
{

– Page 40 –
Constructeurs et destructeurs, surcharge

complx resultat;
[Link] = re;
[Link] = - im;
return resultat;
}

double operator & (const complx & c)


{
return sqrt ([Link]*[Link] + [Link]*[Link]);
}

void main (void)


{
complx x (4,4);
complx y = -x; // appel implicite
complx z = [Link] - ( ); // appel explicite
double m = &x; // appel implicite
double n = operator & (x); // appel explicite
}

4.3.6 Surcharge de l'opérateur d'affectation =


Lorsque les données membres d'une classe contiennent des pointeurs sur des variables
dynamiques, il est indispensable de redéfinir l'opérateur d'affectation =. En effet, dans la plupart
des cas, il faut non seulement recopier les données membres de la classe, mais aussi recopier les
valeurs des variables dynamiques pointées, après leur avoir réservé un emplacement mémoire
propre (cf constructeur copie).

Lors de la surcharge de l'opérateur =, il est judicieux de passer l'unique paramètre en


référence constante pour ne pas risquer de le modifier. De même, on peut donner à cet opérateur
une valeur de retour du même type classe. Cela permet de "cascader" les affectations, comme
c'est l'usage en C++.

Exemple : class X
{
...
public:
X operator = (const X&);
}

X X::operator = (const X & x)


{
...
return (*this);
}

void main (void)


{
X x1, x2, x3, x4;
...

– 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 : retourné operator ~ (type); // opérateur fn membre


ou
friend retourné operator ~ (classe, type); // opérateur fonction amie

Utilisation : objet ~ var [Link] ~ (var) //opérateur fn membre


ou
objet ~ var operator ~ (objet, var) // opérateur fonction amie

Remarques : Dans la syntaxe d'utilisation des opérateurs binaires ci-dessus, var


représente soit un objet, soit un paramètre de type quelconque.
Comme pour les opérateurs unaires, les deux écritures implicites et
explicites sont complètement équivalentes.
Lorsqu'une redéfinition concerne un opérateur dont le membre de gauche
n'est pas une classe, cette redéfinition doit être obligatoirement réalisée
par une fonction amie. En effet, le premier opérande d'un opérateur
binaire redéfini dans une classe est nécessairement une instance de cette
classe.

Exemple : #include <iostream.h>


class complx
{
private:
double re, // partie réelle
im; // partie imaginaire
public:
complx (double r = 0, double i = 0) { re = r; im = i; }
complx operator + (const complx &);
friend complx operator + (double, const complx &);
};

complx complx::operator + (const complx& c)


{
complx resultat;
[Link] = re + [Link];

– Page 42 –
Constructeurs et destructeurs, surcharge

[Link] = im + [Link];
return resultat;
}

complx operator + (double r, const complx& c)


{
complx resultat;
[Link] = r + [Link];
[Link] = r + [Link];
return resultat;
}

void main (void)


{
complx x (4,4);
complx y (6,6);
complx t = [Link] + (y); // appel explicite
complx u = x + y; // appel implicite
complx v = operator + (3.0, x);
complx w = 3.0 + x;
}

4.3.8 Surcharge des opérateurs [ ] et ( )


La redéfinition de l'opérateur d'indiçage permet d'utiliser une classe tableau définie par le
programmeur, tout en conservant la notation traditionnelle des tableaux. Cet opérateur [ ] admet
comme un paramètre de type quelconque, ce qui permet de travailler avec des tableaux de
tableaux. Une utilisation astucieuse de cet opérateur consiste à définir une classe tableau qui
conduise à une véritable notion de tableau dynamique avec vérification de débordement. De
même, en donnant une valeur de retour qui soit de type référence à l'opérateur [], il sera possible
de mettre cet opérateur [] à gauche d'un signe d'affectation, et donc de modifier le contenu de
l'élément considéré du tableau.

Exemple : class CTab


{
private:
double t[TMAX];
public:
double& operator [] (int);
};

double& CTab::operator [] (int i)


{
if ( (i<0) || (i>TMAX-1) )
cerr << "Débordement de tableau" << endl;
return t[i];
}

void main (void)


{

– 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; // coordonnées (x,y) d'un point
public:
...
}

class cercle : point


{
double ray; // rayon d'un cercle
public:
...
}

Dans cet exemple, la classe cercle a trois données membres : x, y et ray.

5.1.2 Affectation
En programmation orientée objet, un objet de classe dérivée peut être manipulé comme
un objet de classe de base. Ceci signifie par exemple qu'un objet dérivé peut être affecté à un
objet de base, contrairement à la règle traditionnelle de l'affectation qui précise que les membres
à gauche et à droite du signe = doivent être de même type. En fait, pour être autorisée, il suffit
que l'affectation (ou projection) d'un objet dans un deuxième objet initialise tous les champs de
ce deuxième objet. C'est le cas lorsqu'un objet dérivé est projeté sur un objet de base, mais pas
l'inverse. Valide pour l'affectation d'objets, cette propriété est aussi très utilisée pour le passage
de paramètres objets dans les fonctions et pour l'affectation de pointeurs sur les objets.

Exemple : (suite de l'exemple précédent)


void main ( )
{
point p;
cercle c;

p = c; // valide, toutes les données de p sont initialisées


c = p; // interdit, ray n'est pas initialisé.
}

5.1.3 Constructeurs et destructeurs


Lorsque la classe de base d'une classe dérivée possède un constructeur (ce qui devrait
toujours être le cas), cette classe dérivée doit elle aussi obligatoirement avoir un constructeur,
sauf s'il y a dans la classe de base un constructeur sans paramètre. En effet, lors de la création
d'une instance d'une classe dérivée, le constructeur de la classe de base est exécuté préalablement
à celui de la classe dérivée. Quand une classe dérivée possède plusieurs classes de base
(hiérarchie), le constructeur appelé en premier est celui de la classe la plus ancêtre, le dernier

– 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: 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.

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
[Link] ( ) est appelée quand le pointeur pa contient un objet de la classe A, et la fonction [Link] ( )
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; // affiche 1000
cout << hex << i << endl; // affiche 200
cout << 255 << endl; // affiche FF
cout << dec << i << endl; // affiche 512

– Page 56 –
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
[Link] (8);
cout << pi // affiche 3.14159265
[Link] (ios::showpoint,ios::floatfield); // setf pour options exclusives
cout << 10.0; //affiche 10.00000000
[Link] (ios::fixed,ios::floatfield); // notation scientifique
[Link] (6); // affichage sur 6 caractères
[Link] ('@'); // caractère de remplissage @
cout << 12.5; // affiche @@12.5
[Link] (ios::showpos); // affiche + pour les nombres > 0
[Link] (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* ( ) convertit le flux courant en pointeur pour pouvoir le comparer à
operator const void* () NULL. Retourne 0 si ios::failbit ou ios::badbit sont activés.
int operator ! ( ) const ! ( ) retourne une valeur non nulle si ios::badbit ou ios::failbit sont
activés
streambuf* rdbuf ( ) retourne un pointeur sur l'objet streambuf associé au flux

Exemple : if (!cin)
{
cout << "ios::failbit ou ios::badbit est activé" << endl;
if ([Link] ( ))
[Link] (ios::badbit|[Link]( )); // 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 <iostream.h>


#include <fstream.h> #include <fstream.h>
#include <stdlib.h> #include <stdlib.h>
#include <math.h> #include <math.h>
#define N 16 #define N 16

void main (void) void main (void)


{ {
int i; int i;
double s[N], t[N]; double s[N], t[N];
char * flux_name = "[Link]"; char * flux_name = "[Link]";

for (i=0;i<N;i++) for (i=0;i<N;i++)


s[i] = sin (2 * M_PI * i / (double) N); s[i] = sin (2 * M_PI * i / (double) N);
ofstream fr (flux_name, ios::out); ofstream fr (flux_name, ios::out);
if (!fr) if (!fr)
{ {
cerr << "Erreur d'ouverture de "; cerr << "Erreur d'ouverture de ";
cerr << flux_name << endl; cerr << flux_name << endl;
exit (1); exit (1);
} }
for (i=0;i<N;i++) [Link] ((char*) s, N*sizeof (double));
fr << s[i]; [Link] ( );
[Link] ( );

ifstream fd (flux_name, ios::in); ifstream fd (flux_name, ios::in);


if (!fd) if (!fd)
{ {
cerr << "Erreur d'ouverture de "; cerr << "Erreur d'ouverture de ";
cerr << flux_name << endl; cerr << flux_name << endl;
exit (1); exit (1);
} }
for (i=0;i<N;i++) [Link] ((char*) t, N*sizeof (double));
fd >> t[i]; [Link] ( );
[Link] ( );
} }

– 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 { goodbit=0, eofbit=1, failbit=2, badbit=4, hardfail=0200 };
enum open_mode { in=1, out=2, ate=4, app=010, trunc=020, nocreate=040, noreplace=0100 };
enum seek_dir { beg=0, cur=1, end=2 };
enum { skipws=01,left=02,right=04, internal=010, dec=020, oct=040, hex=0100, showbase=0200, showpoint=0400,
uppercase=01000, showpos=02000, scientific=04000, fixed=010000, unitbuf=020000, stdio=040000 } ;
static const long basefield; /* dec|oct|hex */
static const long adjustfield; /* left|right|internal */
static const long floatfield; /* scientific|fixed */
ios (streambuf*);
virtual ~ios ();
long flags () const { return x_flags ; }
long flags (long f);
long sef (long setbits, long field);
long setf (long);
long unsetf (long);
int width () const { return x_width ; };
int width (int w) {int i = x_width ; x_width = w ; return i ; }
ostream* tie (ostream*);
ostream* tie () { return x_tie ; }
char fill (char);
char fill () const { return x_fill ; }
int precision (int);
int precision () const { return x_precision ; }
int rdstate () const { return state ; }
operator void*() { if (state&(failbit|badbit|hardfail)) return 0 ; else return this ; }
operator const void*() const { if (state&(failbit|badbit|hardfail)) return 0 ; else return this ; }
int operator! () const { return state&(failbit|badbit|hardfail); }
int eof () const { return state&eofbit; }
int fail () const { return state&(failbit|badbit|hardfail); }
int bad () const { return state&badbit ; }
int good () const { return state==0 ; }
void clear (int i =0);
streambuf* rdbuf () { return bp ; }
long & iword (int);
void* & pword (int);
static long bitalloc ();
static int xalloc ();
static void sync_with_stdio ();
int skip (int);
protected:
ostream* x_tie;
long x_flags;
short x_precision;
char x_fill;
short x_width;
void init (streambuf*);
ios (); /* No initialization at all. Needed by multiple inheritance versions */
int assign_private; /* needed by with_assgn classes */
private:
ios (ios&);
void operator = (ios&);
};

– Page 63 –
Cours de C++

6.4.2 Déclaration de la classe istream (iostream.h)


class istream : virtual public ios
{
public:
istream (streambuf*);
virtual ~istream ();
public:
int ipfx (int noskipws=0);
istream& seekg (streampos p);
istream& seekg (streamoff o, ios::seek_dir d);
streampos tellg ();
istream& operator >> (istream& (*f)(istream&)) { return (*f)(*this) ; }
istream& operator >> (ios& (*f)(ios&) );
istream& operator >> (char*);
istream& operator >> (signed char*);
istream& operator >> (unsigned char*);
istream& operator >> (char&);
istream& operator >> (signed char&);
istream& operator >> (unsigned char&);
istream& operator >> (short&);
istream& operator >> (int&);
istream& operator >> (long&);
istream& operator >> (unsigned short&);
istream& operator >> (unsigned int&);
istream& operator >> (unsigned long&);
istream& operator >> (float&);
istream& operator >> (double&);
istream& operator >> (long double&);
istream& operator >> (streambuf*);
istream& get (char* , int lim, char = '\n');
istream& get (signed char*, int, char = '\n');
istream& get (unsigned char*, int, char = '\n');
istream& getline (char*, int, char = '\n');
istream& getline (signed char*, int, char = '\n');
istream& getline (unsigned char*, int, char = '\n');
istream& get (streambuf&, char = '\n');
istream& get (char&);
istream& get (signed char&);
istream& get (unsigned char&);
int get ();
int peek ();
istream& ignore (int = 1,int = EOF);
istream& read (char *, int);
istream& read( signed char * s, int n) { return read((char*)s,n) ; }
istream& read (unsigned char *s, int n) { return read((char*)s,n) ;}
int gcount ();
istream& putback (char);
int sync () { return bp->sync() ; }
protected:
istream ();
private:
int x_gcount;
void xget (char *);
};

6.4.3 Déclaration de la classe istream_with_assign (iostream.h)


class istream_withassign : public istream
{
public:
istream_withassign ();
virtual ~istream_withassign ();
istream_withassign& operator = (istream&);
istream_withassign& operator = (streambuf *);
};

– Page 64 –
Flux

6.4.4 Déclaration de la classe ostream (iostream.h)


class ostream : virtual public ios
{
public:
ostream (streambuf*);
virtual ~ostream ();
public:
int opfx () { if ( ospecial ) return do_opfx() ; else return 1 ; }
void osfx () { if ( osfx_special ) do_osfx() ; }
ostream& flush ();
ostream& seekp (streampos);
ostream& seekp (streamoff, ios::seek_dir);
streampos tellp ();
ostream& put (char );
ostream& operator << (char c);
ostream& operator << (signed char);
ostream& operator << (unsigned char);
ostream& operator << (const char*);
ostream& operator << (const signed char*);
ostream& operator << (const unsigned char*);
ostream& operator << (int);
ostream& operator << (long);
ostream& operator << (double);
ostream& operator << (long double);
ostream& operator << (float);
ostream& operator << (unsigned int);
ostream& operator << (unsigned long);
ostream& operator << (const void*);
ostream& operator << (streambuf*);
ostream& operator << (short i) { return *this << (int)i ; }
ostream& operator << (unsigned short i) { return *this << (int)i; }
ostream& operator << (ostream& (*f)(ostream&)) { return (*f)(*this) ; }
ostream& operator << (ios& (*f)(ios&) );
ostream& write (const char *,int);
ostream& write (const signed char* s, int n) { return write((const char*)s,n); }
ostream& write (const unsigned char* s, int n) { return write((const char*)s,n); }
protected:
ostream ();
};

6.4.5 Déclaration de la classe ostream_with_assign (iostream.h)


class ostream_withassign : public ostream
{
public:
ostream_withassign ();
virtual ~ostream_withassign ();
ostream_withassign& operator = (ostream&);
ostream_withassign& operator = (streambuf*);
};

6.4.6 Déclaration de la classe iostream (iostream.h)


class iostream : public istream, public ostream
{
public:
iostream (streambuf*);
virtual ~iostream ();
protected:
iostream ();
};

– 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 = (ios&);
iostream_withassign& operator = (streambuf*);
};

6.4.8 Déclaration des variables globales du fichier iostream.h


extern istream_withassign cin;
extern ostream_withassign cout;
extern ostream_withassign cerr;
extern ostream_withassign clog;

ios& dec (ios&);


ostream& endl (ostream& i);
ostream& ends (ostream& i);
ostream& flush (ostream&);
ios& hex (ios&);
ios& oct (ios&);
istream& ws (istream&);

6.4.9 Déclaration de la classe fstreambase (fstream.h)


class fstreambase : virtual public ios
{
public:
fstreambase ();
fstreambase (const char*, int , int = filebuf::openprot);
fstreambase (int);
fstreambase (int fd, char*, int);
~fstreambase ();
void open (const char*, int, int = filebuf::openprot);
void attach (int fd);
void close ();
void setbuf (char*, int);
filebuf* rdbuf () { return &buf ; }
private:
filebuf buf;
protected:
void verify (int);
};

6.4.10 Déclaration de la classe ifstream (fstream.h)


class ifstream : public fstreambase, public istream
{
public:
ifstream ();
ifstream (const char*, int = ios::in, int = filebuf::openprot);
ifstream (int fd);
ifstream (int, char*, int);
~ifstream ();
filebuf* rdbuf () { return fstreambase::rdbuf(); }
void open (const char*, int = ios::in, int = filebuf::openprot);
};

– Page 66 –
Flux

6.4.11 Déclaration de la classe ofstream (fstream.h)


class ofstream : public fstreambase, public ostream
{
public:
ofstream ();
ofstream (const char*, int = ios::out, int = filebuf::openprot);
ofstream (int);
ofstream (int, char*, int);
~ofstream ();
filebuf* rdbuf () { return fstreambase::rdbuf(); }
void open (const char* , int = ios::out, int = filebuf::openprot);
};

6.4.12 Déclaration de la classe fstream (fstream.h)


class fstream : public fstreambase, public iostream
{
public:
fstream ();
fstream (const char*, int, int = filebuf::openprot);
fstream (int);
fstream (int, char*, int);
~fstream ();
filebuf* rdbuf () { return fstreambase::rdbuf(); }
void open (const char*, int, int = filebuf::openprot);
};

6.5 MOTS CLEFS


Buffer
cerr/cin/cout
Descripteur de flux
Entrées/Sorties
Etat de format
Etat d'erreur
Flot
Flux
Flux de sortie
Flux d'entrée
Flux d'erreur
fstream.h
iostream.h
Périphérique
Pointeur d'élément
Tampon

– 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 Réels simples Caractères

int min (int a, int b) float min (float a, float b) char min (char a, char b)
{ { {
return (a<b) ? a : b; return (a<b) ? a : b; return (a<b) ? a : b;
} } }

Une alternative envisageable mais dangereuse consisterait à utiliser la directive #define


du pré-processeur au lieu d'écrire explicitement chaque instance de min ().

#define min(a,b) ( ( (a) < (b) ) ? (a) : (b) )

En effet, bien que cette définition fonctionne correctement pour des cas simples, elle ne
se comporte pas comme on l'attend pour des appels plus complexes, puisque le pré-processeur
effectue une simple substitution du texte de ses paramètres. Ainsi, le plus petit des paramètres
est évalué deux fois : une première fois pendant le test (a < b), et une seconde fois pendant
l'exécution de l'instruction retournée.

Cas simples : fonctionnement correct


min (10, 20); // retourne 10
min (10.0, 20.0); // retourne 10.0
min ('d', 'f'); // retourne 'd'

Cas plus complexes : fonctionnement anormal


int i=10;
min (i++,20); // retourne 11, et i vaut 12 (i++ est réalisée 2 fois)

La notion de généricité permet de tirer parti de la notation compacte de la directive


#define du pré-processeur, sans perdre aucun avantage des langages fortement typés. Elle
autorise le programmeur à paramétrer à volonté le type des paramètres, le type des variables
locales, et le type de retour de fonctions dont le corps reste le même (instructions identiques).
Cette possibilité s'appelle "généricité", puisqu'on décrit toute une famille de fonctions que le
compilateur instancie à la demande.

De la même manière qu'on définit des fonctions génériques, aussi appelées fonctions
templates, il est aussi possible de décrire toute une famille de classes à l'aide d'une seule

– 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); instancie la fonction int min (int, int);


min (i, j); instancie la fonction int min (int, int);
min (x,y); instancie la fonction double min (double, double);

le compilateur crée deux instanciations différentes, l'une utilisant des entiers, l'autre des doubles,
à partir de la fonction générique min (). Pour cela, il est nécessaire que chaque paramètre de type
apparaisse au moins une fois dans l'entête de la fonction.

Le type courant à donner aux paramètres formels est déterminé en évaluant les
paramètres passés à la fonction. Le type de retour n'est pas pris en compte.

Par exemple,

double d = min (3,4)

instancie la fonction int min (int, int), mais la valeur renvoyée est transtypée en double avant
l'affectation à d.

Une fonction générique ne peut être compilée à partir de sa seule définition, puisque c'est
l'utilisation de cette fonction qui permet au compilateur d'implémenter le code conforme aux
types utilisés. Mais elle doit être connue du compilateur pour qu'il puisse instancier la fonction
appropriée. C'est pourquoi les définitions de fonctions génériques figurent en général dans des
fichiers de déclaration d'extension .h, de façon à pouvoir les compiler dès leur utilisation.

7.2.3 Redéfinition de fonctions génériques


Il est possible de définir plusieurs fonctions génériques de même identificateur,
possédant des paramètres formels en nombre ou de types différents, qui puissent permettre au
compilateur de distinguer sans ambiguïté les instances de ces fonctions génériques.

Par ailleurs, il peut arriver que l'instanciation d'une fonction générique ne soit pas
appropriée pour un type particulier de données. Il est alors possible de fournir la définition d'une
ou plusieurs fonctions particulières, qui seront utilisées en lieu et place de celles instanciées à
partir de la fonction générique. On parle alors de définition explicite de fonction générique. Par
exemple, l'instanciation de la fonction min (T, T) pour deux paramètres de type char* ne
donnera pas le résultat attendu si le programmeur souhaite que chaque paramètre soit interprété
comme une chaîne de caractères et non comme un pointeur de caractère. Pour résoudre cela, il
suffit de fournir une instance spécialisée de la fonction min :
template <class Type> Type min (Type a, Type b) char *min (char *s1, char *s2)
{ {
return (a<b) ? a : b; return (strcmp (s1,s2)<0?s1:s2);
} }

– 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 excep-
tions peuvent survenir.

Exemple : try
{
func1 ();
func2 ();
}

8.1.3 Catch
Une fois l'exception lancée, celle-ci doit être interceptée par un gestionnaire d'exception
pour pouvoir être traitée de manière appropriée.

Un tel gestionnaire est un bloc d'instructions appelé bloc catch. Ce bloc commence par le
mot réservé catch, suivi par une déclaration (entre parenthèses) d'exceptions. Vient ensuite une
séquence d'instructions entre accolades. La déclaration permet de spécifier les types des objets
que le gestionnaire d'exceptions doit "attraper".

Syntaxe : catch (objet identificateur)


{
...
}

Remarques : Un bloc catch suit immédiatement un bloc try, ou un autre bloc catch.
Plusieurs blocs catch peuvent se succéder les uns aux autres. La sélection
du gestionnaire d'exception est alors réalisée en fonction du type de
l'exception envoyée.
Une adéquation entre l'exception lancée et celle attendue par un bloc
catch est réalisée si :
1. les deux types sont exactement les mêmes,
2. le type du gestionnaire catch est une classe de base publique de l'objet
envoyé,

– 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. la fonction terminate () est appelée explicitement,


2. aucun gestionnaire catch ne peut être trouvé pour gérer une exception lancée,
3. des problèmes de piles sont rencontrés lors de la gestion d'une exception,
4. la fonction unexpected () est appelée.

Par défaut, la fonction terminate () appelle la fonction abort () qui arrête l'exécution du
programme. Mais cet appel à abort () peut être remplacé par un appel à une autre fonction au
moyen de la fonction prédéfinie set_terminate ().

8.3.3 set_unexpected () et set_terminate ()


Il est possible d'utiliser les fonctions set_unexpected () et set_terminate () pour
remplacer respectivement les appels aux fonctions terminate () et abort () des fonctions
unexpected () et terminate (). Les déclarations de ces deux fonctions set_unexpected () et
set_terminate () sont incluses dans les fichiers <unexpected.h> et <terminate.h>. Le type de
retour et le paramètre de ces deux fonctions sont un pointeur sur une fonction sans paramètre et
ne retournant pas de valeur. La fonction passée en paramètre remplace alors les fonctions
appelées dans unexpected () et terminate (). Les deux valeurs retournées par set_unexpected ()
et set_terminate () sont des pointeurs sur les fonctions appelées auparavant par unexpected () et
terminate (). Ainsi, en sauvant les valeurs retournées, il est possible de restaurer les fonctions
d'origine.

– 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 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++

2.5 Structures conditionnelles ________________________________________________ 16


2.5.1 if ... else ...___________________________________________________________________ 16
2.5.2 switch ______________________________________________________________________ 16
2.6 Structures itératives _____________________________________________________ 17
2.6.1 Boucle while _________________________________________________________________ 17
2.6.2 Boucle do ... while ____________________________________________________________ 17
2.6.3 Boucle for ___________________________________________________________________ 18
2.7 Branchements __________________________________________________________ 18
2.7.1 break _______________________________________________________________________ 18
2.7.2 continue_____________________________________________________________________ 19
2.7.3 goto ________________________________________________________________________ 19
2.7.4 return_______________________________________________________________________ 19
2.8 Directives pour le préprocesseur ___________________________________________ 20
2.8.1 #define et #undef______________________________________________________________ 20
2.8.2 #include_____________________________________________________________________ 20
2.8.3 Compilation conditionnelle #if/#ifdef/#ifndef ... #else ... #endif _________________________ 20
2.9 Mots clefs ______________________________________________________________ 21
3. POINTEURS ET FONCTIONS___________________________________________23
3.1 Pointeurs et variables dynamiques _________________________________________ 23
3.1.1 Type pointeur ________________________________________________________________ 23
3.1.2 Pointeur sur void ______________________________________________________________ 23
3.1.3 Opérateur & _________________________________________________________________ 24
3.1.4 Opérateur * __________________________________________________________________ 24
3.1.5 Variables dynamiques, opérateurs new et delete______________________________________ 24
3.1.6 Pointeurs constants ____________________________________________________________ 25
3.1.7 Opérations sur les pointeurs _____________________________________________________ 25
3.2 Tableaux_______________________________________________________________ 25
3.2.1 Déclaration __________________________________________________________________ 26
3.2.2 Relation entre tableaux et pointeurs _______________________________________________ 26
3.2.3 Tableau de pointeurs___________________________________________________________ 27
3.2.4 Chaînes de caractères __________________________________________________________ 27
3.2.5 Initialisation des tableaux _______________________________________________________ 27
3.3 Structures, unions et énumérations_________________________________________ 28
3.3.1 Structures ___________________________________________________________________ 28
3.3.2 Unions______________________________________________________________________ 29
3.3.3 Énumérations ________________________________________________________________ 29
3.4 Définition de type : typedef _______________________________________________ 30
3.5 Fonctions ______________________________________________________________ 30
3.5.1 Déclaration et définition ________________________________________________________ 30
3.5.2 Paramètres d'entrée : transmission par valeur ________________________________________ 31
3.5.3 Paramètres de sortie : transmission par adresse ______________________________________ 31
3.5.4 Paramètres de sortie : transmission par référence _____________________________________ 32
3.5.5 Paramètres multivalués : transmission par référence constante___________________________ 33
3.5.6 Valeurs par défaut _____________________________________________________________ 33
3.5.7 Paramètres de la ligne de commande ______________________________________________ 33
3.6 Mots clefs ______________________________________________________________ 34
4. CONSTRUCTEURS ET DESTRUCTEURS, SURCHARGE ___________________35
4.1 Constructeurs et destructeurs _____________________________________________ 35
4.1.1 Constructeurs ________________________________________________________________ 35
4.1.2 Constructeurs copies ___________________________________________________________ 36
4.1.3 Destructeurs _________________________________________________________________ 36

– Page 86 –
Table des Matières

4.2 Pointeur this ___________________________________________________________ 37


4.3 Surcharge______________________________________________________________ 37
4.3.1 Surcharge de fonctions_________________________________________________________ 38
4.3.2 Surcharge de constructeurs _____________________________________________________ 39
4.3.3 Surcharge d'opérateurs_________________________________________________________ 39
4.3.4 Règles générales de surcharge des opérateurs _______________________________________ 39
4.3.5 Surcharge d'opérateurs unaires __________________________________________________ 40
4.3.6 Surcharge de l'opérateur d'affectation =____________________________________________ 41
4.3.7 Surcharge d'opérateurs binaires __________________________________________________ 42
4.3.8 Surcharge des opérateurs [ ] et ( ) ________________________________________________ 43
4.3.9 Surcharge des opérateurs new et delete ____________________________________________ 44
4.3.10 Correspondance des paramètres _______________________________________________ 44
4.4 Mots clefs ______________________________________________________________ 44
5. HÉRITAGE __________________________________________________________ 45
5.1 Héritage _______________________________________________________________ 45
5.1.1 Syntaxe ____________________________________________________________________ 45
5.1.2 Affectation __________________________________________________________________ 46
5.1.3 Constructeurs et destructeurs ____________________________________________________ 46
5.1.4 Accès aux membres hérités _____________________________________________________ 48
5.2 Héritage multiple _______________________________________________________ 49
5.2.1 Héritage direct _______________________________________________________________ 49
5.2.2 Classes de base identiques ______________________________________________________ 50
5.2.3 Classes de base virtuelles_______________________________________________________ 51
5.2.4 Types de hiérarchies __________________________________________________________ 52
5.3 Polymorphisme _________________________________________________________ 52
5.3.1 Fonctions virtuelles ___________________________________________________________ 52
5.3.2 Fonctions virtuelles pures ______________________________________________________ 53
5.4 Mots clefs ______________________________________________________________ 54
6. FLUX _______________________________________________________________ 55
6.1 Librairie i/o stream______________________________________________________ 55
6.2 Entrées-sorties standards_________________________________________________ 56
6.2.1 États de formats ______________________________________________________________ 56
6.2.2 États d'erreurs _______________________________________________________________ 58
6.3 Manipulation de flux ____________________________________________________ 58
6.3.1 Constructeur [i/o]fstream ( ) ____________________________________________________ 58
6.3.2 Ouverture de flux open ( ) ______________________________________________________ 58
6.3.3 Pointeur de tampon rdbuf ( ) ____________________________________________________ 58
6.3.4 Fermeture du flux close ( )______________________________________________________ 58
6.3.5 Flux en écriture ______________________________________________________________ 58
6.3.6 Flux en lecture _______________________________________________________________ 58
6.4 Fichiers iostream.h et fstream.h ___________________________________________ 58
6.4.1 Déclaration de la classe ios (iostream.h) ___________________________________________ 58
6.4.2 Déclaration de la classe istream (iostream.h)________________________________________ 58
6.4.3 Déclaration de la classe istream_with_assign (iostream.h) _____________________________ 58
6.4.4 Déclaration de la classe ostream (iostream.h) _______________________________________ 58
6.4.5 Déclaration de la classe ostream_with_assign (iostream.h) _____________________________ 58
6.4.6 Déclaration de la classe iostream (iostream.h)_______________________________________ 58
6.4.7 Déclaration de la classe iostream_with_assign (iostream.h) ____________________________ 58
6.4.8 Déclaration des variables globales du fichier iostream.h _______________________________ 58
6.4.9 Déclaration de la classe fstreambase (fstream.h) _____________________________________ 58
6.4.10 Déclaration de la classe ifstream (fstream.h) _____________________________________ 58

– Page 87 –
Cours de C++

6.4.11 Déclaration de la classe ofstream (fstream.h)______________________________________ 58


6.4.12 Déclaration de la classe fstream (fstream.h)_______________________________________ 58
6.5 Mots clefs ______________________________________________________________ 58
7. GÉNÉRICITÉ_________________________________________________________58
7.1 Introduction____________________________________________________________ 58
7.2 Fonctions génériques ____________________________________________________ 58
7.2.1 Déclaration __________________________________________________________________ 58
7.2.2 Instanciation _________________________________________________________________ 58
7.2.3 Redéfinition de fonctions génériques ______________________________________________ 58
7.3 Classes génériques_______________________________________________________ 58
7.3.1 Déclaration __________________________________________________________________ 58
7.3.2 Instanciation _________________________________________________________________ 58
7.3.3 Spécialisation d'une classe générique ______________________________________________ 58
7.3.4 Identité de classes génériques ____________________________________________________ 58
7.3.5 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
8. EXCEPTIONS ________________________________________________________58
8.1 Gestion des exceptions ___________________________________________________ 58
8.1.1 Throw ______________________________________________________________________ 58
8.1.2 Try ________________________________________________________________________ 58
8.1.3 Catch _______________________________________________________________________ 58
8.2 Exemple 1______________________________________________________________ 58
8.3 Fonctions spéciales ______________________________________________________ 58
8.3.1 unexpected ()_________________________________________________________________ 58
8.3.2 terminate () __________________________________________________________________ 58
8.3.3 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 : [Link]@[Link]

– Page 89 –

Vous aimerez peut-être aussi