Vous êtes sur la page 1sur 164

Ecole Nationale des Sciences Appliquées d ’Oujda

Cours C++
M.RAHMOUNE

Historique

 Langage C  Langage C++


 Programmation structurée  Programmation orientée objet
 B. Kernighan & D. Ritchie  B. Stroustrup
 1973  ~1980
ANSI/ISO  ANSI/ISO
 1989  1998
(et 1999)
Bibliographie
[Ellis & Sroustrup 90] Ellis M.A &StroustrupB.,
‘ The annotated C++ reference manual ’ Addision-Wesley 19990
…...

Claude Delannoy Ivor Horton David Chapman


C++ versus C
Le C++ est un langage structuré, fortement typé et modulaire, en plus il autorise
la programmation orientée objet
Principal avantage : compatibilité C/C++
• même syntaxe de base
• code C "propre" directement compilable en C++
• facilité d'intégration de fichiers C++ et C dans un même programme
Principal inconvénient : quelque incompatibilité C/C++
• C++ hérite de certains choix malencontreux du langage C !
Ressemblances
• syntaxe en partie similaire
• fonctionnalités objet de même nature
Différences
• gestion mémoire (pas de garbage collecting, etc.)
• héritage multiple
• redéfinition des opérateurs
• Templates et STL
• langage compilé (et ... plus rapide)
Plan sommaire du cours C++

C++ = C!!
C!! + POO

• Langage de base du C++ (analogue au C)


• Programmation orientée objet (POO) du C++

C++ est un langage compilé … normalisation tardive (97/98...)


Remarque : contrairement à Java (purement orienté objet)
Protocole de communication entre le programmeur et le processeur.
processeur.

Code C/C++

Librairies
Code préprocesseurs

