Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Cours 2023

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 274

Programmation C++

2ème année Informatique

J. Allali
Prog. C++
ENSEIRB-MATMECA

1
Plan

Introduction

2
Historique (wikipedia)

• 198x: Bjarne Stroustrup (AT&T Bell): “C with classes”:


• Classes
• fonctions virtuelles
• surcharge d’opérateurs
• héritage multiple
• ...

Normes:
• 1998: Normalisation du C++ par l’ISO (International Organization for
Standardization) et ANSI ISO/CEI 14882:1998
• 2003: ISO/CEI 14882:2003
• 2011: ISO/CEI 14882:2011
• 2014: ISO/CEI 14882:2014
• 2017: ISO/IEC 14882:2017
• 2020: ISO/IEC 14882:2020
3
C (++)

Le C++ est un language impératif, orienté objets:

L’ajout de fonctionnalités permettant la mise en oeuvre de concepts objets dans


le language C:

• l’objet: attributs(données internes) + méthodes (comportements),


encapsulation.
• Typage des objets.
• Polymorphisme: un objet peut avoir plus d’un type.
• redéfinition.
• classe: description et génération des objets.

Il permet aussi la programmation générique: écriture de fonctions (et d’objets)


indépendantes du type de ces arguments. C’est l’idée du “code à trous”.
4
Incompatibilité en le C et le C++

Source C qui ne compile (invalidité syntaxique) pas en C++:

• Si le source contient un des nouveaux mots clés du C++

• Le C autorise la conversion implicite de void * en n’importe quel autre


type de pointeur:
exemple
i n t *i=malloc(4);

5
Trame du cours

Allocation
automatique & dynamique
variables & instances

copie? références affectation? operateurs:


Classe A a(b) a=b a=b
const a+b
a<b
...

destructeur
héritage
constructeur
this
attributs
visibilité polymorphisme?
définition syntaxique

virtual

metaprog templates

6
Plan

Allocation

7
Allocation automatique

L’allocation automatique se fait dans la pile.

Exemple
int i;

La variable i est allouée dans la pile automatiquement.

Ainsi, &i correspond à une adresse dans la pile à laquelle sizeof (int) octets sont
réservés.

À la sortie du bloc dans lequel est déclarée i, il y a dépilement et donc l’adresse


&i correspond à une zone mémoire qui n’est plus réservée.

8
Allocation dynamique

L’allocation dynamique en C++ se fait à l’aide de l’opérateur new.


On distingue deux types d’allocation dynamique: l’allocation d’un objet ou
l’allocation d’un tableau d’objets.
Exemple
new int
new int [10]

Dans le premier cas, on réserve sizeof(int) octets dans le tas, l’opérateur new
retourne l’adresse de début de cette zone.
Dans le deuxième cas, on réserve sizeof(int)*10 octets dans le tas, l’opérateur
new retourne l’adresse de début de cette zone.
Ces zones mémoires sont réservées tant que l’on n’a pas indiqué explicitement
que l’on souhaitait les libérer avec l’opérateur delete.
9
Allocation dynamique: libération

Il faut libérer la mémoire allouée avec new en utilisant delete.

Il faut libérer la mémoire allouée avec new type[] en utilisant delete[].

exemple
delete (new int); ou int *i=new int; delete i;
delete [] (new int [10]); ou int *p=new int[10]; delete [] p;

Une fois l’opérateur delete appelé, la mémoire qui était réservée à cette adresse
ne l’est plus.

10
Allocation: exemple complet
exemple complet
i n t *i = new i n t ;
i n t *t = new i n t [10];
delete i;
delete [] t;

Question: combien y-a-t-il d’allocations effectuées dans cet exemple?


• A) 1
• B) 2
• C) 3
• D) 4
Réponse: 4, 2 automatiques et 2 dynamiques.
En effet, il y a deux allocations automatiques de sizeof(int *) pour les variables
i et t 11
Allocation: exemple complet

12
Allocation: exemple complet

12
Allocation: exemple complet

12
Typage

Comme le C, le C++ dispose des types primitifs de base: int, float, double,
char, wchar_t, bool et void. A chacun de ces types est associée une taille en
octets.

À cela s’ajoute les pointeurs: type *. Une caractéristique importante est que,
quelque que soit type, un pointeur fait toujours la même taille (4 octets en 32
bits et 8 octets en 64 bits).

Toute variable doit être typée. Le type indiquera le nombre d’octets réservés
pour cette variable (dans la pile ou le segment de données) et l’interprétation
associée à ces octets.

13
Le mot clé auto

La norme C++-11 introduit un nouveau sens au mot clé: auto.

auto déclenche une inférence de type statique (à la compilation) en fonction


d’une affectation faite au moment de la déclaration d’une variable:
exemple
auto i=0; / / i e s t de t y p e i n t
auto d=10.0; / / d e s t de t y p e d o u b l e
auto f=2.0f; / / f e s t de t y p e f l o a t
auto s=" h e l l o "; / / s e s t de t y p e c o n s t c h a r *

14
Le mot clé auto

Cela fonctionne également avec le type de retour d’une fonction:

fonction
i n t f();
a u t o i=f(); / / i e s t de t y p e i n t
a u t o t = new i n t [10]; / / t e s t de t y p e i n t *

tableau
a u t o s=" h e l l o "; / / s e s t de t y p e c o n s t c h a r *
a u t o c=s[0]; / / c e s t de t y p e c h a r e t non c o n s t c h a r

On reviendra plus tard sur auto

15
initialisation de variable = et {}

Lors de la déclaration d’une variable, on utilise = pour initialiser celle-ci:

exemple
i n t i=0;
i n t j=12.2; / / j v a u t 12
char c= ’ d ’; / / c v a u t 100
char d=35.0; / / d v a u t 35
char e=35.8; / / e v a u t 35
f l o a t f=2.5; / / f v a u t 2 . 5

L’utilisation du = repose sur des mecanismes de conversion.

16
initialisation de variable = et {}

Il est possible d’utiliser {} pour l’initialisation:

exemple
i n t i{0};
i n t j{12.2}; / / ERREUR
char c{ ’ d ’}; / / c v a u t 100
char d{35.0}; / / ERREUR
f l o a t f{2.5}; / / f v a u t 2 . 5
d o u b l e x{3.0};
f l o a t y{x}; / / WARNING

Les {} implique un contrôle d’imbrication: un float peut être contenu dans un


double mais pas l’inverse par exemple.

17
initialisation de variable = et {}

{} pour l’initialisation d’une variable est plus "sécurisée" que =

Nous verrons d’autres usages de {} par la suite.

18
Plan

Classes

19
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
20
exemple matrix
Les classes: déclaration

Une classe consiste en un regroupement de méthodes et d’attributs.


Exemple
c l a s s NomClasse {
attributs
...
methodes
...
};

L’ordre des déclarations ne compte pas.

La classe est une description des données internes et comportements qu’aura


une instance générée par cette classe.

21
Les classes: instanciation

L’instanciation (ou réification), c’est à dire la création d’un objet instance


(ressource) à partir de l’objet classe (description/générateur) peut se faire de
façon dynamique:
allocation dynamique
A *x=new A();
A *y=new A{};

ou automatique:
allocation automatique
A z;
A t{};

Dans les deux cas, une ressource (adresse mémoire) est associée à l’instance.
Dans l’allocation dynamique, cette ressource est située dans le tas, dans le cas
automatique elle est située dans la pile.
22
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
23
exemple matrix
Visibilité: définition

On contrôle l’accès aux méthodes et attributs pour une sous-classe ou un objet


exterieur avec public,protected et private:

la classe sous-classe exterieur


public oui oui oui
protected oui oui non
private oui non non

Remarque: Les membres protected et private ne peuvent être accédés qu’à


partir d’une fonction membre de l’instance

24
Visibilité: déclaration

La visibilité par défaut dans une classe est private. On modifie la visibilité de
la façon suivante
Source
c l a s s NomClasse {
attributs et méthodes privés
public:
attributs et méthodes publiques
protected:
attributs et méthodes protégés
...
};

On peut à tout moment changer la visibilité, celle-ci sera appliquée à toutes les
déclaractions suivantes jusqu’au prochain changement de visibilité.
25
Les structures: déclaration

Les structures se déclarent comme en C:


Exemple
s t r u c t Nom {
...
attributs et méthodes publiques
...
};

En C++, les structures sont des classes dont la visibilité par défaut est public.

Par conséquent, elle peuvent contenir des attributs et des méthodes.

Deux noms de type sont associés à la structure: struct Nom et Nom

on voit que les structures C sont alors un cas particulier des structures C++

26
Les structures: instanciation

L’instanciation des structures se fait comme pour les classes.


allocation dynamique:
struct A { ... };
A *x = new A(); / / e r r e u r s i f o n c t i o n A
A *x = new s t r u c t A();
struct A *x=new s t r u c t A();

allocation automatique:
s t r u c t A { ... };
A x;
s t r u c t A x;

27
Friend: définition

Dans une classe A, le mot clé friend permet de donner à une fonction ou une
autre classe les même droits qu’une méthode de A.

la classe friend sous-classe exterieur


public oui oui oui oui
protected oui oui oui non
private oui oui non non

une fonction f amie d’une classe A pourra accéder aux attributs privés de A
ainsi qu’aux attributs protected d’une des classes parents de A

28
Friend: déclaration

La déclaration se fait dans la classe en indiquant soit le prototype de la fonction


amie soit le nom de la classe amie:
Classe amie
c l a s s NomClasse {...
f r i e n d c l a s s NomClasseAmie ;
};

Fonction amie
c l a s s NomClasse { ...
f r i e n d v o i d fonctionAmie( i n t ,char ,NomClasse) ;
};

la visibilité courante n’importe pas pour déclarer une classe ou fonction amie.

29
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
30
exemple matrix
Attributs et méthodes: définition

Une classe est un regroupement d’attributs, propriétés internes qu’aura une


instance de la classe, et de méthodes, comportements qu’aura une instance.

Ces attributs sont déclarés à l’intérieur de la classe.

L’implémentation des méthodes peut se faire au moment de la déclaration ou à


l’extérieur de la classe. On l’écrira dans la classe pour:

• les classes template


• les classes à usage locale au fichier
• l’inlining

Sinon on écrit un fichier entête “NomClasse.hpp” et un fichier source


“NomClasse.cpp”.

31
Exemple de classe: 1
Un seul fichier
c l a s s Test {
i n t i; / / a t t r i b u t p r i v é
public:
v o i d print(){ / * m é t h o d e p u b l i q u e * /
printf(" T e s t \ n");
}
};

Test t;
t.print();

La compilation se fait avec la commande:


g++ -Wall Test.cpp -o test

32
Exemple de classe: 1

Un seul fichier
c l a s s Test {
i n t i; / / a t t r i b u t p r i v é
public:
v o i d print(){ / * m é t h o d e p u b l i q u e * /
printf(" T e s t \ n");
}
};

Test *t = new Test;


t->print();

La compilation se fait avec la commande:


g++ -Wall Test.cpp -o test

32
Exemple de classe: 2
Deux fichiers
Test.hpp: Test.cpp:
c l a s s Test { # i n c l u d e " T e s t . hpp"
i n t i; / / a t t r i b u t p r i v é v o i d Test::print(){
public: printf(" T e s t \ n");
/ * méthode p u b l i q u e * / }
v o i d print(); Exemple.cpp:
}; Test t;
t.print();

La compilation se fait en deux temps:

g++ -Wall -c Test.cpp -o Test.o


g++ -Wall -c Exemple.cpp -o Exemple.o
g++ Test.o Exemple.o -o Exemple 33
Exemple de classe: 2
Deux fichiers
Test.hpp: Test.cpp:
c l a s s Test { # i n c l u d e " T e s t . hpp"
i n t i; / / a t t r i b u t p r i v é v o i d Test::print(){
public: printf(" T e s t \ n");
/ * méthode p u b l i q u e * / }
v o i d print(); Exemple.cpp:
}; Test *t = new Test;
t->print();

La compilation se fait en deux temps:

g++ -Wall -c Test.cpp -o Test.o


g++ -Wall -c Exemple.cpp -o Exemple.o
g++ Test.o Exemple.o -o Exemple

33
Accès aux attributs

L’accès aux champs d’une classe se fait à l’aide de . ou -> selon que l’on
dispose d’une instance de classe ou d’un pointeur sur une instance de classe.

Tout membre de classe a pour nom NomClasse::membre. Lorsqu’il n’y a pas


d’ambiguité, NomClasse:: peut être omis.

Cas automatique
ClasseA x;
x.i = 1 ; / / i e s t un a t t r i b u t p u b l i c de C l a s s e A
x.ClasseA::i = 1 ; / / idem
x.methode(); / / a p p e l de m é t h o d e
x.ClasseA::methode(); / / idem

Dans ce cas l’instance se trouve dans la pile.

34
Accès aux attributs

L’accès aux champs d’une classe se fait à l’aide de . ou -> selon que l’on
dispose d’une instance de classe ou d’un pointeur sur une instance de classe.

Tout membre de classe à pour nom NomClasse::membre. Lorsqu’il n’y a pas


d’ambiguité, NomClasse:: peut être omis.

Cas dynamique
ClasseA *y = new ClasseA();
y->i = 1 ; / / i e s t un a t t r i b u t p u b l i c de C l a s s e A
y->ClasseA::i = 1 ; / / idem
y->methode(); / / a p p e l de m é t h o d e
y->ClasseA::methode(); / / idem

Dans ce cas l’instance est allouée dynamiquement dans le tas.

35
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
36
exemple matrix
Le mot clé this

Le mot clé this permet, lors d’un appel de méthode, de faire référence à
l’instance source (site d’appel).
Le type de la variable this est NomDeClasse const *: c’est un pointeur. (Nous
verrons plus tard le sens de const.)
Exemple
c l a s s A{
i n t attributPrive;
public:
v o i d setAttribut( i n t valeur){
t h i s ->attributPrive = valeur;
}
};

37
Le mot clé this

