Académique Documents
Professionnel Documents
Culture Documents
Cours C++
M.RAHMOUNE
Historique
C++ = C!!
C!! + POO
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
int main( )
{
Return 0;
}
Espaces de noms
• Les variables définies dans un espace de noms sont des variables globales
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 :
Les fonctions
Les fonctions en C peuvent être définies suivant deux modèles :
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++
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++
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
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++
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++
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). :
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.
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
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)
• 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.
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
Nous aboutissons finalement au constat suivant : la création d'un nouvel objet est
constituée de deux phases :
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é.
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
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:
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;
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:
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:
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();
}
rectplein::~rectplein() {
efface(); // appel implicite de rectangle::~rectangle();
}
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 :
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 :
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 { ... };
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 :
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
}
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.
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.
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;
}
• 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.
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;
}
Acquisition de ressources :
• création dynamique d’objet,
• allocation dynamique de mémoire,
• ouverture d’un fichier,
• verouillage d’un fichier (lecture/écriture)
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
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
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
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 :
• 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
Pour utiliser des chaînes de caractères string, il faut tout d’abord importer la librairie
#include <string>
#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.
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
Librairie iostream.h :
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;
C C++
C C++
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
C C++
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");
#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
C C++
C C++
• 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 );
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 ();
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
file.seekg( 0, ios_base::beg );
file >> c;
cout << c << endl; Affichage