Fichier(s)) Objet
Fichier(s
Programme
La gestion de la mémoire
La mémoire d’un ordinateur
• La mémoire comporte trois zone d allocation
• Chaque zone sert a mémoriser trois types de variables

exécutable
Cache Registres
Code
(cmos) Unité
RAM Logique
(silicium)
Zone statique
Dynamique
(Stack/pile)
Mémoire

Mémoire
virtuelle Zone d ’allocation
(disque) mémoire
(Heap, tas)
Mémoire
Statique
Les Fichiers Hello C++ Word

// Helloword.Cpp : " salut le monde " en C++


//
#include <iostream> //on utilise la gestion des flots

using namespace std; //le nom d'espace contenant cout et cin

/* Ce commentaire multi-ligne précède


La fonction 'main' , qui est toujours
Le programme principale
*/

int main( )
{

cout<<"hello C++ world!\n";

Return 0;

}
Espaces de noms

Le C++ définit les espaces de noms :


namespace MaBiblio
MaBiblio{{
fonc1() ;
fonc2() ;
...}

• Les variables définies dans un espace de noms sont des variables globales

• Un espace de nom à un comportement similaire à un répertoire pour les systèmes


d’exploitations
Espaces de noms

Appel des fonctions des namespace

Ce qui est défini dans un espace de noms n’existe pas en dehors. Pour
l’utiliser il faut utiliser le chemin d’accès :
int main(){
fon1() // erreur fonc1() n’existe pas
MaBiblio::fonc2() // ok
...}
Pour éviter d’alourdir le code, il est possible de préciser que les membres
d’un espace de noms seront utilisés directement grâce
aux mot clef using
using. Il y a deux méthodes :

Accéder à tout l’espace de noms Préciser les fonctions à utiliser

using namespace MaBiblio using Mabiblio::fonc1( )


fonc1() // ok fonc1() // ok
fonc2() // ok fonc2() // fonc2 n’existe pas en dehors
// de l’espace de nom
Espaces de noms

La plupart des fonctions et classes de la librairie standard du C++ sont


déclarées dans un espace de nom (namespace)

L’espace de nom std contient la grande majorité des fonctions de la librairie


standard

• Habituellement, il est nécessaire d’utiliser « using namespace std » au début de


chaque programme C++ qui utilise cette librairie

• Certains compilateurs permettent de ne pas le préciser, grâce à des options de


compilation ( CC -LANG std .... )
Quelques Incompatibilités entre le C et le C++
Le C (90/99) et le C++ sont deux langages proches au niveau de leur
syntaxe et de leur grammaire (sans les POO du C++), mais il existe
comme même certain nombre d'incompatibilités qui fait qu'un programme
écrit en C peut ne pas compiler avec un compilateur C++.

Les fonctions
Les fonctions en C peuvent être définies suivant deux modèles :

En C Le C++ n’accepte pas la première méthode


int CalculeSomme ( a, b ) int CalculeSomme ( int a, int b )
{ {
int a; ... /* Fonction */
int b; }
... /* Fonction */
}
Quelques Incompatibilités entre le C et le C++
const
Le C++ a quelque peu modifié l'utilisation "Const" de ce
qualificatif. Pour rappel, "const" est utilisé pour définir une variable
constante. C'est une bonne alternative à un define.
La portée en C++ est désormais plus locale. En C, un const
permettait pour une variable globale d'être "visible" partout. C++ limite quant
à lui la portée d'une telle variable, au fichier source contenant la déclaration.
En C, une variable déclarée constante est, par défaut, externe
(extern) alors qu'en C++, elle est interne (static).
Quelques Incompatibilités entre le C et le C++

Les pointeurs de type void


En C ANSI, un "void*" est compatible avec En C++, seule la conversion int*->void* est
tout autre type de pointeurs, et inversement. implicite. L'autre reste possible, mais nécessite
un "cast" :
En C En C++
void *p_void; void *p_void;
int *p_int; int *p_int;
... ...
p_void = p_int; p_void = p_int;
p_int = p_void; (int*) p_void;
p_int = p_void;
Le pointeur void est générique Erreur de compilation
La conversion implicite Invalid conversion from
en C est possible `void*' to `int*'
void<=>autre type
Erreure avant la conversion
Quelques Incompatibilités entre le C et le C++
L'instruction goto
L'instruction goto permet de sauter à une étiquette donnée. Pour exemple,
voici un code qui compile parfaitement en C :

En C En C++ En C++
int i = 2; int i = 2; int i = 2;
goto start; i goto start; i goto start; i
int v = 5; int v = 5; if (i == 2)
printf ("%d\n", v); printf ("%d\n", v);
start: start:
{
printf ("%d\n", i); printf ("%d\n", i); int v = 5;
printf ("%d\n", v);
Erreur de compilation
}
Crosses initialization of start:
`int v' printf ("%d\n", i);

Car en C++, l'instruction goto ne peut pas être utilisée pour sauter une déclaration comportant
une initialisation sauf si le bloc qui contient cette déclaration est entièrement sauté :
Quelques Incompatibilités entre le C et le C++

Le type d'un caractère


En C++
En C void printchar (int c)
void printchar (int c) {
{ printf ("%c\n", c);
printf ("%c\n", c); }
} void printchar (char c)
void printchar (char c) {
{ printf ("%c\n", c);
printf ("%c\n", c); }
} printchar (a);
Appellera la seconde fonction
en C++.
Les types utilisés pour représenter les caractères ne sont pas les mêmes en C et en
C++. Pour le premier, il s'agit d'un entier (int) alors que pour le second ce sont
des char. Cela a son importance au niveau de la surcharge des fonctions
(mécanisme spécifique au C++) puisque dans cet exemple :
Quelques Incompatibilités entre le C et le C++

Initialisation de tableaux de caractères


En C, il est possible d'initialiser un tableau de caractères avec une chaîne de
caractères de même longueur (sans compter le caractère de fin de chaîne \0) :

En C En C++
char tab[10] ="Developpez"; char tab[10] ="Developpez";
dans ce cas, on obtient un tableau de Initializer-string for array of
caractères et non une chaîne de chars is too long
! caractères puisque le caractère \0
n'est pas présent
Quelques compatibilités entre le C et le C++

Type de retour d'une fonction


En C et C++, si vous omettez le type de retour d'une fonction lors de sa déclaration, le
compilateur considère que la fonction retourne un int, mais se n’est pas la règle
générale pour tous les types de compilateur
compilateur..

En C En C++
exemple (float a, ...) exemple (float a, ...)
{ {
float a;
float a;
.. .
.. .
return a;
} return a;
}
Un compilateur C bien paramètré
i vous signalera cet oubli par un
warning.
Quelques Incompatibilités entre le C et le C++

Les booléens

Le type booléen est présent en C99 et en C++ cependant en C, si vous n'incluez


pas le fichier stdbool
stdbool..h il est tout à fait possible de redéfinir les termes bool
bool,, false
et true
true.. Par contre en C++ c’est déjà défini : :

En C En C++
<stdbool.h> bool a;
typedef short bool;
#define false ('\0')
#define true ( !false)
bool flag = false;
Quelques Incompatibilités entre le C et le C++
Liste de paramètres vide
void foo() {}
int main (void)
{
foo ();
foo (0);
foo (1, 2);
return (0);
}
En C En C++
Si, lors de la déclaration d'une Par contre en C++ une liste de
fonction, la liste des paramètres n'est paramètres vide est considérée
pas mentionnée, un compilateur C n‘ comme valant void
effectura pas de vérification. Ainsi le
code suivant est correct :
Quelques Incompatibilités entre le C et le C++
Structure et typedef portant le même nom
typedef int var;
struct var { ... }; /* enum ou union revient au même */
int main (void)
{
var var1;
struct var var2;
...
return (0);
}

En C En C++
En C, la présence du mot struct, il y a redéfinition
enum ou union permet de
différencier un typedef des types
plus complexes (structure,
énumération ou union) qui portent le
même nom
Quelques Incompatibilités entre le C et le C++

Structure et typedef portant le même nom

enum etat { TRAVAIL, MANGE, DORT };


int v1 = TRAVAIL;
enum etat v2 = 1;

En C En C++
une constante énumérée est en fait la conversion implicite entre
un entier signé (signed int) pas de une énumération et un entier
problème donc n'est pas possible, il faut utiliser
l'opérateur de cast :
Exemple du cast
enum etat v2 = (int)1;
Quelques Incompatibilités entre le C 90 et le C++

La déclaration implicite de fonction

En C, si une fonction n'est pas explicitement déclarée avant sa


première utilisation, le compilateur considère qu'il s'agit d'une
fonction externe (extern) retournant un entier (int) alors que la
déclaration est obligatoire en C99 comme en C++.

Déclaration implicite du type d'une variable


En C, il est possible d'omettre le type de la variable lors de son
initialisation. Par défaut la variable est considérée comme étant un int.
Un compilateur C++ vous répondra poliment

ISO C++ forbids declaration of `i' with no type


Quelques Incompatibilités entre le C99 et le C++
Initialisation des champs d'une structure
En C99 En C++
struct client struct client
{ char nom[21]; { char nom[21];
int cp; int cp;
char pays[21]; char pays[21];
}; ... }; ...
struct client client1 = struct client client1 =
{ Non autorisé {
.nom = "Paul", en C++ "Paul",
.cp = 75, 75,
.pays = "France" "France"
}; };
En C++
Expected primary-expression before '.' token
Quelques Incompatibilités entre le C99 et le C++
Passage de tableau comme paramètre d'une fonction

En C, il est possible de déclarer un


pointeur constant sur un int

En C
int *const str;

En C99 En C++
void foo (int str[const ]); void foo (int str [ const ]);
uniquement dans une liste de Expected primary-expression before
paramètres d'une fonction "const" Expected `]' before "const"
Expected `,' or `...' before "const"
Les Références

Nous savons qu’il y a deux façon de transmettre une variable en C à une fonction par
valeur ou par adresse (pointeur). :

int FaitQqch( int a, int b) int FaitQqch2( int * a, int * b)


{ {
int nRet; int nRet;
If ( a==0 ) If ( *a==0 )
a=10; *a=10;
nRet = a + b; nRet = *a + *b;
return nRet; return nRet;
} }

On va introduire une nouvelle façon de transfert de variable fourni par les normes
C++, elle n’existe pas dans le langage C
Les Références

• Déclarer une référence j sur une variable i permet de créer un nouveau nom j qui
devient synonyme de i.

• La déclaration d’une référence se fait en précisant le type de l’objet référencé, puis


le symbole &, et le nom de la variable référencé qu’on crée.

int i=10; // i est un entier valant 10


int & j = i; // j est une référence sur un entier,
// cet entier est i. A partir d’ici j est synonyme
de i, ainsi
j=j+1; // est équivalent à i=i+1 ! j=j+1;

le = dans la déclaration de la référence n’est pas réellement une affectation


! puisqu’on ne copie pas la valeur de i. En fait, on affirme plutôt le lien
entre i et j. En conséquence,…

int & k = 44; // est donc parfaitement illégal…


L’Opérateur new
• L’opérateur new permet l’allocation de l’espace mémoire nécessaire pour stocker un
élément d’un type T donné.
• Pour que l’opérateur new puisse connaître la taille de l’espace à allouer il faut donc lui
indiquer le type T
• Si l’allocation réussit ( il y a suffisamment d’espace en mémoire) alors l’opérateur new
retourne l’adresse de l’espace alloué.
• Cette adresse doit être alors affectée à un pointeur de type T pour pouvoir utiliser cet
espace par la suite
• Si l’allocation échoue (il n’y a pas suffisamment d’espace mémoire) alors l’opérateur
new retourne NULL (0)
T *p;
P = new T ou T *p = new T
• Il est possible d’indiquer après le type T une valeur pour initialiser l’espace ainsi alloué.
Cette valeur doit être indiquer entre parenthèses : new T (val)

• Allocation d’un entier


//Sans initialisation //Avec initilisation
int *p = new int; int *p = new int (10);
*p =5; cout <<*p; // la valeur affichée 10
cout <<*p; // la valeur affichée 5
• On n’a pas vérifié ici si l’allocation a réussi. Dans la pratique, il faudra le faire
systématiquement
L’Opérateur new
• Pour allouer dynamiquement un tableau d’éléments il suffit d’utiliser l’opérateur new []
en indiquant entre les crochets le nombre d’éléments
• Contrairement aux tableaux statiques où le nombre d’éléments devait être une
constante ( valeur connue à la compilation) ici le nombre d’éléments peut être variable
dont la valeur ne sera connue qu’a l’exécution (la valeur saisie par l’utilisateur, lue à
partir d’un fichier)
• On récupère alors du premier élément du tableau
T *p = new T[n]; // p contiendra ladresse de T[0]
• Il n’est pas possible ici d’indiquer les valeurs d’initialisation
L’Opérateur delete et delete []
• Pour que l’allocation dynamique soit utile et efficace, il est impératif de libérer l’espace
réservé avec new dès que le programme n’en a plus besoin.
• Cet espace pourrait alors être réutilisé soit par le programme lui-même soit par d’autre
programmes
• Libération d’un élément simple : opérateur delete
int *p = new int;
*p =5;
cout<< *p<<endl;
delete p;

• Libération d’un tableau : opérateur delete []


int *p ,i,n;
cout<<“Nombre delements ?: “;
cin>> n;
p= new int [n];
for (i=0;i<n;i++) cin >>p[i];
for (i=0;i<n;i++) cout<< p[i]*p[i]<<endl;
delete [] p;
Gestion des erreurs d’allocation
• On peut gérer les erreurs d’allocation dynamique de la mémoire de trois façons
différentes

 Test de la valeur retournée par new après chaque allocation


Utilisation de set_new_handler
 Utilisation de l’exception standard bad_alloc

 Test de la valeur retournée par new après chaque allocation


const int CentMegas= 100*1024*1024; 100 Megas octets alloués
char *p; 200 Megas octets alloués
int i=0;
while (1) 300 Megas octets alloués
{ 400 Megas octets alloués
p = new char [CentMegas]; 500 Megas octets alloués
if (p) cout<< ++i<< "00 Megas octets alloués"<<endl; …..
else {cerr<<"Plus de memoires"<<endl; exit (1);}
} 1500 Megas octets alloués
Plus de memoires
 Lourde car nécessite un test après chaque new
Gestion des erreurs d’allocation
 Test en utilisant set_new_handler

 On indique une fonction (dans l’exemple erreurMemoire) qui sera appelée


automatiquement si new échoue
 Inclure la librairie <new>
void erreurMemoire() {
cerr<<"Plus de memoires"<<endl; 100 Megas octets alloués
exit (1); 200 Megas octets alloués
} 300 Megas octets alloués
int main () { 400 Megas octets alloués
const int CentMegas =100*1042*1042;
char *p;int i=0; 500 Megas octets alloués
set_new_handler (&erreurMemoire); …..
while (1){ 1500 Megas octets alloués
p = new char [CentMegas]; Plus de memoires
cout<< ++i<< "00 Megas octets alloués"<<endl;
}
return 0;
}
Gestion des erreurs d’allocation
 Test en utilisant l’exception standard bad_alloc

 L’exception est levée automatiquement si new échoue


 Inclure la librairie <stdexcept>

const int CentMegas= 100*1024*1024;


char *p; int i=0; 100 Megas octets alloués
try
{ 200 Megas octets alloués
while (1) 300 Megas octets alloués
{ 400 Megas octets alloués
p = new char [CentMegas]; 500 Megas octets alloués
cout<< ++i<< "00 Megas octets alloués"<<endl;
} …..
} 1500 Megas octets alloués
catch (bad_alloc & e) Plus de memoires
{ St9bad_alloc
cerr<<"Plus de memoires"<<endl; Aborted (core dumped)
cerr<<e.what()<<endl;
terminate();
}
Les Caractéristique d ’un bon logiciel

Exactitude ( précision / qualité):


qualité):
• aptitude d’un programme à fournir un résultat donné dans des conditions
spécifiées..
spécifiées
La robustesse :
• Doit bien réagir si on s’éloigne des conditions normales d’utilisation.
d’utilisation.
L’extensibilité :
• Doit être modifiable pour satisfaire l évolutions des spécifications
spécifications..
La réutilisabilité :
• Utiliser des parties d’un programme pour traiter d’autres problèmes.
problèmes.
L’efficacité :
• Le code doit être suffisamment rapide.
rapide.
La portabilité :
• Doit être utilisé sur d ’autres processeur et/ou système d’exploitation
Pourquoi la POO
Structural ou procédural
• Si la programmation dite procédurale, structurale ou encore modulaire est
constituée de modules, procédures et fonctions sans liens particuliers agissant sur
des données dissociées pouvant mener rapidement à des difficultés en cas de
modification de la structure des données.

POO
• La programmation orientée objet, tourne autour d'une unique entité : l'objet,
basé sur les paradigmes suivants :
• Encapsulation  protection
• Héritage  Réutilisation
• Polymorphisme  générique
Les structures
Extension du concept struct, hérité du langage C, au C++.
Déclaration d’une structure Grouper des variables de différents types
struct Nom_Structure{ Champs struct Personne
ou {
type_champ1 Nom_Champ1; données membres int Age;
type_champ2 Nom_Champ2;
char Sexe;
type_champ3 Nom_Champ3; };
} [var1, var2, ..., varM ];
Il présente néanmoins quelques fonctionnalités supplémentaires offertes en C++
Définition d’une variable structurée

struct Nom_Structure Nom_Variable_Structuree, ...; struct Personne Pierre, Paul;


Accès aux champs d’une variable structurée
.
Nom_Variable Nom_Champ; Pierre.Age = 18;
personne *pierre;
Pierre -> Age=18
Nom_pointeur Nom_Champ; Pierre.Sexe = 'M';
Pierre -> sexe=‘M’
Tableau de structures
struct Nom_Structure Nom_Tableau [Nb_Elements]; Détaillé en POO
Les structures
Déclaration d’une structure...
Il est toutefois possible de déclarer une structure sans en donner sa définition
struct exemple;
La définition doit se trouver plus loin dans le programme. Cela est très
semblable aux prototypes de fonctions.

On peut aussi définir des structures croisées :


struct exemple1;
struct exemple2 {
exemple1* pex1;
//...
};
struct exemple1 {
exemple2* pex2;
// ...
};
Les Structures et Classes
• Tout ce qui est expliqué sur les structures dans ce qui suit est valable aussi pour
les classes
• On sait qu’une structure permet de regrouper des variables différentes, pour
qu’il soit gérer comme une seule entité parfaitement identifiable exemple:
struct fiche {
char *nom, *prenom;
int age; // etc ...
};
Un tel type peut être manipulé comme n’importe quel type. On peut définir
des pointeurs, ou des tableaux sur ce type
fiche employes[];
Pour accès aux données voir structure
Données membres (Types des champs)
Les données membres (les champs) d’une structure peuvent être de type
structures. Précisons toutefois que les structures récursives sont interdites :
struct recursive {
recursive interne; // NON, interdit };
cette structure aurait virtuellement une taille infinie  Error : Size of 'interne' is
unknown or zero
Les Structures et Classes

Types des champs...


Cependant, on peut employer des références ou (plus fréquemment) des pointeurs sur le
type structure courant : struct fiche {
fiche *suivante;
On peut ainsi créer une
char *nom, *prenom;
liste chaînée
// ... };
Exemple d’une liste chaînée
typedef long element; // par exemple)
struct noeud {
noeud *suivt; // le suivant dans la liste
element elm; // information contenue
};

Cette liste chaînée est cependant fragile. Si par erreur on modifie l’un des pointeurs, la
liste sera coupée, les informations perdues et le programme complètement égaré .
Les Structures et Classes
Données membres...
Pour limiter les risques le langage C++ nous offre des possibilité de protection, on utilisant
les mots clés private
struct noeud {
private :
noeud *suivt; // le suivant dans la liste
element elm; // information contenue
};
Nos champs sont protégés en écriture noeud no;
no.suivt = NULL; Impossible
Comment modifier le contenu des champs (les données membres) ?
Fonctions membres...
struct noeud {
private :
noeud *suivt; // le suivant dans la liste
element elm; // information contenue
public :
noeud *suivant(void) { return suivt; }
element &contenu(void) { return elm; }
noeud* supprime_svt (void);
noeud* insere (element e);
};
C++ permet de définir en même temps la structure et les fonctions
qui agissent sur elle.
Les Structures et Classes
Fonctions membres...
Contrairement aux deux première fonctions membresmembres, les deux dernière n’ont
pas été écrites en ligne, car elles sont un peu plus complexes (mais on aurait tout
de même pu le faire, elles ne contiennent pas de boucle).
Il faut bien définir les fonctions insere et suprime.
noeud* noeud::insere(element e) noeud* noeud::supprime_svt(void);
// insère un nouvel élément de valeur e derrière // supprime le noeud suivant et
this.
// renvoie un pointeur sur le nouveau suivant
// renvoie 0 si plus de mémoire, sinon suivt.
{ {
noeud *nouveau = new noeud; if (!suivt) return 0; // pas de suivant
if (!nouveau) return 0; noeud *s = suivt;
// plus de mémoire
nouveau->suivt = suivt; suivt = s->suivt;
nouveau->elm = e;
delete s;
return suivt = nouveau;
} return suivt; }

Toutefois, de telles fonctions sont très désordonnées si elles sont dispersées à travers
le programme. Le C++ ajoute au C un moyen bien plus simple de traiter ce
problème : les fonctions membres ou méthodes.
Les Structures et Classes
Encapsulation des données (Protections des données)

• Premier paradigmes du langage orienté objet

• Minimise les erreurs d’affectations des valeurs aux données membes

• Mais l’un des meilleurs mécanismes, est en effet de déclarer certains données
comme privés, en utilisant le mot clé private. Ces données et ces méthodes privés ne
peuvent plus être utilisés par des fonctions extérieures à la structure ; seules les
fonctions membres méthodes peuvent les lire ou les modifier.

• Au contraire, les méthodes et (plus rarement) les données librement modifiables


seront déclarés publics à l’aide du mot clé public. Par défaut, les membres d’une
structure sont publics, c’est pourquoi nous avons pu écrire les exemples précédents
de manière correcte.
Les Structures et Classes
Arguments de fonctions membres...
struct fiche {
private:
int age;
char *nom, *prenom;
void ecrit_np(char *nouv_nom , char *nouv_prenom);
};
• Le compilateur distingue la fonction membre d’une donnée usuelle à cause des
parenthèses. Observons un point important : la structure elle-même n’a pas été
passée en paramètre. En effet, une fonction membre reçoit toujours l’objet par lequel
elle est appelée, sous la forme d’un paramètre implicite de type pointeur, nommé
this.
• L’implantation de la fonction membre sera donnée plus loin dans le programme,
à tout endroit jugé adéquat :
void fiche::ecrit_np (char *nouv_nom, char *nouv_pre) {
this->nom = nouv_nom;
this->prenom= nouv_pre;
}
Les Structures et Classes
Arguments de fonctions membres...
void fiche::ecrit_np (char *nouv_nom, char *nouv_pre) {
this->nom = nouv_nom;
this->prenom= nouv_pre;
}

• En outre le compilateur sait ainsi immédiatement qu’il doit passer un paramètre


implicite fiche *this dans la fonction. C’est pourquoi le nom de la structure est
obligatoire : il ne doit jamais être omis, même s’il n’y a qu’une fonction portant ce
nom dans tout le programme.
• Il est permis de l’abréger ainsi de ôter les this :
void fiche::ecrit_np (char *nouv_nom, char *nouv_pre) {
nom = nouv_nom;
prenom= nouv_pre;
}
• En effet, toutes les fonctions membres « connaissent » automatiquement le nom
de tous les membres (fonctions et données) de la structure. De ce fait, on utilise
assez peu le paramètre this explicitement, sauf lorsqu’on souhaite connaître
l’adresse de la structure (c’est pourquoi this est un pointeur, et non une référence).
Les Structures et Classes
Fonction membre en ligne
• Comme Toute fonctions, les fonctions membres peuvent, être déclarées en ligne par le mot
clé inline : struct fiche {
char *nom, *prenom;
int age;
void ecrit_np(char *nouv_nom, char *nouv_pre)
}
inline void fiche::ecrit_np(char *nouv_nom, char *nouv_pre) {
nom = nouv_nom;
prenom = nouv_pre;
};
struct fiche {
char *nom, *prenom;
int age;
void ecrit_np(char *nouv_nom, char *nouv_pre)
{ nom = nouv_nom; prenom = nouv_pre; }
};
• Dans ce cas, la fonction membre est automatiquement en ligne, bien qu’on n’ait pas écrit le
mot inline
• Certains compilateurs ne placent pas en ligne les fonctions contenant une boucle ou une
instruction de branchement multiple switch. Ils le signalent par un message Warning, mais
l’écriture n’est pas fautive : il suffit d’ignorer le message.
Les Structures et Classes
Ordre de déclaration
L’ordre de déclaration des champs et des méthodes dans une structure est
indifférent, car le compilateur lit la structure en bloc, avant même d’interpréter
les méthodes en ligne. En conséquence, l’écriture suivante est parfaitement
possible :
struct bizarre {
int i;
int methode1(void) { if (j) return methode2();
else return 0 }
int methode2(void) { if (i) return i;
else return j; } int j;
};

Cependant, on écrit en général d’abord les champs, puis les méthodes pour des
raisons de clarté.
Les méthodes peuvent être récursives, en accord avec le principe énoncé
précédemment
Les Structures et Classes
Différences entre données et fonctions membres
Il est important de comprendre la différence qui sépare les données membres et
les fonctions membres (méthodes) d’une structure. Elle est assez proche de la
différence entre la structure elle-même (en tant que type, c’est-à-dire d’objet
théorique) et les variables de ce type structure, que l’on appelle instances de la
structure, qui sont au contraire des objets pratiques ayant une adresse et un
volume mémoire déterminé. Les champs ont des valeurs différentes d’une
instance à l’autre. fiche employe1, employe2;
Les champs nom différents. employe1.nom = "Dupont";
employe2.nom = "Durand
Les méthodes sont identiques d’une instance à l’autre. Ainsi, si l’on écrit :
employe1.ecrit_np("Dupont", "Jean"); employe2.ecrit_np("Durand", "Paul");
ce sera la même fonction fiche::ecrit_np qui sera appelée dans les deux cas ; seules
différeront les valeurs des arguments, non seulement les arguments effectifs, mais
aussi l’argument caché this, qui vaudra &employe1 dans le premier cas, et
&employe2 dans le second.
Il n’est pas possible de faire en sorte que certaines instances d’une structure donnée
utilisent certaines méthodes, et d’autres différentes. Il existe un moyen simple,
l’héritage, de créer une nouvelle structure identique à la première, sauf pour certaines
méthodes qui différeront éventuellement (voir chapitre héritage).
Les Structures et Classes
Pointeurs sur membres...
Il est parfaitement possible de prendre l’adresse d’un membre de structure (ou de
classe), comme ceci :
employe *pempl;
Les parenthèses sont imposées
int *empl_age = &(pempl->age);
Un problème se pose toutefois avec les fonctions membres non statiques. Celles-
ci admettent un paramètre implicite this . L’écriture suivante :
struct exemple { // ...
void methode(int);
};
void (*pf)(int) = exemple::methode; // incorrect

est interdite car le type de la méthode n’est pas void (*)(int). Le langage
fournit donc des opérateurs spéciaux pour indiquer des pointeurs sur des
membres d’une classe ; en l’occurrence, il faut ici utiliser ::* pour avoir le
type correct de fonction :
void (exemple::*pf)(int) = exemple::methode; // ok
Les Structures et Classes
Pointeurs sur membres...
Il est obligatoire de préciser exemple:: devant le nom de la méthode, car on verra
plus tard que d’autres classes «appelées classes dérivées », pourraient être utilisées
dans certains cas. Pour appeler ces méthodes, il faut toujours une instance de
exemple. On utilise les opérateurs .* et ->* :
exemple ex, *pex;
(ex.*pf)(6); // équivaut à ex.methode(6);
(pex->*pf)(9); // équivaut à pex -> methode(9);
Noter que les parenthèses sont obligatoires car les opérateurs .* et ->* ont une
précédence plus faible que les parenthèses d’appel de fonction. De plus, on ne peut
pas ici omettre le déréférencement de pf comme on le ferait pour des pointeurs sur
des fonctions normales.
On peut prendre des pointeurs sur des fonctions en ligne, mais celles-ci ne le sont
plus forcément dans ce cas.
Les Structures et Classes
Membres statiques
Nous avons vu que les méthodes d’une structure sont identiques pour toutes les
instances, contrairement aux données membres. Cependant, il peut arriver que l’on
souhaite qu’une donnée soit partagée par toutes les instances de la classe. Nous en
verrons des exemples dans la suite.
Pour cela, il suffit de déclarer le champ comme statique, avec le mot clé static. Par
exemple, chaque employé a un supérieur direct, mais il n’y a qu’un seul patron
struct fiche {
fiche *superieur; // supérieur direct
static fiche *patron; // le PDG
char *nom, *prenom;
int age;
int estpatron (void) { return (this = = patron); }
int estdirecteur (void) { return (superieur = = patron); }
};
Nous avons ajouté deux méthodes qui indiquent si l’employé en question est le
patron, ou si c’est un directeur (employé dont le supérieur direct est le patron).
Les membres statiques permettent d’économiser des variables globales. Ils sont
initialisés à zéro comme toute variable statique ; il n’est pas possible de leur donner
une autre valeur initiale (mais les constructeurs suppléent à ce défaut, comme on le
verra plus loin
Les Classes
• La structure est composé de parties privées et publiques. Les premiers membres qui
apparaissent sont publics, jusqu’à la rencontre du mot private. Les suivants sont alors
privés, jusqu’à rencontrer public, etc.
• En pratique, les champs sont généralement privés, et les méthodes publiques. De ce
fait, le mot private viendra logiquement dès le début de la déclaration de la structure,
comme dans notre exemple.
• Pour raccourcir les écritures, on utilise le mot class à la place du mot struct. Une
classe est en tout point identique à une structure, sauf que ses membres sont privés par
défaut, et non publics. class noeud {
noeud *suivt; // le suivant dans la liste
Les deux champs element elm; // information contenue
public :
sont ici privés noeud *suivant(void) { return suivt; }
element &contenu(void) { return elm; }
noeud* supprime_svt (void);
noeud* insere (element e);
};
• En C++ ; Les structures sont surtout fournies pour des raisons de compatibilité avec
le langage C. Dans la suite, nous utiliserons presque toujours des classes.
• La classe doit contenir au moins une fois le mot public ou protected, sauf si elle
est vide ; dans le cas contraire on ne pourrait rien faire de ses membres.
Les Classes
En général les étapes sont les suivantes : #ifndef _nondeclasse_H
#define _nomdeclasse_H
Interface (fichier *.h) class nomdelaclasse
{
• Définition de la classe que l’on stocke private :
dans un fichier d’en-tête Attributs membres
nomdeclasse.h. public :
Fonctions membres
};
Implémentation (fichier *.cpp) #endif
#include <iostream>
• Définition des fonctions membres #include ‘‘booleen.h’’
dans un fichier nomdelaclasse.cc (ou using namespace std;
.ccp) void nomdeclasse::Fonctionsmemnres(arg)
{
structure;
Instanciation (*.cpp)
}
• Le programme principal et les #include ‘‘booleen.h’’
fonctions globales int main()
{ // Instanciation et utilisation de la classe }
Les objets

• Un objet initialisé à partir de la description figée d’une classe


Objets de la classe = instances de la classe
• Un objet est un état de sa classe.
• Le Mécanisme de création d’une instance = instanciation
• Un objet peut être automatique, statique ou dynamique.
 Création d’un objet statique en C++
nom de la classe nom de l’objet(arguments);
 Création d’un objet dynamique en C++
pointeur vers classe = new nom de la classe(argument)
1- Bouteilledeau* Mabouteille;
2- Mabouteille = new Bouteilledeau( "sidiali", "froide",25.0,5.5,30);
3- delete Mabouteille
Les objets

• On peut manipuler des tableaux d’objets


• Un objet peut être alloué dynamiquement :
#include ‘‘nomdeclasse.h’’
int main()
{
nomdeclasse *ptr_bool = new nomdeclasse;
ptr_bool->fonctionsmembres(arg);
delete ptr_bool;
}

Nous aboutissons finalement au constat suivant : la création d'un nouvel objet est
constituée de deux phases :

• Une phase du ressort de la classe : allouer de la mémoire pour le nouvel objet et


lui fournir un contexte d'exécution minimaliste
• Une phase du ressort de l'objet : initialiser ses attributs d'instance
conséquence d’encapsulation

On vient d’introduire un des trois grands principes du paradigme objet :


l'encapsulation. Ce principe, digne héritier des principes d'abstraction de données et
d'abstraction procédurale.
• L’accès aux données des objets est réglementé
• Données privées  accès uniquement par les fonctions membres
• Données publiques  accès direct par l’instance de l’objet
• Les données sont encapsulées dans la classe et accessible par
l’intermédiaire des fonctions membres.
• Les fonctions ne sont plus globales
• Conséquence
• Un objet n’est vu que par ses spécifications
• Une modification interne est sans effet pour le fonctionnement général du
programme
• Meilleure réutilisation de l’objet
Les Classes
Comment modifier un objet si tous les membres sont masqués ?
• Par l’intermédiaire de méthodes ;
• Les méthodes qui permettent l’échange avec l’extérieur ne sont pas masquées,
elles sont public ;
• Les membres d’une classe peuvent être :  private  public  protected
• Les membres privés d’une classe ne sont
accessibles que par les méthodes de la
classe (exception méthodes amies) ;
• Les membres publiques sont accessibles
Structures aussi bien dans la classe qu’en dehors ;
de Données • Les membres protégés sont utilisés dans
Attributs le cadre de l’héritage.
Les méthodes peuvent être classées en 3
Fonctions propres catégories :
À la classes • Méthodes de gestion de la classe
Méthodes (constructeurs - detructeur ...) ;
• Méthodes d’accès à la classe (affichage,
consultation de la valeur d’un attribut ...) ;
• Méthodes modifiant l’état de l’objet
(modifient les attributs).
Accès aux données membres
Encapsulation des données (Rappel de l’exemple des noeud)
struct noeud {
private :
noeud *suivt; // le suivant dans la liste
element elm; // information contenue
public :
noeud *suivant(void) { return suivt; }
element &contenu(void) { return elm; }
noeud* supprime_svt (void);
noeud* insere (element e);
};
Quel fondement essentiel manque dans notre liste chaînée, qui la rend inutilisable ?
Pour insérer un nouvel élément dans une liste, il faut déjà disposer d’un noeud. Mais
on ne peut pas initialiser un noeud correctement, puisqu’il n’y a pas d’accès aux
champs. Une solution consisterait à définir une méthode élémentaire qui crée un
nouveau noeud, point de départ d’une liste chaînée :
noeud* noeud::cree ( element e) { noeud *nouveau = new noeud;
if (!nouveau) return 0; nouveau->suivt = 0; nouveau->elm = e; return nouveau; }
Cette méthode peut être appelée par un noeud non initialisé puisqu’elle n’utilise pas
les champs de this. On aurait pu aussi déclarer cette méthode statique. Les
constructeurs sont toutefois une solution plus simple (voir ultérieurement dans le
chapitre).
Les Constructeurs
Constructeurs et destructeurs
• Lorsqu’on déclare une variable, le compilateur lui alloue automatiquement une
place en mémoire. Éventuellement, il lui donne une valeur initiale si celle-ci est
précisée : int i = 1;
• Dans le cas d’une structure sans partie privée, il est possible d’initialiser aussi une
instance en donnant la liste des valeurs des champs, dans l’ordre de leur
déclaration : struct exemple {
int i;
Les champs non précisés sont char c;
mis à zéro dans ce cas }
ex = { 1, 'A' }, ex2 = { 2 };
exemple ex3 = { 5, 'a' };
• Cette possibilité d’initialiser des données au moment de leur déclaration est
intéressante. Cependant, elle possède trois défauts importants. D’abord, dans le cas
de classes ou de structures ayant des parties privées, on ne peut pas l’employer,
puisque ces parties sont inaccessibles ; même si elles l’étaient, rien ne prouve que le
programmeur qui utilise une classe sait l’initialiser correctement.
D’autre part, les initialisations complexes, nécessitant par exemple des appels de
fonctions successifs, ne sont pas possibles directement. Enfin, les variables
automatiques n’étant pas initialisées par le compilateur, le programmeur risque
d’oublier de le faire, et il en résultera des erreurs graves.
Les Constructeurs
Constructeurs et destructeurs
•De ce fait, le langage fournit à l’intention des classes et structures un système un peu
plus complexe, mais bien plus sophistiqué, les constructeurs et les destructeurs. Ce
système ne répond non seulement au premier défaut mais également aux deux autres,
comme nous allons le voir.
• Chaque classe (ou structure) peut avoir un ou plusieurs constructeurs. Ce sont
des méthodes qui se distinguent de deux façons : leur nom est celui de la classe, et
elles n’ont aucun résultat (pas même void). Voici une classe dotée de deux
constructeurs class exemple {
int i;
char c;
constructeur public :
exemple() { i = 0; c = 0 } // constructeur 1
par défaut exemple( int ii, char cc ) { i = ii; c = cc } // constructeur 2
};
• Lorsqu’une variable est déclarée, elle est initialisée par un constructeur. Le
choix se fait en fonction des arguments, comme pour les fonctions recouvertes :
exemple ex1(1,'A'); // constructeur 2 appelé
exemple ex2() // constructeur 1 appelé
• Les constructeurs ne sont pas des fonctions comme les autres. Par exemple, il
n’est pas possible de les appeler explicitement, ni de prendre leur adresse
Les Constructeurs
Constructeurs par défaut
S’il n’y a aucun constructeur défini dans la classe, C++ en crée un automatiquement
qui est un constructeur par défaut, et qui ne fait rien. De ce fait, une classe a toujours
un constructeur, mais n’a pas forcément un constructeur par défaut ; en effet, si l’on
définit un constructeur avec paramètre seul, C++ ne fournit pas le constructeur par
défaut automatique, et de ce fait il n’y en a pas.
Lorsqu’un constructeur par défaut existe, on peut déclarer une instance de classe
sans préciser de paramètres : exemple ex2; // plus pratique que ex2()
Il n’en est pas de même si aucun constructeur par défaut n’existe. Par exemple cette
autre classe : class autre {
double d;
public:
autre(double dd) { d = dd; }
};
n’a pas de constructeur par défaut, aussi l’écriture : autre a; erreur
Lorsqu’on veut créer un tableau d’instances de la classe sans donner de valeurs
initiales, un constructeur par défaut doit exister ; il est alors appelé pour tous les
éléments du vecteur :
exemple table[10]; // ok, 10 appels du constructeur 1
autre tab[3] ; // NON, pas de constructeur par défaut
Les Constructeurs
Constructeurs par défaut
Précisons un point très important au sujet de la présence d’un constructeur par défaut :
bien que les constructeurs puissent avoir des arguments par défaut comme toute autre
fonction, un constructeur dont tous les arguments possèdent une valeur par défaut
n’est pas un constructeur par défaut. Ainsi, si l’on modifie la classe autre comme
ceci :
class autre {
double d;
public:
autre(double dd = 0) { d = dd; }
};

alors l’initialisation suivante est acceptée : autre a; // ok, appel de autre::autre(0)


mais le tableau est toujours refusé : autre tab[3]; // NON, pas de constructeur
par défaut
Appel des constructeurs
Un constructeur ne peut pas être appelé autrement que lors d’une initialisation.
Cependant, il peut l’être de différentes façons. Par exemple, s’il existe un constructeur
qui n’admet qu’un seul paramètre, ou plusieurs mais tel que tous les arguments sauf le
premier ont une valeur par défaut, on peut l’appeler en écrivant le signe égal suivi du
paramètre
Les Constructeurs
Appel des constructeurs...
Exemple : autre au = 1.2; // appel de autre::autre(1.2)
Cette écriture est équivalente à la forme classique : autre au(1.2);
En outre, il est possible d’initialiser des tableaux de cette façon :
autre atab [4] = { 1.2, 2, 0.7, -9.9 };
Il est obligatoire de préciser toutes les valeurs initiales lorsqu’aucun constructeur par
défaut n’existe pas.
Le compilateur gère lui-même les initialisations de variables automatiques, parmi
lesquelles figurent les arguments de fonction. Par conséquent, si l’on a écrit une
fonction comme celle-ci : void f(exemple ex);
il est légitime de l’appeler sous cette forme : f(exemple(1, 2)); f(exemple());
Dans ce cas les constructeurs adéquats sont appelés à l’entrée de la fonction (et des
destructeur à la sortie). De même, si l’on a une fonction :
void g(autre au); g(1); // appel de g( autre::autre(1) )
En d’autres termes, le constructeur autre::autre(double) définit un changement de
type automatique du type double vers le type autre (et donc de tous les types
numériques vers ce type
Les Constructeurs
Appel des constructeurs...
Il est parfaitement possible de préciser une valeur par défaut à un argument de type
classe, pourvu qu’on utilise un constructeur
void f(exemple ex = exemple() );
void f2(exemple ex = exemple(1, 1) );
void g(autre au = 0);
Dans le dernier cas, on a encore utilisé le changement de type automatique
Constructeurs de copie
Les constructeurs d’une classe donnée classe peuvent avoir n’importe quoi comme
arguments, sauf des données de type classe. Ils peuvent avoir des pointeurs *classe
comme arguments, ainsi que des références &classe. Cependant, dans ce dernier
cas, le constructeur ne doit avoir qu’un seul argument &classe, et les autres
arguments, s’il y en a, doivent avoir une valeur par défaut. Ce constructeur est
alors appelé constructeur de copie. Il sert lors d’affectations du genre :
class classexmpl { // champs ...
public : classexmpl(); // constructeur implicite;
classexmpl (int i); // un autre constructeur
classexmpl(classexmpl & c); // constructeur de copie
// méthodes... };
Les Constructeurs
Constructeurs de copie
classexmpl c1; // constructeur par défaut
classexmpl c2 = c1; // appel du constructeur de copie
// équivaut à classexmpl c2(c1);
Toute classe a nécessairement un constructeur de copie. Si aucun n’est défini, le
compilateur en crée un automatiquement, qui se contente de recopier champ par
champ l’argument dans this. Ce constructeur suffit dans les cas simples.
Rappel : Tous les constructeur de copie ne sont appelés que lors d’une affectation
pendant l’instantiation.
Dans c2 = c1; ce n’est pas le constructeur qui est appelé, mais l’opérateur
d’affectation =, par défaut recopie les champs un à un ; il faut également le redéfinir
si nécessaire (voir ultérieurement). exemple c2 = c1; // appel du constructeur de copie
c2 = c1 ; // appel de l'opérateur d'affectation
l’affectation n’est appelé qu’une fois (la seconde), tandis que c’est le constructeur de
copie qui est appelé dans la première fois. Si ces deux appels n’ont pas été
différenciés jusqu’alors, c’est que par défaut ils provoquent le même effet ; il n’en est
pas nécessairement ainsi dans des classes définies par un programmeur
Pour la même raison, il faut comprendre que, lors de l’appel d’une fonction f(exemple
ex) sous la forme f(e1), ce n’est pas une affectation qui est réalisée, malgré les
apparences, sur l’argument ex, mais le constructeur de copie de nouveau ; ici aussi les
effets peuvent être différents
Les Constructeurs
Destructeurs
•Les destructeurs ont le rôle inverse des constructeurs. Ils sont appelés lorsqu’une
instance de classe sort de la visibilité courante. Par exemple, lors de l’appel de la
fonction g(autre au), sous la forme g(1), nous avons dit que le constructeur
autre::autre(double) était appelé pour la variable automatique au, argument de g. À
la fin de la fonction g, le destructeur est appelé.
• La tâche d’un destructeur est souvent de libérer la mémoire prise par l’instance
sur le tas, s’il y a lieu. Souvent le destructeur standard est créé implicitement par le
compilateur s’il n’est pas défini. Imaginons une classe qui utilise un grand tableau
créé dans le tas par new; dans ce cas, un destructeur sera défini qui libérera cette
place :
class troisieme {
char *tampon;
unsigned taille;
public : troisieme() { taille = 0; tampon = 0; }
// constructeur par défaut
troisieme (int t) { tampon = new char [taille = t]; }
// ce constructeur prend une place dans le tas
~troisieme() { } // destructeur // ...
};
• Le destructeur a pour nom le nom de la classe précédé du symbole tilde ~. Un
destructeur n’a aucun résultat, comme les constructeurs, et n’admet aucun
argument ; de ce fait, il ne peut y avoir qu’un destructeur par classe
Les Destructeurs
Appel explicite d’un destructeur
• Il n’est pas possible d’appeler explicitement un constructeur (il faut passer par
une initialisation), mais on peut appeler explicitement un destructeur.
• Il s’agit d’une technique un peu spéciale pour programmeurs chevronnés. Elle
est basée sur le fait que l’on peut créer un objet qui ne réserve pas de place
mémoire. Voici un exemple :
static char tampon[sizeof( exemple)];
exemple *pex = (exemple*) tampon; // pas de place occupée dans le tas
• Ici la variable pointée par pex est donc logée dans le tampon, et non dans le tas ;
on suppose de plus qu’il n’est pas nécessaire d’appeler un constructeur (ce qui
n’est guère prudent, mais peut marcher si la classe utilise le constructeur par défaut
standard).
• Comme pex n’est pas dans le tas, il ne faut surtout pas appeler delete avec pex,
car on risquerait des ennuis. Pour appeler le destructeur, il faut donc un appel
explicite, avec le nom complet :
pex->exemple::~exemple();
Les Constructeurs
Les Classes
et Destructeurs
New et delete avec constructeurs et destructeurs...
• Pour ce qui est de l’instruction delete, il n’y a pas le choix : chaque classe ayant
un seul destructeur (éventuellement implicite), c’est celui-là qui est appelé avant de
supprimer la place mémoire. Précisons toutefois un problème particulier aux
tableaux. Si l’on écrit ceci :
exemple *pex = new exemple[10]; // ...
delete pex // incorrect;
• le compilateur, qui n’a aucun moyen de connaître la taille du tableau pointé par
pex, n’appellera le destructeur que pour le premier élément, ce qui peut poser
problème. Pour lui demander de tout détruire, il faut préciser explicitement avec
delete, le nombre d’éléments à supprimer

exemple *pex = new exemple[10]; // ...


delete[10] pex;
• Dans ce cas, le destructeur est appelé dix fois pour les dix éléments du tableau.
• On notera que pour les types simples, dont le destructeur ne fait rien, il n’est pas
nécessaire de procéder ainsi. Nos écritures des chapitres précédents étaient donc
correctes
Les Constructeurs et Destructeurs
New et delete avec constructeurs et destructeurs
• L’opérateur new réserve primo la place mémoire nécessaire à l’objet dans le tas ;
secundo, il appelle un constructeur. Inversement, delete appelle d’abord le
destructeur, puis libère la place mémoire
• L’allocation d’un objet
point *p1 = new point; // l’allocation d’un objet point et appel du constructeur
//sans paramètres ( ou avec les valeurs par défauts)s
Point *p2= new point(30,40); // Allocation et appel du constructeur avec paramètres
cout << p1-> getAbs()<< " " <<p1->getOrd() <<endl; // 0 0
cout <<p2 -> getAbs()<< " " <<p2->getOrd() <<endl; // 30 40
• Comme une classe peut avoir plusieurs constructeurs, on peut préciser quel
constructeur est appelé au moment de l’appel de new. Il suffit pour cela d’écrire la
liste des arguments derrière le nom de classe qui suit new.
exemple *pe1 = new exemple(1, 2); // appel du constructeur 2
exemple *pe2 = new exemple; // appel du constructeur 1
autre *pa = new autre(1); // appel de autre::autre(1)
classexmpl *c1 = new classexmpl; // constructeur par défaut
classexmpl *c2 = new classexmpl(*c1); // constructeur de copie
• Lorsqu ’aucun paramètre n’est précisé, par défaut c’est le constructeur par défaut
qui est appelé ; s’il n’existe pas, une erreur de compilation se produit
• On peut créer un tableau avec new, dans ce cas c’est le constructeur par défaut qui
est appelé ; il n’y a pas de moyen d’en préciser un autre.
Les Classes
Initialisations multiples
Il est parfaitement permis à une classe d’avoir un ou plusieurs membres
instances d’autres classes. En utilisant toujours les classes d’exemples du
début, voici une classe contenant des membres instances :
class multiple {
exemple ex;
autre au;
public :
multiple() {} // constructeur implicite
// ... };
Tout le problème consiste à initialiser ces membres dans le constructeur de
la classe. La solution immédiate :
multiple::multiple() { est incorrecte, car on ne peut
ex.exemple::exemple(); // incorrect pas appeler explicitement un
au.autre::autre(0); // idem } constructeur.
Le langage fournit un mécanisme spécial pour résoudre ce problème. Derrière
la liste des arguments du constructeur, on place le symbole deux-points :,
suivi des initialisations de chaque membre, séparées par une virgule :
multiple::multiple() : ex(), au(0); //
Les constructeurs sont appelés dans l’ordre de leur écriture (donc ici d’abord
exemple::exemple(), puis autre::autre(0)).
Les Classes
Initialisations multiples…
Cette séquence d’initialisation doit être écrite dans chaque constructeur, s’il y
a lieu : class multiple {
exemple ex; autre au; public : multiple() : ex(), au(0) {}
multiple(double d) : ex(), au(d) {}
multiple(double d, int i, char c) : ex(i, c), au(d) {}
multiple(autre& a, exemple& e) : ex(e), au(a) {}
multiple(multiple& m) : ex(m.ex), au(m.au) {} // ... };
Le dernier constructeur est un constructeur de copie ; comme celui qui le
précède, il appelle les constructeurs de copie des classes exemple et autre (qui
rappelons-le sont toujours définis).
Tous les types ont un constructeur de copie, pas seulement les classes. De ce
fait, même une classe très ordinaire peut initialiser ses membres de cette
façon :
L’intérêt de la première notation
class ordinaire { est l’abréviation dans ce cas, la
double dd; seconde est bien plus claire.
int ii;
public :
ordinaire(int i, double d) : ii(i), dd(d) {} ordinaire(int i, double d) { ii = i;
// ... }; dd = d;}
Les Classes
Initialisations multiples…
Lorsque le destructeur d’une classe est appelé, automatiquement ou non, les
destructeurs des membres sont exécutés à la fin de celui-ci de manière automatique.
Lorsqu’une classe contient des références à d’autres types (classes ou non), le
mécanisme d’initialisation est strictement identique. Cependant, dans ce cas,
l’initialisation est obligatoire. Ainsi l’écriture :

class fautive {
int& i;
public: fautive() {}; // NON, pas d’init. de la référence
};
provoque une erreur Error : Reference member 'i' is not initialisé in function fautive::
fautive::fautive
fautive()
(),,
le membre référence 'i' n’est pas initialisé.
initialisé.

Il faut dans ce cas écrire obligatoirement une initialisation utilisant le constructeur


de copie, et aucun autre. Par exemple l’écriture suivante pour le constructeur de
fautive : fautive() : i(22) {}
est refusée par le compilateur (Error : Reference member 'i' need a temporary for
initialization, le membre référence 'i' nécessite une variable temporaire pour
l’initialisation).
Les Classes
Initialisations multiples…
C’est tout à fait logique : le compilateur ne peut pas savoir si un membre référence a été
initialisé sur une variable temporaire au moment de la destruction. En conséquence, il
interdit d’initialiser une référence sur une variable temporaire, et en contrepartie il n’y a
jamais d’appel de destructeur sur les membres références au moment de la destruction de
l’instance. Si ce mécanisme ne vous convient pas, il faut utiliser un membre normal, non
une référence.
Les seuls constructeurs acceptables pour que la classe fautive ne le soit plus seraient :

fautive::fautive() : i(j) { } où j est une variable globale, ou


fautive::fautive(int k) : i(k) { }; un autre membre de la classe
fautive.
Une classe peut aussi contenir des pointeurs sur des instances d’autres classes. Dans
ce cas, il n’y a aucune difficulté particulière :
class multiple {
exemple *pex;
autre *pau;
public :
multiple() { pex = new exemple; pau = new autre(0); }
multiple(autre& a, exemple& e) { pex = new exemple(e); pau =new autre(a);}
multiple(multiple& m) { pex = new exemple(*m.pex); pau = new autre(*m.pau); }
~multiple() { delete pau; delete pex; }
// Le destructeur doit alors explicitement libérer la mémoire occupée par les instances pointées.
};
Les fonctions Amies
Une fonction est une amie (friend) d’une classe lorsqu’elle est autorisée à
s’adresser directement aux membres privés de cette classe. Pour la déclarer ainsi, il
faut donner, à l’intérieur de la classe, la déclaration complète de la fonction
précédée du mot clé friend. Voici un exemple simple :
class exemple {
int i, j;
public:
exemple() { i = 0; j = 0; }
friend exemple inverse(exemple);
};
exemple inverse(exemple ex) // renvoie ex avec tous les bits inversés
{
exemple ex2 = ex;
ex2.i = ex.j; // accès aux champs
ex2.j = ex.i;
return ex2; }
La fonction inverse est amie de la classe exemple.
La fonction amie ne doit modifier que les données membres de la classe.
Une fonction peut être amie d’autant de classes que nécessaire, mais évidemment
cela n’est utile que lorsque la fonction utilise une instance de la classe, et plus
précisément lorsqu’elle modifie un membre privé de la classe (car en général il
existe des fonctions membres en ligne permettant de lire ces membres privés ou
une interprétation de ceux-ci)
Les Classes et fonctions membres Amies
Nous avons vu qu’une classe avait généralement des membres privés, non
accessibles par des fonctions non membres. Cette restriction est la force de
l’encapsulation qui fait partie de la puissance de la POO en général.
Dans certains cas, cependant, on souhaite pouvoir utiliser une fonction qui puisse
accéder aux membres d’une classe, sans toutefois nécessairement disposer d’une
instance de cette classe.
Une première possibilité consiste à utiliser un membre statique. Si l’on écrit par
exemple :
class exemple {
// parties privées... il est possible d’appeler la
public : fonction f sans passer par un
membre, comme ceci :
static exemple* f(void); // ...
};

exemple *p = exemple::f();

Cependant cette notation, si elle a l’avantage de la clarté, est assez lourde. C’est
pourquoi le langage fournit les fonctions amies pour résoudre ce problème.
Les fonctions Amies
La « déclaration d’amitié » doit se faire à l’intérieur de la classe. C’est un moyen
sûr et efficace de protéger des données. De ce fait, avant de « verrouiller » une
classe, on prendra soin de fournir tous les moyens d’accès raisonnables (en lecture
notamment) aux champs utiles, afin de permettre la création de fonctions non amies
utilisant cette classe.
Lorsque cette précaution est prise, il n’est plus besoin d’une fonction amie, en dépit
des apparences. Par exemple, la librairie <complex.h> fournit une classe complex
(qui est fondamentalement formée de deux nombres à virgule flottante nommés
partie réelle et partie imaginaire) et un ensemble de fonctions la manipulant ;
cependant, les concepteurs de la librairie n’ont pas implanté une opération
importante sur les nombres complexes, nommée conjugaison, qui consiste
simplement à changer le signe de la partie imaginaire. Est-ce à dire qu’il faut
modifier <complex.h> pour déclarer « amie » la fonction ayant cet effet ? Non, car
on dispose de deux fonctions amies real et imag donnant les parties réelle et
imaginaire d’un complexe, ainsi que du constructeur complex(double, double) qui
crée un complexe à partir de ses deux parties. De ce fait, il suffit d’écrire une
fonction normale :
inline complexe conjug(complexe c)
{
return complexe(real(c), -imag(c));
}

Cette fonction n’est pas amie de la classe complex, elle n’accède qu’à des parties
publiques de celle-ci (les fonctions amies real et imag).
Les fonctions membres Amies
On souhaite parfois qu’une méthode d’une classe puisse accéder aux parties
privées d’une autre classe. Pour cela, il suffit de déclarer la méthode friend
également, en utilisant son nom complet (nom de classe suivi de :: et du nom de
la méthode). Par exemple :
class autre { // ...
void combine(exemple); };
class exemple { // ...parties privées
public :
friend void autre::combine(exemple); };
void autre::combine(exemple ex) { // utilise les membres privés de ex }
La fonction combine, qui fait une modification quelconque de l’instance de autre qui
l’appelle, à l’aide des données contenues dans une instance de exemple, a libre accès
aux parties privées des deux classes.
On aurait pu aussi écrire une fonction amie des deux classes
class exemple { // ...parties privées
public :
friend void combine(autre&, exemple);
};
class autre { // ...
friend void combine(autre&, exemple); };
void combine(autre& au, exemple ex) {
// accède aux membres des deux arguments
}
mais la syntaxe d’appel est alors différente : combine(au,ex) contre au.combine(ex).
Les Classes Amies
Lorsqu’on souhaite que tous les membres d’une classe puissent accéder aux parties
privées d’une autre classe, on peut déclarer « amie » une classe entière :
class exemple// déclaration class exemple
{ // parties privées...
public : friend autre;
// ... };
class autre { // ... };
Les membres de la classe autre peuvent tous modifier les parties privées des
instances de exemple. Noter la déclaration de autre avant celle de exemple,
obligatoire (sinon on obtient Error : Undefined symbol 'autre', symbole 'autre' non
défini). Pour l’éviter, on peut éventuellement changer l’ordre de définition, mais il
suffit en fait de préciser le sélecteur class derrière friend :
class exemple { // parties privées...
public : friend class autre;
// ... };
class autre { // ... };

Cette écriture, comme la précédente avec une déclaration, est inutilisable pour des
méthodes isolées. De ce fait, si l’on souhaite qu’une méthode de autre soit amie de
exemple et une de exemple amie de autre, il faut déclarer les deux classes
entièrement amies l’une de l’autre.
Polymorphisme

Nous avons déjà vu un premier avantage de la programmation orientée


objet : l’encapsulation (la protection des données), fournie par la déclaration de
membres privés dans une classe.
Il existe un deuxième plus importants est le polymorphisme (plusieurs formes).
Il traite la capacité de l’objet à posséder plusieurs formes.

Le polymorphisme est une notion très difficile, il existe encore des divergence entre
les expert du C++ au niveau de sa définition. On retient trois type de polymorphisme:

• Polymorphisme d’héritage appelé spécialisation (overiding)


associée généralement au mot clef virtual (on revient la dessus dans l’héritage)

• Polymorphisme Ad-Hoc appelé aussi surcharge (overloading en anglais)

• Polymorphisme paramétrique appelé aussi générique associé généralement au


mot clef Template
Polymorphisme
Polymorphisme Ad-Hoc
Nous allons définir les quatre opérations de base pour la classe fraction. Pour cela, il
suffit de nommer operator+, operator-, etc., les fonctions opératoires :

class fraction {
long num, den; // numérateur, dénominateur
public :
fraction(long numer, long denom = 1)
{ num = numer;
den = denom; }
friend fraction operator+(fraction,
operator+ fraction);
friend fraction operator-(fraction, fraction);
friend fraction operator*(fraction, fraction);
friend fraction operator/(fraction, fraction); };
inline fraction operator+(fraction f1, fraction f2) {
return fraction(f1.num*f2.den + f1.den*f2.num, f2.den*f1.den);}
inline fraction operator-(fraction f1, fraction f2) {
return fraction(f1.num*f2.den - f1.den*f2.num, f2.den*f1.den); }
inline fraction operator*(fraction f1, fraction f2) {
return fraction(f1.num*f2.num, f2.den*f1.den); }
inline fraction operator/(fraction f1, fraction f2) {
return fraction(f1.num*f2.den, f2.num*f1.den); }
Polymorphisme
Polymorphisme Ad-Hoc
On peut alors écrire : fraction f = 1 + 2/fraction(5) - fraction(1,3)*8;

fraction f = operator-(operator+(1, operator/(2, fraction(5)), operator*(fraction(1,3), 8));

Tous les opérateurs sont redéfinissables, sauf (?:), sizeof, et ceux


directement liés aux classes, à savoir le point (.), (:: et ::*), et les pointeurs sur
membres (.*)
On ne peut pas créer de nouveaux opérateurs, comme par exemple (**)
ou (:=). De plus, il n’est pas possible de changer l’ « arité » d’un opérateur, c’est-à-
dire son caractère binaire ou unaire.
Enfin on ne peut pas modifier leur précédence (la priorité).
Il en résulte que, lorsque le nom d’un opérateur n’est pas clairement
imposé, il faut faire le bon choix du nom, notamment en fonction de la précédence
souhaitée. Ainsi, on pourrait imaginer, sur une classe numérique comme fraction,
d’utiliser l’opérateur ^ pour symboliser l’exponentielle ( « x à la puissance y »). Ce
choix est possible, car la précédence de cet opérateur est faible. De ce fait,
l’expression a + b^c est interprétée comme (a+b)^c confusion. On définit donc une
méthode, nommée par exemple pow, et écrire a + b.pow(c) qui ne prête pas à
erreur.
Polymorphisme
Polymorphisme Ad-Hoc
En dehors des deux règles énoncées ci-dessus, il n’y a aucune restriction pratique
sur la redéfinition d’opérateurs. En particulier, le compilateur ne fait aucune
hypothèse fonctionnelle à leur sujet ; il ne suppose jamais qu’ils sont symétriques
par exemple. Si dans un contexte naturel, a + b est égal à b + a, il n’en est pas
forcément ainsi pour un opérateur redéfini, et le compilateur ne le supposera donc
pas : la première expression correspond à operator+(a, b), la seconde à operator+(b,
a). Cela peut sembler anecdotique, mais est très important en pratique, pour des
objets comme les matrices, dont la multiplication n’est pas commutative.

Types dont on peut redéfinir les opérateurs,

 Incrémentation et décrémentation,
 Opérateurs [] et (),
 Opérateurs d’affectation,
 Opérateurs new et delete,
 Opérateurs membres ou amis
Polymorphisme
Polymorphisme générique
Exemple d’ application pour la fonction echange:

template <class T>


void echange (int &x, int& y)
void echange (T &x, T & y)
{ Template {
int tmp =x;
T tmp =x;
x=y;
x=y;
y= tmp;
y= tmp;
}
}

int main ()
{
int x,y; echange(x,y) // ok
float x,y; echange(x,y) // ok
char x,y; echange(x,y) // ok
double x,y; echange(x,y) // ok
}
Polymorphisme
Polymorphisme générique
Exemple d’ application de classe echange:

template <class T>


class tab
{
T* x,y; int main ()
public : {
tab(int x=0, int y=0) tab<int> a;
~tab() tab<float> b;
void echange( T & x, T & y) ….
void inverse(T &x, T &y) }
}

Contrairement au fonction de patron, les classes de patron ( instanciation de classe


Template) ont besoins d’être explicitement paramétrées.
Héritage
Nous avons vu comment les classes permettent la protection des données en C++.
Cette protection peut cependant paraître gênante : si l’on souhaite faire une
modification mineure d’une classe, sans avoir accès au code de celle-ci, il semble
qu’il faille tout réécrire. Il n’en est pas ainsi, grâce au mécanisme de l’héritage, dont
les applications sont extrêmement étendues, comme nous allons le voir à présent.
Réutilisation du code
Imaginons qu’on vous a fourni une bibliothèque de formes graphiques contenant par
exemple une classe rectangle ayant l’allure suivante :
class rectangle { // membres privés
public :
rectangle();
rectangle(int gche, int haut, int drte, int bas);
~rectangle();
void trace();
void efface();
void valeur(int& gche, int& haut, int& drte, int& bas);
void change(int gche, int haut, int drte, int bas);
};
Vous ne connaissez pas les membres privés, ni le code des méthodes dont vous
connaissez simplement le nom et l’usage : valeur donne les coordonnées des bords du
rectangle, change les modifie, trace dessine le rectangle à l’écran tandis que efface le
supprime ; le constructeur implicite crée un rectangle vide, l’autre crée un rectangle
dont on fournit les coordonnées des bords.
Héritage
Réutilisation du code …
Pour cela, nous allons écrire que notre nouvelle classe rectplein
class rectplein : rectangle {
int coul;
public :
rectplein();
rectplein(int gche, int haut, int drte, int bas, int couleur = 0);
~rectplein();
void trace(void);
void efface();
int couleur() { return coul; } // donne couleur
int couleur(int nouvelle) { // donne la couleur et la change
int ancienne = coul;
coul = nouvelle;
trace();
return ancienne; }
};

On dit que l’on a dérivé la classe rectplein de rectangle. Dans ce cas, la classe
dérivée hérite des caractéristiques de la classe de base, et en particulier de ses
membres. Dans certains cas, les membres de la classe de base doivent être redéfinis
(cas de trace et efface notamment), dans d’autres les méthodes de la classe de base
conviennent aussi (cas de change et valeur dans notre exemple).
Héritage
Réutilisation du code …

La classe dérivée peut utiliser les membres publics de la classe de base, même si
elle les redéfinit. Par exemple, la fonction trace de rectplein se réduit à deux
opérations : remplir le rectangle avec la couleur de remplissage, puis dessiner le
bord de ce rectangle. Si l’on suppose qu’on dispose d’une fonction remplirrect
réalisant le premier travail, il suffit d’écrire :

void rectplein::trace(void) {
if (coul) {
int gche, drte, haut bas;
valeur(gche, haut, drte, bas);
remplirrect(gche, haut, drte, bas, coul);
}
rectangle::trace();
}

On a appelé la méthode valeur héritée de rectangle (puisqu’on ne connaît pas les


coordonnées du rectangle qui sont des membres privés de la classe de base) ainsi
que la méthode trace de rectangle ; dans ce dernier cas, il faut absolument écrire
rectangle::trace() et non trace() qui ferait un appel récursif infini.
Héritage
Méthodes héritées:
Tous les membres d’une classe, sont hérités par la classe dérivée. Cependant, il y a
quelques exceptions. D’abord les constructeurs et destructeurs ne sont pas hérités, ils
ont leur propres règles (voir ci-après).
Les opérateurs sont hérités normalement. Or, aucune opération n’est réalisée sur les
nouveaux membres, il est préférable généralement de redéfinir ces opérateurs.
Enfin l’opérateur d’affectation est un cas particulier, car il n’est pas hérité non plus.
Lorsqu’il n’est pas redéfini explicitement dans une classe dérivée, il recopie membre à
membre les nouveaux membres de cette classe dérivée, et appelle l’opérateur
d’affectation de la classe de base pour la copie de la partie héritée. Lorsqu’on le
redéfinit pour la classe dérivée, l’opérateur pour la classe de base n’est pas appelé, il
faut donc le faire explicitement, comme ceci :
rectplein& rectplein::operator=(rectplein rp)
{ *(rectangle*)this = rp;
coul = rp.coul; }
Le changement de type sur this permet la recopie sous la forme d’une instance
rectangle, soit par l’opérateur d’affectation de celle-ci, s’il existe, soit par la copie
membre à membre par défaut. On aurait pu aussi écrire un appel explicite à
rectangle::operator=, si l’on savait que celui-ci était défini (le compilateur refuse en
effet cet appel lorsque seule l’affectation par défaut est définie).
Notons que notre exemple est assez inutile, puisqu’il fait exactement ce que ferait
l’opérateur d’affectation par défaut (il n’y a aucune opération particulière de réalisée).
Héritage
Constructeurs et destructeurs
Lorsque la classe de base possède un constructeur par défaut, celui-ci est appelé
automatiquement avant l’appel du constructeur de la classe dérivée, pour initialiser
les données membres de base. Il est cependant permis à un constructeur de la classe
dérivée de faire un appel explicite à un constructeur de la classe de base, afin
d’initialiser les membres hérités ; cet appel se fait de la même façon que pour les
membres qui sont des classes, c’est-à-dire en plaçant derrière la liste des arguments
le symbole : puis le nom de la classe de base avec ses arguments. Voici donc
comment définir de manière naturelle les deux constructeurs de la classe rectplein :
rectplein::rectplein() { // appel implicite de rectangle: rectangle();
couleur = 0; }
rectplein::rectplein(int gche, int haut, int drte, int bas, int couleur)
: rectangle(gche, haut, drte, bas) // explicite
{ couleur=0; }

Le premier constructeur appelle en fait le constructeur implicite de rectangle (qui


crée un rectangle vide), ce qu’il n’est pas nécessaire de préciser. Par contre, dans le
second, on souhaite utiliser l’autre constructeur (qui crée un rectangle à partir de ses
coordonnées), et il faut alors le mentionner explicitement.
Il résulte de ces règles que lorsqu’une classe n’a pas de constructeur par défaut, les
classes dérivées doivent obligatoirement appeler un constructeur de la classe de
base.
Héritage
Constructeurs et destructeurs …
Le destructeur d’une classe dérivée appelle le destructeur de la classe de base
après l’exécution de ses tâches explicites. Ainsi on peut écrire :

rectplein::~rectplein() {
efface(); // appel implicite de rectangle::~rectangle();
}

Comme il n’y a qu’un destructeur par classe, il n’y a pas à choisir.


On retiendra que les constructeurs sont appelés dans l’ordre ascendant des
classes (de base vers dérivées), tandis que les destructeurs le sont dans l’ordre
inverse. Il s’agit bien là d’un ordre conforme à la logique. En effet, le
constructeur d’une classe dérivée peut avoir besoin des membres de la classe
de base (c’est le cas dans notre exemple, puisque la fonction trace utilise les
coordonnées du rectangle) : il en résulte que la partie de base de l’objet doit
être initialisée avant qu’on ne commence la construction explicite.
Inversement, le destructeur aussi peut avoir besoin des membres hérités : il ne
faut donc pas les détruire en premier, mais seulement après.
Héritage
Membres privés, publics, protégés
On sait que certains membres d’une classe pouvaient être publics.
Il existe une troisième catégorie de membres, les membres protégés (protected).
Dans la classe qui les déclare, ils sont identiques à des membres privés : on ne
peut pas y accéder de l’extérieur. Or, une classe dérivée peut accéder aux
membres protégés de sa classe de base (elle ne le peut pas pour les membres privés).
Pour dériver une classe de manière publique, comme ce n’est pas la valeur par
défaut, il faut placer le mot public devant le nom de la classe de base. Voici
quelques exemples :
class A {
int a1;
protected : int a2;
public : int a3;
};
class B : A { class C : public A {
// héritage privé // héritage public
int b1; int c1;
protected : int b2; protected : int c2;
public : int b3; public : int c3;
}; };
La classe A possède trois membres, un privé a1, un protégé a2, un public a3 ; de
l’extérieur seul a3 est accessible. La classe B possède six membres :
Héritage
Membres privés, publics, protégés …
un indisponible directement a1, trois privés a2, a3 et b1, un protégé b2 et un public b3 ; de
l’extérieur, seul b3 est accessible. Enfin la classe C possède aussi six membres : un
indisponible a1, un privé c1, deux protégés a2 et c2 et deux publics a3 et c3 ; de
l’extérieur, seuls a3 et c3 sont accessibles.
Une classe peut dériver de manière publique d’une classe de base, ou de manière privée.
Par défaut, une classe dérive de manière privée, et de manière publique. Voici comment
les deux types d’héritages influent sur la nature des membres hérités :
membres publics : ils restent publics dans une classe dérivée de manière publique,
mais deviennent privés dans une classe dérivée de manière privée.
 membres protégés : ils restent protégés dans une classe dérivée de manière
publique, mais deviennent privés dans une classe dérivée de manière privée.
 membres privés : ils ne sont jamais accessibles dans une classe dérivée.
Pour les structures c’est le contraire, puisque l’héritage est par défaut public. Pour le
rendre privé, il suffit de placer le mot private devant le nom de la classe de base. Dans les
deux cas, il n’existe pas de dérivation « protégée » .
Il arrive que l’on souhaite modifier ces états par défaut pour un membre ou deux
seulement. Dans ce cas, il suffit de renommer les membres hérités en les plaçant au bon
endroit. Voici un exemple : class E : public A { // héritage public
class D : private A { // héritage privé int e1;
int d1; A::a2;
protected : int d2; protected : int e2;
A::a2; A::a3;
public : int d3; }; public : int e3; };
Héritage
Membres privés, publics, protégés …
Dans la classe D, on a précisé un héritage privé (inutilement, c’est la valeur par défaut),
diffère de B en ce que le membre hérité a2 est protégé dans D, alors qu’il était privé
dans B. La classe E diffère de C en ce que le membre hérité a2 est privé pour elle
(protégé pour C) et a3 est protégé pour elle (public pour C).
On ne peut pas diminuer la protection d’un membre par héritage. Si l’on essaie dans
une classe dérivée de déclarer public un membre protégé de la classe de base, ou si l’on
essaie de redéclarer un membre privé de la classe de base, on obtient une erreur (Error :
Access declarations cannot grant or reduce access, les déclarations d’accès ne peuvent
pas octroyer ou réduire le niveau d’accès).
Bien que l’héritage des classes soit privé par défaut, il est en général préférable de le
déclarer public. Par exemple, notre classe rectplein est mal déclarée avant, il faut un
héritage public :
class rectplein : public rectangle { // ... };
Dans le cas contraire, il ne serait pas possible d’appeler les méthodes héritées valeur et
change à partir d’une variable de type rectplein.
Pour les membres, le choix entre les différents accès n’est pas toujours évident. S’il est
clair que l’on doit déclarer publics les membres que l’on souhaite accessibles de
l’extérieur, il n’est pas forcément facile de choisir entre privés et protégés pour les
autres, puisque cela exige de réfléchir à ce que pourraient être d’éventuelles classes
dérivées de la classe courante. Dans la suite de ce chapitre, nous nous efforcerons de
donner quelques indications sur des exemples, car il n’y a pas réellement de règle
générale en la matière : cela dépend si l’on souhaite que les classes dérivées connaissent
bien le contenu de leur base ou non
Héritage
Méthodes virtuelles
Revenons à notre exemple rectangle et rectplein. Imaginons qu’on connaît
seulement les quatre coordonnées h, b, g, d de la classe rectangle. Dans ce cas, la
méthode change a probablement l’allure suivante :
void rectangle::change(int gche, int haut, int drte, int bas) {
efface();
if ( (gche >= drte) || (haut >= bas) )
{ g = d; return; } // rectangle vide
g = gche; d = drte;
h = haut; b = bas;
trace(); }
A priori, il n’y a aucune raison de changer cette méthode pour notre classe
rectplein. Pourtant, si l’on fait un essai, un appel de change avec une instance de
rectplein ne donnera pas le bon résultat. Pourquoi ?
Rappelons-nous que la classe rectangle a été compilée avant la classe rectplein.
Dès lors, lorsque le compilateur, agissant sur le code source de la méthode change
ci-dessus, rencontre un appel à efface et un autre à trace, il cherche les méthodes
de ce nom ; il n’en connaît alors que deux, celles de la classe rectangle. En mettant
les points sur les i, le compilateur « voit » donc ceci :
void rectangle::change(//...) {
rectangle::efface();
// ...
rectangle::trace();
}
Héritage
Méthodes virtuelles …
On voit dès lors pourquoi cette méthode ne fonctionnera pas correctement avec
rectplein : le rectangle ne sera ni correctement effacé, ni correctement retracé, parce que
l’on a modifié les méthodes correspondantes dans notre nouvelle classe.
On ne sait pas comment les coordonnées du rectangle sont stockées dans la classe
rectangle, et on ne peut pas les modifier de toute façon puisque les membres sont
inaccessibles dans la classe rectplein.
Ce problème est classique en programmation orientée objet, et résulte du principe
même de compilation. Les langages de POO interprétés comme SmallTalk n’ont pas de
difficultés de cet ordre.
Le C++ nous permet de résoudre en déclarant la méthodes virtuelles. Il suffit donc de
placer le mot réservé virtual devant le nom de la méthode. Si le programmeur qui a
conçu la classe rectangle était prévoyant, il a compris que certaines des fonctions
membres de la classe auraient à être modifiées dans des classes descendantes :
class rectangle { // membres privés
public :
rectangle();
rectangle(int gche, int haut, int drte, int bas);
virtual ~rectangle();
virtual void trace();
virtual void efface();
void valeur(int& gche, int& haut, int& drte, int& bas);
void change(int gche, int haut, int drte, int bas);
};
Héritage
Méthodes virtuelles …
Le compilateur ne place pas un appel direct à une méthode virtuelle, mais recherche la
dernière méthode redéfinie dans la classe de this ayant le même nom et les mêmes
types d’arguments, et appelle celle-ci. Il en résulte que la méthode change aura cette
fois-ci le bon comportement : si l’on appelle r.change où r est de type rectangle, la
méthode appellera les fonctions rectangle::efface et rectangle::trace ; si l’on appelle
rp.change, où rp est de type rectplein, la méthode appellera cette fois rectplein::efface
et rectplein::trace.
Pas besoin de définir les méthodes change et valeur comme virtuelles, parce que leur
code n’a aucune raison d’être modifié par les classes dérivées de rectangle (elles ne
peuvent pas le modifier, car elles n’accèdent pas aux coordonnées du rectangle).
Si la méthode est déclarée virtuelle dans une classe, elle l’est automatiquement pour
toutes les classes dérivées .
On prendra garde que les méthodes ne sont pas seulement caractérisées par leur nom,
mais aussi par leurs arguments. En conséquence, si l’on définit par exemple une
méthode rectplein::trace(int couleur), celle-ci ne sera pas virtuelle (sauf déclaration
explicite) car il s’agit d’une méthode différente de rectplein::trace() (elle ne répond pas
aux règles de recouvrement de fonctions) ; de toute façon, ce ne sera pas elle qui sera
appelée par rectangle::change, ne serait-ce que parce que les arguments ne
correspondent pas. Précisons qu’en parlant des arguments, nous parlons aussi des
arguments par défaut : en définissant une unique méthode rectplein::trace(int couleur =
-1) par exemple, on s’expose à des ennuis car la méthode change appellera alors
rectangle::trace(), seule méthode de ce nom ayant zéro argument dans la classe
rectplein.
Héritage
Méthodes virtuelles …
Pour éviter tout ennui, on redéfinira une méthode virtuelle avec exactement les mêmes
arguments, quitte à fournir aussi des homonymes ayant des arguments supplémentaires
(ou en moins). On évitera aussi les argument par défaut dans les méthodes virtuelles,
pour la même raison.
Destructeurs virtuels
Les constructeurs ne peuvent jamais être déclarés virtuels, pour des raisons
évidentes : ils sont spécifiques à une classe et doivent être redéfinis dans les
classes descendantes pour une initialisation correcte. De la même façon, les
opérateurs new et delete, lorsqu’ils sont redéfinis pour une classe, ne sont pas
virtuels.
Les fonctions membres statiques ne peuvent pas être virtuelles non plus,
puisqu’elles peuvent être appelées « hors contexte ». C’est là une différence
importante avec les fonctions membres normales.
Les méthodes en ligne peuvent être virtuelles, mais le compilateur ne les placera
pas en ligne dans ce cas.
Les destructeurs cependant peuvent être déclarés virtuels, et c’est même
préférable en général. En effet, nous verrons plus loin qu’un pointeur pr sur une
classe de base (rectangle*) peut en fait pointer sur un objet d’une classe dérivée
(rectplein*) ; dès lors, si l’on écrit delete pr, le mauvais destructeur sera appelé, à
moins que l’on ait pris la précaution de le déclarer virtuel. Nous recommandons
de toujours le faire si l’on compte dériver des classes à partir de la classe
courante, même si le destructeur ne fait rien
Héritage
Polymorphisme et classes abstraites
Nous avons vu que la protection des données particulière à la programmation
objets permet une certaine forme de polymorphisme : une classe peut être
implantée de différentes façons
façons..
L’héritage permet de perfectionner ce processus, en faisant cohabiter deux
implantations différentes (ou plus) d’une même classe, sous une forme homogène.
homogène.
Pour cela, il existe des règles de compatibilité particulières à l’héritage
l’héritage..
Compatibilité
Lorsqu’on utilise une variable d’une classe, il est possible de lui affecter une
variable de la classe dérivée : rectangle r;
rectplein rp; // rectplein dérive de rectangle
r = rp // parfaitement correct
Dans ce cas, seuls les champs de la classe rectangle, hérités par rp, sont recopiés
dans r. Plus généralement, une variable d’une classe dérivée peut être utilisée
partout où cela est possible pour la classe de base.
Le contraire n’est naturellement pas vrai : la classe rectangle n’a pas de champ
couleur, il lui est impossible de se comporter de la même façon que rectplein.
L’affectation inverse rp = r est donc impossible, sauf si l’on définit un opérateur
d’affectation adéquat, et un constructeur pour les initialisations. On pourrait le
faire ainsi par exemple :
Héritage
Compatibilité
class rectplein : public rectangle {
// ...
public :
rectplein(rectangle& r, couleur = 0) : rectangle(r)
{
coul = couleur;
trace(); }
// ...
rectplein& operator=(rectangle& r)
{
efface;
*(rectangle*)this = r;
trace();
}
}

Noter toutefois que l’affectation explicite par changement de type dans operator=
est un appel à l’affectation rectangle::operator=, qui provoque a priori aussi un
effacement et un appel à rectangle::trace() indésirables (quoique sans gravité ici).
Il est donc préférable d’éviter les affectations ayant de tels effets de bord, dans la
mesure du possible.
Héritage
Compatibilité des pointeurs
Les pointeurs d’une classe de base et des classes dérivées sont compatibles :
rectangle *pr;
rectplein rp, *prp = &rp;
pr = prp; // autorisé
Il n’est pas nécessaire de préciser un changement de type. Par contre, en sens
inverse il faut le faire, et l’opération est alors périlleuse, car l’ordinateur risque fort
de se planter si l’on fait un appel à une méthode de rectplein avec un pointeur à qui
l’on a affecté une valeur rectangle*. On sait de toute façon que les changements de
types sur les pointeurs doivent être employés avec prudence.
Contrairement à l’affectation d’une instance de la classe dérivée vers la classe de
base, qui fait perdre de l’information (les membres spécifiques à la classe dérivée
sont perdus), l’affectation identique avec les pointeurs ne fait rien perdre : les
membres dérivés sont simplement momentanément inaccessibles. Les méthodes
virtuelles de la classe de base sont cependant appelées correctement. Par exemple, si
l’on a défini un destructeur virtuel et deux méthodes virtuelles trace et efface, les
appels suivants seront corrects :
rectangle r, *pr = new rectangle(r);
pr->trace(); // appel de rectangle::trace
delete pr; // appel de rectangle::~rectangle
pr = new rectplein();
pr->trace(); // appel de rectplein::trace
delete pr; // appel de rectplein::~rectplein
Héritage
Compatibilité des pointeurs …
Cela explique pourquoi l’on parle de polymorphisme. On retiendra l’importance
qu’il y a à déclarer des destructeurs virtuels, même s’ils ne font rien : il n’en sera
pas forcément de même dans les classes dérivées, et le compilateur, comme
l’exemple ci-dessus le montre clairement, ne peut pas déterminer correctement
sur quel genre d’objet pointe pr, et donc quel destructeur appeler s’il n’est pas
virtuel. Noter que pour la même raison, l’appel de sizeof(*pr) donnera toujours la
taille de la classe de base rectangle, même si pr pointe sur un objet rectplein. On
se méfiera de cet opérateur qui ne peut de surcroît pas être redéfini.
Héritage
Classes abstraites
Une classe est abstraite lorsque l’une au moins de ses méthodes virtuelles est pure.
Pour déclarer une méthode virtuelle pure, il suffit de ne pas donner d’implantation et
d’écrire = 0 ; derrière sa déclaration. Seules les fonctions virtuelles peuvent être
déclarées pures, sous peine d’erreur (Error : Non-virtual function 'xxx' declared pure).
Lorsqu’une classe est abstraite, elle ne peut être utilisée directement ; en particulier,
on ne peut pas déclarer d’objets de cette classe, ni d’arguments ou de résultats de
fonction. Si vous tentez de le faire, vous obtiendrez Error : Cannot create a variable
for abstract class 'xxx', on ne peut pas créer une variable de la classe abstraite 'xxx'.
On peut par contre utiliser des références, des pointeurs et dériver de nouvelles
classes, et c’est en fait l’usage de ces classes abstraites. Voici comment déclarer une
classe abstraite liste, puis les deux classes de listes concrètes identiques à celles du
chapitre classes structures :

class liste { // classe abstraite


protected : int nombre;
public : virtual ~liste() { nombre = 0; };
virtual void avance(int combien = 1) = 0; // pure
void recule(int combien = 1) { avance(-combien); }
virtual element& valeur(void) = 0; // pure
unsigned nombre_elt(void) { return nombre; }
void affiche(unsigned combien = 65535);
Héritage
Classes abstraites …
virtual int insere(const element&) = 0; // pure
virtual void supprime(int n = 1) = 0; // pure
};
class listech : public liste {
// liste chaînée
noeud* courant;
public : listech() { nombre = 0; courant = 0; }
listech(int n, const element*); // c. avec table
~listech();
void avance(int combien = 1);
element& valeur(void) { if (courant) return courant->contenu(); }
int insere(const element&);
void supprime(int n = 1); };
class listetab : public liste {
element *tab, *courant;
public : listetab() { courant = tab = 0; }
listetab(int n, const element*); // c. avec table
~listetab();
void avance(int combien = 1);
element& valeur(void) { if (courant) return *courant; }
int insere(const element&);
void supprime(int n = 1);
};
La classe liste est abstraite, puisque quatre de ses méthodes ont été déclarées pures. On
notera que certaines ne le sont pas : il s’agit essentiellement (et pas par hasard) de celles
qui n’étaient pas virtuelles dans notre exemple précédent.
Héritage
Classes abstraites …
Le petit programme de démonstration reste identique, sauf que le premier élément
de listes doit être initialisé en écrivant new listech..., au lieu de new liste.
Les classes abstraites n’ont généralement pas de constructeur, sauf si l’initialisation
des membres est un peu compliquée (ici il suffit de mettre la valeur adéquate dans le
champ nombre, et les constructeurs de listch et listtab le font). Par contre, il est
généralement souhaitable d’y placer un destructeur virtuel, même s’il ne fait rien
comme dans notre exemple : on est ainsi certain de la bonne destruction des objets
des classes dérivées.
Exemple 8. 4
Comment créer un opérateur d’affectation pour les listes ? Et les constructeurs de copie ?
On peut bien sûr créer un opérateur d’affectation pour chaque classe concrète, mais cela ne
permet pas de faire des affectations de l’une de ces classes à l’autre. Voici une solution à ce
problème, basée sur la remarque simple que les méthodes de liste permettent de connaître
entièrement le contenu d’une liste, et donc d’en construire une copie :
class liste { // classe abstraite
// ...
virtual liste& operator=(liste&) = 0;
};
class listech : public liste { // ...
listech(liste& ls) { nombre = 0; *this = ls; }
listech(listech& lc) { nombre = 0; *this = lc; }
liste& operator=(liste&);
};
Héritage
Classes abstraites …
Exemple 8. 4 …
class listetab : public liste { / /...
listetab(liste& ls) { nombre = 0; *this = ls; }
listetab(listetab& lt) { nombre = 0; *this = lt;}
liste& operator=(liste&);
};
liste& listech::operator=(liste& ls) // copie une liste dans this
{ supprime(nombre);
int reste = ls.nombre_elt();
if (!reste) return *this;
noeud *np = 0;
while ( (np = new noeud(ls.valeur(), courant = np)) && (reste--) )
{ ls.avance(); nombre++; }
ls.avance(reste); // rétablir début
if (np) courant = np->suivant(); return *this;
}
liste& listetab::operator=(liste& ls) // copie la liste dans this
{ supprime(nombre);
if ( !ls.nombre_elt() || !(tab = new element[ls.nombre_elt()]) )
return *this;
courant = tab;
nombre = ls.nombre_elt();
for (int i = nombre; i; i--) { *courant++ = ls.valeur(); ls.avance(); }
courant = tab;
return *this;
}
Héritage
Classes abstraites …
Exemple 8. 4 …
On notera que dans le cas de la classe listech par exemple, il faut définir un constructeur de
copie pour un argument de liste& et un pour un argument listech&, bien qu’il y ait une
conversion automatique de la seconde classe vers la première ; en effet, le premier constructeur
avec son argument liste& n’est pas considéré comme un constructeur de copie par C++,
puisqu’il n’inclut aucun argument de type listech& ; de ce fait, le compilateur fournit un
constructeur implicite qui fait une copie membre à membre, ce qui n’est pas souhaitable ici. Le
même raisonnement vaut pour listetab évidemment.
Remarquer également que la copie n’est pas parfaitement identique dans ses effets dans les
deux cas, s’il n’y a pas assez de mémoire : listech recopie tout ce qui est possible, tandis que
listetab ne recopie rien.

Ce type de classe peut paraître curieux au premier abord. En fait, il est assez
pratique, notamment quand, comme dans notre exemple, on souhaite implanter de
plusieurs façons différentes une forme d’objet ; l’utilisateur n’a plus alors qu’à
choisir celle qu’il préfère. Le seul inconvénient, assez léger, vient de ce qu’il faut
utiliser des pointeurs dans ce cas ; cela évite cependant de se tromper en utilisant un
tableau de liste alors qu’il faut un tableau de pointeurs.
Héritage

Polymorphisme automatique

Un comportement idéal serait qu’une classe abstraite ait plusieurs implantations, et que
celles-ci puissent changer toutes seules pour passer de l’une à l’autre. Par exemple,
lorsqu’une liste chaînée classique listech commencerait à déborder, elle se transformerait
toute seule en liste-tableau listetab qui est plus compacte.
Cela n’est pas possible directement, car il peut exister plusieurs pointeurs sur une même
instance de classe dans un programme. Si celle-ci change, elle changera probablement de
position en mémoire, et les pointeurs vont se retrouver incorrects ; une telle chose n’est pas
prévisible directement dans les méthodes des classes.
Ce polymorphisme automatique peut cependant être implanté.
Héritage Multiple

Jusqu’à présent nous avons utilisé des classes qui dérivaient d’une unique classe de base. Il
est parfaitement possible qu’une classe hérite de plusieurs classes. Voici un exemple :

class A { // ... }; class B { // ... }; class C : public A, B { // ... };

La classe C hérite de manière publique de A et de manière privée de B (il faut préciser à


chaque classe le type de dérivation, sinon c’est le type par défaut qui s’applique). Elle a trois
sortes de membres : les siens propres ; ceux hérités de A ; ceux hérités de B. Les règles
d’héritage sont les mêmes que dans l’héritage simple. Le constructeur de C appelle les
constructeurs de A et B, implicitement ou non :

C::C() : A(), B() { // ... }

Noter que dans cette écriture, tout comme dans la déclaration d’héritage, c’est une virgule
qui sépare les différentes classes de base, et non le symbole deux-points.
Héritage
Conflits de nom
Lorsqu’une classe hérite de plusieurs autres, il se peut que deux des classes de base aient
des champs ou des méthodes ayant le même nom. S’il s’agit d’un champ d’une part, et
d’une méthode d’autre part, ou de deux méthodes mais avec des listes d’arguments
différents, il n’y a pas d’ambiguïté et le compilateur se débrouillera en fonction du contexte
d’utilisation.
Par contre, lorsqu’il s’agit de deux champs, ou de deux méthodes ayant les mêmes
arguments, le compilateur se trouve face à une ambiguïté insoluble. Pour la résoudre, il faut
utiliser le nom d’une des classes de base et l’opérateur de résolution de portée. Par exemple,
si les classes A et B ont toutes deux un champ x, il faudra écrire :
C c; c.A::x = 0;
Lorsqu’il s’agit de méthodes, il est préférable de recouvrir les méthodes de base en
déclarant une méthode dans la nouvelle classe ayant le même nom et les mêmes arguments.

Arbre de dérivation
Il n’est pas permis de faire des cycles en cours de dérivation ; càd qu’une classe C ne peut
pas hériter d’elle-même, par l’intermédiaire d’une ou plusieurs autres classes.
Par contre, si deux classes A et B dérivent toutes deux d’une classe Z, on peut dériver une
classe C de A et B ; la classe comprendra alors deux instances de Z. (Il faut alors utiliser les
:: pour distinguer les membres hérités par l'intermédiaire de l'une ou l'autre.)
Assez étrangement, si une classe B dérive de A, on ne peut pas dériver une classe C de A et
B ; le compilateur affiche une erreur (Error : 'A' is inaccessible because also in 'B', 'A' est
inaccessible car également dans 'B').
Héritage
Héritage virtuel
Nous avons dit précédemment que si deux classes A et B dérivent d’une même
troisième Z, une dérivation de A et B placera dans la classe dérivée C deux copies
de Z.
Il est possible d’éviter ce comportement lorsqu’il n’est pas souhaitable. Il faut pour
cela que les classes A et B aient été dérivées de manière virtuelle de Z :
class Z { ... };
class A : virtual public Z { ... };
class B : virtual Z { ... };
class C : public A, B { ... };

La classe C dans ce cas ne contient qu’une instance de Z. Les classes A et B sont


identiques à ce qu’elles étaient auparavant, sauf que le compilateur sait que
l’instance de Z peut être à un emplacement inhabituel (c’est le cas dans C) ; les deux
classes doivent être dérivées virtuellement de Z.
On notera que dans ce cas, il n’y a pas d’ambiguïté lorsqu’on utilise avec une
instance de C les membres hérités de Z (alors qu’il y en a une si la dérivation n’est
pas virtuelle, même pour les méthodes puisqu’elles ne savent sur quel instance
s’appliquer si l’on n’utilise pas un spécificateur A:: ou B::), sauf si les deux classes
intermédiaires ont redéfini une méthode virtuelle de Z (auquel cas le compilateur ne
peut choisir) ; si une seule des deux classes intermédiaires a redéfini une méthode
virtuelle de Z, c’est la méthode redéfinie, et non la méthode de Z, qui sera utilisée.
Héritage
Fonctionnement interne
Après toutes ces observations sur l’héritage des classes, le lecteur se demande peut-être
« comment ça marche » . Nous expliquons ci-après comment C++ implante les classes (il
peut y avoir des variations selon les compilateurs) ; pourquoi les instances de classes
contenant des méthodes virtuelles prennent deux octets de mémoire de plus que les autres ;
pourquoi certaines opérations sont impossibles.
Prenons d’abord le cas simple suivant :
class A { int a1;
public : // ... méthodes
};
class B : A { int b1;
public : // ... méthodes
};
La configuration en mémoire d’une instance de B est alors la suivante (chaque petit carré
représente un octet) :
La partie grisclair représente ce qui est hérité
this
de la classe A, tandis que la partie blanche
a1 b1 indique ce qui est défini directement dans B.

Si une méthode de A est appelée, elle reçoit comme les autres l’adresse de l’objet par
l’intermédiaire du pointeur this
this. Or, vu l’ordre dans lequel les champs sont placés, ce
pointeur indique en mémoire la partie « instance de A » de l’objet ; de ce fait, les méthodes
de A fonctionnent exactement de la même façon sur une instance de A ou de B, et
n’utilisent dans ce dernier cas que la partie gris clair.
Héritage
Fonctionnement interne …
Compliquons un peu les choses en supposant que B a des méthodes virtuelles :
class B : A { int b1;
public :
virtual void b2();
virtual void b3();
void b4();
};
Dans ce cas, les instances de B sont représentées différemment en mémoire :
this Chaque instance contient à présent un
a1 b1 pointeur caché sur une table fixe (il en
existe une seule pour toute la classe B)
La tables des virtuelles de B qui contient les adresses des méthodes
virtuelles. Lorsque le compilateur
&B:b2 rencontre un appel à b2 par exemple, il
&B:b3 regarde ce pointeur caché dans this (ou
dans l’objet qui appelle b2), puis
Ce processus s’appelle lien dynamique ou lien tardif l’augmente d’autant que nécessaire (ici 0)
(en anglais late binding). On notera que les méthodes pour être sur la bonne méthode ; ayant
non virtuelles en sont exclues (b4). ainsi l’adresse de la méthode, il ne reste
plus qu’à y sauter
Nous allons voir comment cela fonctionne plus exactement en imaginant une nouvelle classe :
class C : B { int c1;
public : void b2();
void b3();
virtual void c2(); };
Héritage
Fonctionnement interne …
Cette classe recouvre les deux méthodes virtuelles de B. Une instance de C a l’allure suivante
this
a1 b1 c1
La tables des virtuelles de C
&C:b2
&C:b3
&C:c2
Le pointeur caché n’a maintenant plus la même valeur que dans les instances de B : il pointe
sur une nouvelle table particulière à C, et dans laquelle les adresses des méthodes recouvertes
figurent à la place de celles de B. Lorsque le compilateur rencontrera un appel à b2 avec une
instance de C, il ira chercher dans cette table-ci (sans le savoir, car il exécute exactement le
même travail qu’avant), et passera donc dans la bonne méthode recouverte C::b2. Ceci
explique le fonctionnement des méthodes virtuelles : le pointeur caché est identique pour
deux instances d’une même classe, mais différent pour deux classes distinctes ; de ce fait, il
caractérise la classe à laquelle appartient l’instance, et permet donc de choisir la bonne
méthode.
Compliquons encore le jeu avec un héritage multiple :
class D : B { class E : D, C {
int d1; int e1;
public : public :
virtual void d2(); void b3();
void b3(); void c2();
}; };
Héritage
Fonctionnement interne …
L’allure d’une instance de D est la suivante
this
a1 b1 d1
La tables des virtuelles de D
On remarque que, comme la classe D n’a pas
recouvert la méthode virtuelle b2, c’est l’adresse &B:b2
de B::b2 qui figure en première place dans la table. &D:b3
&D:d2
Jusqu’à présent nous n’avons augmenté la taille des objets que de deux octets au maximum.
Il n’en est plus de même avec l’héritage multiple. Voici l’allure en mémoire d’une instance
de E :
this
a1 b1 c1 a1 b1 d1 e1
La tables des virtuelles de E
&C:b2 Les tables sur lesquelles ils pointent sont
semblables à ce qu’elles étaient dans C et
Dans ce cas, il y a deux pointeurs cachés, &E:b3 D, sauf que les méthodes recouvertes de E
dans chacune des deux instances de base &E:c2 y remplacent celles de C ou D. On notera
C et D contenues dans E.
&B:b2 que la méthode E::b3 figure deux fois
&E:b3 dans la table de E, parce qu’elle recouvre
à la fois C::b3 et D::b3.
&D:d2
Héritage
Fonctionnement interne …
Lorsqu’on appelle une méthode de D avec une instance de E, ce n’est pas le pointeur this qui est
passé, mais celui que nous avons nommé « this bis » , qui correspond au début de D en mémoire.
Lorsqu’on utilise un héritage virtuel, la situation est beaucoup plus complexe. Supposons que les
classes C et D aient été déclarées en héritage virtuel de B :

class C : virtual B { ... }


class D : virtual B { ... }
Dans ce cas, l’allure d’une instance de C en mémoire est bien différente :
this
c1 a1 b1
La tables des virtuelles de C
Trois pointeurs sont ajoutés ; le premier, &C:b2 Au milieu, un troisième pointeur
en tête, indique l’emplacement dans donne les adresses des méthodes
&C:b3 virtuelles de C (dans l’ordre de
l’instance du début de la partie héritée de
B (cet emplacement variera dans les &C:c2 déclaration) ; les méthodes b2 et
classes dérivées de C). À la fin de l’objet, b3 sont ici remplacées par b2* et
&C:b2 b3*, qui sont identiques à ceci
un pointeur désigne une table
formellement identique à celle de B, mais &C:b2 près qu’un petit bout de code
avec les adresses des méthodes virtuelles &C:b3 avant fait remplacer this par le
recouvertes pointeur de tête, afin que les
méthodes aient la bonne adresse.
Héritage
Fonctionnement interne …

L’allure de D est assez semblable, sauf que b2, qui n’est pas recouverte dans D, n’apparaît pas
dans la première table :
: this
d1 a1 b1
La tables des virtuelles de D
&D:d2
&D:b3
&B:b2
&D:b3
Héritage
Fonctionnement interne …
On a à présent ceci dans E :
*this bis
this
c1 d1 c1 a1 b1
La tables des virtuelles de E

La partie initiale correspond à C ; elle


&C:b2
comprend le pointeur de tête sur la base &E:b3
héritée de B, le champ c1 et un pointeur sur &E:c2
les trois méthodes virtuelles de C, toutes
recouvertes dans E ; notons que la partie B de &D:d2
C n’existe plus (pas de duplication). La suite &E:b3
correspond à D : on trouve le pointeur de tête
sur la partie B, le champ d1 et un pointeur &C:b2
sur les deux méthodes virtuelles de D, dont &E:b3 Tout cela est compliqué par le
une recouverte dans E (b3). Vient ensuite le fait que les méthodes doivent
nouveau champ e1 ; puis enfin la partie être augmentées de petits bouts
héritée de B, en un seul exemplaire, avec à la de code destinés à récupérer la
fin un pointeur sur les méthodes virtuelles de bonne adresse de this. L’adresse
B, toutes deux modifiées dans E (une « this bis » est celle qui est
directement par E, l’autre indirectement par utilisée pour les méthodes de D.
C).
On retiendra surtout qu’il s’agit d’un processus complexe, dans lequel il est préférable de
ne pas intervenir, et que la taille des objets est difficile à prévoir (utiliser l’opérateur
sizeof pour la connaître).
La gestion de l’erreur

//Classe vecteur d'entier


class vect
{
int nb_elem,* tab,
public :
vect(int n){tab = new int[nb_elem = n];};
~vect() {delete tab;}
int & operator [] (int i){
return tab[i];};
};
int main()
{

vect v(3); v[8] = 1; //dépassement d'indice

}
Les exceptions en C++
Motivation et objectifs
• Détection des conditions « exceptionnelles » pouvant être rencontrées
au court de l’exécution d’un programme (problèmes d’allocation
mémoire, mauvaise manipulation de l’utilisateur,…)
• Traitement des incidents rencontrés.
• Dissociation entre la détection et le traitement des incidents.
• Distinguer les exceptions des erreurs de programmation.

La gestion des exceptions en C++


• C++ offre un mécanisme puissant de gestions de ces anomalies
• Découplage total de la détection d’une exception et de son traitement
• Affranchissement de la hiérarchie des appels + assurance d’une bonne
gestion des objets automatiques
Avertissement
Le bon usage des exceptions et leur bonne gestion est un sujet non trivial
qui demande de l’expérience.
Principe général des exceptions
exceptions

1. Exception = une rupture de séquence dans l’exécution du programme


lancée par throw.

2. La rupture de la séquence est déclenchée (ou lever) par une instruction


throw(expression d’un type donné). Le type permet l’identification de
l’exception.

3. Branchement à un gestionnaire d’exception.

4. Pour pouvoir être détectée, une exception doit se trouver au sein d’un
bloc try qui est immédiatement suivi des gestionnaires d’exceptions.

5. Le choix du gestionnaire est déterminé par le type de l’exception :


catch(expression d’un type donné).
Exemple d’une ex
exceptions
ceptions
Définition de l’excepion
//declaration d'une classe pour un type d'exception (vide)
class vect_depassement
{
};
//Classe vecteur d'entier //Interception d'une exception vect_depassement
class vect int main()
{ { try {
int nb_elem,* tab, try // Code susceptible de générer des
public : { exceptions... }
vect(int){tab = new int[nb_elem = n];}; vect v(3); v[4] = 1; //dépassement d'indice
~vect() {delete tab;} }
int & operator [] (int){ catch( vect_depassement l)
if (i<0 || i>nb_elem) {
{ cout << "Exception : depassement d'indice \n";
vect_depassement l; }
throw
catch (l);
(classe [&][temp]) { // Traitement}de l'exception associée à la classe }
}
return tab[i];};
};
Sortie du programme : Exception : dépassement d'indice
Exemple d’une ex
exceptions
ceptions

Au moment où on constate l’erreur :


1. On crée une instance d’un type choisi
2. On initialise cette instance de manière à donner le plus d’informations
possibles sur l’erreur qui s’est produite.
3. On envoie cette instance avec throw.
4. L’instance remonte la pile des fonctions appelantes, jusqu’à trouver un catch
qui attrape ce type choisi.
5. Le bloc correspondant du catch est exécuté.
6. Si aucun bloc catch n’est rencontré alors c’est le gestionnaire d’exception par
défaut qui sera utilisé (celui là existe toujours!), et en général, il provoque la
fin du programme avec un message d’erreur signalant qu’une exception est
survenue (mais sans plus de détail puisqu’il n’a aucune raison de savoir
comment traiter cette exception là).
Il est aussi possible d’attraper n’importe quel type en indiquant « … » à la
place du type à attraper : catch(…) { } .
Interprétation de l’Exemple d’une ex
exceptions
ceptions

• Dans l’exemple précédent :


- un seul type d’exception,
- un seul gestionnaire d’exception,
- pas d’information transmise au gestionnaire
(classe vect_depassement vide).
• Le gestionnaire est défini indépendamment des fonctions
pouvant déclencher les exceptions : l’utilisateur de la classe vect
peut définir des gestionnaires différents suivant les utilisations.
• Attention, si le bloc try n’était pas présent, l’exception lancée par
[] provoquerait un arrêt du programme.
On a pu reprendre le programme sans l’interrompre
• Le gestionnaire d’exception n’est pas obligé de mettre fin au programme.
• Dans ce cas, l’exécution du programme se poursuit à la suite du bloc try
concerné.
Question de la destruction des objets définis au sein du bloc try et
non détruits au moment ou l’exception est lancée (voir plus loin).
Exemple d’une ex
exceptions
ceptions
class vect_depassement vect::vect(int n) //constructeur
{ {
public: if (n <= 0)
int out; {
//indice de depassement vect_bad_size c(n); //taille non admissible
vect_depassement(int i) { out = i; } throw c;
//constructeur }
}; tab = new int[nb_elem = n]; //construction normale
}
class vect_bad_size
{ int & vect::operator [] (int i)
public: {
int nb; if (i<0 || i>nb_elem)
//taille demandee {
vect_bad_size(int n) { nb = n; } vect_depassement l(i); //depassement indice
//constructeur throw (l);
}; }
return tab[i];
}
Exemple d’une ex
exceptions
ceptions

int main()
{
try
{
vect v(-1); //provoque exception vect_bad_size
v[5] = 3; //Provoquerait exception vect_depassement
}
catch(vect_depassement l)
{
cout << "Exception depassement indice : " << l.out << "\n";
}
catch(vect_bad_size c)
{
cout << "Exception vect_bad_size : " << c.nb << "\n";
}
return 0;
}

Sortie : Exception vect_bad_size : -1


Interprétation de l’Exemple d’une ex
exceptions
ceptions

• La seconde exception n’est pas lancée : la fin du bloc try n’est pas exécutée.
• Les membres des classes d’exceptions permettent de transmettre des
informations au gestionnaires.
• Le choix du gestionnaire se fait en fonction du type de l’exception.

Poursuite de l’exécution
• Lorsque le gestionnaire d’exception ne met pas fin à l’exécution :
1. Exécution du code du gestionnaire
2. Reprise de l’exécution à la suite du bloc try concerné
• Le mécanisme de traitement des exceptions supprime toutes les
variables automatiques des blocs dont on provoque la sortie.

Attention! Ce mécanisme ne s’applique pas aux objets dynamiques.


Exemple d’une ex
exceptions
ceptions
#include <iostream>
using namespace std;
int main()
{ int x = 5;
int y = 0;
int result;
int exceptionCode = 25;
try
{
if ( y == 0 ) { throw exceptionCode; } throw "Division par 0 ;
result = x/y;
}
catch ( int e )
{
if (e == 25) { cout << "Division par 0" << endl; }
else {cout << "Exception inconnue" << endl; } catch (char *e) { cout << e << endl; }
}
cout << "Goodbye" << endl;
system("PAUSE");
return EXIT_SUCCESS;
}
sortie Division par 0 ;
Goodbye
Regroupement des exceptions
exceptions

Permet un regroupement des exceptions pour un traitement plus


ou moins fin.

class vect_erreur {…};


class vect_depassement : public vect_erreur {…};
class vect_bad_size : public vect_erreur {…};
try {…}
try {…} ou
catch (vect_depassement l) {…}
catch (vect_erreur e) {…}
catch (vect_bad_size c) {…}

Intercepte à la fois vect_depassement et


vect_bad_size
Interprétation de l’Exemple d’une ex
exceptions
ceptions

int main()
{ Création d’un objet dynamique…
try
{
v = new vect(3); //allocation dynamqie d'un vecteur
v[5] = 3; … pas détruit si n != 0,1,2
delete v;
}
catch(vect_depassement l)
{
cout << "Exception depassement indice : " << l.out << "\n";
.... //gestion de l'exception vect_depassement
..... //poursuite de l'exécution
}
return 0;
}

Le mécanisme de gestion ne détruit pas les objets alloués dynamiquement…


Problème de gestion des ressources

Poursuite de l’exécution après traitement d’une exception

Problèmes d’acquisition de ressources.

Acquisition de ressources :
• création dynamique d’objet,
• allocation dynamique de mémoire,
• ouverture d’un fichier,
• verouillage d’un fichier (lecture/écriture)

Nécessité de pouvoir libérer les ressources déjà acquises en cas


d’exception.
La gestion des exceptions
Déclarer des exceptions dans des fonctions

#include <iostream> void f()


#include <cstdlib> {
using namespace std; try
{
int main() int n=2;
{ throw n;
try }
{ catch (int)
void f(); {
f(); cout << "exception int dans f
} \n";
catch (int) throw;
{ }
cout << "exception int dans main \n"; }
exit(-1);
}
return 0;
}
La gestion des exceptions

Quand une exception est levée par une fonction :

1. On cherche d’abord un gestionnaire dans le bloc try (éventuel) associé


à cette fonction .

2. En cas d’échec, on cherche dans un bloc try (éventuel) d’une fonction


appelante, etc..

3. Si aucun gestionnaire n’est trouvé : on appelle la fonction terminate


par défaut, elle met fin à l’exécution.

4. Dans tous les cas, dès qu’un gestionnaire qui convient a été trouvé,
l’exception est traitée…
La librairie standard des exceptions
exceptions

• La classe exception (std::exception) est la classe de base de toutes les exceptions


lancées par la bibliothèque standard <exception>

class exception
{ public:
exception() throw(){ } // Constructeur.
……..
virtual ~exception() throw(); // Destructeur.
virtual const char* what() const throw(); // Renvoie une chaîne de caractère contenant des infos sur
l'erreur. };

• Elle est aussi spécialement pensée pour qu'on puisse la dériver afin de réaliser notre
propre type d'exception.
La librairie standard des exceptions
exceptions

• Plusieurs exceptions standard susceptibles d’être déclenchées par une


fonction ou un opérateur de la bibliothèque standard
• Hiérarchie :

exception
logic_error
domain_error
invalid_argument
length_error
out_of_range
runtime_error
range_error
overflow_error
underflow_error
bad_alloc Lancée s'il se produit une erreur lors d'un new.
bad_cast Lancée s'il se produit une erreur lors d'un dynamic_cast.
bad_exception Si aucun catch ne correspond à un objet lancé.
bad_typeid Lancée s'il se produit une erreur lors d'un typeid.
La librairie standard des exceptions
exceptions
Ex. Bad_alloc en cas d’échec d’allocation mémoire par new

vect::vect (int n) // Constructeur de la class vect


{ adr = new int [nelem = n] ;}
int main ()
{ try { vect v(-3) ; }
catch (bad_alloc) // Si le new s’est mal passé
{ cout << "exception création vect avec un mauvaise nombre
d'éléments " << endl ;
exit (-1) ;
}
}
exception création vect avec un mauvaise nombre d'éléments

• En cas d’exception non gérée appel automatique à terminate() qui


exécute abort();
La librairie standard des exceptions
exceptions

Notez que l’exception bad_alloc lancée par les gestionnaires de mémoire lorsque
l’opérateur new ou l’opérateur new[ ] n’a pas réussi à faire une allocation n’est
pas déclarée dans l’en-tête stdexcept non plus. Sa déclaration a été placée avec
celle des opérateurs d’allocation mémoire. Cette classe dérive toutefois de la
classe exception, comme le montre sa déclaration :

class bad_alloc : public exception


{
public:
bad_alloc() throw();
bad_alloc(const bad_alloc &) throw();
bad_alloc &operator=(const bad_alloc &) throw();
virtual ~bad_alloc() throw();
virtual const char *what() const throw();
}
Gestion des exceptions
exceptions
• Classe exception ayant une méthode virtuelle what() renvoi un pointeur sur une
chaîne de caractères précisant la nature de l’exception. cette fonction doit être
redéfinie dans les classes dérivées (elle l’est déjà dans les classes précédentes).
int main ()
{try { vect v(-3) ;}
catch (bad_alloc b)
{ // Appel de la méthode what pour l’exception bad_alloc
cout << b.what() << endl ; exit (-1) ;
}}

• Toutes les classes possèdent un constructeur dont l’argument est une chaîne
dont la valeur peut être récupérée par what.
• Possibilité de dériver ses propres classes de la classe exception
class mon_exception : public exception
{ public :
mon_exception (char * texte) { ad_texte = texte ; }
const char * what() const throw() { return ad_texte ; }
private :
char * ad_texte ;
};
Gestion des exceptions
exceptions
int main()
{ try
{ cout << "bloc try 1" << endl;
throw mon_exception ("premier type") ;
}
catch (exception & e)
{ cout << "exception : " << e.what() << endl; }
try
{ cout << "bloc try 2" << endl;
throw mon_exception ("deuxieme type") ;
}
catch (exception & e)
{ cout << "exception : " << e.what() << endl;
}}

bloc try 1
exception : premier type
bloc try 2
exception : deuxieme type
La librairie standard des exceptions
exceptions

L’en-tête exception contient également la déclaration de la classe d’exception


bad_exception. Cette classe n’est, elle aussi, pas utilisée en temps normal. Le seul
cas où elle peut être lancée est dans le traitement de la fonction de traitement d’erreur
qui est appelée par la fonction std::unexpected
lorsqu’une exception a provoqué la sortie d’une fonction qui n’avait pas le droit de la
lancer. La classe bad_exception est déclarée comme suit dans l’en-tête exception :

class bad_exception : public exception


{
public:
bad_exception() throw();
bad_exception(const bad_exception &) throw();
bad_exception &operator=(const bad_exception &) throw();
virtual ~bad_exception() throw();
virtual const *char what() const throw();
};
La classe string
"Bonjour tout le monde !"

Les chaînes de caractères C++ sont définies par le type string.


En toute rigueur, ce n’est pas un type comme les types élémentaires mais une classe.

Pour utiliser des chaînes de caractères string, il faut tout d’abord importer la librairie
#include <string>

La déclaration d’une chaîne de caractères se fait alors avec : string identificateur;

#include <string>
...
// déclaration
string un nom;
// déclaration avec initialisation
string maxime("Why use Windows when there are doors?");
La classe string
Affectation de chaînes
Comme pour n’importe quel autre type, toute variable de type string (qui n’a
pas été déclarée comme constante) peut être modifiée par une affectation.

Exemple : string chaine ; // -> chaine vaut ""


string chaine2("test") ; // -> chaine2 vaut "test"
chaine = "test3" ; // -> chaine vaut "test3"
chaine = chaine2 ; // -> chaine vaut "test"
chaine = ’a’ ; // -> chaine vaut "a"

Remarques :
 dans le cas de l’affectation d’un caractère, la valeur affectée à la chaîne est
la chaîne réduite au caractère affecté.
 Toute variable déclarée mais non initialisée est automatiquement initialisée
à la valeur correspondant à la chaîne vide ("").
La classe string
La concaténation

La concaténation de chaînes est représentée par l’opérateur +.


chaine1 + chaine2
Les combinaisons suivantes sont possibles pour la concaténation de deux chaînes :
string + string, string + "...", "..." + string, string + char, char + string
• string correspond à une variable de type string,
• "..." correspond à une valeur littérale
• char à une variable ou une valeur littérale de type char.
Remarque : les concaténations de la forme string+char (resp.char+string) constituent un
moyen très pratique pour ajouter des caractères à la fin (resp. au début) d’une chaîne.
exemple:
string nom, prenom, famille;
...
nom = famille + ’ ’ + prenom;
...
if (n > 1) {
reponse = reponse + ’s’;
}
...
La classe string
Les fonctions suivantes sont définies (où chaine est une variable de type string) :
• chaine.size() : renvoie la taille (i.e. le nombre de caractères) de chaine.
• chaine.insert(position, chaine2) : insère, à partir de la position (indice) position dans la
chaîne chaine, la string chaine2
string exemple("abcd"); // exemple vaut "abcd"
exemple.insert(1,"xx"); // exemple vaut "axxbcd" construit la chaîne "axxbcd".

• chaine.replace(position, n, chaine2) : remplace les n caractères d’indice position,


position+1,..., position+n-1 de chaine par la string chaine2.
string exemple("abcd");
exemple.replace(1,2,"1234"); //construit la chaîne "a1234d" (dans exemple).
• chaine.substr(depart, longueur) : renvoie la chaîne de chaine, de longueur longueur et
commençant à la position depart. string("Salut à tous !").substr(8, 4) //renvoie "tous".
• chaine.find(souschaine) : renvoie l’indice dans chaine du 1er caractère de l’occurrence la
plus à gauche de la string souschaine. string("baabbaab").find("ab") //renvoie 2.
• chaine.rfind(souschaine) : renvoie l’indice dans chaine du 1er caractère de l’occurrence la
plus à droite de la string souschaine. string("baabbaab").rfind("ab") //renvoie 6.
Remarque : Dans les cas où les fonctions find() et rfind() ne peuvent s’appliquer, elles
renvoient la valeur prédéfinie string::npos
Généralité Sur Les Flux (Ou Flots)

• La lecture et l’écriture dans un fichier sont des opérations d’entrée / sortie.


– Utilisation de la bibliothèque iostream
Généralité Sur Les Flux (Ou Flots)

Librairie de gestion d ’entrée/sortie:

• <iostream.h> : contient les objets cin, cout, cerr et clog


• <iomanip.h> : contient les manipulations paramétrées du flux
• <fstream.h> : contient importante informations de l’utilisation des procès
du contrôle du fichiers
Fonctions membres de iostream
iostream

Librairie iostream.h :

-Librairie de gestion des entrées istream qui contient :

- cin: flux d’entrée standard (par défaut le clavier)

- Librairie de gestion des sorties ostream qui contient :::

- cout : flux de sortie standard (par défaut l’écran) ;


- cerr : flux de sortie des messages d’erreur (par défaut l’écran)
- clog : flux d’erreur mais avec un tampon.
Fonctions membres de ostream
Comparaison entre C / C++ pour les flux

C C++
Afficher à l'écran
Un message Printf ("salut"); cout << "Salut";
Type Entier Printf ("%d",iAge); cout << iAge;
Type Float Printf ("%f",fAge); cout << fAge;
Type Caractère Printf ("%c",cGenre); cout << cGenre;

• << est un opérateur de sortie qui lui est associé à cout.


• cout << ‘\n’;n’; new line : imprime une nouvelle ligne
• cout << endl
endl;; end line : fin de la ligne
• cout << flush
flush;; vide le tampon d'entrée/sortie, en assurant que toutes les
opérations précédantes sont effectuées.
• cout.setf
cout. setf((ios::
ios::floatfield
floatfield |scientific |fixed|
fixed| uppercase )
• cout.setf
cout. setf(( ios::
ios::showpoint
showpoint | ios::
ios::showpos
showpos||ios::
ios:: ||left
left ||ios
ios::
::right|ios
right|ios::
::adjustfield
adjustfield))
• cout.fill
cout.fill(( '*'),
• cout << octoct|| hex
hex|| dec << n;
Fonctions membres de iomanip

Comparaison entre C / C++ pour les flux

C C++

Lire a partir du clavier

Type Entier scanf("%d",&iAge); cin >> iAge;

Type Float scanf("%f",&fAge); cin >> fAge;

Type caractère scanf("%c",&cGenre); cin >> cGenre;


Fonctions membres
Fonctions membres de
deistream
istream

• >> : opérateur de sortie qui lui est associé à cin.


cin.
• cin.get()
cin. get() : entre un caratère du flot (avec espace) et le retourne
• cin.get(
cin. get( c ) : entre un caractère du flot et le stoke dans c
• cin.get(
cin. get(array,size
array,size,‘,‘\\n’) : accepte 3 arguments: array du caractère, la taille limite,
et a délimite (par défaut de ‘\ ‘\n’).
• cin.getline
cin. getline((array,
array, size) Lecture d’une ligne qui est mise dans le tableau de
caractère
• cin.width
cin. width(n)
(n) décalage de n espaces
• cin.eof()
cin. eof() : retourne vrais si la fin du fichier
• cin.bad()
cin. bad() : retourne vrais si l’opération flot est faut
• cin.clear
cin. clear()
() eof
eof,, bad
bad,, fail (vrai si une erreure est apparue), and good (vrai si
l’ouverture est bonne)
• .peek : retourne le caractère suivant de la stream sans le déplacer

cin peut être remplacer par un fichier


Fonctions membres de iomanip

• setbase() : change de base la donnée entrée


• setprecision()
• setf, unsetf and flags
• setw(x)
setw(x) : la prochaine sortie serait sur une largeur de x caractères.
• setfill(c)
setfill (c) : utilisera c comme caractère de remplissage.
• left : la prochaine sortie sera cadrée à gauche.
• right : la prochaine sortie sera cadrée à droite (défaut).
• uppercase : prochaine sortie en majuscule.
• lowercase : prochaine sortie en minuscule.
• flush : vide le tampon d'entrée/sortie, en assurant que toutes les
opérations précédantes sont effectuées.
Fonctions membres de iomanip

Comparaison entre C / C++ pour les flux

C C++

Afficher à l'écran avec un format

cout <<setw(4)<< iAge;


un entier avec 4 espaces Printf ("%4d",iAge);

Un float avec 3 chiffres Printf ("%0.3f",fAge); #include <iomanip.h>


après le décimal
cout.setf(ios::showpoint);
cout << setiosflags
(ios::fixed) ;
cout.precision (3);
cout << fAge;
Déclaration d’un fichier

• Un fichier possède un nom physique (connu du système d’exploitation)


(exemple: Cours.ppt).
• Pour utiliser un fichier dans un programme, on lui associe un nom logique
(le nom de la variable de type fichier).
• Avec la bibliothèque iostream, le type fichier est fstream.
• Déclaration:
fstream nom_logique ;
On a déclaré une variable de type fichier (fstream).
Cette variable ne correspond encore à aucun fichier physique.
Ouverture d’un fichier

• C’est la qu’on associe un fichier physique à une notre variable


de type fichier (fstream).
• Syntaxe:

nom_logique . open (nom_physique , mode_ouverture);

Nom de la variable
de type fichier Mode d’ouverture
Nom physique du du fichier
fichier sur le disque
Les modes d’ouverture de fichiers

– Ecriture ios::out : Le pointeur de position du fichier est placé au début


(le fichier est effacé).
– Lecture ios::in : Le pointeur de position du fichier est placé au début du
premier enregistrement du fichier.
– Mise à jour : ios::app : Le pointeur de position du fichier est placé à la
fin du fichier physique (après le dernier enregistrement).
• Fermeture d’un fichier: nom_logiques . close( );
• Vérifier l’ouverture : nom_logique .is_open() ;
Les modes d’ouverture de fichiers

 L’opérateur >> (fonctionne comme cin).


nom_fichier_logique >> variable1 [>> variable2>> …];
Espace et le saut de ligne agissent comme des séparateurs
 read(char* chaine, int taille)
Lecture de n caractères qui sont mis sous forme de chaine
 gcount : Retourne le nombre total de caractères lues dans la dernière opération
d’input
Les modes d’ouverture de fichiers

Comparaison entre C / C++ pour les flux

C C++

Ouvrir le <FP>= fopen(<Nom>,"r"); ifstream<FP> (<Nom>,ios::nocreate);


fichier en fEntreClients= ifstream fentreClients("client.dat",ios:
lecture fopen("client.dat« "r"); :nocreate);

ofstream<FP>(<Nom>,ios::out) ;
Ouvrir le <FP>= fopen(<Nom>,"w"); ofstream fSortieClients
fichier en fSortieClients= ("sortie.dat",ios::out)
écriture fopen("sortie.dat","w");

Fermer le fclose(<FP>); <FP>.close(); (optionnel)


fichier fclose (fSortieClients); fSortieClients.close()
Lecture séquentielle d’un fichier

#include <fstream.h>
int main ()
{
char car;
fstream f; // déclaration d’une variable de type fichier (ou flux)
f . open ("fich1.txt" , ios::in); /* ouverture en lecture. */
// lecture et affichage du fichier caractère par caractère
f.get (car); // on lit le premier caractère
while (! f.eof ()) // tant que le fichier n’est pas fini
{
cout<< car;
f . get (car); // on lit 1 caractère //le pointeur de position est alors décalé
}
f.close();
return 0;

}
Lecture séquentielle d’un fichier
#include <fstream.h>
int main ()
{
char chaine[255];
// déclaration et ouverture d'un fichier texte
fstream fich;
fich . open ("fich1.txt" , ios::out);
// lecture au clavier et écriture sur chaîne de caractères
cin >> chaine;
while (strcmp(chaine,"stop")!=0) //on arrête quand l’utilisateur marque stop
{
fich<<chaine; //on écrit la chaîne de caractères
/*on aurrait pu utiliser fich1.write(chaine, 255)
mais les 255 caractères aurait été affiché !*/
cin>>chaine;
}
// fermeture du fichier
fich.close ();
return 0;
}
Lecture séquentielle d’un fichier

Comparaison entre C / C++ pour les flux

C C++

Lire a partir d'un


fichier fscanf(<FP>,"…",<Adr>); <FP> >> variable;
fscanf(fEntreClients, " %d", &trans); fEntreClients >> trans;
Ecriture séquentielle d’un fichier

• Différentes fonction de lecture:


– L’opérateur << (fonctionne comme cout).
nom_fichier_logique << variable1 [<< variable2<< …];
– L’opérateur put (char car)
nom_fichier_logique . put ( car) ;
Ecriture d’un caractère
– L’opérateur write(char * chaine, int taille)
nom_fichier_logique . write ( chaineCar , n) ;
Ecriture des n caractères contenus dans ChaineCar
• Vérification que le fichier a bien été ouvert
nom_fichier_logique . good( )
• Remise à 0 du statut de fin de fichier activé à la fin d’une lecture.
nom_fichier_logique . clear( );
Ecriture séquentielle d’un fichier

Comparaison entre C / C++ pour les flux

C C++

fprintf(<FP>,"...",<Adr>); <FP> << variable;

fprintf(fSortieClients, "%d",trans); fSortieClients << trans;


Les fichiers binaires en C++
• Le fichier est beaucoup plus léger en terme de taille !
• La taille de la zone ne dépend que du type écrit et non de sa valeur.
• Chaque donnée ayant une taille connu dans le fichier binaire. On peut se
positionner aisément ou on veut.
• Pas besoin de convertir les int, float ou structures en chaînes de caractères.

On mémorise dans un fichier un tableau de n structures TDate.


En texte, il est très difficile d’accéder à la iéme date sans tout lire avant,
car on ne connais pas la taille prise par l’écriture des autres dates.
En binaire, on connais la taille d’une date. On peut donc se positionner
aisément à la date que l’on veut lire.

• Par contre le contenu des fichiers binaires est difficile à interpréter ou a


visualiser.
Les fichiers binaires en C++
• Pareil que les fichiers textes, mais il faut spécifier que l’on va travailler
en binaire. ios
ios::
::binary
binary .
• Ecriture:
fstream f;
f.open (nom_physique , ios::out|ios::bin);
ios::out|ios::bin);
f.write ( (char*)buff
(char*)buff , n );

Adresse du bloc de Taille du bloc (en octets)


donnée que l’on veut écrire
transtypé en (char*)

• Ecriture:
fstream f;
f.open (nom_physique , ios::in|ios::bin
ios::bin);
);
f.read ( (char*)buff
(char*)buff , n );
Exemple
#include <fstream.h>
int main ()
{
// déclaration et ouverture d'un fichier texte
fstream fich;
// déclaration d’un réel que l’on veut écrire en binaire
float k=123.458;
// Ouverture du fichier en ecriture binaire
fich . open ("fich1.txt" , ios::out|ios::binary );

// Ecriture en binaire du reel k;


fich . write ((char*)&k, sizeof ( float ));
// on a écrit à partir de l’adresse de k, un bloc de 4 octet
// (on a donc écrit k en binaire)

fich.close ();
return 0;
}
Exemple
fstream fich;
short int Tab[5] = {1,2,3,4,5}; // tableau que l’on va écrire
short int *TabLu = new short int [5]; // tableau résultant de la lecture
//Ouverture en écriture binaire
fich . open ("fich2.txt" , ios::out |ios::binary );
// Ecriture du tableau Tab. (sizeof ( short int ) egale 2 octets.
fich . write ((char*)Tab, sizeof ( short int ) * 5);
// fermeture du fichier
fich.close ();

//Ouverture en lecture binaire


fich . open ("fich2.txt" , ios::in |ios::binary );
//Lecture de 5*2 = 10 octet (5 éléments de type short int)
//que l’on écrit à partir de l’adresse TabLu
fich . read ((char*)TabLu, sizeof ( short int ) *5);
cout<<TabLu[0]<<TabLu[1]<<TabLu[2]<<TabLu[3]<<TabLu[4];
fich.close ();
delete [ ]TabLu;
Le positionnement

stream f;
f.open (nom_physique , ios::
ios::in
in | ios
ios::
::bin
bin));
f. seekg (int position); On positionne le pointeur de
fichier à l’octet n° Position

f. seekg (int n , ios_base::


ios_base::beg
beg );
On positionne le pointeur de
fichier n octets après le début

f. seekg (int n , ios_base::


ios_base::end
end );
On positionne le pointeur de
fichier n octets après la fin.
Avant la fin si n est négatif
Exemple
char c ;
file.seekg(2); Contenu du fichier
file >> c;
cout << c << endl; "0123456789"

file.seekg( 0, ios_base::beg );
file >> c;
cout << c << endl; Affichage

file.seekg( -1, ios_base::end ); >2


file >> c1; >0
cout << c1 << endl; >9

Vous aimerez peut-être aussi