Le mot clé this permet, lors d’un appel de méthode, de faire référence à
l’instance source (site d’appel).
Le type de la variable this est NomDeClasse const *: c’est un pointeur. (Nous
verrons plus tard le sens de const.)
Exemple
c l a s s A{
i n t attributPrive;
public:
v o i d setAttribut( i n t valeur) {
attributPrive = valeur ;
}
};

Lorsqu’il n’y a pas de variable locale de même nom que l’attribut, on peut
omettre this. 38
Opérateur de portée

L’opérateur de portée :: peut-être utilisé pour indiquer précisement la variable


que l’on souhaite manipuler.
Exemple:
i n t l u n a t i q u e ; / / v a r i a b l e globale , beurk !
class A {
char l u n a t i q u e ; / / a t t r i b u t p r i v é de l a c l a s s e
public :
void p r i n t A l l V a r i a b l e ( double l u n a t i q u e ){ / / v a r i a b l e l o c a l e
/ / à l a méthode
lunatique ; / / f a i t référence à la variable locale
A: : lunatique ; / / f a i t référence à l ’ attribut d ’ instance
t h i s −>A : : l u n a t i q u e ; / / f a i t r é f é r e n c e à l ’ a t t r i b u t d ’ i n s t a n c e
t h i s −> l u n a t i q u e ; / / f a i t r é f é r e n c e à l ’ a t t r i b u t d ’ i n s t a n c e
: : lunatique ; / / f a i t référence à la variable globale
}
};

39
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
40
exemple matrix
Les espaces de nom

Les espaces de nom permettent de regrouper un ensemble d’éléments (classes,


variables globales, fonctions, . . . ). Les espaces de nom sont utilisés pour
structurer le code et pour éviter les problèmes de collisions. Pour déclarer un
espace de nom on utilise le mot clé namespace:
Espace de nom:
namespace Nom { / / d é b u t de l ’ e s p a c e de nom
class A {
...
};
i n t i;
i n t fonction();
} / / f i n de l ’ e s p a c e de nom

41
Espace de noms

Lors de l’écriture du code des fonctions ou des méthodes des classes dans le
fichier .cpp on peut soit ré-ouvrir l’espace de nom, soit indiquer explicitement
le nom de l’objet avec l’opérateur de portée.

A.hpp A.cpp
namespace tec {
class A { #include “A.hpp”
public: namespace tec {
v o i d m(); void A::m(){
}; ...
} }
}

42
Espace de noms

Lors de l’écriture du code des fonctions ou des méthodes des classes dans le
fichier .cpp on peut soit ré-ouvrir l’espace de nom, soit indiquer explicitement
le nom de l’objet avec l’opérateur de portée.

A.hpp A.cpp
namespace tec {
class A { #include “A.hpp”
public: void tec::A::m(){
v o i d m(); ...
}; }
}

Il n’y pas pas de limite sur le nombre d’espaces de nom différents pouvant être définis dans un
fichier ni sur le niveau d’imbrication de ces espaces. Cependant la taille des noms de fonctions
est limitée et les espaces de nom font partis du nom de la fonction. D’une certaine manière, une
classe peut-être vue comme un espace de nom particulier. 42
Espace de noms: using

Le mot clé using permet de rendre accessible tout ou partie d’un espace de nom
dans une fonction, un espace de nom ou tout le code:

namespace spatial {
class Navette {};
}
namespace missions {
Navette n; //Erreur!
}
Navette n; //Erreur!
void starWars(){
Navette n; //Erreur!
}

43
Espace de noms: using

Le mot clé using permet de rendre accessible tout ou partie d’un espace de nom
dans une fonction, un espace de nom ou tout le code:

namespace spatial {
class Navette {};
}
namespace missions {
spatial::Navette n; //ok
}
spatial::Navette n; //ok
void starWars(){
spatial::Navette n; //ok!
}

43
Utilisation de l’opérateur de portée
Espace de noms: using

Le mot clé using permet de rendre accessible tout ou partie d’un espace de nom
dans une fonction, un espace de nom ou tout le code:

namespace spatial {
class Navette {};
}
using spatial::Navette;
namespace missions {
Navette n; //ok
}
Navette n; //ok
void starWars(){
Navette n; //ok!
}
43
Espace de noms: using

Le mot clé using permet de rendre accessible tout ou partie d’un espace de nom
dans une fonction, un espace de nom ou tout le code:

namespace spatial {
class Navette {};
}
namespace missions {
Navette n; //Erreur!
}
using spatial::Navette;
Navette n; //ok
void starWars(){
Navette n; //ok!
}
43
Espace de noms: using

Le mot clé using permet de rendre accessible tout ou partie d’un espace de nom
dans une fonction, un espace de nom ou tout le code:

namespace spatial {
class Navette {};
}
namespace missions {
using spatial::Navette;
Navette n; //ok
}
Navette n; //Erreur!
void starWars(){
Navette n; //Erreur!
}
43
Espace de noms: using

Le mot clé using permet de rendre accessible tout ou partie d’un espace de nom
dans une fonction, un espace de nom ou tout le code:

namespace spatial {
class Navette {};
}
namespace missions {
Navette n; //Erreur!
}
Navette n; //Erreur!
void starWars(){
using spatial::Navette;
Navette n; //ok
}
43
Espace de noms: using

Le mot clé using permet de rendre accessible tout ou partie d’un espace de nom
dans une fonction, un espace de nom ou tout le code:

namespace spatial {
class Navette {};
}
namespace missions {
Navette n; //Erreur!
}
Navette n; //Erreur!
void starWars(){
using namespace spatial;
Navette n; //ok
}
43
Using

Le using crée des synonymes locaux entre les espaces de nom:


Fichier exemple.hpp: Fichier exemple.cpp:
namespace math{ # i n c l u d e ’ ’exemple.hpp ’ ’
c o n s t d o u b l e pi=3.14;
} d o u b l e cercle::surf( d o u b l e r)
{
namespace cercle{ r e t u r n pi*r*r;
u s i n g math::pi; }
d o u b l e surf( d o u b l e );
}

• Permet de remplacer facilement une référence externe par une autre.


• Permet aussi l’introduction de modularité sans modification profonde du
code.
“using namespace nom;” permet de créer des synonymes pour tout les
éléments de nom 44
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
45
exemple matrix
Constructeur: définition

Un constructeur est une méthode de classe qui est appelée après la réservation
des ressources nécessaires à la création d’une instance de la classe et qui a pour
objectif d’initialiser les attributs de cette instance.

En C++, le constructeur est une méthode sans valeur de retour de même nom
que la classe.

On appelle constructeur par défaut le constructeur ne prenant aucun argument.

Dans le cas où la classe ne comporte aucun constructeur, le compilateur ajoute


un constructeur par défaut. Celui-ci n’est plus présent à partir du moment où
l’on a écrit au moins un constructeur.

46
Constructeur: exemple 1 fichier

Exemple.cpp
c l a s s Exemple {
d o u b l e pop;
public:
Exemple() { / / C o n s t r u c t e u r p a r d é f a u t
pop=0;
}
Exemple( d o u b l e d){ / / C o n s t r u c t e u r
pop=d;
}
};

47
Constructeur: exemple 2 fichiers

Exemple.hpp Exemple.cpp
c l a s s Exemple{ # i n c l u d e "Exemple . hpp"
d o u b l e pop;
public: Exemple::Exemple(){
Exemple(); pop=0;
Exemple( d o u b l e ); }
}; Exemple::Exemple( d o u b l e d){
pop=d;
}

48
Constructeur: initialisation des attributs

Les attributs de l’instance peuvent être initialisés de la façon suivante:


Initialisation:

class A{
int pop;
public:
A(int value):pop(value){
}
};

49
Constructeur: initialisation des attributs

Les attributs de l’instance peuvent être initialisés de la façon suivante:


Initialisation:

class A{
int pop;
public:
A(int pop):pop(pop){
}
};

Il n’y a pas d’ambiguïté sur les noms de variables.

49
Constructeur: initialisation des attributs

Les attributs de l’instance peuvent être initialisés de la façon suivante:


Initialisation:

class A{
int pop;
public:
A(int pop):pop(pop*2){
}
};

On peut effectuer des opérations

49
Constructeur: initialisation des attributs

Les attributs de l’instance peuvent être initialisés de la façon suivante:


Initialisation:

intdecremente(int i){
return i-1;
}
class A{
int pop;
public:
A(int pop):pop(decremente(pop)*2){
}
};

On peut appeler des fonctions


49
Constructeur: initialisation des attributs

Les attributs de l’instance peuvent être initialisés de la façon suivante:


Initialisation:

class A{
int pop;
int m(){
return 1;
}
public:
A(int pop):pop((m()+pop)*2){
}
};

On peut appeler des methodes!


49
Constructeur: initialisation des attributs C++11

La norme C++11 autorise le placement de valeur par défaut pour les attributs
non statiques à la déclaration:
c l a s s A{
i n t v=10; / / ou v { 1 0 } ;
std::string s{" h e l l o "};
public:
A(){
printf("%d %s \ n",v,s.c_str());
/ / a f f i c h e : 10 h e l l o
}
};

50
Constructeur: initialisation des attributs

On peut initialiser plusieurs attributs de cette façon. Cependant l’ordre des


initialisations se fait toujours selon celui des déclarations des attributs dans la
classe:
Source:
class A {
i n t a;
d o u b l e d;
char c;

public:
A();
};

A::A():c( ’ a ’),a(0){} / / Warning

51
Constructeur: initialisation des attributs

On peut initialiser plusieurs attributs de cette façon. Cependant l’ordre des


initialisations se fait toujours selon celui des déclarations des attributs dans la
classe:
Source:
class A {
i n t a;
d o u b l e d;
char c;

public:
A();
};

A::A():a(0),c( ’ a ’){} / / ok

52
Constructeur: et instanciation

Dans le cas de l’allocation automatique, il existe deux façons d’indiquer le


constructeur à appeler:

Parenthésée Affectation
A x; A y;
A x(1); A y=1;
A x( ’ c ’); A y= ’ c ’;
A x(); / / E r r e u r :
/ / pas d ’ i n s t a n c i a t i o n !

L’utilisation du ’=’ au moment de l’instanciation fait nécessairement référence


à un constructeur.

53
Constructeur: et instanciation

Pour le cas dynamique, seule la version parenthésée est légale.


Exemple
A *p=new A; / / C o n s t r u c t i o n p a r d e f a u t
A *p=new A(3); / / U t i l i s a t i o n du c o n s t r u c t e u r
/ / p r e n a n t un e n t i e r
A *p=new A(); / / C o n s t r u c t i o n p a r d e f a u t
A *p=new A=5; / / E r r e u r !

54
Constructeur: et instanciation

Cette syntaxe est valable pour les types primitifs:

Affectation Parenthésée
i n t i=0; i n t i(0);
char c= ’ a ’; char c( ’ a ’);
char *p=NULL; char *p(NULL);

55
Constructeur: et instanciation

Il est possible d’instancier des classes de façon anonyme (aucun nom de


variable n’est associé à l’instance):

Source:
A(); / / I n s t a n c i a t i o n u t i l i s a n t l e
/ / c o n s t r u c t e u r par d é f a u t
A(1); / / Avec l e c o n s t r u c t e u r A ( i n t ) ;
A; / / E r r e u r ! s y n t a x e i n v a l i d e .

Ce type d’instanciation peut-être utilisé lors du passage d’un argument à une


fonction ou lors d’une levée d’exception.

56
Constructeur: et instanciation

Source:
v o i d f(A a){...}

...
f(A(1)); / / 1 . A p p e l de f o n c t i o n
f(1); / / 2 . U t i l i s a t i o n de l ’ i n s t a n c i a t i o n i m p l i c i t e
...
throw A(); / / 3 . L e v é e d ’ e x c e p t i o n

L’appel au constructeur dans le cas 2 se fait de façon implicite. Le compilateur


cherche une fonction f(int), il liste l’ensemble des fonctions disponibles:
f(A) et cherche une façon de construire A à partir d’un entier.

57
Constructeur: et tableaux

Lors de l’allocation de tableau, les instances du tableaux doivent être créées à


l’aide du constructeur par défaut.

Source:
A *t=new A[10]; / / d y n a m i q u e

A tableau2[10]; / / a u t o m a t i q u e

⇒ Une classe ne comportant pas de constructeur par défaut ne peut pas être
utilisée avec les tableaux.

⇒ On utilisera un tableau de pointeurs, chaque élément du tableau sera


instancié séparément avec le bon constructeur.

58
Constructeur: et tableaux

Tableaux:
c l a s s A{
public:
A( i n t );
};
...
A t[10]; / / E r r e u r !
A *t[10]; / / T a b l e a u de p o i n t e u r s
f o r ( i n t i=0;i<10;i++)
t[i]=new A(3); / / u t i l i s a t i o n du c o n s t r u c t e u r
/ / a l l o c a t i o n dynamique
...
f o r ( i n t i=0;i<10;i++)
d e l e t e t[i];

59
Constructeur: conversion

Comme nous l’avons vu, l’appel au constructeur peut se faire de façon


implicite.
Exemple
i n t f(A p);
...
f(1);

Le conversion se fait sur au plus un niveau

60
Constructeur: conversion

c l a s s A{
public:
A( i n t );
};

c l a s s B{
public:
B(A );
};

v o i d f(B q);

f(1); / / Ne marche p a s !
f(A(1)); / / ok

La notation A a=1; fait appel au système de conversion.

61
Constructeur: explicit

Un constructeur déclaré explicit ne sera pas utilisé pour effectuer une


conversion:
c l a s s A{
public:
e x p l i c i t A( i n t );
};
A::A( i n t i){} / / e x p l i c i t n ’ e s t p a s r e p o r t e i c i
...
v o i d f(A p);
...
f(1); / / E r r e u r : ne t r o u v e p a s l a f o n c t i o n f ( i n t )
A a=1; / / E r r e u r !
A b(1); / / ok

62
Constructeur: par recopie

Le constructeur par recopie est le constructeur qui permet d’instancier une


classe à partir d’une autre instance de cette classe.

Ce constructeur est utilisé entre autre lors de la transmission de paramètres à


une fonction et lors d’une valeur de retour de fonction.
A f(A p) { r e t u r n p;}
....
A x;
f(x)

• La variable p, locale à la fonction f, est allouée automatiquement et


instanciée avec le constructeur par recopie à partir de l’instance x.
• La valeur de retour de f est instanciée par recopie de la valeur locale.

63
Constructeur: par recopie

L’idée est donc d’avoir un constructeur dans la classe A qui prend une instance
de type A en argument:
c l a s s A{
public:
A(A x);
};

Le problème est que pour instancier la variable locale x au constructeur il nous


faut le constructeur par recopie!
La solution repose sur l’utilisation des références:
c l a s s A{
public:
A( c o n s t A&x);
A(A &x);
};
64
Références

Une référence peut être vue comme un “alias” sur une instance.

Une référence doit obligatoirement être initialisée au moment de sa création et


ne peut référencer une autre instance par la suite.
i n t i;
i n t &r=i;

Après l’initialisation:

• les variables i et r représentent la même donnée


• l’adresse de r (&r) est égale à l’adresse de i (&i).

⇒ contrairement à un pointeur, une référence “pointe” toujours sur une


instance.

65
Références

Exemple:

v o i d swap( i n t &i, i n t &j){ i n t a=3,b=5;


i n t k=i; swap(a,b);
i=j; i n t t[5];
j=k; swap(t[1],t[2]);
}

On peut aussi utiliser les références comme valeur de retour:


i n t &element( i n t *t, i n t i){
r e t u r n t[i];
}

element(t,5)=10; / / m o t i f i e t [ 5 ]

Règle: Sauf pour les types primitifs, on ne prendra plus les instances par
recopie mais par référence.
66
Références

Parmis les avantages des références, on pourra noter qu’une référence ne peut
pas être nulle:

v o i d swap( i n t *i, i n t *j){ v o i d swap( i n t &i, i n t &j){


assert(i!=NULL); i n t k=i;
assert(j!=NULL); i=j;
i n t k=*i; j=k;
*i=*j; }
*j=k;
}

Sauf si l’on souhaite laisser la possibilité de passer le pointeur NULL ou bien de


pouvoir modifier la valeur du pointeur, on remplace le pointeur par une
référence.

67
Références

Pour les valeurs de retour, on ne peut retourner une référence que si l’objet
renvoyé existe en dehors de la fonction (comme pour les pointeurs):

i n t &f(){ i n t &p( i n t *l){


i n t i; r e t u r n *l;
r e t u r n i; }
}
f()=0;
i n t &g( i n t j){ i n t x;
r e t u r n j; g(x)=0;
} h(x)=0;
p(&x)=0;
i n t &h( i n t &k){ p(NULL)=0;
r e t u r n k;
}

68
Références

v o i d print( i n t &i){ v o i d print( i n t i){


// 1 entier // 2 entiers
} }

v o i d print(Matrix &m){ v o i d print(Matrix m){


/ / 1 instance // 2 instances
} }

i n t j;
print(j);
Matrix n(10000);
print(n);

Problème de l’intégrité des données : comment être sûre que la fonction ne va


pas modifier l’instance passée en argument ?
69
const: définition

Le mot clé const sert à indiquer qu’une donnée ne peut être modifiée:

c o n s t i n t i=5; c o n s t i n t *p=0; / / i n t c o n s t * p =0;


i n t j=10; p=new i n t [10];
c o n s t i n t &r=j; p[0]=5; / / e r r e u r !
r=4; / / e r r e u r ! i n t * c o n s t q=new i n t [10];
j=4; / / ok q=NULL; / / e r r e u r !
i=1; / / e r r e u r ! q[0]=5; / / ok

Un variable déclarée const doit être initialisée lors de sa déclaration.


déclaration: const type const * const * const p
lecture seule: **p= **p= *p= p=
Lorsque l’on dispose d’une référence constante sur une instance de classe,
comment garantir que l’appel à une méthode de cette instance de va pas
modifier l’instance ?
70
const: et instances

Illustration du problème:

c l a s s A{ v o i d f( c o n s t A &x){
i n t i; x.get();
public: x.set(0); / / !
i n t get(){ r e t u r n i;} }
v o i d set( i n t v){
t h i s ->i=v; A y;
} f(y);
};

La référence est constante (l’instance ne doit pas être modifiée) mais l’appel de
méthode modifie l’objet !

⇒ On distingue deux types de méthodes: celles qui sont susceptibles de


modifier l’objet et celles qui ne modifient pas l’objet.
71
const: méthodes

Une méthode de classe peut être typée const. Cela indique que dans cette
méthode le type de this est const NomClasse const *

L’ensemble des attributs de la classe deviennent alors constants eux aussi:

c l a s s A{ v o i d A::p() {
i n t i; // this : A * const
i n t *d; }
public:
v o i d m() c o n s t ; v o i d A::m() c o n s t {
v o i d p() ; // this : const A * const
}; // this : A const * const
// t h i s −> i : c o n s t i n t
// t h i s −>d : i n t * c o n s t
}

72
const

Lorsque l’on dispose d’une variable constante sur une instance, seules les
méthodes indiquées comme constantes peuvent être appelées sur cette instance:
A x;
c o n s t A & y=x;

x.m(); // ok
x.p(); // ok
y.p(); // erreur !
y.m(); // ok

Une méthode const peut appelée sur tout type de variable tandis qu’une
méthode non const ne peut être appelée que sur une variable non constante.
Règle: lors de l’écriture d’une méthode, on ne doit pas se demander si la
méthode doit être const mais plutôt si on a besoin qu’elle ne soit pas const.
Règle: Les arguments de fonction (non primitifs) seront des références
73
constantes sauf si l’on veut modifier l’argument.
const: surcharge

Il est possible de surcharger une méthode en utilisant le const:

c l a s s A{ A x;
public: const A &y=x;
v o i d m(){ A const *p=&x;
printf(" l e s i t e d ’ a p p e l x.m(); //?
n ’ e s t pas c o n s t a n t "); y.m(); //?
} p->m(); //?
v o i d m() c o n s t {
printf(" l e s i t e d ’ a p p e l
e s t c o n s t a n t ")
}
};

Le const fait partie de la signature de la méthode, il doit être présent dans le


.cpp et le .hpp.

74
const: argument de fonction

Exemple d’utilisation du const pour les arguments d’une fonction.

c l a s s A{ v o i d h( c o n s t A &c){
i n t i; g(c); / / !
public: f(c);
i n t get() c o n s t { r e t u r n i; } c.get();
}; }

v o i d f(A a){ / / c o p i e A x;
printf("%d \ n",a.get()); c o n s t A &y=x;
} h(x);
g(x);
v o i d g(A &b){ g(y);
printf("%d \ n",b.get());
}

75
Constructeur: recopie (suite)

Ainsi, le constructeur par recopie peut s’écrire de deux façons:


c l a s s A{
public:
A( c o n s t A &x); / / 1
A(A &x); / / 2
};

Si l’on n’écrit pas de constructeur par recopie, le compilateur en fournit un. Ce


constructeur fera un recopie attribut par attribut en utilisant les constructeurs
par recopie des types des attributs.

Le constructeur fournit sera de type 1, si tous les constructeurs par recopie des
attributs sont de type 1, sinon il sera de type 2.

76
Constructeur: recopie par le compilateur

c l a s s Comp{
i n t i;
std::string s;
Matrix m;
public:
Comp( c o n s t Comp&c):i(c.i),s(c.s),m(c.m){}
};

77
Constructeur: par recopie

On préférera toujours écrire le constructeur par recopie sous la forme 1.

Règle: Toute classe nécessitant une copie profonde (allocation dynamique des
attributs) devra avoir un constructeur par recopie.

78
Constructeur: par recopie

c l a s s A{ A x;
i n t *i; A y(x);
public:
A():i(new i n t (0)){}
~A(){ d e l e t e i; }
};

Ici, il y a une erreur car on libère deux fois le même espace mémoire.

79
Constructeur: par recopie

c l a s s A{ A x;
i n t *i; A y(x);
public:
A():i(new i n t (0)){}
~A(){ d e l e t e i; }
A( c o n s t A &a):i(new i n t (*(a.i))){}
};

80
Constructeur: protected et private

Une classe n’ayant que des constructeurs protected ne pourra être instanciée
que par

• Une sous-classe
• Une classe ou fonction amie
• Une classe ou fonction amie d’une sous-classe
• Une méthode statique

Une classe n’ayant que des constructeurs private ne pourra être instanciée que
par

• Une classe ou fonction amie


• Une méthode statique

81
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
82
exemple matrix
Destructeur: définition

Le destructeur est une méthode d’instance n’ayant pas de type de retour et


ayant pour nom: ~NomClasse

v o i d f(){
c l a s s Nom{
Nom n;
public:
Nom *p=new Nom;
~Nom(){}
d e l e t e p; / / A p p e l de p−>~Nom ( )
};
} / / A p p e l de n . ~ Nom ( )

Règle: toute classe ayant allouée dynamiquement ces attributs devra écrire un
destructeur.

83
Si l’instance a été allouée dynamiquement alors l’appel au destructeur est
déclenché par l’opérateur delete.

Si l’instance a été allouée automatiquement alors l’appel au destructeur se fait à


la sortie du bloque d’instructions qui contient cette instance.

Les ressources mémoires associées à l’instance sont libérées après l’appel au


destructeur.

84
Destructeur: exemple

c l a s s Tab{ i n t main(){
i n t *data; Tab t(10); / / a l l o c a t i o n
public: } // liberation
Tab( i n t s){
data= new i n t [s];
}
~Tab(){
d e l e t e [] data;
}
};

Il est ainsi possible de "simuler" un tableau dans la pile avec une désallocation
automatique.

85
Destructeur: auto

c l a s s AutoInt{ // Utilisation :
i n t *t; i n t main(){
public: AutoInt t(new i n t [100]);
AutoInt( i n t *t):t(t); t.get(0);
i n t get ( i n t i) c o n s t { t.set(0,0);
r e t u r n t[i]; } / / l i b é r a t i o n auto .
}
v o i d set( i n t i, i n t v){
t[i]=v;
}
~AutoInt(){
i f (t!=NULL)
d e l e t e [] t;
}
};

Nous verrons comment améliorer ce code avec les opérateurs, cependant


86
attention à la recopie!
Destructeur: auto

c l a s s AutoInt{ // Utilisation :
i n t *t;
public: v o i d f(AutoInt ai){
AutoInt( i n t *t):t(t); } / / l i b é r a t i o n auto .
i n t get( i n t i) c o n s t ;
v o i d set( i n t i, i n t v); AutoInt tab(){
~AutoInt(){ AutoInt x(new i n t [100]);
i f (t!=NULL) r e t u r n x;
d e l e t e [] t; }
} i n t main(){
AutoInt(AutoInt &p){ AutoInt q(new i n t [100]);
t=p.t; f(q);
p.t=NULL;
} AutoInt r(tab());
}; } / / l i b é r a t i o n auto .

Une solution consiste à transferer le pointeur...


87
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
88
exemple matrix
Static: définition

Une variable statique est une variable dont la durée de vie est égale à celle du
programme et dont la portée peut être réduite à une fichier, une classe, une
fonction ou méthode selon l’endroit où elle est déclarée.

Fichier.hpp Fichier.cpp
# i n c l u d e " F i c h i e r . hpp"
v o i d f();
s t a t i c d o u b l e v;
c l a s s A{
s t a t i c i n t i; i n t A::i=0;
public:
s t a t i c v o i d m(); v o i d A::m(){
}; this; / / erreur !
}
v o i d f(){
s t a t i c i n t compteur=0;
}
89
Static: et classes

Dans le contexte des classes, une méthode publique statique est équivalente à
une fonction amie (sauf pour la portée).

On pourra utiliser les méthodes statiques en jonction avec un constructeur privé.

On peut utiliser le mot clé static en jonction avec const afin de définir des
propriétés constantes de classes:
c l a s s Chaine {
public:
s t a t i c c o n s t char END= ’ \ 0 ’;
};

Les variables static et const sont les seules à pouvoir être initialisées lors de la
déclaration dans le fichier .hpp.

90
Static: et classes

Dans le contexte des classes, une méthode publique statique est équivalente à
une fonction amie (sauf pour la portée).

On pourra utiliser les méthodes statiques en jonction avec un constructeur privé.

On peut utiliser le mot clé static en jonction avec const afin de définir des
propriétés constantes de classes:
c l a s s Chaine {
public:
s t a t i c c o n s t char END;
};
c o n s t char Chaine::END= ’ \ 0 ’;

L’initialisation peut se faire à la déclaration dans ce cas. Chaine::END n’a pas


d’existence réelle durant l’execution (équivalent à une macro).

91
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
92
exemple matrix
Une classe compteur

L’objectif est d’écrire une classe Compteur telle que plusieurs instances de cette
classe puissent partager un même compteur qui sera ici implanté par un entier.

Ceci implique que le compteur soit alloué dynamiquement. En effet, on


souhaite qu’il puisse être transmis entre des instances.

93
Compteur: Exemple1

94
Compteur: Exemple1

94
Compteur: Exemple1

94
Compteur: Exemple1

94
Compteur: Exemple1

94
Compteur: Exemple1

94
Compteur: Exemple1

94
Compteur: Exemple1

94
Compteur: Exemple1

94
Compteur: Exemple1

94
Compteur: Exemple2

95
Compteur: Exemple2

95
Compteur: Exemple2

95
Compteur: Exemple2

95
Compteur: Exemple2

95
Compteur: Exemple2

95
La classe Compteur

La classe Compteur doit avoir les méthodes suivantes:

• void incremente() : private


• void decremente() : private
• bool dernier() : public

Ainsi que les constructeurs par défaut, recopie et destructeur.

• Compteur();
• Compteur(const Compteur \&);
• ~Compteur();

implémentation de cette classe...

96
Problème:

Examinons le source suivant:

i n t main(){
Compteur c;
Compteur d;

d=c; / / R e c o p i e de c d a n s d
}

La recopie se fait de la même façon que la construction par recopie. Le


compilateur ajoute ici une recopie champs par champs!

97
Problème:

98
Problème:

98
Problème:

98
Problème:

98
Problème:

98
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
99
exemple matrix
Exemple d’une classe basic Point

Point.hpp Point.cpp
class Point { # i n c l u d e " P o i n t . hpp"
double x , y ;
public : Point : : Point ( ) : x (0) , y (0){
Point ( ) ; }
P o i n t ( double x , double y ) ; P o i n t : : P o i n t ( double x , double y ) : x ( x ) , y ( y ){
}; }

#include <Point.hpp>
i n t main(){
Point p(2,3);
Point q;
Point *t=new Point[20];
d e l e t e [] t;
}

100
Exemple d’une classe basic Point

Point.hpp Point.cpp
class Point { # i n c l u d e " P o i n t . hpp"
double x , y ;
public : Point : : Point ( ) : x (0) , y (0){
Point ( ) ; }
P o i n t ( double x , double y ) ; P o i n t : : P o i n t ( double x , double y ) : x ( x ) , y ( y ){
double g e t x ( ) const ; }
void s e t x ( double ) ; double P o i n t : : g e t x ( ) const { return x ; }
}; v o i d P o i n t : : s e t x ( d o u b l e d ) { x=d ; }

# i n c l u d e < P o i n t . hpp >


i n t main ( ) {
Point p (2 ,3);
Point q ;
P o i n t * t =new P o i n t [ 2 0 ] ;
q . getx ( ) ;
t [ 0 ] . getx ( ) ;
( t +5) − > s e t x ( 1 0 ) ;
delete [] t ;
}

101
Exemple d’une classe basic Point

Point.hpp Point.cpp
class Point { # i n c l u d e " P o i n t . hpp"
double x , y ;
public : Point : : Point ( ) : x (0) , y (0){
Point ( ) ; }
P o i n t ( double x , double y ) ; P o i n t : : P o i n t ( double x , double y ) : x ( x ) , y ( y ){
v o i d add ( c o n s t P o i n t &p ) ; }
P o i n t sum ( c o n s t P o i n t &p ) c o n s t ; v o i d P o i n t : : add ( c o n s t P o i n t &p ) { x+=p . x ; y+=p . y ; }
}; P o i n t P o i n t : : sum ( c o n s t P o i n t &p ) c o n s t {
P o i n t fsum ( c o n s t P o i n t &a , c o n s t P o i n t &b ) ; r e t u r n P o i n t ( x+p . x , y+p . y ) ;
}
P o i n t fsum ( c o n s t P o i n t &a , c o n s t P o i n t &b ) {
return Point ( a . getx ()+ b . getx ( ) , a . gety ()+ b . gety ( ) ) ;
}

# i n c l u d e < P o i n t . hpp >


i n t main ( ) {
Point p (2 ,3);
Point q ;
P o i n t * t =new P o i n t [ 2 0 ] ;
t [ 2 ] = p . sum ( q ) ;
q . add ( p ) ;
t [ 4 ] = fsum ( t [ 5 ] , t [ 6 ] ) ;
delete [] t ;
} 102
Plan
Classes
Définition et déclaration
Visibilité, friend, struct
Attributs et méthodes
this
espace de nom
constructeur
destructeur
static
exemple
exemple point
103
exemple matrix
Exemple d’une classe Matrix

Matrix.hpp Matrix.cpp
c l a s s Matrix { # i n c l u d e " Matrix . hpp"
int l , c ;
double * d a t a ; Matrix : : Matrix ( int l , int c ) : l ( l ) , c ( c ){
public : d a t a = new d o u b l e [ l * c ] ;
Matrix ( int l , int c ) ; }
};

#include <Matrix.hpp>
i n t main(){
Matrix m(3,3);
} / / −> f u i t e memoire , a j o u t d ’ un d e s t r u c t e u r

104
Exemple d’une classe Matrix

Matrix.hpp Matrix.cpp
c l a s s Matrix { # i n c l u d e " Matrix . hpp"
int l , c ;
double * d a t a ; Matrix : : Matrix ( int l , int c ) : l ( l ) , c ( c ){
public : d a t a = new d o u b l e [ l * c ] ;
Matrix ( int l , int c ) ; }
~Matrix ( ) ; Matrix : : ~ Matrix ( ) {
}; delete [] data ;
}

#include <Matrix.hpp>
i n t main(){
Matrix m(3,3);
} / / ok

105
Exemple d’une classe Matrix

Matrix.hpp Matrix.cpp
c l a s s Matrix { # i n c l u d e " Matrix . hpp"
int l , c ;
double * d a t a ; Matrix : : Matrix ( int l , int c ) : l ( l ) , c ( c ){
public : d a t a = new d o u b l e [ l * c ] ;
Matrix ( int l , int c ) ; }
~Matrix ( ) ; Matrix : : ~ Matrix ( ) {
}; delete [] data ;
}

#include <Matrix.hpp>
i n t main(){
Matrix m(3,3);
Matrix n(m);
} / / erreur ! double free ,
/ / => a j o u t d ’ un c o n s t r u c t e u r p a r c o p i e

106
Exemple d’une classe Matrix

Matrix.hpp Matrix.cpp
c l a s s Matrix { # i n c l u d e " Matrix . hpp"
int l , c ;
double * d a t a ; Matrix : : Matrix ( int l , int c ) : l ( l ) , c ( c ){
public : d a t a = new d o u b l e [ l * c ] ;
Matrix ( int l , int c ) ; }
M a t r i x ( c o n s t M a t r i x &x ) ; M a t r i x : : M a t r i x ( c o n s t M a t r i x &x ) : l ( x . l ) , c ( x . c ) {
~Matrix ( ) ; d a t a = new d o u b l e [ l * c ] ;
}; f o r ( i n t i = 0 ; i < l * c ;++ i )
d a t a [ i ]= x . d a t a [ i ] ;
}
Matrix : : ~ Matrix ( ) {
delete [] data ;
}

#include <Matrix.hpp>
i n t main(){
Matrix m(3,3);
Matrix n(m);
} / / ok

107
Exemple d’une classe Matrix

Matrix.hpp Matrix.cpp
c l a s s Matrix { # i n c l u d e " Matrix . hpp"
int l , c ;
double * d a t a ; Matrix : : Matrix ( int l , int c ) : l ( l ) , c ( c ){
public : d a t a = new d o u b l e [ l * c ] ;
Matrix ( int l , int c ) ; }
M a t r i x ( c o n s t M a t r i x &x ) ; M a t r i x : : M a t r i x ( c o n s t M a t r i x &x ) : l ( x . l ) , c ( x . c ) {
~Matrix ( ) ; d a t a = new d o u b l e [ l * c ] ;
}; f o r ( i n t i = 0 ; i < l * c ;++ i )
d a t a [ i ]= x . d a t a [ i ] ;
}
Matrix : : ~ Matrix ( ) {
delete [] data ;
}

#include <Matrix.hpp>
i n t main(){
Matrix m(3,3);
Matrix n(m);
m=n;
} / / e r r e u r ! d o u b l e f r e e + f u i t e memoire
108
Plan

Operateurs

109
Opérateurs: définition

Un opérateur est une notation raccourcie permettant de représenter une


opération fréquemment utilisée, par exemple:

a+b*c;

On ajoute à a le résultat de la multiplication de b par c

Si a est un pointeur, le sens change: on ajoute à a, b*c*sizeof(T) octets (si T est


le type pointé par a).

Dans le cas où a n’est pas un type primitif, cette notation est invalide sauf si
l’on fournit la fonction permettant d’effectuer cette opération.

110
Opérateurs: liste

Voici la liste des opérateurs pouvant être définis en C++:

Unaire: Binaire:

! & * % %= * *=
+ ++ - -= + +=
- -- / /=
~
< <= > >=
spéciaux:
new != == && ||
delete
[ ] & &= | |=
() ^ ^= << <<=
conversion >> >>=
111
Opérateurs: conditions

Pour pouvoir écrire un opérateur, au moins une des opérandes de cet opérateur
doit correspondre à un type utilisateur (pas primitif). Par exemple, on ne peut
pas modifier l’addition sur les entiers.

Les opérateurs ne sont pas considérés comme commutatifs par le compilateur.


C’est à dire que si l’on a écrit un opérateur permettant de faire l’addition d’un A
avec un B,
B()+A();

ne sera pas résolu pas cet opérateur.

La précédence des opérateurs ne peut être modifiée.

112
Opérateurs: syntaxe

Un opérateur α s’écrira sous le nom d’une fonction ou méthode non statique de


nom operator α.

Le type de retour de cette fonction est libre.

C’est le premier paramètre qui est prioritaire pour la résolution. Dans le cas
d’une méthode, le premier paramètre (correspondant à this dans la méthode)
sera une référence du type de la classe.
c l a s s A{
public:
i n t o p e r a t o r +( i n t i);
};

Il faut voir cette méthode comme définissant l’opération + entre une instance de
A non constante et un entier.
113
Opérateurs: syntaxe

On peut écrire les opérateurs comme des fonctions ou des méthodes:


fonction:
c l a s s Dix{ };

i n t o p e r a t o r +( c o n s t Dix &d, i n t i){


r e t u r n i+10;
}

i n t main(){
Dix g;
i n t douze=g+2;
douze= o p e r a t o r +(g,2);
o p e r a t o r +(2,g); / / E r r e u r !
}

114
Opérateurs: syntaxe

On peut écrire les opérateurs comme des fonctions ou des méthodes:


méthode:
c l a s s Dix{
public:
i n t o p e r a t o r +( i n t i) c o n s t {
r e t u r n i+10;
}
};

i n t main(){
Dix g;
i n t douze=g+2;
douze=g. o p e r a t o r +(2);
2. o p e r a t o r +(g); / / E r r e u r !
}

115
Opérateurs: précédence

La précédence des opérateurs ne peut être modifiée, l’évaluation se fait donc


obligatoirement selon le respect des précédences standards.
exemple
c l a s s A{};
c l a s s B{};

A o p e r a t o r +( c o n s t A&, c o n s t B&);
B o p e r a t o r *( c o n s t A&, c o n s t A&);

i n t main(){
A x;
B y;
x+y*x; / / E r r e u r !
(x+y)*x; / / ok
}

116
Opérateurs: unaires

On écrira les opérateurs unaires dans les classes:

c l a s s Point{
i n t x,y;
public:

Point o p e r a t o r -() c o n s t {
r e t u r n Point(-x,-y);
}
};

On écrira l’opérateur comme fonction si l’on n’a pas accès à la classe.

117
Opérateurs: binaires

On écrira les opérateurs binaires dans la classe si les deux opérandes sont de
même type et que l’on a accès à la classe.

c l a s s Point{
i n t x,y;
public:
Point o p e r a t o r +( c o n s t Point &e) c o n s t {
r e t u r n Point(x+e.x,y+e.y);
}
};

118
Opérateurs: binaires

Sinon on écrira l’opérateur à l’extérieur de la classe:

c l a s s Point{
i n t x,y;
...
};
Point o p e r a t o r +( c o n s t Point &p, c o n s t Entier &e){
r e t u r n Point(p.getx()+e.v(),p.gety()+e.v());
}
Double o p e r a t o r +( c o n s t Entier &f, c o n s t Point &q){
r e t u r n q+f; / / o p e r a t o r +(q , f )
}

Eventuellement, on pourra déclarer l’opérateur comme fonction amie s’il est


nécessaire d’accéder à des attributs privés (???).

119
Opérateurs: conception

Lorsque l’on écrit un nouvel opérateur, il faut faire attention à ce que


l’introduction de ce “raccourci” n’apporte pas d’ambiguité: le but de
l’opérateur est de simplifier, pas de compliquer.

En cas de doute, préférer toujours l’utilisation de fonctions ou méthodes


classiques.

Un exemple de mauvaise utilisation des opérateurs: la bibliothèque standard!

120
Opérateurs: iostream

La bibliothèque standard iostream fournit un modèle orienté-objet des entrées


sorties, elle se decompose en

• ios : bases pour les entrées/sorties


• istream : entrées
• ostream : sorties
• streambuf : flux bufferisés
• iostream : entrées et sorties
• fstream : fichiers
• sstream : chaines de caractères

L’ensemble des classes font parties de l’espace de nom std


Outre un ensemble de fonctionnalités permettant la gestion des flux, il a été
ajouté un support pour la lecture et l’écriture basé sur les opérateurs
121
Opérateurs: iostream

L’opérateur << est utilisé pour écrire dans un flux de type std::ostream:
i n t main(){
i n t i;
std::cout<<"un e n t i e r : "<<i<<std::endl; / / 1
...
f<<1; / / 2
f<<i<<1; / / 3

La ligne 1 doit être interprétée comme ceci:


o p e r a t o r <<(
o p e r a t o r <<(
o p e r a t o r <<(std::cout,"un e n t i e r ")
,i)
,std::endl)

Qu’en déduire sur le type de retour de l’opérateur << ?


122
Opérateurs: iostream

i n t main(){
i n t i;
std::cout<<"un e n t i e r : "<<i<<std::endl; / / 1
...
f<<1; / / 2
f<<i<<1; / / 3

Qu’en déduire sur le type de retour de l’opérateur << ?

le type de retour de l’opérateur << dans ce cas doit être compatible avec le
premier argument.

123
Opérateurs: iostream

std::cout<<"un e n t i e r : "<<i<<std::endl; / / 1
f<<1; / / 2
f<<i<<1; / / 3

Dans le cas 2, est-ce que l’on est en train de décaler un entier ou bien d’afficher
1 dans un flux ?
Dans le cas 3, est-ce:

• f décalé de i puis de 1 ?
• i décalé de 1 affiché dans f ?

⇒ d’une façon générale on essayera de ne pas “détourner” les opérateurs pour


une utilisation qui n’est pas en rapport avec le sens premier de cette opérateur
Implanter le décalage à gauche pour une classe Entier qui multiplie l’entier par
2i a du sens.
124
Opérateurs: iostream

La lecture depuis un flux se fait de la façon suivante


i n t i,j,k;
char texte[10];
std::cin>>i; / / l e c t u r e d ’ un e n t i e r
std::cin>>texte; / / l e c t u r e d ’ une c h a i n e ! !
std::cin>>i>>j>>k; / / l e c t u r e de 3 e n t i e r s

Pour la lecture de chaîne de caractère on utilisera un type qui encapsule des


mécanismes de réallocation telle que la classe std::string
Quel est le type de retour de cet opérateur?
Le type de retour doit compatible avec le premier argument:
std::istream & o p e r a t o r >>(std::istream &, ... )

Pourquoi les références ne sont pas constantes ?


125
classe et iostream

Comment adapter votre classe pour pouvoir utiliser l’affichage de la


bibliothèque standard:
c l a s s Point{ i n t x,y; };
i n t main(){
Point p;
std::cerr<<"debug"<<p;
}

Erreur à la compilation:

A.cpp: In function ’int main()’:


A.cpp:42: error: no match for ’operator<<’ in
’std::operator<< [with _Traits = std::char_traits<char>]
(((std::basic_ostream<char, std::char_traits<char> >&)(& s
((const char*)"debug")) << p’

suivi d’une bonne cinquantaine de lignes ! 126


Solution

nous devons écrire l’opérateur << pour la classe std::ostream et Point:


c l a s s Point{
i n t x,y;
};
std::ostream & o p e r a t o r <<(std::ostream &stream,Point p){
stream<<p.x<< ’ / ’<<p.y;
r e t u r n stream;
}

Combien d’erreurs dans ce code ? Au moins 2:


• p devrait être une référence constante
• p.x p.y ne sont pas accessibles
solutions:
• Ajout d’un accesseur dans le code.
• Déclarer l’opérateur comme amie de la classe 127
Solution

c l a s s Point{
i n t x,y;
public:
i n t getx() c o n s t { r e t u r n x;}
i n t gety() c o n s t { r e t u r n y;}
};

std::ostream & o p e r a t o r <<(std::ostream &stream,


c o n s t Entier &p){
stream<<p.getx()<< ’ / ’<<p.gety();
r e t u r n stream;
}

128
Solution

c l a s s Point{
i n t x,y;
public:
f r i e n d std::ostream & o p e r a t o r <<(
std::ostream &stream,
c o n s t Point &p);
};
std::ostream & o p e r a t o r <<(std::ostream &stream,
c o n s t Point &p){
stream<<p.x<< ’ / ’<<p.y;
r e t u r n stream;
}

Si l’implémentation change (l’attribut privé est renommé), on doit aussi


changer le code de l’opérateur :(
⇒ les fonctions amies ne doivent pas être utilisées à la place des accesseurs!
129
Exemple classique

class Point {
int x , y ;
public :
P o i n t ( i n t x =0 , i n t y = 0 ) : x ( x ) , y ( y ) { }
b o o l o p e r a t o r ==( c o n s t P o i n t &p ) c o n s t {
r e t u r n ( x==p . x ) && ( y==p . y ) ;
}
b o o l o p e r a t o r < ( c o n s t P o i n t &p ) c o n s t {
if ( x<p . x ) r e t u r n t r u e ;
i f ( x==p . y ) r e t u r n y<p . y ;
return f a l s e ;
}
/ / . . . <= > >= != . . .
P o i n t o p e r a t o r + ( c o n s t P o i n t &p ) c o n s t {
r e t u r n P o i n t ( x+p . x , y+p . y ) ;
}
P o i n t&o p e r a t o r +=( c o n s t P o i n t &p ) {
x+=p . x ; y+=p . y ;
return * t h i s ;
}
/ / . . . − −= * *= . .
};

130
Opérateur: d’affectation

L’opérateur d’affectation correspond au ’=’ et doit nécessairement être écrit


sous la forme d’un membre de classe:
c l a s s Point{
i n t x,y;
public:
c o n s t Point & o p e r a t o r =( i n t i){
x=i; y=0;
return * t h i s ;
}
};
...
Point p;
p=1;
Point f=1; / / E r r e u r : c ’ e s t un a p p e l au c o n s t r u c t e u r !

131
Opérateur: d’affectation

Le compilateur fournit toujours un opérateur d’affectation permettant de copier


une instance dans une instance de même type:
s t r u c t _S a,b;
a=b;

L’opérateur d’affectation fournit est de la forme:


X &X:: o p e r a t o r =( c o n s t X&); / / 1
X &X:: o p e r a t o r =(X&); // 2

Dans les deux cas, la copie est effectuée attribut par attribut dans l’ordre de leur
déclaration.
La forme 1 est utilisée si chaque attribut possède un opérateur d’affectation en
forme 1.
Si l’on déclare explicitement un opérateur d’affectation en forme 1 ou 2 alors le
compilateur n’ajoute plus l’opérateur. 132
Opérateur: d’affectation

L’opérateur d’affectation fournit par le compilateur ressemble à:


c l a s s X{
A a;
B b;
C c;
// . . . .
public:
X & o p e r a t o r =( c o n s t X&y){
a=y.a;
b=y.b;
c=y.c;
// ...
return * t h i s ;
}
};

133
Matrix

Matrix.hpp Matrix.cpp
c l a s s Matrix { # i n c l u d e " Matrix . hpp"
int l , c ;
double * d a t a ; Matrix : : Matrix ( int l , int c ) : l ( l ) , c ( c ){
public : d a t a = new d o u b l e [ l * c ] ;
Matrix ( int l , int c ) ; }
M a t r i x ( c o n s t M a t r i x &x ) ; M a t r i x : : M a t r i x ( c o n s t M a t r i x &x ) : l ( x . l ) , c ( x . c ) {
M a t r i x &o p e r a t o r = ( c o n s t M a t r i x &m ) ; d a t a = new d o u b l e [ l * c ] ;
~Matrix ( ) ; f o r ( i n t i = 0 ; i < l * c ;++ i )
}; d a t a [ i ]= x . d a t a [ i ] ;
}
M a t r i x &M a t r i x : : o p e r a t o r = ( c o n s t M a t r i x &m) {
À partir du moment où une i f ( t h i s ==&m) r e t u r n * t h i s ;
delete [] data ; / / libération
classe effectue de l’allocation c=m. c ; l =m. l ; / / re − i n i t i a l i s a t i o n
d a t a =new d o u b l e [ c * l ] ;
dynamique, on écrira f o r ( i n t i = 0 ; i < l * c ;++ i )
d a t a [ i ] =m. d a t a [ i ] ;
systématiquement un }
Matrix : : ~ Matrix ( ) {
constructeur par recopie, un delete [] data ;
}
destructeur et un opérateur
d’affectation
134
Opérateur: foncteur

L’opérateur de fonction() est le seul opérateur d’instance (i.e. à part new et


delete) qui peut prendre un nombre variable d’arguments. Pour les autres, le
nombre d’arguments est fixé par l’arité de l’opérateur.
Une classe qui implémente cet opérateur est appelée foncteur (ou fonctor) car
l’on peut alors utiliser une instance comme une fonction:
c l a s s Greater{
public:
b o o l o p e r a t o r ()( i n t a, i n t b){ r e t u r n a>b; }
b o o l o p e r a t o r ()( d o u b l e a, d o u b l e b){
r e t u r n a>b;
}
b o o l o p e r a t o r ()( c o n s t char *a, c o n s t char *b){
r e t u r n strcmp(a,b)>0;
}
};
135
Opérateur: foncteur

c l a s s Greater{
public:
b o o l o p e r a t o r ()( i n t a, i n t b);
b o o l o p e r a t o r ()( d o u b l e a, d o u b l e b);
b o o l o p e r a t o r ()( c o n s t char *a, c o n s t char *b);
};
...
Greater greater;
i f (greater(1,0)){ ... }
i f (greater("aba","abb")){...}

On aurait aussi bien pu écrire plusieurs fonctions greater en utilisant la


surcharge.
Un autre exemple plus utile (et complexe) est l’implémentation d’une fonction
de hachage paramétrable (attributs).
Concept utilisé avec les templates (prog. générique).
136
Opérateur: de conversion

Il est possible d’écrire des opérateurs prenant en charge la conversion d’une


instance vers un autre type:
c l a s s Entier{
i n t _value;
public:
o p e r a t o r i n t () c o n s t {
r e t u r n _value;
}
};

Le prototype de l’opérateur de conversion est operator type() const, ne


possède pas de type de retour (puisque c’est type) et ne prend pas d’argument.

L’opérateur de conversion s’écrit obligatoirement sous la forme d’une fonction


membre.
137
Opérateurs: de conversion

L’opérateur de conversion peut être appelé explicitement avec un cast ou bien


implicitement:
v o i d f( i n t );
...
Entier e;
i n t i=e; / / c o n v e r s i o n i m p l i c i t e
( i n t )e; / / c o n v e r s i o n e x p l i c i t e
f(e); / / c o n v e r s i o n i m p l i c i t e

Les opérateurs de conversions sont pris en compte par l’algorithme de


résolution.

138
Opérateur: de conversion

Une utilisation intéressante de l’opérateur de conversion est la conversion en


std::string:
# i n c l u d e <string>
c l a s s Entier{
public:
o p e r a t o r i n t () c o n s t { r e t u r n _value;}
o p e r a t o r std::string () c o n s t {
std::stringstream s;
s<<_value;
r e t u r n s.str();
}
};
...
Entier e;
cout<<(std::string )e;

Cette approche est plus logique (que l’implémentation de <<) et permet plus de
139
possiblités.
Opérateurs: autres

L’opérateur [] est souvent utilisé. Il prend un seul argument et doit


obligatoirement être écrit comme fonction membre.
c l a s s Tab{
i n t *data;
i n t size;
public:
i n t o p e r a t o r []( i n t i) c o n s t {
r e t u r n data[i];
}
i n t & o p e r a t o r []( i n t i){
r e t u r n data[i];
}
};

permet la lecture et l’écriture.


remarque: ce n’est pas équivalent à un ’setter’ car on n’a pas de contrôle sur
la valeur modifiée 140
Opérateur []

Attention: l’operateur [] ne peut prendre qu’un seul paramètre:


Matrix m(5,5);
m[2][3] = 10;
m. o p e r a t o r [](2). o p e r a t o r [](3) = 10;

Cela donnerait par exemple:


c l a s s Matrix{
i n t **data;
// ...
i n t * o p e r a t o r []( i n t i){ r e t u r n data[i];}
c o n s t i n t * o p e r a t o r []( i n t i) c o n s t { r e t u r n data[i];}
};

Le deuxième appel à [] est l’opérateur classique des pointeurs. On n’a pas de


contrôle sur le dépassement!
141
Opérateur () vs []

Il pourrait être préférable d’utiliser l’operateur ():

Cela donnerait par exemple:


c l a s s Matrix{
i n t **data;
// ...
i n t o p e r a t o r ()( i n t i, i n t j) c o n s t {
r e t u r n data[i][j];
}
v o i d o p e r a t o r ()( i n t i, i n t j, i n t v) {
r e t u r n data[i][j]=v;
}
};

142
Opérateur: conclusion

• Les opérateurs sont des raccourcis syntaxiques permettant l’appel à des


méthodes
• L’opérateur d’affectation = à une fonction très particulière. Il est fournit
par le compilateur si l’on en écrit pas un.
• Il est impératif d’implémenter l’opérateur d’affectation si une classe gère
des ressources via ses attributs (allocation dynamique).
• Les opérateurs permettent de donner à des instances des comportements
similaires aux types primitifs : leur usage prend sens dans le contexte de la
programmation générique.

143
Plan

Héritage, polymorphisme

144
Héritage: intro

Jusqu’à présent, nous avons vu des mechanismes d’encapsulation: le C++


permettant de faire des struct améliorées:

• ajout de comportements
• ajout de la visibilité
• méthodes d’initialisation (constructeurs) et de libération (destructeurs)
• contrôle de la copie

Plus l’ajout de nouvelles méthodes d’allocation dynamique (new), des espaces


de noms (encapsulation), des références.

145
Héritage: définition

L’héritage est un mécanisme permettant de construire un type T à partir d’un


autre type Base. Le type T se retrouve doté des comportements (méthodes) et
propriétés (attributs) du type de Base. Une relation de typage relie ces deux
types: T est un sous-type de Base (ou classe dérivée de Base) tandis que Base
est un super-type de T.

En C++, les membres hérités et la relation de typage qu’il existe entre T et Base
ne sont pas necessairement visible de l’extérieur.

La relation est indiquée lors de la déclaration de T:


c l a s s T: p u b l i c Base{ ...

Nous reviendrons sur le sens du public plus tard.

146
Héritage: exemple

Voici un exemple classique d’héritage:

c l a s s Tuyau{ c l a s s TuyauPer : p u b l i c Tuyau{


d o u b l e _diametre; d o u b l e _densite;
public: public:
d o u b l e debit() c o n s t ; d o u b l e densite() c o n s t ;
d o u b l e diametre() c o n s t ; };
};

TuyauPer tper;
Tuyau &tuyau=tper; // r e l a t i o n de t y p a g e
tper.densite(); // ok
tuyau.densite(); // erreur !
tper.debit(); // ok

147
Héritage: pointeurs et référence

L’utilisation de la relation de typage ne peut se faire qu’avec les pointeurs ou


les références.

En effet, pour faire jouer la relation de sous-typage on souhaite manipuler une


même instance à travers différentes variables de types différents.

TuyauPer tper;
Tuyau t=tper; / / E r r e u r !
Tuyau *p=&tper; / / ok
Tuyau &r=tper; / / ok
TuyauPer &r2=r; / / E r r e u r !

148
Héritage: appel de méthodes

En C++ l’appel de méthode doit être vu comme un appel de fonction avec


passage d’un premier paramètre caché ( this dans la méthode).
Lors d’un appel, la méthode choisie est celle du type de la variable. Si la
méthode n’est pas présente directement dans le type, c’est la méthode
compatible de son parent le plus proche qui est choisie.

c l a s s A{ C c;
public : c.f(1) ; / / =>B
v o i d f ( char ) { p u t s ( "A\ n" ) ; } c.f( ’ a ’); / / =>B
}; c.A::f( ’A ’); / / => A
c l a s s B : p u b l i c A{ c.B::f( ’ a ’); / / => B
public : B &b=c;
v o i d f ( i n t ) { p u t s ( "B \ n" ) ; } b.f( ’ a ’); / / => B
};
c l a s s C: public B{ } ;

On utilise l’opérateur de portée pour spécifier la méthode à appeler. 149


Héritage: appel

c l a s s A{ c l a s s B : p u b l i c A{
public: public:
v o i d m(){} v o i d m(){}
}; };

B b;
A a;
A &r=b;
b.m(); / / b e s t de t y p e B => B : : m ( )
a.m(); / / a e s t de t y p e A => A : : m ( )
r.m(); / / r e s t de t y p e A => A : : m ( )
A *p=&b;
p->m(); / / p e s t de t y p e A * => A : : m ( )

150
Héritage: et mangling

Le “mangling” désigne la façon de générer un nom de symbole à partir d’une


fonction ou méthode:

$g++ -c /tmp/mang.c
$nm mang.o
U __gxx_personality_v0
00000000 T _ZN1A1mEv
00000006 T _ZN1B1mEv

La première fonction correspond à A::m(), la deuxième à B::m()


Il faut savoir que cette étape d’encodage n’est pas normalisée et est propre à
chaque compilateur. Ainsi, les objets compilés avec un compilateur ne seront
sans doute pas exploitable avec un autre compilateur!
En C, il n’y a pas de “mangling”, une fonction génère un symbole de même
nom que cette fonction. 151
Question

Le polymorphisme ne fonctionne qu’avec l’utilisation des pointeurs et des


références?
A) vrai
B) faux

152
Question

A ne possède ni attribut, ni méthode, ni classe de base. Que vaut


sizeof(A)?
A) 0
B) 1
C) 4
D) 4 si 32 bits, 8 en 64 bits

153
Héritage: et constructeur

Lors de la contruction d’une instance de la classe Fille, sa classe de base, la


classe Mere doit elle aussi être initialisée.
Pour initialiser la partie Mere, le compilateur ajoute un appel au constructeur
par défault:

c l a s s Mere{
Fille f;
public:
/*
Mere(){puts("Mere");}
Affiche :
};
c l a s s Fille: p u b l i c Mere{
Mere
public:
Fille
Fille(){puts(" F i l l e ");}
}; */

Comment faire si la classe Mere n’a pas de constructeur par défaut?

154
Héritage: et constructeur

On indique le constructeur à appeler comme ceci:

c l a s s Mere{
public: Fille f;
Mere( i n t i){puts("Mere");} /*
}; Affiche :
c l a s s Fille: p u b l i c Mere{
public: Mere
Fille():Mere(1) Fille
{puts(" F i l l e ");} */
};

L’appel au constructeur de la classe Mere doit se faire avant l’initialisation des


attributs de la classe.
Dans le cas contraire le compilateur inverse les initialisations.

155
Héritage: et constructeur

c l a s s Mere{
public:
Mere( i n t i){
Fille f;
printf("Mere : %d \ n",i);
/*
}
Affiche :
};
c l a s s Fille: p u b l i c Mere{
Mere : −1479451508
i n t v;
Fille
public:
Fille():v(1),Mere(v) */
{puts(" F i l l e ");}
};

Si l’on compile avec -Wall, le compilateur nous signal l’inversion, sinon rien
n’est dit.

156
Héritage: et destructeur

De même que pour le constructeur, les appels au destructeur se font en


“cascade”. C’est à dire qu’à la fin du destructeur de la classe Fille, un appel
au destructeur de la classe Mere est ajouté:

{
c l a s s Mere{
Fille f;
public:
}
Mere(){puts("Mere");}
/*
~Mere(){puts("~Mere");}
Affiche :
};
c l a s s Fille: p u b l i c Mere{
Mere
public:
Fille
Fille(){puts(" F i l l e ");}
~Fille
~Fille(){puts("~ F i l l e ");}
~Mere
};
*/

157
Héritage: et opérateur d’affectation

Nous avons vu que le compilateur ajoute dans les classes un opérateur


d’affectation si celui-ci n’est pas écrit.
L’opérateur ajouté par le compilateur fait appel à l’opérateur de la classe Mere:
c l a s s Mere {
i n t _v ; {
public :
Fille f,g;
Mere ( i n t v = 0 ) : _v ( v ) { }
c o n s t Mere &o p e r a t o r = ( c o n s t Mere &){ f=g;
p r i n t f ( "Mere:%d \ n" , _v ) ; }
return * t h i s ; /*
} Affiche :
};
c l a s s F i l l e : p u b l i c Mere {
Mere : 1 0
Mere m;
public :
Mere : 0
F i l l e ( ) : Mere ( 1 0 ) { } */
};
158
Héritage: et opérateur d’affectation

Voici à quoi ressemble l’opérateur d’affectation ajouté par le compilateur:


c l a s s Fille: p u b l i c Mere{
...
public:
c o n s t Fille & o p e r a t o r =( c o n s t Fille &f){
Mere:: o p e r a t o r =(f);
t h i s ->attribut1=f.attribut1;
t h i s ->attribut2=f.attribut2;
...
}
};

Dans le cas où nous voulons écrire notre propre opérateur, c’est à nous
d’appeler explicitement l’opérateur d’affectation de la classe Mere.

159
Héritage: et opérateur d’affectation

c l a s s A{
public:
c o n s t A& o p e r a t o r =( c o n s t A&){
puts("A=A"); r e t u r n * t h i s ;}
};
c l a s s M{ F f,g;
A a;
}; f=g;
c l a s s F: p u b l i c M{ / / Pas d ’ a f f i c h a g e !
public:
c o n s t F& o p e r a t o r =( c o n s t F&f){
return * t h i s ;
}
};

160
Héritage: et opérateur d’affectation

c l a s s A{
public:
c o n s t A& o p e r a t o r =( c o n s t A&){
puts("A=A"); r e t u r n * t h i s ;}
};
c l a s s M{
F f,g;
A a;
};
f=g;
c l a s s F: p u b l i c M{
/ / A f f i c h e A=A
public:
c o n s t F& o p e r a t o r =( c o n s t F&f){
M:: o p e r a t o r =(f);
return * t h i s ;
}
};

On peut faire appel à l’opérateur ajouté par le compilateur!


161
C++11 et delete

Le C++11 introduit un nouvel usage du mot clé delete:


c l a s s M{
public:
v o i d f() = d e l e t e ;
};

M m;
m.f();

Cela produit le message d’erreur suivant:


op_del.cpp:9:6: error: use of deleted
function ’ v o i d M: : f ( ) ’
9 | m.f();

162
C++11 et delete

Un usage courant de cette fonctionalité est de supprimer les fonctionalités


ajoutées automatiquement par le compilateur:
c l a s s A{
i n t m;
public:
A( i n t x) : m(x) {}
A& o p e r a t o r = ( c o n s t A &) = d e l e t e ;
A( c o n s t A&) = d e l e t e ;
};

A a1(1), a2(2);
a1 = a2; / / E r r o r
A a3(a2); / / E r r o r

C’est très souvent associé à l’héritage: dès qu’il y a de l’héritage, on va


supprimer la copie.
163
Héritage: public, protected et private

Il existe trois possibilités pour l’héritage, public: class T: public Base,


protected: class T: protected Base et private: class T: private Base. L’héritage
est privé par défaut dans les classes et publique pour les structures.

Dans le cas de l’héritage public, la relation entre T et Base est visible de tous.
Les méthodes public de Base sont public dans T et les méthodes protected sont
protected dans T.

Dans le cas de l’héritage protected, les méthodes public et protected de Base sont
protected dans T.

Dans le cas de l’héritage private , les méthodes public et protected de Base sont
private dans T.

164
Héritage: public, protected et private

On ne peut utiliser un relation type/sous-type que si l’on peut avoir accès à un


membre publique de la classe Base à travers T.

Si l’héritage est publique, tout le monde voit les méthodes publiques de Base à
travers T. Ainsi on pourra toujours voir un T comme un Base.

Si l’héritage est protégé alors seules les fonctions/classes amies, sous-classes et


amies des sous-classes pourront voir un T comme un Base.

Si l’héritage est privé alors seules les fonctions/classes amies pourront voir un T
comme un Base.

165
Héritage: public, protected et private

class A{
B b;
...
A&a=b; / / ok
};
class B: p u b l i c A{
C c;
...
A *p=&c; / / e r r e u r !
};
class C: p r o t e c t e d A{
v o i d D::f(){
...
A &r=* t h i s ; / / ok
};
}
class D: p u b l i c C{
...
v o i d F::f(){
};
A &r=* t h i s ; / / e r r e u r
class E: p r i v a t e A{
}
...
};
v o i d E::f(){
class F: p u b l i c E{
A &r=* t h i s ; / / ok
...
}
}; 166
Héritage: public, protected et private

L’héritage protected ou private permet de récuperer une implémentation.

C’est une alternative efficace à la délégation (lien a-un).

Un exemple: on veut implémenter une classe Pile. Pour cela on souhaite


utiliser la classe Vector.
Or, une pile n’est pas un vecteur particulier, on ne souhaite donc pas que la
classe Pile présente toutes les méthodes de la classe Vector.

La solution sans héritage privé consiste à utiliser la délégation: on dispose d’un


attribut de type Vector que l’on utilise pour implémenter la pile.

Si l’on souhaite que cette “délégation” soit accessible aux sous classes alors on
utilise l’héritage protégé sinon on utilise l’héritage privé.

167
Héritage: et polymorphisme

Jusqu’à présent, l’héritage permet le factorisation de code mais ne permet pas


de mettre en oeuvre le polymorphisme.
En effet, pour qu’il y ai polymorphisme il faut que la méthode appelée sur une
instance soit celle correspondant au type réel de l’instance et non au type de la
variable manipulant cette instance.

Pour mettre en oeuvre le polymorphisme il faut donc utiliser l’instance


elle-même pour connaître son type.

Ceci est implanté grace à un attribut caché appelé vtable. C’est un pointeur
vers une table référençant toutes les méthodes pour lesquelles le
polymorphisme doit être mis en oeuvre.

168
Héritage: et virtual

Tout d’abord il nous faut indiquer quelles sont les méthodes “virtuelles”, c’est à
dire pour lesquelles on veut du polymorphisme.
c l a s s Base {

public:
v i r t u a l std::string toString() c o n s t {
r e t u r n " Base ";
}
};

On notera que la présence de la vtable fait “grossir” les instances de la classe:


on passe ici de 1 octet à 4 octets (sur un machine 32 bits).

Ceci crée une table associée à la classe Base. Cette table est une table de
pointeurs de fonctions: les méthodes virtuelles
169
Héritage: vtable

Base b;

170
Question

Chaque instance d’une classe ayant des méthodes virtuelles possède une
table des méthodes virtuelles?
A) vrai
B) faux

171
Héritage: vtable

Ajoutons une classe dérivée de Base:


c l a s s F : p u b l i c Base{
public:
};

Si l’on ne redéfini pas la méthode toString alors on obtient le schéma suivant

172
Héritage: vtable

Base b;
F f;

Une table des fonctions virtuelles est créée pour la classe F. Celle-ci est
initialement construite par “recopie” de la table de la classe de base.

Dans les constructeurs, le compilateur ajoute une instruction permettant


d’initialiser l’attribut caché vtable pour qu’il pointe vers la table
correspondant au type de l’instance.

173
Héritage: vtable

Cas où l’on redéfini la méthode dans la classe F:


c l a s s F : p u b l i c Base{
public:
v i r t u a l std::string toString() c o n s t {
r e t u r n " F i l l e ";
}
};

174
Héritage: vtable

Base b;
F f;

Si une méthode est redéfinie: même nom, même arguments (même type) alors
l’entrée dans la table correspondante à cette méthode est modifiée.
On voit que ce mécanisme ne modifie en rien le code de la méthode, c’est la
façon dont la méthode va être appelée qui va être changé.

175
Héritage: appel d’une méthode virtuelle

Lors de l’appel à une méthode sur une instance, le compilateur commence par
chercher cette méthode dans la classe correspondante au type de la variable sur
laquelle la méthode est appelée.
Fille f;
Base &r=f;

r.toString();

Une fois cette méthode trouvée, il y a deux possibilités: soit la méthode est
virtuelle, soit elle ne l’est pas.
Si elle n’est pas virtuelle, alors c’est la méthode du type de la variable qui est
appelée, dans l’exemple Base::toString.
Si elle est virtuelle, alors c’est la méthode dont l’adresse est dans la vtable qui
est appelée, dans l’exemple cela dépend si elle a été redéfinie.
176
Héritage: vtable

Fille f;
Base &r=f;

r.toString();

L’appel à la méthode peut être vu comme


r.vtable[" t o S t r i n g "]();

Dans cet exemple, cela correspond à l’appel de Base::toString() car la


méthode n’a pas été redéfinie.

177
Héritage: vtable

Fille f;
Base &r=f;

r.toString();

L’appel à la méthode peut être vu comme


r.vtable[" t o S t r i n g "]();

Dans cet exemple, cela correspond à l’appel de Fille::toString() car la


méthode a été redéfinie.

178
Héritage: virtuelle pure

Une méthode est dite “virtuelle pure” si elle n’est pas implémentée dans la
classe où elle est déclarée.
classe Base{
public:
v i r t u a l std::string toString() c o n s t =0;
};

Le =0 est bien compréhensible: cela correspond à mettre à 0 le pointeur dans la


table des fonctions virtuelles!

Une classe n’est instanciable que si sa table des fonctions virtuelles ne


comporte pas de 0.

Dans cette exemple, Base n’est donc pas instanciable et toute sous classe de
Base ne sera instanciable que si elle redéfinie la méthode toString.
179
Héritage: virtuelle pure

Soit le code suivant:


c l a s s Base{
public:
v i r t u a l v o i d m( c o n s t Base &)=0;
};

c l a s s Fille : p u b l i c Base{
public:
v i r t u a l v o i d m( c o n s t Fille &){}
}

Question: Quelle affirmation est vraie?


A) Base est instanciable.
B) Fille est instanciable.
C) Base et Fille sont instanciables.
D) aucune n’est instanciable. 180
Héritage: et redéfinition

La redéfinition d’une méthode consiste à écrire dans une classe dérivée une
méthode qui

• porte le même nom


• a le même nombre et type d’arguments
• a comme type de retour un sous-type du type de retour de la méthode
redéfinie ou le même type.

181
Question

L’appel à une méthode non virtuelle sur une instance NULL génère
systématiquement un SEGFAULT
A) vrai
B) faux

182
Héritage: et valeur par défaut

En C++, les arguments des fonctions et méthodes peuvent prendre des valeurs
par défaut pour les arguments:
v o i d f( i n t =0){}

f(); / / A p p e l f ( 0 ) ;

Les valeurs par défaut doivent être précisées dans la déclaration, en effet elle
indique au compilateur comment compléter l’appel avec ces valeurs si
nécessaire.

Les valeurs par défaut doivent être données du dernier argument vers le premier
argument afin d’éviter toute ambiguité.

183
Héritage: et valeur par défaut

c l a s s M{
i n t main()
public:
{
v i r t u a l ~M(){}
F x;
v i r t u a l v o i d h( i n t =0)=0;
M &y=x;
};
c l a s s F: p u b l i c M{
y.h(); / / a p p e l de F : : m ( 0 ) ;
public:
x.h(); / / a p p e l de F : : m ( 1 ) ;
v i r t u a l v o i d h( i n t i=1){}
}
};

Bien que l’appel soit virtuel, c’est le type de la variable qui est utilisé pour
connaître les valeurs par défaut des arguments!

184
Héritage: et valeur par défaut

c l a s s M{
i n t main()
public:
{
v i r t u a l ~M(){}
F x;
v i r t u a l v o i d h( i n t =0)=0;
M &y=x;
};
c l a s s F: p u b l i c M{
y.h(); / / a p p e l de F : : m ( 0 ) ;
public:
x.h(); / / E r r e u r !
v i r t u a l v o i d h( i n t i){}
}
};

D’une manière générale on evitera de mélanger redéfinition, surcharge et valeur


par défaut: il faut toujours (c’est aussi valable pour les opérateurs,
exceptions...) que le comportement attendu soit “limpide”.

185
Héritage: et using

Lorsque l’on mélange redéfinition et surchage il peut arriver des choses


“bizarre”:
c l a s s M{
public: F x,y;
v o i d h( c o n s t M&); M &p=x,&q=y;
}; x.h(y); / / 1 . ok
c l a s s F: p u b l i c M{ p.h(q); / / 2 . ok
public: p.h(x); / / 3 . ok
v o i d h( c o n s t F&); x.h(p); / / 4 . erreur !
};

Question: quelle méthode est appelée par p.h(x);?


A) M::h
B) F::h
Pour le cas 4, comme F hérite de M on pourrait s’attendre à ce que la méthode
186
M::m soit appelée.
Algorithme de résolution

Lors de l’appel d’une méthode m sur une variable v de type V (ptr ou ref)
pointant sur une instance de type I, la méthode est cherchée de la manière
suivante:
• Existe-t-il des méthodes de nom m dans la classe V?⇒ using
• Oui (partie 1):
• Si une méthode est compatible: elle est choisie
• Si plusieurs méthodes sont compatibles: ambiguité
• Si aucune méthode n’est compatible: erreur.
• Non (partie 2):
• On cherche m dans la hiérarchie de V. Lorsqu’une méthode est trouvée, on
applique la partie 1.
• Une méthode a été trouvée (sinon erreur).
• Si elle est virtuelle:
• On effectue l’appel en utilisant la vtable
• Si la méthode a été redéfinie dans I alors c’est I::m qui est appelée.
187
• Sinon c’est la méthode du plus proche parent.
Algorithme de résolution: remarque

Si une variable v de type T n’est ni une référence ni un pointeur, alors la


résolution se fait à la compilation.

Même si la méthode appelée est virtuelle, il n’y aura pas utilisation de la


vtable.

188
Appel avec opérateur de portée

Lorsque l’on utilise l’opérateur de portée, l’appel de méthode se fait en


inspectant la classe indiquée dans l’appel et sans tenir compte du virtual .

s t r u c t A{ C z;
virtual v o i d m(){} A &x=z;
}; B &y=z;
s t r u c t B: A{ x.m();
}; z.B::m();
s t r u c t C: B{ y.m();
virtual v o i d m(){} y.B::m();
};

Les appels utilisant l’opérateur de portée ne passent pas par la vtable, la


résolution se faisant à la compilation.

189
Héritage: et using

On peut utiliser le mot clé using pour inclure des méthodes parent dans les
méthodes à inspecter:

c l a s s M{
public:
F x,y;
v o i d h( c o n s t M&);
x.h(y); / / 1 . ok
};
M &p=x,&q=y;
c l a s s F: p u b l i c M{
p.h(q); / / 2 . ok
public:
p.h(x); / / 3 . ok
u s i n g M::h;
x.h(p); / / 4 . ok
v o i d h( c o n s t F&);
};

Le using M::h; dans la classe F indique au compilateur d’inclure la ou les


méthodes de nom h dans la classe M dans l’algorithme de résolution.

190
Question

À ce stade du cours, l’utilisation de using dans une classe fille par rapport
à une méthode mère suppose l’usage de la surcharge
A) vrai
B) faux

191
Héritage: classe abstraite

On appel "classe abstraite" une classe ayant au moins une méthode virtuelle
pure.

En C++, il n’existe pas de définition d’interface (dans la norme), cependant on


pourra appeler interface une classe ne disposant que de méthodes virtuelles
pures (et un destructeur virtuel).

Les classes abstraites et les interfaces ne sont pas instanciables.

Une classe abstraite ou interface ne peut pas être utilisée comme type de retour
par recopie ou comme argument par valeur (recopie).

On ne peut utiliser que des pointeurs et références sur ces types.

192
Héritage: et interface

Exemple d’une interface:


c l a s s Usager{
public:
v i r t u a l ~Usager(){}
v i r t u a l std::string nom() c o n s t =0;
v i r t u a l v o i d monterDans(Transport &)=0;
};

Soit I une interface, laquelle de ces syntaxes est invalide?


A) I *i;
B) I &i=j;
C) I i;
D) I &*i=p;

193
Héritage: et classe abstraite

Exemple d’une classe abstraite:


c l a s s PassagerAbstrait{
protected:
std::string _nom;
i n t _etat;
i n t _destination;
...
v i r t u a l choixPlaceMontee(Bus &b)=0;
...
public:
v i r t u a l ~PassagerAbstrait(){}
v o i d estDebout() c o n s t {
r e t u r n _etat==DEBOUT;
}
};

194
Héritage: et destructeur

Si une classe va être dérivée alors elle doit avoir un destructeur virtuel
(implémenté):

c l a s s A{
};
c l a s s B{
std::string s;
public:
B():s(" h e l l o "){}
};
...
{
B b;
} / / ok : l e d e s t r u c t e u r de B a p p e l l e l e
/ / d e s t r u c t e u r de l a c l a s s e s t d : : s t r i n g s u r s

195
Héritage: et destructeur

Si une classe va être dérivée alors elle doit avoir un destructeur virtuel
(implémenté):

c l a s s A{
};
c l a s s B{
std::string s;
public:
B():s(" h e l l o "){}
};
...
A *a = new B;
d e l e t e a; / / F u i t e mémoire , l e d e s t u c t e u r de
/ / s t d : : s t r i n g n ’ e s t pas ap pe lé .

196
Héritage multiple

En C++, une classe peut avoir plusieurs classes de base, que celles-ci soient
abstraites ou non:
class I1{ ... };
class I2{ ... };
class B{ ...};
class C: p u b l i c B, p u b l i c I1, p u b l i c I2 {
// i m p l é m e n t a t i o n de t o u t e s l e m é t h o d e s
// v i r t u e l l e s pures .
};

La classe C hérite ici de trois classes. Ainsi, on pourra se représenter C en


mémoire de la façon suivante:

197
Hérite multiple: exemple

198
Héritage multiple

L’opérateur de portée et le using sont très utils pour lever les ambiguités:
c l a s s I1{
public:
v o i d m();
};
c l a s s I2{
public:
v o i d m();
};

c l a s s F: p u b l i c I1, p u b l i c I2{
....
m(); / / E r r e u r !
I1::m(); / / ok
...
};

199
Héritage multiple

L’opérateur de portée et le using sont très utils pour lever les ambiguités:
c l a s s I1{
public:
v o i d m();
};
c l a s s I2{
public:
v o i d m();
};

c l a s s F: p u b l i c I1, p u b l i c I2{
....
u s i n g I2::m;
m(); / / ok
I1::m(); / / ok
...
};
200
Héritage multiple virtuel

Il peut arriver qu’une classe possède plusieurs fois une même classe comme
classe de base:
c l a s s I{ p u b l i c : i n t _value; };

c l a s s I1: p u b l i c I{};

c l a s s I2: p u b l i c I{};

c l a s s C: p u b l i c I1, p u b l i c I2{};

Dans ce cas, C dispose de deux instances de la classe I:

201
Hérite multiple: exemple

202
Héritage multiple: ambiguïté

Ainsi, une instance de la classe C à deux attributs _value:


C c;
c._value ; / / E r r e u r !

L’opérateur de portée permet de lever cette ambiguïté:


C c;
c.I1::_value ; / / Ok
c.I1::I::_value ; / / Ok
c.I2::_value ; / / Ok

203
Héritage multiple: virtuel

Dans le cas d’un héritage en losange comme celui que nous venons de voir, les
classes I1 et I2 pourraient vouloir partager leur classe de base I.
Pour cela, on utilise l’héritage virtuel.
L’héritage virtuel permet d’indiquer qu’une classe est prête à partager une de
ces classes de base avec une autre classe qui aurait fait de même:
class I{};
class I1 : p u b l i c v i r t u a l I{};
class I2 : p u b l i c v i r t u a l I{};
class I3 : p u b l i c I {};
class C : p u b l i c I1, p u b l i c I2{};
class D : p u b l i c I1, p u b l i c I3{};

Une instance de la classe C ne contient qu’un seul I


Une instance de la classe D en possède deux
204
Hérite multiple: virtuel

205
Hérite multiple: virtuel

L’héritage virtuel permet de lever des ambiguïtés:


class I{ p u b l i c : v i r t u a l v o i d m()=0; };
class I1 : p u b l i c v i r t u a l I{};
class I2 : p u b l i c v i r t u a l I{};
class I3 : p u b l i c I {};
class C : p u b l i c I1, p u b l i c I2{};
class D : p u b l i c I1, p u b l i c I3{};

C c;
c.m(); / / ok
D d;

d.m(); / / E r r e u r !

On utilisera systématiquement l’héritage virtuel lors de l’héritage d’une


interface.
Dans le cas ci-dessus, I doit bien entendu posséder un destructeur virtuel. 206
Hérite multiple: virtuel

D’une façon générale, on n’utilisera pas l’héritage virtuel si la classe de base


possède des propriétés (attributs) pouvant changer.

Si l’héritage virtuel concerne une classes de base ayant des attributs, alors un
attribut caché est ajouté par le compilateur (un peu comme la vtable). Il y a
donc un coût mémoire.

On pourra s’autoriser l’héritage virtuel si la classe de base est une interface, ou


bien une classe ne possédant que des méthodes virtuelles pures, un constructeur
par défaut et des attributs dont la valeur est fixée lors de la construction.

207
Plan

Template

208
Template: présentation

En plus du support de la programmation objet, le C++ ajoute au C la


programmation générique.

La programmation générique consiste à écrire du code (fonction ou classe)


indépendamment de certains types. Ceux-ci seront fixés plus tard lors de
l’utilisation de ce code.

En C++ c’est le mot clé template qui permet la définition de la généricité

On préfixera le code générique d’une intruction template<class T> où T devient


un type variable, paramètre du code.

209
Template: présentation

On pourra écrire deux types de code générique: des classes génériques et des
fonctions génériques.
Voici un exemple de fonction générique:
t e m p l a t e < c l a s s T>
v o i d swap(T &a, T&b){
T t=a;
a=b;
b=t;
}

Cette fonction effectue l’echange de valeur entre a et b quel que soit leur type.
On voit cependant que a et b doivent être de même type

210
L’utilisation de cette fonction swap se fait comme suit:
t e m p l a t e < c l a s s T>
v o i d swap(T &a, T&b){...}
swap< i n t >(a,b); / / 1
swap(a,b); / / 2
i n t a=0;
swap(a,c); / / 3
i n t b=1;
char c= ’ a ’;

Dans le cas 1, nous indiquons explicitement le type pour T, le compilateur


remplace alors T par int et trouve la fonction swap<int>(int &,int &) qui
convient à l’appel. Par la même occasion, il compile cette fonction.
Dans le cas 2, le compilateur cherche la fonction swap et essaye de déduire un
type convenable pour T à partir des arguments (ceci n’est pas toujours
possible). Il construit swap<int> et compile cette fonction.
Dans le cas 3, il n’arrive pas à trouver une fonction swap avec le bon prototype
et génére une erreur.
211
Template: et compilation

Un problème se pose pour la compilation d’un code template.


En effet, celui-ci étant à trous, il ne peut être compiler qu’une fois les types de
paramètres connus.
Pour cela, on ne peut plus mettre seulement le prototype dans le .hpp, nous
metterons aussi le code de la fonction:
en deux fois:
en une fois: t e m p l a t e < c l a s s T>
v o i d swap(T &, T& );
t e m p l a t e < c l a s s T>
v o i d swap(T &a, T& b){
...
T t=a;
a=b;
t e m p l a t e < c l a s s T>
b=t;
v o i d swap(T &a, T& b){
}
T t=a;a=b;b=t;
}
212
Template: classe

Voici un exemple de classe paramétrée (ou générique):


t e m p l a t e < c l a s s T>
c l a s s Entier{
T _value;
public:
Entier( c o n s t T &t):_value(t){}
};

Dans le cas d’une classe, on écrira donc le code directement dans le fichier .hpp
On peut tout à fait séparer les déclarations de l’implémentation:

t e m p l a t e < c l a s s T>
c l a s s Entier{
t e m p l a t e < c l a s s T>
T _value;
Entier<T>::Entier( c o n s t T &t):_value(t)
public:
{}
Entier( c o n s t T &t);
}; 213
Template: classe

Lors de l’utilisation d’une classe générique, nous devons indiquer explicitement


le type à utiliser pour les paramètres:
Entier a; / / E r r e u r !
Entier< i n t > b; / / ok
Entier<long > c; / / ok

On parlera de type complet pour désigner une classe générique dont l’ensemble
des paramètres sont spécifiés.

Il est important de retenir qu’il n’existe aucune relation de typage entre deux
types complets correspondant à une même classe générique.

214
Template: valeur par défaut

On peut spécifier des valeurs par défaut pour les paramètres d’une classe
générique (pas des fonctions). Celles-ci doivent être fournies de la droite vers la
gauche:
t e m p l a t e < c l a s s T= i n t >
c l a s s Entier{...

t e m p l a t e < c l a s s D, c l a s s H= i n t >
c l a s s HashTable{...

t e m p l a t e < c l a s s T= i n t , c l a s s C> / / Erreur !


i n t compare(T *, T *)

Entier e; / / ok : )

215
Template: et type primitif

Un code peut aussi être paramétré par des valeurs (constantes) de types
primitifs:
t e m p l a t e < i n t taille>
c l a s s Matrix{
i n t _data[taille][taille];
public:
};

Dans ce cas, Matrix<4> et Matrix<3> sont deux types différents. Cet exemple
pourrait être utilisé en infographie.

216
Template: spécialisation

Il est possible de modifier l’implémentation pour certaines valeurs de


paramètre:
t e m p l a t e < c l a s s T>
v o i d affiche( c o n s t T &t){
std::cout<<t<<std::endl;
}

Dans le cas des entiers, on utilise printf:


t e m p l a t e <>
v o i d affiche< i n t >( c o n s t i n t &t){
printf("%d \ n",t);
}

217
Template: spécialisation

Dans le cas où les paramètres sont des types primifs, on peut spécialiser selon
les valeurs:
t e m p l a t e < i n t n>
v o i d affiche(){
printf("%d \ n",n);
}

t e m p l a t e <>
v o i d affiche<0>(){
printf(" z e r o \ n");
}

...
affiche(); / / e r r e u r !
affiche<1>(); / / a f f i c h e : 1
affiche<0>(); / / a f f i c h e : z e r o

218
Template: spécialisation

Dans le cas des classes, la spécialisation peut aussi se faire sur les pointeurs:
t e m p l a t e < c l a s s T>
c l a s s Tableau{
T *_data;
...
};

t e m p l a t e < c l a s s T>
c l a s s Tableau<T *>{
T **_data; / / A t t e n t i o n i c i !
public:
Tableau( i n t i){
...
_data[i]=NULL;
...
}
};
219
Template: spécialisation partielle

Il est possible de ne spécialiser que certains paramètres d’un code générique:


t e m p l a t e < c l a s s T, c l a s s H>
c l a s s HashTable{
};

t e m p l a t e < c l a s s T, c l a s s H>
c l a s s HashTable<T *,H>{
};

t e m p l a t e < c l a s s T>
c l a s s HashTable<T *, i n t >{
};

Important: il n’y a aucune contrainte entre ces différentes implémentations car


elle ne correspondent pas au même type

220
Template: contraintes

Lorsque l’on utilise un type dans un code générique, il faut faire très attention
aux propriétés que l’on impose sur ce type:

t e m p l a t e < c l a s s T>
t e m p l a t e < c l a s s T>
v o i d swap(T &a,T&b){
v o i d swap(T &a,T&b){
T t;
T t=a;
t=a;
a=b;
a=b;
b=t;
b=t;
}
}

On veillera à toujours indiquer clairement les méthodes attendues pour une type
paramètre.

221
Template: STL et BOOST

Il existe deux grandes bibliothèques utilisant les templates, la Standard


Template Library et BOOST.

La STL contient:

• des conteneurs (tableaux, map, ...),


• des iterateurs (parcours),
• des algorithmes (comparaison, recopie, recherche, tris...),
• des allocateurs mémoire,
• des foncteurs

La bibliothèque BOOST contient beaucoup, beaucoup de choses (conteneurs,


graphes, maths, entrées/sorties, fichiers...).

222
Template: STL

Pour comprendre et utiliser la STL, il faut savoir qu’il est possible de déclarer
un alias de type dans une classe:

t e m p l a t e < c l a s s T>
c l a s s Entier{
public:
t y p e d e f T type;
};

Entier< i n t >::type i;

Ceci fait que la variable i est de type int

223
Template: STL

Parmis les conteneurs les plus connus, il y a les vecteurs, ceux-ci répondent au
concept de “Conteneur à Accès Quelconque” et “Sequence avec insertion en
fin”. Ces concepts définissent un certain nombre des propriétés: compléxité,
nom de méthodes, type interne...
# i n c l u d e <vector>

std::vector< i n t > v;
v.push_back(0);
v.push_back(1);
v[0]=2;
f o r (std::vector< i n t >::iterator i=v.begin();
i!=v.end();
i++)
std::cout<<" "<<*i;

Cet exemple utilise le type interne iterator qui correspond au type


implémentant le concept d’itérateur pour la classe vecteur. 224
Template: STL

Les itérateurs ont été conçus pour fonctionner comme les pointeurs: on utilise
++ et -- pour se déplacer, * et -> pour utiliser la valeur représentée par
l’itérateur.
Voici un exemple permettant de trier un vecteur:
# i n c l u d e <vector>
vector< i n t > v;
v.push_back(0); v.push_back(5); v.push_back(2);
std::sort(v.begin(),v.end());

L’algorithme std::sort effectue un trie des données contenues par la séquence


des deux itérateurs.
Ainsi, cet algorithme pourra être utiliser avec de nombreux autre conteneurs de
la STL qui disposent des itérateurs et même des tableaux:
i n t t[3] = {0, 5, 2};
std::sort(t,t+3); 225
STL

La STL repose sur la notion de concept.


Un concept est un peu comme une "interface" mais qui n’a d’existence que sur
le papier.
Une concept est un ensemble de méthodes, types internes, propriétés que doit
posséder une classe répondant au concept.
Exemple de concept: "container"
• X::value_type type contenu
• X::iterator type à utiliser pour parcourir ("input iterator")
• X::const_iterator type pour parcourir sans modifier
• X::reference type de la référence vers le type contenu
• X::const_reference référence constante
• X::pointer pointeur vers le type contenu
• X::difference_type différence entre deux iterateurs
• X::size_type 226
STL

La STL propose un ensemble de classes paramétrées répondant à un ou


plusieurs concepts :
• Séquences : vector, deque, list, slist, bit_vector
• Conteneurs associatifs :set, map, multiset, multimap, hash_set,
hash_map, hash_multiset, hash_multimap, hash
• Les chaînes de caractères : Character Traits, char_traits,
basic_string

Ainsi qu’un ensemble d’algorithmes :


• for_each, find, find_if, adjacent_find, find_first_of, count,
count_if, mismatch, equal, search, search_n, find_end
• remplacement dans une séquence . . .
• tris, mélange, rotation, renversement, . . .
• recherche d’élément, min, max . . .
227
Template: et metaprogrammation

La metaprogrammation consiste à faire s’executer un programme par le


compilateur au moment de la compilation et donc de façon statique.

Un exemple classique de metaprogrammation est un calcul mathématique


simple:
t e m p l a t e < i n t a, i n t b>
s t r u c t Addition{
s t a t i c c o n s t i n t resultat=a+b;
};

std::cout<< Addition<2,3>::resultat <<std::endl;

228
Template: et metaprogrammation

et un peu plus compliqué:


t e m p l a t e < i n t a, i n t b>
s t r u c t Puissance{
s t a t i c c o n s t i n t resultat=a*Puissance<a,b-1>::resultat;
};

t e m p l a t e < i n t a>
s t r u c t Puissance<a,0>{
s t a t i c c o n s t i n t resultat=1;
};

std::cout<< Puissance<2,3>::resultat <<std::endl;

229
Template: et metaprogrammation

La metaprogrammation permet de faire des choses très puissantes telles que la


composition de types. Cependant, elle reste souvent réservée à
l’implémentation de bibliothèques complexe et est difficile à maîtriser.

Elle repose sur les concepts suivant:

• les templates,
• la spécialisation,
• les types internes,
• les attributs statiques et constants

230
Template: et metaprogrammation

On peut ainsi écrire un “language” sur les types, avec des branchements
conditionnels:
t e m p l a t e < b o o l C, c l a s s T, c l a s s E>
s t r u c t IfThenElse{
t y p e d e f T result;
};

t e m p l a t e < c l a s s T, c l a s s E>
s t r u c t IfThenElse< f a l s e ,T,E>{
t y p e d e f E result;
};

IfThenElse< t r u e , i n t ,char >::result i;

231
Template: et metaprogrammation

Comparaison de types:
t e m p l a t e < c l a s s T, c l a s s U>
s t r u c t Equal{
s t a t i c b o o l result= f a l s e ;
};
t e m p l a t e < c l a s s T>
s t r u c t Equal<T,T>{
s t a t i c b o o l result= t r u e ;
}

b o o l b=Equal< i n t ,size_t>::result;

IfThenElse<Equal< i n t ,ssize_t>::result,
int ,
long >::result i;

232
Template: et metaprogrammation

Ce type de metaprogrammation sert pour l’optimisation:


t e m p l a t e < c l a s s T>
struct IsPointer {
s t a t i c bool r e s u l t = f a l s e ;
};
t e m p l a t e < c l a s s T>
s t r u c t I s P o i n t e r <T * >{
s t a t i c bool r e s u l t =true ;
};
t e m p l a t e < c l a s s T>
c l a s s MaClasse {
t y p e d e f I f T h e n E l s e < I s P o i n t e r <T > : : r e s u l t , l i s t <T> , v e c t o r <T>> i c o n t a i n e r ;
i c o n t a i n e r _data ;
...
};

233
Template: et metaprogrammation

On peut aussi utiliser les fonctions templates pour inférer des types et “cacher”
les templates
struct Interface {
virtual ~ Interface (){}
....
};

t e m p l a t e < c l a s s T>
c l a s s MaClasse : p u b l i c I n t e r f a c e {
t y p e d e f I f T h e n E l s e < I s P o i n t e r <T > : : r e s u l t , l i s t <T> , v e c t o r <T>> i c o n t a i n e r ;
i c o n t a i n e r _data ;
...
public :
MaClasse ( c o n s t T &t ) {
....
}
...
};

t e m p l a t e < c l a s s U>
s t a t i c MaClasse <U> * g e t C l a s s e ( c o n s t U &v a l u e ) {
r e t u r n new MaClasse <U> ( v a l u e ) ;
}

i n t main ( ) {
Interface * i=getClasse (0); 234
}
Template: et metaprogrammation

La métaprogrammation permet de faire des API simplifiées:

s t r u c t RandomContainer{};
s t r u c t ReversibleContainer{};

t e m p l a t e < c l a s s T, c l a s s Type>
s t r u c t Container{
t y p e d e f vector<T> result;
}

t e m p l a t e < c l a s s T>
s t r u c t Container<T,ReversibleContainer>{
t y p e d e f list<T> result;
}

Container< i n t ,RandomContainer>::result container;

235
Plan

Synthèse

236
Synhèse

Le langage C++ dispose de nombreux concepts:

• l’encapsulation (visibilité)
• les références
• les opérateurs
• l’héritage simple et multiple
• les fonctions vituelles
• la programmation générique

Egalement, il laisse au programmeur la gestion de la mémoire.

237
Synthèse

Programmer efficacement en C++, c’est répondre à deux exigences:

• Ecrire du code lisible, dont le fonctionnement est clair.


• Viser la performance: en temps et en mémoire.

238
Ecrire du code lisible

Ecrire du code lisible, dont le fonctionnement est clair implique:

• Ne pas utiliser les opérateurs n’importe comment.


• Ne pas détourner les opérateurs au delà de leurs sens premier.
• Limiter l’usage de la programmation générique à des parties de code
isolée.
• Pas de surchage avec l’héritage
• Pas de valeur par défaut avec l’héritage

Il n’est pas vraiment possible d’interdire la copie (par construction ou par


affectation) aussi il faut la gérer au mieux (utilisation de la visibilité).

239
Ecrire du code lisible

D’une manière générale, on réservera le polymorphisme aux classes


structurantes (architecture logicielle):

• Destructeur virtuel
• méthodes toutes virtuelles
• Heritage virtuel sur les interfaces
• Limiter l’usage de l’héritage multiple.
• pas d’opérateurs
• pas de surcharge

Ces classes servent pour la structuration générale du logiciel: en pratique, le


nombre d’instances est réduit et l’utilisation (appel de méthodes) des instances
est modéré.
240
Viser la performance

Pour les parties calculatoires (pixels d’une image, graphes, etc...) on utilise en
priorité:

• Des classes sans méthodes virtuelles (si beaucoup d’instances)


• Pas d’héritage
• Les opérateurs si l’on veut profiter de code générique
• La programmation générique

Ce code doit être isolé du reste de l’application (api, bibliothèque).

241

Vous aimerez peut-être aussi