Vous êtes sur la page 1sur 382

Programmation orientée objet en C++

Licence 3 Informatique

David Genest
Année universitaire 2018-2019
Chapitre 1
Introduction

Présentation du cours
De C à C++
Présentation du cours

Présentation du cours
De C à C++
Programmation orientée objet C++

Objectif du cours
▶ Approfondir les concepts de la programmation orientée objet.
(Déjà vus en Java dans le cours de POO)
▶ Apprendre le langage C++ (moderne).
▶ Utiliser des outils d’aide au développement.
Environnement de développement, débogueur…

Organisation
▶ 20h de cours : Concepts de la POO et C++.
▶ 44h de TP : Programmation C++ et outils de développement.
▶ + Bases de la programmation d’interfaces graphiques (Qt).
Évaluation
▶ 1 2 1
3 CC + 3 Examen. CC : 2 TP relevés et notés.
Introduction POO

Programmation Orientée Objet


Paradigme (style) de programmation consistant à assembler des
briques logicielles (objets).
Un objet représente un concept ou une entité du monde.

La plupart des langages de programmation populaires aujourd’hui


sont basés sur l’approche objets : Java, C#, Swift, Python, PHP,
Javascript… Mais les objets ne sont pas uniquement utilisés pour la
programmation…
▶ Bases de données objets (Oracle)
▶ Méthodes de conception objets (UML)
▶ HTML+CSS, (accompagné de Javascript, PHP…)
Objectif premier
Faciliter le développement d’une application robuste. 2
Introduction POO

Comparaison avec la programmation impérative (C, Pascal, …)


▶ Impératif. Un problème est décomposé en sous-problèmes, une
fonction est écrite pour résoudre chaque problème. Les fonctions
s’appellent entre elles en se passant des paramètres.
▶ Objets. On identifie les concepts (ou entités) du problème, ainsi
que les relations entre ces concepts. Chacun de ces concepts est
ensuite représenté par les données qu’ il contient et par les
traitements qu’ il est capable d’effectuer.

Langage à classes
La plupart des langages de programmation OO sont des langages à
classes : une classe définit un « moule » pour un ensemble d’objets
qui ont la même structure et fournissent les mêmes traitements.
3
Introduction C++

C++
▶ Langage normalisé par l’ISO
▶ Défini dans les années 1980 (mais a évolué depuis : C++98, C++03,
C++11, C++14, C++17)
▶ « Amélioration » de C ⇒ facilite l’apprentissage pour quelqu’un
qui connaît déjà C. Mais … faire du C en C++ n’est pas programmer
en C++ ! Réutilisation facilitée de code/bibliothèques C
▶ Doté d’une bibliothèque de classes et algorithmes
▶ Portable

Utilisé dans Adobe Photoshop, Google Chrome, Microsoft Windows, MS


Office, LibreOffice, Edge, Mozilla Firefox, Opera, KDE, Oracle, MySQL, ID
Software, Blizzard, EA …
4
http://www.lextrait.com/vincent/implementations.html
Introduction C++ - Objectifs

▶ Efficacité (mémoire et rapidité)


Le langage n’ajoute aucune fonction « cachée » demandant de la
mémoire ou des traitements
→ Pas de ramasse-miettes (garbage collector)
▶ Développement plus rapide
Utilisation de la bibliothèque standard, Réutilisation du code
▶ Gestion de la mémoire plus simple
Gérer les libérations de mémoire (free) plus facilement (voire
automatiquement), Gestion des chaînes plus simple
▶ Vérification plus stricte des types
▶ Avantages de la POO
▶ Programmation générique, programmation fonctionnelle
▶ …
5
De C à C++

Présentation du cours
De C à C++
Types et variables
Allocation dynamique
const
Références
Inférence de type
Conversion de type
Compilation
De C à C++

Types et variables
Types de données

▶ signed ; unsigned
▶ char ; short (short int); int ; long (long int); long
long (long long int)
float ; double ; long double
▶ wchar_t (unicode)
▶ bool → true false

Types entiers de taille fixée int8_t, int16_t, uint16_t…


http://en.cppreference.com/w/cpp/types/integer

6
Types de données

Premier exemple (premierexemple.cc)

#include <iostream>

int f(int a, int b) {


int res(1);
for (int i=1; i<=b; ++i)
res *= a;
return res;
}

int main() {
int a,b;
std::cin >> a;
std::cin >> b;
std::cout << f(a,b) << std::endl;
return 0;
}

7
Portée des variables

Une variable est accessible à partir de sa déclaration dans le bloc


(accolades) dans lequel elle est déclarée.

▶ Une variable globale est déclarée à l’extérieur de tout bloc.


Il est toutefois fortement conseillé de ne pas utiliser de variables
globales.
▶ Cas particulier d’une déclaration dans un for :
La variable est accessible uniquement dans les instructions
répétées par le for.

8
Portée des variables

Portée des variables (porteevariables.cc)

#include <iostream>
signed short varglobale;
int main() {
varglobale=5;
unsigned int varlocale=10; // initialisation
while (varglobale >= 0) {
int v(6);
varglobale += v;
varlocale--;
}
std::cout << varglobale << "\n";
return 0;
}

9
Déclaration et initialisation des variables

Multiple déclarations
int a, b, c; // équivalent à
int a; int b; int c;

Attention
int* a, b;

a est un pointeur sur un entier, b est un entier.

Initialisation C / Initialisation C++


int a=0; // équivalent à
int a(0);

Dans les deux cas, il s’agit d’une initialisation (et non une
affectation).

Initialisation : donner une valeur initiale à une nouvelle variable.


10
Affectation : modifier la valeur d’une variable existante.
Chaînes de caractères

Le type (classe) std::string fournit les opérations courantes sur les


chaînes de façon bien plus simple que les « chaînes » C.
▶ Déclaration et initialisation
std::string s1;
std::string s2a("valinit"), s2b="valinit";
std::string s3a(s1), s3b=s1;
▶ Affectation =
▶ Comparaison == != <= ...
▶ Accès à un caractère s[i]
▶ Lecture au clavier std::cin >> s;
▶ Affichage std::cout << s;
▶ Longueur d’une chaîne s.length()
▶ Concaténation + +=
http://en.cppreference.com/w/cpp/string/basic_string
11
Tableaux C

Exemple
int t1[4];
int t2[4]={1,5,7,9};
int t3[]={12,42,23};
int t2d[4][2];
t2d[2][1] = 12;
t2d[4][2] = 23; // Attention !

Attention
Un tableau est passé par pointeur à une fonction.
Une fonction ne peut pas connaître la taille d’un tableau.
int maximum(int tab[], int taille) { ... }
std::cout << maximum(t1,4);
void fnct(int tab[]) { tab[1]=5; }
fnct(t1); std::cout << t1[1];

À éviter → Préférer les tableaux C++. 12


Tableaux C++ std::array

Le (modèle de) type (classe) std::array fournit les opérations de


base d’un tableau (d’éléments de même type) de façon plus simple et
fiable qu’un tableau C.

▶ n’est pas converti de façon implicite en pointeur.


▶ peut être passé par valeur → Copie.
▶ peut être affecté.
▶ « connaît » sa taille.
▶ peut détecter un accès à un indice invalide.

http://en.cppreference.com/w/cpp/container/array

13
Tableaux C++ std::array

array (array.cc)

#include <iostream>
#include <array>
int max(std::array<int,5> t) {
int m(t[0]);
for (std::size_t i=1; i<5; ++i)
if (t[i]>m)
m=t[i];
return m;
}
int main() {
std::array<int,5> tab {3,7,1,2,6};
std::cout << max(tab) << "\n";
std::array<int,5> tab2(tab);
tab2[2]=12;
tab2.at(5)=15; // Erreur !
tab=tab2;
return 0;
} 14
Tableaux C++ std::array

▶ L’indice d’un tableau est de type std::size_t ou


std::array<...>::size_type.
▶ [] ne fait aucune vérification, alors que at() lève une exception.
terminate called after throwing an instance of
'std::out_of_range'
what(): array::at: __n (which is 5) >= _Nm
(which is 5)
▶ std::max_element calcule le maximum d’un conteneur.

15
Boucle for d’ intervalle

Syntaxe for d’ intervalle


for(typevar nomvar : intervalle)

Un intervalle peut être un std::initializer_list (un ensemble de


valeurs de même type entre accolades séparées par des virgules), un
std::array (ou toute autre classe disposant de méthodes begin et
end).

Le type de la variable peut être une valeur ou une référence.

16
Boucle for d’ intervalle

Boucle for d’ intervalle (array2.cc)

#include <iostream>
#include <array>

void afficher(std::array<int,5> t) {
for (int a : t)
std::cout << a << "␣";
std::cout << "\n";
}

int main() {
std::array<int,5> tab {3,7,1,2,6};
for (int a : {3,7,1,2,6})
std::cout << a << "\n";
for (int a : tab)
a *= 2; // Attention , ne f a i t rien !
afficher(tab);
return 0;
} 17
struct et enum

typedef n’est plus utile lors de la déclaration de struct ou enum.

Une struct peut être initialisée à sa déclaration en passant entre


accolades {} une valeur pour chacun des champs de la structure
(dans l’ordre de déclaration).

18
struct et enum

Usage du typedef (typedef.cc)

#include <string>
typedef unsigned int Annee;
enum EtatPersonne {
MAJEUR,
MINEUR
};
struct Personne {
std::string nom;
std::string prenom;
EtatPersonne etat;
Annee naissance;
};
int main() {
Personne p; // et non struct Personne p
p.nom = "Durand"; p.etat = MINEUR;
Personne p2 { "Dupont", "Joe", MAJEUR, 1990 };
return 0;
} 19
using

using peut être utilisé à la place de typedef avec une syntaxe plus
naturelle.
Exemple
using Annee = unsigned int;
using Tableau = std::array<std::string, 10>;

20
enum

Les types énumérés de C (enum) sont utilisables en C++ mais


conservent leurs défauts.
enums C (enums1.cc)

enum Couleur { Rouge, Vert, Bleu };


enum EtatPersonne { Majeur, Mineur };
int main() {
Couleur c(Rouge);
if (c == Majeur) { }; //seulement un avertissement
int d(c); // même pas un avertissement
return 0;
}

On préfèrera les énumérations fortement typées enum class à enum :


plus sûrs, car ne sont pas convertis silencieusement en int.

21
Énumérations fortement typées : enum class

enum class (enums2.cc)

enum class Couleur { Rouge, Vert, Bleu };


enum class EtatPersonne { Majeur, Mineur };
int main() {
Couleur c(Couleur::Rouge);
if (c == EtatPersonne::Majeur) { }; // Erreur
int d(c); // Erreur
return 0;
}

De plus les valeurs énumérées ne « polluent » pas l’espace des noms,


et doivent être préfixées par le nom de l’énumération.

22
Arguments par défaut

Les derniers arguments d’une fonction peuvent avoir une valeur par
défaut lors de la déclaration de la fonction.
Si les arguments correspondants ne sont pas passés lors d’un appel,
la valeur par défaut est prise en compte.
Exemple
void f(int a=2, int b=3) {
std::cout << a << "␣" << b << "\n";
}
...
f(); // 2 3
f(5); // 5 3
f(5,7); // 5 7

23
De C à C++

Allocation dynamique
Allocation dynamique en C

Syntaxe en C
▶ Allocation simple
int * i = (int *)(malloc(sizeof(int)));
▶ Allocation d’une zone
int * t = (int *)(malloc(sizeof(int)*taille));
▶ Libération
free(t);

24
Allocation dynamique en C++

Syntaxe en C++
▶ Allocation simple
int * i = new int;
int * i = new int(3); Fixer la valeur initiale
▶ Allocation d’une zone
int * t = new int[taille];
▶ Libération simple
delete i;
▶ Libération d’une zone
delete []i;

25
Allocation dynamique en C++

Attention
▶ Tout ce qui est alloué doit être libéré.
▶ Tout ce qui est alloué par new ...[] doit être libéré par
delete [].
▶ Ne pas mélanger new/delete avec malloc/free.

Utiliser l’allocation dynamique uniquement quand elle est requise.


C++ n’est pas Java, l’allocation dynamique n’est requise que dans
certains cas.
C++ n’est pas Java, si on alloue dynamiquement des objets, il faut les
libérer explicitement.

26
De C à C++

const
const

Le mot-clef const spécifie que la valeur d’une variable est constante.


Exemple
const int i(4); // équivalent à
const int i=4; // équivalent à
int const i=4;

i = 5; // erreur de compilation

Une (variable) constante (mais pas une variable (non déclarée const))
peut être utilisée pour spécifier la taille d’un tableau.
Souvent on marque syntaxiquement les constantes (écriture en
majuscules, première lettre en majuscule, préfixe particulier tel que
c_ …)

27
const et pointeurs

Attention à la position du const


▶ Pointeur variable sur des caractères variables char * c;
▶ Pointeur constant sur des caractères variables char * const c;
c = c1; est interdit. *c='a'; c[1]='b'; sont autorisés.
▶ Pointeur variable sur des caractères constants
const char * c; (ou char const * c;)
c = c1; est autorisé. *c='a'; c[1]='b'; sont interdits.
▶ Pointeur constant sur des caractères constants
const char * const c; ou (char const * const c;)
c = c1; *c='a'; c[1]='b'; sont interdits.

Moyen mnémotechnique : lire à l’envers. « Un pointeur sur un


caractère constant », « Un constant pointeur sur un caractère »…
28
De C à C++

Références
Références

Une référence est une variable synonyme (qui porte le même nom)
qu’une autre variable.
Syntaxe (Déclaration d’une référence)
type & nomvar(var_référencée);

La variable référencée doit obligatoirement être précisée au moment


de la déclaration.
Exemple
int i;
int & j(i);

i=5;
std::cout << j; // 5
j=3;
std::cout << i; // 3
29
Références

Les références sont principalement utilisées pour le passage par


référence d’un argument à une fonction. Dans ce cas, on ne doit pas
préciser la variable référencée.
Exemple
void f(int & a, int & b) {
a=2; b=3; }
...
int c,d;
f(c,d);
std::cout << c << "␣" << d; // 2 3

Simplifie dans la plupart des cas le « passage par pointeur » de C.

30
Passage par valeur / référence

Passer par valeur une variable provoque la copie de cette variable.


int maximum(std::array<int, 1000000> t);

Cette copie est gênante dans le cas de structures de données de


grande taille : coût de la copie.
Solution. Passer le paramètre par référence.
int maximum(std::array<int, 1000000> & t);

Problème. La fonction peut modifier le paramètre.

▶ Bug difficile à trouver.


▶ La signature de la fonction doit être une documentation.

31
Passage par valeur / référence

Un passage par référence constante évite de faire une copie, mais


empêche de modifier l’objet passé.
int maximum(std::array<int, 1000000> const & t);

Le passage par référence constante doit être préféré à un passage par


valeur pour les types composés (struct, conteneurs, classes).

32
Passage par valeur / référence

Passage par référence (maximum.cc)

#include <iostream>
#include <array>
#include <algorithm>
const int Tailletab(10000);
using tableau=std::array<int, Tailletab>;
// int maximum( tableau t , std : :size_t n=Tailletab −1) {
// provoque une erreur de segmentation
int maximum(tableau const & t, std::size_t n=Tailletab-1) {
if (n == 0)
return t[n];
else
return std::max(maximum(t, n-1), t[n]);
}
int main() {
tableau t;
for (int & init : t) init = random() % 1000;
std::cout << maximum(t) << "\n";
return 0; 33
De C à C++

Inférence de type
Inférence de type : auto

L’utilisation du mot-clef auto permet d’éviter de préciser le type d’une


variable locale quand cette variable est initialisée à la déclaration.
Attention
auto n’est pas un type dont les variables peuvent contenir
n’ importe quelle valeur.
Le type d’une variable déclarée auto est déterminé au moment de la
compilation en fonction de sa valeur initiale, et ne peut être modifié.

Exemple
auto f; //declaration of ’auto f ’ has no i n i t i a l i z e r
std::array<int,10> tab { ....};
auto premier(tab[0]);
premier = "ch"; //invalid conversion from ’ const char * ’ to ’ int ’
auto t2(tab);
auto max(maximum(tab));
34
auto

auto est souvent utilisé dans des parcours de structures de données.

auto et boucle for d’intervalle (auto.cc)

using tableau=std::array<int,5>;

void afficher(tableau const & t) {


for (auto a : t)
std::cout << a << "␣";
std::cout << "\n";
}

Quel est le type inféré par le compilateur ?


int (et non const int, int & ou int const &)
Vérifions cela…

35
auto

auto (auto.cc)

void doubler(tableau const & t) {


for (auto a : t)
a *= 2;
}

Ne provoque pas d’erreur de compilation… Mais ne fait rien : a est une


variable locale de type int. Le fait que t soit passé par référence
constante n’est pas la cause :
auto (auto.cc)

void doubler(tableau & t) {


for (auto a : t)
a *= 2;
}

Même cause, mêmes effets : a est un int. 36


auto

Il est possible de combiner auto avec const et/ou &.


auto (auto.cc)

void doubler(tableau const & t) {


for (auto & a : t)
a *= 2;
}

On demande au compilateur que a soit une référence…


Le compilateur infère donc qu’il est de type int const &. Il y a donc
une erreur de compilation assignment of read-only
reference 'a'.

37
auto

auto (auto.cc)

void doubler(tableau & t) {


for (auto & a : t)
a *= 2;
}

Fait ce que l’on attend.


Utiliser auto const & permet de spécifier qu’une référence
constante est attendue (pour éviter une copie : utile quand la copie
est coûteuse).
Attention. Une initialisation à partir d’un ensemble de valeurs
(auto t{1,2,3};) ne crée pas un std::array mais un
std:initializer_list.

38
De C à C++

Conversion de type
Conversion de type

Syntaxe en C
(nouveautype)expression

Exemple
int main() {
int a(2), b(3);
float f(a/b);
std::cout << f;
return 0;
}

▶ Affiche 0
▶ Forcer la division sur les réels
float f(((float)a)/b);
▶ Affiche 0.666667
39
Conversion de type

Inconvénient de la syntaxe C : Peu visible (syntaxe) alors que


dangereux.
Syntaxe en C++
▶ static_cast<nouveautype>(expression)
Force la conversion de type de l’expression dans le nouveau type
float f(static_cast<float>(a)/b);
▶ reinterpret_cast<nouveautype_ptr>(expression_ptr)
Force la conversion de type dans le cas de pointeurs
void * p(nullptr);
int* r(reinterpret_cast<int *>(p));

▶ Les conversions sont plus visibles.


▶ Deux opérateurs différents pour deux opérations différentes (il y
40
en a deux autres)
De C à C++

Compilation
Compilation

▶ Comme en C, le code est séparé en un fichier d’entêtes (.hh) et


un fichier d’ implantation (.cc).
▶ L’exécution commence par la fonction
int main(int argc, char * argv[]) ou
int main()
▶ Compilation avec GNU C++
▶ g++ -Wall -std=c++14 fichier.cc
compilation et édition de liens, fichier.cc doit contenir un
main
▶ g++ -Wall -std=c++14 -c fichier.cc
compilation seule, crée fichier.o
▶ g++ -Wall fichier1.o fichier2.o -o fichierexe
édition de liens

41
Compilation avec CMake

Problème. Dans un projet de grande taille, il faut compiler un grand


nombre de fichiers sources (ceux qui sont nécessaires) et faire
l’édition de liens à chaque test.
Solution. Écrire un Makefile. Fastidieux.
Solution. Utiliser un générateur de Makefile (ou d’autres systèmes
de construction).
http://www.cmake.org

42
Compilation avec CMake

Configuration d’un projet : CMakeLists.txt (CMakeLists.txt)

1 cmake_minimum_required(VERSION 3.1.0)
2 project(nomprojet CXX)
3

4 set(CMAKE_CXX_STANDARD 14)
5 set(CMAKE_CXX_STANDARD_REQUIRED on)
6 set(CMAKE_CXX_EXTENSIONS off)
7

8 if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")


9 add_compile_options(-Wall -Wpedantic)
10 endif()
11

12 add_executable(nomexecutable source1.cc source2.cc)

43
Compilation avec CMake

▶ Lignes 1-2 : Entête commun de tous les projets C++. Penser à


changer le nom du projet.
▶ Lignes 4-6 : Compilation en mode C++14 sans extensions du
compilateur.
▶ Lignes 8-10 : Compilation avec les avertissements.
▶ Ligne 12 : Une cible exécutable : nom de l’exécutable (sans
extension) suivi des noms des fichiers sources à compiler.

44
Compilation avec CMake

▶ Création d’un répertoire de construction


cd /tmp ; mkdir build ; cd build
▶ Génération du Makefile
dans le répertoire build :
cmake nom du répertoire contenant CMakeLists.txt
▶ Construction
make

CMake est intégré dans de nombreux environnements de


développement (QtCreator, KDevelop, etc.) et permet de générer des
projets pour Linux, Windows, MacOS.

45
Chapitre 2
Classes : Concepts de base

Introduction
Syntaxe
Encapsulation
Constructeur
Destructeur
Méthodes constantes
Introduction

Introduction
Syntaxe
Encapsulation
Constructeur
Destructeur
Méthodes constantes
Classe

▶ Ensemble d’objets ayant des propriétés communes


▶ Chaque objet d’une classe (instance) a les mêmes attributs.
▶ Chaque objet peut avoir des valeurs différentes des attributs qui
représentent l’état de l’objet.
▶ Tous les objets d’une classe ont le même comportement :
méthodes.

On appelle membres d’une classe les attributs et les méthodes de la


classe.

46
Classe

▶ Habituellement, en C++, on sépare l’interface de la classe de son


implantation.
▶ L’interface (la déclaration) d’une classe est écrite dans un fichier
d’entêtes .hh.
▶ L’implantation (la définition) (code des méthodes) est écrite dans
un fichier .cc.
▶ Il est conseillé de donner le même nom aux deux fichiers.

47
Classe

▶ Une classe est formée de membres : attributs et méthodes.


▶ L’interface d’une classe doit donner les noms des membres…
▶ Pour les attributs, le type doit être donné
▶ Pour les méthodes, la signature doit être donnée
▶ L’implantation d’une classe doit donner le corps (code) des
méthodes.
Dans le cas de méthodes simples, on s’autorise à donner le corps
dans le fichier d’interface.

48
Syntaxe

Introduction
Syntaxe
Encapsulation
Constructeur
Destructeur
Méthodes constantes
Classe

Syntaxe Déclaration
class NomClasse {
typemembre nomattribut;
typeretour nommethode(typearg1 nomarg1, ...);
...
};

49
Classe

Déclaration de classe (fichier1.hh)

#pragma once
#include <string>

class Fichier {
public:
void afficher();
unsigned int taille() {
return _taille; }
std::string nom() {
return _nom; }
void renommer(std::string const & nn);
void fixerTaille(unsigned int nt);

private:
std::string _nom;
unsigned int _taille;
};
50
Classe

Définition de classe (fichier1.cc)

#include "fichier1.hh"
#include <iostream>

void Fichier::afficher() {
std::cout << this->_nom << "␣(";
if (_taille == 0)
std::cout << "vide)";
else
std::cout << _taille << "␣octets)";
}

void Fichier::renommer(std::string const & nn) {


_nom = nn;
}

void Fichier::fixerTaille(unsigned int nt) {


_taille = nt;
}
51
Classe - Déclaration d’ instance

Une fois définie, la classe peut être utilisée comme un type.


Syntaxe Déclaration
▶ NomClasse NomDeLInstance;
▶ NomClasse * NomDuPointeur;
Attention. Seul un pointeur (non initialisé) est déclaré.
▶ NomClasse & NomDeLaReference;
Attention. Obligation de fournir la variable référencée (sauf dans
une signature).

C++ n’est pas Java : On préfèrera quand c’est possible manipuler les
objets par valeur (et non par pointeur).

52
Classe - Accès aux membres

Syntaxe Accès à un attribut


NomDeLInstance.NomDeLAttribut
NomDuPointeur->NomDeLAttribut
(*NomDuPointeur).NomDeLAttribut
NomDeLaReference.NomDeLAttribut
(sous réserve de visibilité)

Dans une méthode, on peut utiliser directement un nom d’attribut


pour désigner l’attribut correspondant de l’ instance sur laquelle la
méthode a été appelée…
Ou utiliser le pointeur this qui repère l’ instance courante. C’est un
pointeur, donc on doit utiliser l’opérateur ->.

53
Classe

Syntaxe Appel à une méthode


NomDeLInstance.NomDeLaMethode(arguments)
NomDuPointeur->NomDeLaMethode(arguments)
(*NomDuPointeur).NomDeLaMethode(arguments)
NomDeLaReference.NomDeLaMethode(arguments)
(sous réserve de visibilité)

Dans une méthode, on peut utiliser directement un nom de méthode


pour appeler cette méthode sur l’instance sur laquelle la méthode a
été appelée.…
Ou utiliser le pointeur this qui repère l’ instance courante.

54
Classe

Instanciation et utilisation d’un classe (mainfichier1.cc)

#include "fichier1.hh"

int main() {
Fichier f;
f.renommer("exemple.txt");
f.fixerTaille(2567);
f.afficher();
return 0;
}

55
Encapsulation

Introduction
Syntaxe
Encapsulation
Constructeur
Destructeur
Méthodes constantes
Encapsulation

▶ L’encapsulation permet de « cacher » des membres d’une classe


afin que l’interface de la classe ne dispose que des membres qui
ont été choisis par le concepteur de la classe.
▶ L’encapsulation facilite la mise au point, la réutilisation, et
l’évolution.

56
Encapsulation

▶ En C++, l’encapsulation permet de choisir parmi 3 niveaux de


visibilité :
▶ private
Le membre est visible dans toutes les méthodes de la classe et
invisible ailleurs.
▶ protected
Le membre est visible dans toutes les méthodes de la classe (ainsi
que dans les méthodes des sous-classes), et invisible ailleurs.
▶ public
Le membre est visible partout.

57
Encapsulation

Habituellement, tous les attributs d’une classe sont déclarés private,


et certaines méthodes sont déclarées public.
Syntaxe Visibilité
class NomClasse {
[private | protected | public]:
typemembre nommembre ... ;
...
};

Tous les membres qui sont déclarés après une étiquette de visibilité
ont cette visibilité là. Les membres qui sont déclarés avant la
première étiquette de visibilité sont privés.
Exemple
_nom et _taille sont privés, les 5 méthodes sont publiques.
58
Encapsulation

Un attribut public peut être lu et modifié (comme une variable).


Parfois, on veut montrer la valeur de l’attribut sans autoriser la
modification.
⇒ Rendre l’attribut privé, et fournir un accesseur (en lecture).
Définition

▶ Un accesseur (ou accesseur en lecture) est une méthode qui


retourne la valeur d’un attribut (privé).
▶ Un mutateur (ou accesseur en écriture) est une méthode qui
permet de modifier la valeur d’un attribut (privé).

Exemple
Fichier::nom() est un accesseur, Fichier::fixerTaille() est
un mutateur.
59
Constructeur

Introduction
Syntaxe
Encapsulation
Constructeur
Présentation
Liste d’initialisations
Constructeur par défaut et par recopie
Destructeur
Méthodes constantes
Constructeur

Présentation
Constructeur

Un constructeur est une méthode qui est appelée pour construire


(initialiser) une instance d’une classe.
Intérêt
Imposer l’exécution d’une méthode pour s’assurer de l’ initialisation
de l’objet.

Exemple
Fichier f;
f.renommer("exemple.txt");
f.afficher(); // f . _taille n ’ est pas i n i t i a l i s é .

60
Constructeur

▶ Un constructeur est une méthode qui a comme nom le nom de la


classe et qui ne retourne rien (̸= qui retourne void).
▶ Un constructeur est souvent public.
▶ Une classe peut avoir plusieurs constructeurs (avec des
signatures différentes).
Dans ce cas, on choisit le constructeur à utiliser au moment de
créer une instance.

61
Constructeur

Syntaxe Constructeurs
class NomClasse {
...
// Constructeur par défaut .
NomClasse();
// Constructeur avec paramètres .
NomClasse(typearg1 nomarg1, ...);
// Constructeur par recopie .
NomClasse(NomClasse const & nomarg);
...
};

62
Constructeur

Constructeur - déclaration (fichier.hh)

class Fichier {
public:
Fichier(std::string const & nom, unsigned int taille);
void afficher();

Constructeur - définition (fichier.cc)

Fichier::Fichier(std::string const & nom, unsigned int taille) {


_nom = nom;
_taille = taille;
}

63
Constructeur

Liste d’ initialisations
Constructeur - Liste d’ initialisations

Habituellement, un constructeur d’une classe se charge d’ initialiser


les attributs de l’ instance en cours de construction.
Ces initialisations peuvent être réalisées avant même l’exécution de la
première ligne de code de la méthode du constructeur.
Constructeur - définition (fichier.cc)

Fichier::Fichier(std::string const & nom, unsigned int taille)


:_nom(nom),_taille(taille) {
}

64
Constructeur - Liste d’ initialisations

La liste d’initialisations est formée du caractère :, des attributs à


initialiser séparés par des , chaque attribut étant suivi de la valeur
d’initialisation :
▶ Si l’attribut est d’un type primitif (unsigned int), syntaxe C++
d’initialisation.
▶ Si l’attribut est d’un type classe (std::string)
▶ Initialisation explicite par appel à un constructeur
→ les paramètres passés doivent correspondre à un constructeur
de la classe.
▶ Si aucune initialisation n’est donnée, le constructeur par défaut
(sans argument) de l’attribut est utilisé.
Ici std::string dispose d’un constructeur prenant comme
paramètre une instance de std::string, c’est ce constructeur
qui est appelé.
65
Constructeur - Liste d’ initialisations

Différences entre :
Fichier::Fichier(std::string const & nom, unsigned int taille) {
_nom = nom;
_taille = taille;
}

et
Fichier::Fichier(std::string const & nom, unsigned int taille)
:_nom(nom),_taille(taille) {
}

▶ Dans le premier cas, le constructeur par défaut de std::string


est utilisé, et construit une chaîne vide. Puis l’opérateur
d’affectation = est utilisé et copie la valeur du paramètre nom
dans l’attribut _nom.
▶ Dans le second cas, le constructeur de std::string prenant
comme paramètre une std::string est appelé pour construire 66
directement _nom avec une copie de nom.
Constructeur - Constructeur délégué

La liste d’initialisations peut aussi être utilisée pour définir un


constructeur délégué, c’est à dire un constructeur qui fait appel à un
autre constructeur (cible).
Dans ce cas, la liste d’ initialisations ne contient qu’un appel à un
appel à un (autre) constructeur de la même classe.

67
Constructeur - Constructeur délégué

Constructeur délégué (fichierconstrdelegue.hh)

#pragma once
#include <string>
struct fichierexterne { // struct externe à notre projet
char const * nom;
long taille;
};
class Fichier {
public:
Fichier(std::string const & nom, unsigned int taille)
:_nom(nom), _taille(taille) {}
Fichier(fichierexterne const & info)
:Fichier(info.nom, static_cast<unsigned int>(info.taille)) {}
private:
std::string _nom;
unsigned int _taille;
};

68
Constructeur - Constructeur délégué

Constructeur délégué (mainfichierconstrdelegue.cc)

#include "fichierconstrdelegue.hh"

int main() {
fichierexterne fe {".bashrc", 742};
Fichier f(fe);
return 0;
}

69
Constructeur - Liste d’ initialisations

Il est toujours préférable d’utiliser une liste d’ initialisations :

▶ Séparer les initialisations du code du constructeur.


▶ Plus rapide.
▶ Parfois indispensable : attribut référence, attribut ne disposant
pas d’un constructeur par défaut.
▶ Quand le code du constructeur est exécuté, les attributs sont déjà
initialisés.

70
Constructeur

Constructeur par défaut et par recopie


Constructeur

Si aucun constructeur n’est déclaré dans une classe, elle dispose tout
de même d’un constructeur par défaut qui :
▶ Appelle le constructeur par défaut sur tous les attributs de types
classes.
▶ Ne fait rien pour les attributs de types primitifs.
Si (au moins) un constructeur a été déclaré dans une classe, le
constructeur par défaut implicite n’est plus disponible. Mais un
constructeur par défaut peut être déclaré.
Constructeur et usage (mainfichier.cc)

int main() {
// Fichier f ; // Provoque une erreur
Fichier f1("ex", 25);
f1.renommer("exemple.txt");
f1.fixertaille(2567);
71
f1.afficher();
Constructeur par recopie

Un constructeur par recopie permet d’effectuer une copie d’une


instance pour créer une nouvelle instance.
Il est utilisé dans les cas suivants :

▶ Créer une nouvelle variable comme copie d’une variable


existante.
Exemple
Fichier f2(f);

72
Constructeur par recopie


▶ Passer une copie d’une variable comme paramètre à une fonction
ou méthode.
Passage par valeur (fichier.cc)

bool Fichier::estpluspetitque(Fichier f) {
return _taille < f._taille;
}

Constructeur et usage (mainfichier.cc)

Fichier f2(f1);
std::cout << f1.estpluspetitque(f2) << "\n";

Une copie de f2 est passée à estpluspetitque afin que


d’éventuelles modifications du paramètre f dans
estpluspetitque ne modifient pas f2.
73
→ Passage par valeur = copie.
Constructeur par recopie

Toute classe dispose d’un constructeur par recopie implicite qui :

▶ Appelle le constructeur par recopie sur tous les attributs de types


classes.
▶ Copie la valeur des attributs de types primitifs.

Il est possible de définir explicitement le constructeur par recopie.


Attention lors de la déclaration d’un constructeur par recopie
Le constructeur par recopie ne prend pas comme paramètre une
instance de la classe :
Fichier(Fichier f);
Mais une référence (constante) sur une instance de la classe :
Fichier(Fichier const & f);

74
Destructeur

Introduction
Syntaxe
Encapsulation
Constructeur
Destructeur
Méthodes constantes
Destructeur

Le destructeur est une méthode qui est appelée implicitement sur


une instance, quand celle-ci cesse d’exister.
Intérêt
Imposer l’exécution d’une méthode pour s’assurer de la destruction
de l’objet.

▶ Fermer des fichiers qui ont été ouverts dans les méthodes de la
classe.
▶ Fermer une connexion réseau.
▶ Libérer les allocations dynamiques.

75
Destructeur

▶ Un destructeur est une méthode qui a comme nom le caractère ~


suivi du nom de la classe, qui n’a aucun paramètre et qui ne
retourne rien (̸= qui retourne void).
▶ Un destructeur est habituellement public.
▶ Une classe ne peut avoir qu’un seul destructeur.
▶ Le destructeur n’est jamais appelé explicitement.
▶ Quand une instance est détruite, les objets qui composent cette
instance sont détruits implicitement.
Mais pas les objets qui sont pointés par un pointeur qui compose
l’instance → À faire dans le destructeur.

76
Destructeur

Exemple. Rajouter une information optionnelle aux fichiers :


l’identifiant du propriétaire et la date de création. Cette information
est optionnelle, tous les fichier n’en disposent pas.
Informations sur fichiers (fichier2.hh)

#pragma once
#include <string>
using fichierproprietaire = unsigned int;
using fichierdate = unsigned int;
using fichiernom = std::string;
using fichiertaille = unsigned int;

struct infofichier {
fichierproprietaire _proprietaire;
fichierdate _creation;
};

77
Destructeur

Informations sur fichiers (suite) (fichier2.hh)

class fichier {
public:
fichier(fichiernom const & nom, fichiertaille taille);
fichier(fichiernom const & nom, fichiertaille taille,
fichierproprietaire proprietaire, fichierdate creation);
void afficher();
fichiertaille taille() {
return _taille; }
fichierdate creation();
bool estpluspetitque(fichier f);
private:
fichiernom _nom;
fichiertaille _taille;
infofichier * _infosuppl;
};

78
Destructeur

Informations sur fichiers (suite) (fichier2.cc)

#include "fichier2.hh"
#include <iostream>

fichier::fichier(fichiernom const & nom, fichiertaille taille)


:_nom(nom),_taille(taille), _infosuppl(nullptr) {
}

fichier::fichier(fichiernom const & nom, fichiertaille taille,


fichierproprietaire proprietaire, fichierdate creation)
:fichier(nom, taille) {
_infosuppl = new infofichier {proprietaire, creation};
}

void fichier::afficher() {
std::cout << _nom << "␣(";
std::cout << _taille << "␣octets)";
if (_infosuppl)
std::cout << "␣création␣" << creation(); 79
}
Destructeur

Informations sur fichiers (suite) (fichier2.cc)

fichierdate fichier::creation() {
if (_infosuppl)
return _infosuppl->_creation;
else
return 0;
}

bool fichier::estpluspetitque(fichier f) {
return _taille < f._taille;
}

80
Destructeur

Problème
Quand une instance de fichier cesse d’exister, l’infofichier
associé continue d’exister.

▶ Il ne peut pas être détruit ailleurs


(par un delete f._infosuppl;)
car l’attribut est privé.
▶ On aimerait automatiser la destruction.

Bonne pratique : Propriété d’un objet


Quand une instance A d’une classe est propriétaire d’une instance B,
c’est (uniquement) une méthode de A qui doit créer B, et c’est
(uniquement) une méthode de A qui doit libérer B.

81
Destructeur

destructeur (fichier2bis.hh)

class fichier {
public:
fichier(fichiernom const & nom, fichiertaille taille);
fichier(fichiernom const & nom, fichiertaille taille,
fichierproprietaire proprietaire, fichierdate creation);
~fichier();

destructeur (fichier2bis.cc)

fichier::~fichier() {
delete _infosuppl;
}

De façon générale, tout ce qui est alloué dynamiquement dans une


méthode pour être stocké dans un attribut est une propriété de
l’instance, et doit donc être détruit avec l’ instance, dans le
destructeur. 82
Constructeur par recopie et destructeur

Erreur à l’exécution (mainfichier2bis.cc)

std::cout << f1.estpluspetitque(f2) << "\n";

Error in `./a.out': double free or corruption (fasttop)

▶ L’appel à estpluspetitque crée une copie de f2 (dans f).


▶ Le constructeur par recopie implicite est appelé.
▶ Il appelle le constructeur par recopie de string _nom, copie la
valeur de l’int _taille et copie la valeur du pointeur
_infosuppl. Un pointeur est un type primitif.
▶ Quand estpluspetitque se termine, le paramètre f est détruit,
le destructeur est appelé (implicitement).
▶ Ce destructeur libère f._infosuppl… qui est aussi
l’infofichier utilisé par f2.
▶ Tout accès à f2._infosuppl est incorrect.
83
Constructeur par recopie et destructeur

⇒ Définir un constructeur par recopie… qui recopie l’infofichier (et


pas simplement un pointeur sur un infofichier).
constructeur par recopie (fichier2ter.hh)

class fichier {
public:
fichier(fichiernom const & nom, fichiertaille taille);
fichier(fichiernom const & nom, fichiertaille taille,
fichierproprietaire proprietaire, fichierdate creation);
fichier(fichier const & f);
~fichier();

constructeur par recopie (fichier2ter.cc)

fichier::fichier(fichier const & f)


:fichier(f._nom, f._taille) {
if (f._infosuppl) // nullptr est considéré comme faux
_infosuppl = new infofichier(*f._infosuppl); // copie de la
struct 84
}
Constructeur par recopie et destructeur

Règle des 3
Quand dans une classe, on écrit l’une de ces méthodes, on écrit les
3:

▶ Constructeur par recopie


▶ Destructeur
▶ Opérateur d’affectation (operator=)

85
Constructeur par recopie et destructeur

Mais dans la majorité des cas, on essaie d’appliquer la règle des 0.


Règle des 0
Sauf si c’est vraiment justifié, dans une classe on n’écrit ni
constructeur par recopie, ni destructeur, ni opérateur d’affectation.

Ce qui ne veut pas dire qu’il n’y a pas de constructeur par recopie ou
de destructeur !
Mais que c’est le comportement par défaut du constructeur par
recopie/destructeur qui est attendu. Il est possible de l’écrire
explicitement :
Exemple
class C {
public:
C(C const & c)=default;
~C()=default; 86
...
Méthodes constantes

Introduction
Syntaxe
Encapsulation
Constructeur
Destructeur
Méthodes constantes
Méthodes const
const_cast
Méthodes constantes

Méthodes const
Méthodes const

Définition
Quand une méthode est déclarée constante :

▶ Elle ne peut pas modifier les valeurs des attributs de l’objet sur
lequel elle s’applique.
▶ Elle ne peut pas appeler sur l’objet courant une méthode qui
n’est pas constante.
▶ Elle peut être appelée sur un objet constant (contrairement à une
méthode non constante).

Syntaxe Méthode constante


Le mot-clef const doit suivre la déclaration de la méthode, il fait
alors partie de la signature.

87
Méthodes const

Méthodes const (personnev1.hh)

#pragma once
#include <string>

class personne {
public:
personne(std::string const & nom, unsigned int age)
:_nom(nom), _age(age) {}
std::string const & nom() const;
unsigned int age(); // n ’ est pas const
bool estplusjeuneque(personne const & p2) const;
private:
std::string _nom;
unsigned int _age;
};

88
Méthodes const

Exemple
std::string const & personne::nom() const {
// _nom = ” test ” ; // erreur
// std : :cout << age( ) ; // erreur
// std : :cout << _age ; // OK
return _nom;
}

Remarque. Si la méthode nom() retournait une référence (non


constante, i.e. std::string &), la ligne return _nom provoquerait
une erreur.

89
Méthodes const

Il est autorisé d’appeler une méthode const sur un objet sur lequel
les modifications sont autorisées.
Exemple
unsigned int personne::age() {
// _nom = ” test ” ; // OK
// std : :cout << nom( ) ; // OK
return _age;
}

Méthodes const (personnev1.cc)

bool personne::estplusjeuneque(personne const & p2) const {


return age() < p2.age(); // erreur !
}

passing 'const personne' as 'this' argument discards


qualifiers
90
Méthodes const

▶ On définit toujours une méthode const si son comportement ne


« devrait pas » modifier l’objet.
▶ Un paramètre passé par référence est toujours passé par
référence constante si la méthode qui reçoit ce paramètre ne
« devrait pas » modifier l’objet.

Avantage
Le compilateur vérifie à la compilation que l’objet n’est pas modifié.

Problème
Si le code utilisé n’emploie pas toujours const là où il le devrait,
certaines méthodes ne peuvent pas être appelées…

Exemple. appel à age() dans estplusjeuneque().


91
Méthodes constantes

const_cast
const_cast

const_cast est un opérateur de conversion qui est capable de faire


(uniquement) les conversions suivantes :

▶ Convertir une référence sur un objet constant en une référence


sur un objet non constant.
personne const & p= ...;
std::cout << const_cast<personne &>(p).age();
▶ Convertir un pointeur sur un objet constant en un pointeur sur un
objet non constant.
personne const * ptr= ...;
std:::cout << const_cast<personne *>(ptr)->age();
▶ Convertir un pointeur constant en un pointeur non constant.
Rarement utilisé.

92
const_cast

Exemple
bool personne::estplusjeuneque(personne const & p2) const {
return const_cast<personne *>(this)->age() < const_cast<personne
&>(p2).age();
}

Il n’y a pas de raison d’écrire cela : age() doit être déclarée const.

▶ Dans le cas de classes bien écrites, const_cast est rarement


(jamais) utilisé.
▶ Pour convertir une référence (ou pointeur) sur un objet non
constant en une référence (ou pointeur) sur un objet constant,
aucune conversion explicite n’est nécessaire.

93
Chapitre 3
Héritage

Syntaxe
Exemple
Constructeurs et destructeurs
Redéfinition de méthodes
Liaison dynamique / liaison statique
Classes abstraites
Héritage

▶ L’héritage permet de former une nouvelle classe à partir de


classes existantes.
▶ La nouvelle classe (classe fille, sous-classe) hérite des attributs et
des méthodes des classes à partir desquelles elle a été formée
(classes mères, super-classes).
▶ De nouveaux attributs et de nouvelles méthodes peuvent être
définis dans la classe fille.
▶ Des méthodes des classes mères peuvent être redéfinies dans la
classe fille.
▶ La redéfinition de méthode consiste à (re)définir dans la classe
fille le comportement (code de la méthode) qui existait déjà dans
une classe mère.
Si une méthode d’une classe mère n’est pas redéfinie dans la
classe fille, alors le code défini dans la classe mère sera utilisé
94
« tel quel » sur les instances de la classe fille.
Héritage

Intérêt : Réutilisation
Utiliser une classe existante…

▶ En lui rajoutant des membres


▶ et/ou en modifiant certains de ses comportements

sans modifier le code de la classe existante.

95
Syntaxe

Syntaxe
Exemple
Constructeurs et destructeurs
Redéfinition de méthodes
Liaison dynamique / liaison statique
Classes abstraites
Héritage - Syntaxe

Syntaxe héritage
class NomClasseFille:
public NomClasseMere1, public NomClasseMere2 ... {
// Déclaration des nouveaux attributs
// Déclaration des nouvelles méthodes
// Redéfinition de méthodes
...
};

▶ C++ permet aussi d’utiliser l’héritage protected et private,


rarement utilisés.
▶ Souvent, on utilise l’héritage simple : une classe fille a une classe
mère. On parle d’héritage multiple quand une classe fille a
plusieurs classes mères.

96
Héritage - Intérêts

Intérêts de l’héritage :

▶ Favoriser la réutilisation.
▶ Factoriser le code.
▶ Catégoriser les concepts dans une relation de généralisation
Chaque concept est caractérisé par ses différences par rapport à
son (ses) parent(s).

97
Exemple

Syntaxe
Exemple
Constructeurs et destructeurs
Redéfinition de méthodes
Liaison dynamique / liaison statique
Classes abstraites
Héritage - Exemple

Gérer des produits en vente dans un magasin.


▶ Chaque produit comporte une référence, un nom, un prix HT et un
prix TTC.
▶ Le taux de TVA est de 20, sauf pour les produits culturels : 5.5.
▶ Les produits périssables ont une date limite de vente.
▶ Le matériel multimédia a une durée de garantie exprimée en
années.
▶ Chaque produit peut afficher une « étiquette » contenant
référence, prix, et informations supplémentaires.
Sans héritage :
▶ Définir les classes produitstandard, produitculturel,
produitperissable, produitmultimedia… et copier/coller du
code !
98
Héritage - Exemple

▶ Tous les produits ont des données communes (la référence…) et


des comportements communs (l’affichage de l’étiquette, le calcul
du prix TTC)
⇒ Créer une classe produit qui regroupe (factorise) ce qui est
commun à tous les produits et une classe fille
produitperissable qui contient ce qui est particulier à un
produit périssable.
Et on crée d’autres classes filles de produit pour les autres
produits particuliers.

99
Héritage

Déclaration de produit (produitv1.hh)

#pragma once
#include <string>
using reference=unsigned int;
using prix=float;
class produit {
public:
produit(reference ref, std::string const & nom, prix prixht);
std::string const & nom() const;
prix prixttc() const;
void afficher() const;
// + accesseurs prixht , référence
private:
reference _ref;
std::string _nom;
prix _prixht;
};

100
Héritage

Définition de produit (produitv1.cc)

#include "produitv1.hh"
#include <iostream>
produit::produit(reference ref, std::string const & nom, prix prixht
)
:_ref(ref), _nom(nom), _prixht(prixht) {
}
std::string const & produit::nom() const {
return _nom;
}
float produit::prixttc() const {
return _prixht * 1.20;
}
void produit::afficher() const {
std::cout << _ref << "␣" << _nom << "\n";
std::cout << "prix␣HT␣:␣" << _prixht << "\n";
std::cout << "prix␣TTC␣:␣" << prixttc() << "\n";
}
101
Héritage

Déclaration de produitperissable (produitperissablev1.hh)

#pragma once
#include "produitv1.hh"
using date=std::string;
class produitperissable: public produit {
public:
produitperissable(reference ref, std::string const & nom, prix
prixht, date const & peremption);
date const & peremption() const {
return _peremption; }
void afficher() const;
private:
date _peremption;
};

102
Héritage

Définition de produitperissable (produitperissablev1.cc)

#include "produitperissablev1.hh"
#include <iostream>

produitperissable::produitperissable(reference ref, std::string


const & nom, prix prixht, date const & peremption)
:produit(ref, nom, prixht), _peremption(peremption) {
}

void produitperissable::afficher() const {


produit::afficher();
std::cout << "peremption␣:␣" << _peremption << "\n";
}

103
Héritage

Test (produittestv1.cc)

#include "produitv1.hh"
#include "produitperissablev1.hh"
#include <iostream>
int main() {
produit p(1, "test", 100);
// Ceci provoque une erreur :
// produit p ;
// Ceci provoque une erreur :
// produit p=new produit ( 1 , ” test ” , 100) ;
// Ceci est inutilement compliqué :
// produit * p = new produit ( 1 , ” test ” , 100) ;

104
Héritage

Test (suite) (produittestv1.cc)

p.afficher();
produitperissable p2(2, "test2", 100, "15/10");
std::cout << p2.nom() << "\n";
p2.afficher();
return 0;
}

Remarquer que la méthode nom n’a pas été redéfinie dans


produitperissable. Pourtant, elle peut être appelée sur les
instances de produitperissable car elle a été héritée.

105
Constructeurs et destructeurs

Syntaxe
Exemple
Constructeurs et destructeurs
Redéfinition de méthodes
Liaison dynamique / liaison statique
Classes abstraites
Héritage et constructeurs

▶ Les constructeurs des classes mères ne peuvent être utilisés pour


construire des instances de la classe fille.
▶ → Il faut définir de nouveaux constructeurs.
▶ Les constructeurs de la classe fille font appel aux constructeurs
des classes mères au début de la liste d’ initialisations.

Constructeur (produitperissablev1.cc)

produitperissable::produitperissable(reference ref, std::string


const & nom, prix prixht, date const & peremption)
:produit(ref, nom, prixht), _peremption(peremption) {
}

106
Héritage et destructeurs

▶ Le destructeur des classes mères est toujours appelé


implicitement après l’exécution du destructeur de la classe fille.
▶ On n’appelle jamais explicitement le destructeur des classes
mères dans le destructeur de la classe fille.

107
Redéfinition de méthodes

Syntaxe
Exemple
Constructeurs et destructeurs
Redéfinition de méthodes
Liaison dynamique / liaison statique
Classes abstraites
Redéfinition

▶ Quand une méthode est redéfinie dans la classe fille, elle doit
être déclarée avec la même signature que dans la classe mère.
▶ Le code d’une méthode redéfinie fait souvent appel à la méthode
de la classe mère (super méthode)
NomDeLaClasseMere::NomDeLaMethode(arguments)

Redéfinition de méthode (produitperissablev1.cc)

void produitperissable::afficher() const {


produit::afficher();
std::cout << "peremption␣:␣" << _peremption << "\n";
}

108
Liaison dynamique / liaison statique

Syntaxe
Exemple
Constructeurs et destructeurs
Redéfinition de méthodes
Liaison dynamique / liaison statique
Classes abstraites
Présentation du problème

Ajouter une méthode calculant le prix TTC et modifier la méthode


d’affichage pour afficher ce prix. Dans l’exemple produit dispose déjà de
cette méthode, mais on veut maintenant prendre en compte différents taux
de TVA.

▶ Quel que soit le produit, le prix TTC est toujours obtenu par
_prixht * (1 + taux_de_tva)
▶ Ce qui change est le taux de TVA, pas le calcul du prix TTC
⇒ La méthode prixttc sera définie dans produit et ne sera pas
redéfinie dans les sous-classes.
▶ Cette méthode a besoin du taux de TVA appliqué au produit (qui,
lui, est spécifique au produit).

109
Une première (mauvaise) solution

▶ Ajouter un attribut _tauxtva dans produit


▶ Modifier les constructeurs avec un paramètre supplémentaire.
▶ Ajouter la méthode suivante dans produit :
Exemple (mauvaise solution)
prix produit::prixttc() const {
return _prixht * (_tauxtva + 1);
}

Défauts
▶ Nécessité de fournir le taux de TVA à chaque création de produit
▶ Chaque instance de produit contient une valeur « différente »
▶ Que faire si le taux de TVA est mis à jour ?
Le taux de TVA n’est pas une information associée à un(e instance de)
produit mais à une classe (ensemble d’ instances) de produits. 110
Une meilleure solution

Définir une méthode tauxtva dans produit (retourne 0.2) et la


redéfinir dans produitculturel (sous-classe de produit) (retourne
0.055).
Méthode produit::tauxtva (produitv2.hh)

class produit {
public:
produit(reference ref, std::string const & nom, prix prixht);
std::string const & nom() const;
float tauxtva() const;
prix prixttc() const;

Méthode produit::tauxtva (produitv2.cc)

float produit::tauxtva() const {


return 0.2;
}
float produit::prixttc() const {
return _prixht * (tauxtva() + 1); 111
Une meilleure solution

Méthode produitculturel::tauxtva (produitculturelv2.hh)

#pragma once
#include "produitv2.hh"
class produitculturel: public produit {
public:
produitculturel(reference ref, std::string const & nom, prix
prixht);
float tauxtva() const;
};

Méthode produitculturel::tauxtva (produitculturelv2.cc)

#include "produitculturelv2.hh"
produitculturel::produitculturel(reference ref, std::string const &
nom, prix prixht)
:produit(ref, nom, prixht) {
}
float produitculturel::tauxtva() const {
return 0.055; 112
}
Une meilleure solution… vraiment ?

Testons les classes…


Test (produittestv2.cc)

#include "produitperissablev2.hh"
#include "produitculturelv2.hh"
#include <iostream>
int main() {
produitperissable pp(1, "yaourt", 100, "15/10");
produitculturel pc(2, "livre", 100);
std::cout << pp.tauxtva() << "␣";
std::cout << pc.tauxtva() << "\n";
std::cout << pp.prixttc() << "␣";
std::cout << pc.prixttc() << "\n";
return 0;
}

0.2 0.055
120 120 113
Liaison statique

Liaison statique
Quand une méthode à liaison statique est appelée sur un objet,
c’est la méthode correspondant à la classe de cet objet déterminée
au moment de la compilation qui est exécutée.

tauxtva est une méthode à liaison statique.

▶ Dans main elle est appelée sur


▶ pp qui est un produitperissable
tauxtva n’est pas redéfinie dans produitperissable, c’est
donc le code hérité de produit qui est exécuté.
▶ pc qui est un produitculturel
tauxtva est redéfinie dans produitculturel, c’est ce code qui
est exécuté.

114
Liaison statique

▶ Quand prixttc est exécuté sur pp comme sur pc, c’est le code de
la méthode de produit qui est exécuté.
Quand la ligne _prixht + (tauxtva()+ 1); est exécutée,
l’objet courant (this) est un (pointeur sur un) produit.
tauxtva étant à liaison statique, c’est le code de
produit::tauxtva qui est exécuté.
▶ Dans le cas de pp, ce n’est pas gênant car tauxtva n’est pas
redéfinie dans produitperissable
▶ Dans le cas de pc, la redéfinition de tauxtva est ignorée, comme
si pc n’était qu’un produit

115
Liaison dynamique

Liaison dynamique
Quand une méthode à liaison dynamique est appelée sur un objet,
c’est la méthode correspondant à la classe « réelle » de cet objet
déterminée au moment de l’exécution qui est exécutée.

Syntaxe Déclaration de méthode à liaison dynamique


▶ Utilisation du mot-clef virtual devant la déclaration de la
méthode.
▶ Ce mot-clef ne doit pas être utilisé devant la définition de la
méthode, ni devant les redéfinitions dans les sous-classes.
▶ Lors de la redéfinition d’une méthode virtual, utilisation du mot
clef override après la déclaration de la redéfinition.

116
Liaison dynamique

Méthode produit::tauxtva (produitv3.hh)

class produit {
public:
produit(reference ref, std::string const & nom, prix prixht);
std::string const & nom() const;
virtual float tauxtva() const;
prix prixttc() const;

Méthode produit::tauxtva (produitv3.cc)

float produit::tauxtva() const {


return 0.2;
}

117
Liaison dynamique

Méthode produitculturel::tauxtva (produitculturelv3.hh)

class produitculturel: public produit {


public:
produitculturel(reference ref, std::string const & nom, prix
prixht);
float tauxtva() const override;
};

Méthode produitculturel::tauxtva (produitculturelv3.cc)

float produitculturel::tauxtva() const {


return 0.055;
}

118
Liaison dynamique

Résultat affiché :

0.2 0.055
120 105.5

Quand produit::prixttc est exécutée sur pc, elle appelle tauxtva.


Comme tauxtva est à liaison dynamique, le code correspondant est
cherché à partir de la classe « réelle » de l’objet, i.e.
produitculturel.

119
Liaison dynamique

▶ De façon générale, les méthodes susceptibles d’êtres redéfinies


dans les sous-classes devraient être définies virtual.
▶ L’appel à des méthodes à liaison dynamique est légèrement plus
lent.
⇒ ne pas utiliser de façon systématique.
▶ Il n’est pas conseillé d’appeler une méthode à liaison dynamique
dans un constructeur.

120
Liaison dynamique

Que définir virtual ? (produitv3.cc)

void produit::afficher() const {


std::cout << _ref << "␣" << _nom << "\n";
std::cout << "prix␣HT␣:␣" << _prixht << "\n";
std::cout << "prix␣TTC␣:␣" << prixttc() << "\n";
}

▶ prixttc n’a pas besoin d’être déclarée virtual


▶ afficher non plus (vraiment ?)

121
Classes abstraites

Syntaxe
Exemple
Constructeurs et destructeurs
Redéfinition de méthodes
Liaison dynamique / liaison statique
Classes abstraites
Présentation du problème

▶ Dans la solution précédente, tous les produits (les instances de


produit et les sous-classes) ont un taux de TVA de 20, sauf pour
les instances de produitculturel.
▶ On peut faire un autre choix :
▶ Le taux de TVA d’un produit (en général) est « indéterminé »
▶ Il y a deux sous-classes de produit : les produitcourant (20)
et les produitculturel (5.5).

122
Une solution (qui ne fonctionne pas)

▶ La classe produit ne dispose pas de la méthode tauxtva


▶ produitcourant et produitculturel (classes-filles de
produit) ont une méthode tauxtva
▶ Le calcul du prix TTC est le même, quel que soit le type de produit
Il est donc dans produit :
Calcul du prix TTC dans produit (produitv3.cc)

float produit::prixttc() const {


return _prixht * (tauxtva() + 1);
}

▶ ⇒ Erreur de compilation : La classe produit ne dispose pas


d’une méthode tauxtva.

123
Pourquoi cette erreur ?

Au moment de la compilation, le compilateur vérifie les appels des


méthodes…

▶ tauxtva est appelée sur l’objet courant, un produit…


▶ Or, produit ne dispose pas de cette méthode.
▶ Avec des méthodes virtual, c’est au moment de l’exécution que
le code exécuté est choisi, mais…
Au moment de la compilation, l’existence d’une méthode
(virtual ou pas) est testée.
▶ Il faut déclarer tauxtva dans produit.
Même si on n’est pas capable d’écrire son code.

124
Méthode virtuelle pure (ou Méthode abstraite)

Méthode virtuelle pure


Une méthode virtuelle pure (abstraite) est une méthode déclarée
dans une classe, destinée à être définie dans les sous-classes, mais
pour laquelle aucun code n’est donné.

Syntaxe Déclaration
virtual typeretour nommethode(arguments)=0;

Pas de définition d’une méthode virtuelle pure.


Ne pas confondre Méthode virtuelle et Méthode virtuelle pure (=
Méthode abstraite).

125
Méthode virtuelle pure

▶ Définir tauxtva comme virtuelle pure dans produit règle le


problème : il existe une méthode de ce nom lors de la
compilation de prixttc.
Déclaration méthode virtuelle (produitv4.hh)

class produit {
public:
produit(reference ref, std::string const & nom, prix prixht);
std::string const & nom() const;
virtual float tauxtva() const =0;

126
Méthode virtuelle pure

▶ Définir tauxtva dans les deux classes filles de produit.


Produit culturel (produitculturelv4.hh)

class produitculturel: public produit {


public:
produitculturel(reference ref, std::string const & nom, prix
prixht);
float tauxtva() const override;
};

Produit culturel (produitculturelv4.cc)

produitculturel::produitculturel(reference ref, std::string const &


nom, prix prixht)
:produit(ref, nom, prixht) {
}
float produitculturel::tauxtva() const {
return 0.055;
} 127
Méthode virtuelle pure

Produit courant (produitcourantv4.hh)

#pragma once
#include "produitv4.hh"
class produitcourant: public produit {
public:
produitcourant(reference ref, std::string const & nom, prix prixht
);
float tauxtva() const override;
};

Produit courant (produitcourantv4.cc)

#include "produitcourantv4.hh"
produitcourant::produitcourant(reference ref, std::string const &
nom, prix prixht)
:produit(ref, nom, prixht) {
}
float produitcourant::tauxtva() const {
return 0.2; 128
}
Méthode virtuelle pure

▶ Dans prixttc (de produit) l’appel à tauxtva exécute le code de


la méthode correspondante de la classe dont l’objet est
réellement instance. (car tauxtva est à liaison dynamique)
▶ Mais si l’objet est un produit (sans être instance d’aucune classe
fille), Quel code est exécuté ?

129
Classe abstraite

Classe abstraite

▶ Toute classe contenant (au moins) une méthode virtuelle pure est
appelée abstraite.
▶ Une classe abstraite ne peut être instanciée.

Exemple
produit p; // provoque une erreur
produitcourant p1; // ok
produit * pp; // ok
produit * pp1 = new produitcourant(...); // ok

Attention
Si une classe B est classe fille de A, si dans A est déclarée une
méthode m virtuelle pure, et si B ne redéfinit pas m…
alors B contient une méthode virtuelle pure, elle est donc abstraite.130
Classe abstraite

Dans l’exemple,

▶ produit est abstraite et ne peut être instanciée.


▶ produitcourant et produitculturel sont concrètes et peuvent
être instanciées.

Pour finir l’exemple…

▶ produitmultimedia et produitperissable peuvent être


définies comme des sous-classes de produitcourant.
▶ Elles héritent alors du taux de TVA des produitcourant.
▶ Dans ces 2 classes, les constructeurs doivent être définis, et la
méthode afficher doit être redéfinie.

131
Chapitre 4
Classes : Compléments

Attributs et méthodes de classe


Polymorphisme
Surcharge d’opérateurs
Attributs et méthodes de classe

Attributs et méthodes de classe


Polymorphisme
Surcharge d’opérateurs
Attributs de classe - Présentation du problème

On veut modifier les classes produit précédentes afin d’attribuer


automatiquement une référence unique à tous les produits.
⇒ Les constructeurs ne prennent plus comme paramètre un
reference ref, et l’attribut _ref est initialisé automatiquement par
une valeur unique.
Exemple
produit::produit(reference ref, std::string const & nom, prix prixht
)
:_ref(ref), _nom(nom), _prixht(prixht) {
}
produit::produit(std::string const & nom, prix prixht)
:_ref(???), _nom(nom), _prixht(prixht) {
}

132
Attributs de classe - Une solution

▶ Utiliser un compteur
▶ initialisé à 0 au début de l’exécution du programme ;
▶ incrémenté à chaque instanciation de produit.
▶ Ce compteur est utilisé par le constructeur de produit pour
récupérer une valeur unique afin d’ initialiser _ref.
▶ Ce compteur devrait être « caché » afin d’être visible des seules
méthodes de produit.
En faire un attribut private de produit ?

133
Attributs de classe - Une solution qui ne fonctionne pas

Exemple (ne fonctionne pas)


class produit {
public:
produit(std::string const & nom, prix prixht);
private:
reference _ref; std::string _nom; prix _prixht;
reference _compteur;
};
produit::produit(string const & nom, prix prixht)
:_ref(_compteur), _nom(nom), _prixht(prixht) {
_compteur++;
}

Code incorrect
Chaque instance de produit dispose d’une valeur de l’attribut
_compteur… alors qu’un compteur partagé (par toutes les instances)
est requis.
134
Attributs de classe

Définition (Attribut/méthode d’ instance)


Un attribut d’ instance est associé à une instance : chaque instance
dispose d’une valeur pour cet attribut.
Une méthode d’ instance s’applique sur une instance.

Définition (Attribut de classe)


Un attribut de classe est associé à une classe : toutes les instances
de cette classe « partagent » la même valeur pour cet attribut.

Le compteur est un attribut de classe, il a une unique valeur.

135
Attributs et méthodes de classe - Syntaxe

Syntaxe Attribut de classe


▶ L’emploi du mot-clef static devant la déclaration d’un attribut
fait de cet attribut un attribut de classe.
▶ Sans mot-clef static, c’est un attribut d’ instance.

136
Attribut de classe - Déclaration

Déclaration (produitv5.hh)

#pragma once
#include <string>
using reference=unsigned int;
using prix=float;
class produit {
public:
produit(std::string const & nom, prix prixht);
std::string const & nom() const;
virtual float tauxtva() const =0;
prix prixttc() const;
void afficher() const;
private:
reference _ref;
std::string _nom;
prix _prixht;
static reference _compteur;
};
137
Attribut de classe - Définition et initialisation

Attention
La seule déclaration dans le fichier .hh ne suffit pas. Il faut aussi
définir (et initialiser) cet attribut dans le fichier .cc correspondant.

Cette définition (et initialisation)

▶ doit être faite en dehors de tout bloc de code.


▶ a la forme syntaxique d’une déclaration (et initialisation) de
variable qui aurait pour nom NomClasse::NomAttribut

138
Attribut de classe - Définition et initialisation

Définition (produitv5.cc)

#include "produitv5.hh"
#include <iostream>

reference produit::_compteur(0);

produit::produit(std::string const & nom, prix prixht)


:_ref(_compteur++), _nom(nom), _prixht(prixht) {
}

À l’ intérieur du code d’une méthode, on accède directement à un


attribut de classe de cette classe.
Écrire this->_compteur n’a aucun sens (même si ce n’est pas faux).

139
Attributs de classe

▶ Toutes les visibilités peuvent être appliquées aux attributs de


classe.
▶ Si un attribut de classe est visible depuis une autre classe, on y
accède en écrivant NomClasse::NomAttribut.
▶ Un attribut de classe peut être constant, ce qui permet de définir
des constantes de classe.
▶ La valeur d’une constante de classe peut être donnée dans le
fichier .hh si le type de la constante est primitif.

140
Méthodes de classe

▶ Une méthode d’ instance est appelée sur une instance.


▶ Le code d’une telle méthode peut accéder à l’objet sur lequel elle
a été appelée à l’aide du pointeur this.

Parfois, on veut appeler une méthode sans fournir d’objet sur lequel
appeler la méthode.

141
Méthodes de classe - Exemple

Écrire une méthode de comparaison de 2 produits retournant vrai si


le premier est le moins cher, faux sinon.
Exemple
class produit {
bool comparaison(produit const & p2) const;
};
bool produit::comparaison(produit const & p2) const {
return _prixht < p2._prixht;
}
if (prod1.comparaison(prod2)) ...

⇒ Les deux produits ont le même rôle dans la comparaison,


pourtant, l’un des deux est « privilégié » : on appelle la méthode sur
cet objet.

142
Méthodes de classe

Définition (Méthode de classe)


Une méthode de classe est appliquée sur sa classe (et non sur une
instance de sa classe).

Syntaxe méthode de classe


▶ L’emploi du mot-clef static devant la déclaration d’une
méthode fait de cette méthode une méthode de classe.
▶ Sans mot-clef static, c’est une méthode d’ instance.

Une méthode de classe …


▶ ne peut accéder à l’objet courant this.
▶ ne peut accéder directement aux attributs (d’ instance) ou appeler

directement des méthodes (d’instance) sur l’objet courant.


▶ peut accéder aux attributs de classe ou appeler des méthodes de
143
classe.
Méthodes de classe

Déclaration (produitv6.hh)

class produit {
public:
produit(std::string const & nom, prix prixht);
std::string const & nom() const;
virtual float tauxtva() const =0;
prix prixttc() const;
void afficher() const;
static bool comparaison(produit const & p1, produit const & p2);

Définition (produitv6.cc)

bool produit::comparaison(produit const & p1, produit const & p2) {


return p1._prixht < p2._prixht;
}

Exemple (Usage)
if (produit::comparaison(prod1,prod2)) ...
144
Méthodes de classe

On définira une méthode de classe :

▶ Quand la méthode s’applique sur plusieurs instances, et


qu’aucune de ces instances n’a de rôle particulier.
▶ Quand la classe doit fournir un service sans nécessiter la
moindre création d’ instance.
▶ … Quand, d’un point de vue « conception », il est préférable de
rattacher un comportement à la classe plutôt qu’à une instance
donnée.

145
Polymorphisme

Attributs et méthodes de classe


Polymorphisme
Exemple
Le cas particulier du destructeur
dynamic_cast
Constructeur virtuel
Surcharge d’opérateurs
Polymorphisme

Lorsque des méthodes à liaison dynamique sont appelées, le simple


examen du code d’un appel à une méthode ne suffit pas à déterminer
quel code sera exécuté.

▶ Un pointeur (resp. référence) sur un objet peut en fait pointer sur


(resp. référencer) une instance d’une sous-classe.
▶ Lors d’un appel d’une méthode à liaison dynamique sur ce
pointeur (référence), la méthode redéfinie dans la sous-classe est
exécutée.

Exemple. Le calcul du prixttc considère l’objet courant comme un


produit, mais si une sous-classe de produit redéfinit tauxtva, c’est
la méthode redéfinie qui est appelée.

146
Polymorphisme

Exemple
Polymorphisme - Exemple

On veut mémoriser un ensemble de produits.


Exemple (Une mauvaise solution)
class stock {
private:
std::vector<produitculturel> _pc;
std::vector<produitmultimedia> _pm;
std::vector<produitstandard> _ps;
std::vector<produitpersissable> _pp;
...
};

▶ Chaque opération qui porte sur la totalité des produits doit faire
4 boucles.
▶ La définition d’une nouvelle sous-classe de produit demande de
modifier le code de stock.
▶ À éviter 147
Polymorphisme - Exemple

Utiliser un seul vector ?


Exemple (Une solution fausse)
class stock {
private:
std::vector<produit> _p;
...
};

Dans cette solution, chaque élément du vector est une instance de


produit (et uniquement de cette classe-là).
Le vector ne peut donc pas contenir des produitculturel, etc.
Dans une solution correcte, tous les éléments du vector doivent être
du même type (déclaration de l’attribut) mais permettre de
« contenir » des types différents (sous-classes de produit)
Un pointeur d’une super-classe (produit) peut pointer sur des
148
instances des sous-classes (produitculturel, etc.)
Polymorphisme - Solution utilisant le polymorphisme

Déclaration (stockv1.hh)

#pragma once
#include "produitv6.hh"
#include <vector>

class stock {
public:
~stock();
void afficher() const;
float tvamoyenne() const;
void afficherperemptions() const;
private:
std::vector<produit *> _prod;
};

149
Polymorphisme - Solution utilisant le polymorphisme

▶ Seule la classe produit est utilisée.


▶ Nécessite d’utiliser des pointeurs de produit et non des
instances.
Quand le polymorphisme est utilisé, on ne peut mémoriser des
instances (de quelle classe ?), il faut utiliser des pointeurs (de la
classe mère, qui peuvent pointer sur des instances de toutes les
classes filles).
▶ Les instances de produits étant manipulées par pointeur et
allouées dynamiquement, il faut définir un destructeur (et un
constructeur par recopie, et un opérateur d’affectation, règle des
3).

150
Polymorphisme - Solution utilisant le polymorphisme

Définition (stockv1.cc)

#include "stockv1.hh"
#include "produitperissablev6.hh"
#include <iostream>
stock::~stock() {
for (auto p : _prod)
delete p;
}
void stock::afficher() const {
for (auto p : _prod)
p->afficher();
}
float stock::tvamoyenne() const {
float s(0);
for (auto p : _prod)
s += p->tauxtva();
return s/_prod.size();
}
151
Polymorphisme - Solution utilisant le polymorphisme

Problème
Lors de l’exécution d’afficher, les produitperissable n’affichent
pas leur date limite de vente.

Pourquoi ? Parce que la méthode produit::afficher n’a pas été


définie virtual.
⇒ La méthode de produit est appelée et non sa redéfinition dans
produitperissable.

Donc…
On déclarera toujours virtual les méthodes pouvant être
redéfinies et destinées à être utilisées par polymorphisme.

152
Polymorphisme

Le cas particulier du destructeur


Polymorphisme et destructeur

Quand une instance de stock est détruite, le contenu de l’ instance


est automatiquement détruit : le vector et les pointeurs qui sont
dans ce vector…pas les objets pointés par ces pointeurs.
Il faut donc les détruire explicitement. C’est le défaut d’utiliser des
pointeurs (plutôt que des instances).

153
Polymorphisme et destructeur

Destructeur de stock (stockv1.cc)

stock::~stock() {
for (auto p : _prod)
delete p;
}

Tous les produits du stock sont détruits, le destructeur est appelé sur
chacun de ces produits…

154
Polymorphisme et destructeur

Que se passerait-il si des sous-classes de produit définissaient un


destructeur ?
▶ Le destructeur est (presque) une méthode comme les autres.
▶ Il est donc, par défaut, à liaison statique.
▶ Un élément de _prod est de type produit *, c’est donc le
destructeur de produit qui est appelé, même si l’objet détruit
est une instance d’une sous-classe.→ À éviter
Donc…
▶ On déclarera toujours virtual le destructeur d’une classe qui
contient au moins une méthode virtual.
▶ Sauf si la super-classe déclare déjà un destructeur virtual.
▶ Le compilateur détecte d’ailleurs cela deleting object of
abstract class type ‘produit’ which has non-virtual
155
destructor will cause undefined behavior
Polymorphisme et destructeur

Attention. Ne pas déclarer de destructeur dans une classe revient à


déclarer un destructeur qui « ne fait rien »… et qui est à liaison
statique.
Exemple
class produit {
...
virtual ~produit() =default;
...
};

Dans produit le destructeur « ne fait rien », mais le déclarer virtual


permet de le redéfinir dans les sous-classes et permet l’appel du
destructeur des sous-classes par polymorphisme.

156
Polymorphisme

dynamic_cast
dynamic_cast

On suppose que la classe produitperissable dispose d’une


méthode date const & peremption()const accesseur à l’attribut
« date de peremption ».
Produit périssable (produitperissablev6.hh)

#pragma once
#include "produitcourantv6.hh"
using date=std::string;
class produitperissable: public produitcourant {
public:
produitperissable(std::string const & nom, prix prixht, date const
& peremption);
date const & peremption() const {
return _peremption; }
void afficher() const;
private:
date _peremption;
};
157
dynamic_cast

Problème.
On veut écrire dans stock une méthode d’affichage de toutes les
dates de péremption.
Exemple (qui ne fonctionne pas)
void stock::afficherperemtions() const {
for (auto p : _prod)
std::cout << p->peremption();
}

158
dynamic_cast

Ce code est incorrect car

▶ On veut afficher les dates de péremption uniquement pour les


produitperissable.
▶ Un élément p de _prod est un produit * et peremption() n’est
pas définie dans produit. ⇒ Erreur de compilation.

Comment savoir si un produit * pointe sur un produitperissable ?

159
dynamic_cast

Définition (dynamic_cast)
dynamic_cast est un opérateur de conversion qui permet de
convertir un pointeur (une référence) sur une super-classe vers un
pointeur (une référence) sur une de ses sous-classes.

▶ Dans le cas d’une conversion de pointeur, si la conversion n’est


pas possible, retourne nullptr.
▶ Dans le cas d’une conversion de référence, si la conversion n’est
pas possible, lève une exception std::bad_cast.

160
dynamic_cast

afficherperemptions (stockv1.cc)

void stock::afficherperemptions() const {


for (auto p : _prod) {
auto pp = dynamic_cast<produitperissable const*>(p);
if (pp != nullptr)
std::cout << pp->peremption();
}
}

161
dynamic_cast

Utilisation de dynamic_cast :

▶ Ne jamais utiliser dynamic_cast sur un pointeur valant nullptr.


▶ Uniquement pour convertir vers une sous-classe.
▶ Uniquement si la classe du pointeur (ou référence) contient au
moins une méthode virtuelle.
▶ A un coût à l’exécution (contrairement à reinterpret_cast).
Mais permet de tester et de contrôler.

162
Polymorphisme

Constructeur virtuel
Problèmes de la classe stock

▶ La classe stock ne respecte pas la règle des 3.


Un destructeur a été défini, mais pas de constructeur par recopie
(ni d’opérateur d’affectation).
Que se passe-t-il si une copie de stock est faite ? Une
catastrophe !
▶ Comment ajouter des produits au stock ?
De façon robuste, simple, générique.

163
Ajout de produits au stock

version 1 - incorrect (constrvirtuel/stock.hh)

void ajouterproduit1(produit p) {
_prod.push_back(&p);
}

Erreur de compilation : cannot declare parameter ‘p’ to be


of abstract type ‘produit’
Lors d’un appel à ajouterproduit1 avec un produitperissable le
constructeur par recopie de produit est appelé et instancie donc un
produit copie du produitperissable.

Même si produit n’était pas abstraite, ça n’aurait aucun sens :


Problème de slicing.

164
Ajout de produits au stock

version 2 - incorrect (constrvirtuel/stock.hh)

void ajouterproduit2(produitperissable p) {
_prod.push_back(&p);
}

Erreur à l’exécution : ce qui est rajouté au std::vector est un


pointeur sur une variable locale.
Il faut donc ajouter au vector un pointeur sur un objet qui n’est pas
détruit à la fin de l’exécution de la méthode.

165
Ajout de produits au stock

version 3 - incorrect (constrvirtuel/stock.hh)

void ajouterproduit3(produitperissable & p) {


_prod.push_back(&p);
}

produitperissable pp1("pp1", 100, "10/10");


s.ajouterproduit3(pp1);
auto pp2 = new produitperissable("pp1", 100, "10/10");
s.ajouterproduit3(*pp2);

Le premier ajout provoquera une erreur, pas le second.


→ Code peu robuste.

166
Ajout de produits au stock

version 4 - incorrect (constrvirtuel/stock.hh)

void ajouterproduit4(produitperissable * p) {
_prod.push_back(p);
}

Ce code est toujours peu robuste :


produitperissable pp1("pp1", 100, "10/10");
s.ajouterproduit4(&pp1);
auto pp2 = new produitperissable("pp1", 100, "10/10");
s.ajouterproduit4(pp2);
delete pp2;

Même en prenant un pointeur comme paramètre, aucune garantie


que ce pointeur pointera toujours sur une instance dont le stock est
propriétaire.

167
Ajout de produits au stock

Le seul moyen de s’assurer de la propriété des produits par le stock


est de créer dynamiquement les produits dans une méthode de
stock, de les détruire dans une méthode de stock et ne jamais
montrer les pointeurs à l’extérieur de stock.
version 5 (constrvirtuel/stock.hh)

void ajouterproduit5a(produitperissable const & p) {


_prod.push_back(new produitperissable(p));
}
void ajouterproduit5b(std::string const & nom, prix prixht, date
const & peremption) {
_prod.push_back(new produitperissable(nom, prixht, peremption));
}

168
Ajout de produits au stock

Cette solution est correcte et robuste mais…

▶ Nécessite de connaître les sous-classes de produit.


▶ Nouvelle sous-classe de produit → rajouter une méthode à
stock.

La structure de données utilise le polymorphisme


(std::vector<produit*>) mais pas les méthodes d’ajout.

169
Ajout de produits au stock

▶ On voudrait une seule méthode d’ajout → prenant comme


paramètre un produit (passé par référence ou pointeur).
▶ On voudrait appeler un constructeur pour créer un nouvel objet,
copie de l’objet passé en paramètre → le constructeur par recopie
de la classe dont l’objet est réellement instance, pas produit.

On aurait donc besoin d’un constructeur virtuel.


Un constructeur ne peut être virtual.

170
Constructeur virtuel

Un « constructeur virtuel » est une façon de construire un nouvel


objet, qui est instance de la même classe qu’un objet existant, peu
importe la façon dont l’objet existant est manipulé (par exemple
référence/pointeur de super-classe).
Constructeur virtuel (constrvirtuel/produit.hh)

class produit {
public:
produit(std::string const & nom, prix prixht);
virtual ~produit() =default;
virtual produit * clone() const =0;

171
Constructeur virtuel

Constructeur virtuel (constrvirtuel/produitperissable.hh)

class produitperissable: public produitcourant {


public:
produitperissable(std::string const & nom, prix prixht, date const
& peremption);
produit * clone() const override {
return new produitperissable(*this);
}

Utilisation (constrvirtuel/stock.hh)

void ajouterproduit6(produit const & p) {


_prod.push_back(p.clone());
}

Une seule méthode d’ajout, code robuste (à condition de ne jamais


montrer les pointeurs du vector), code réutilisable (d’autres
sous-classes de produit peuvent être utilisées sans modifier stock172
).
Retour sur la règle des 3

Faire une copie de stock provoque une erreur d’exécution.


Il est possible d’empêcher la copie de certains objets en
« supprimant » le constructeur par recopie (implicite).
Suppression d’une méthode implicite (constrvirtuel/stock.hh)

stock(stock const & s) =delete;

Utile quand la copie des instances d’une classe n’a pas de sens.
Sinon, il faut écrire un constructeur par recopie qui fait effectivement
une copie…Et créer des copies des produits contenus dans le stock à
copier.

173
Constructeur virtuel

incorrect (constrvirtuel/stock.hh)

stock(stock const & s) {


for (auto p : s._prod)
_prod.push_back(new produit(*p));
}

Appelle le constructeur de produit, classe abstraite.

174
Constructeur virtuel

Faire une suite de tests avec des dynamic_cast pour appeler le


constructeur de la « bonne » sous-classe de produit.
incorrect (constrvirtuel/stock.hh)

stock(stock const & s) {


for (auto p : s._prod) {
if (dynamic_cast<produitperissable const *>(p))
_prod.push_back(new produitperissable(*(dynamic_cast<
produitperissable const *>(p))));
else if (dynamic_cast< ... >(p))
...
}

Lors de l’ajout d’une nouvelle sous-classe de produit, si on oublie de


mettre à jour le constructeur par recopie de stock…pas d’erreur de
compilation, mais comportement incorrect.
175
Constructeur virtuel

Solution. Appeler le constructeur virtuel.


Appel au constructeur virtuel (constrvirtuel/stock.hh)

stock(stock const & s) {


for (auto p : s._prod)
_prod.push_back(p->clone());
}

Ainsi stock est une classe dont les instances peuvent être copiées.
Pour respecter la règle des 3, il faudrait définir aussi l’opérateur
d’affectation.

176
Surcharge d’opérateurs

Attributs et méthodes de classe


Polymorphisme
Surcharge d’opérateurs
Opérateur d’affectation

Par défaut, il est possible de copier un objet dans un autre, en


utilisant l’opérateur =.
Il ne faut pas confondre la construction par recopie (création d’un
nouvel objet par copie d’un autre objet) et l’affectation (remplacement
de la valeur d’un objet existant par la copie d’un autre objet).
Exemple (Construction et affectation)
fichier f1; // Constructeur par défaut
fichier f2(f1); // Constructeur par recopie
fichier f3=f1; // Constructeur par recopie
fichier f4;
f4 = f1; // Affectation

177
Opérateur d’affectation : Exemple

Rappel, la classe fichier (fichierv3.hh)

struct infofichier {
fichierproprietaire _proprietaire;
fichierdate _creation;
};
class fichier {
public:
fichier(fichiernom const & nom, fichiertaille taille);
fichier(fichiernom const & nom, fichiertaille taille,
fichierproprietaire proprietaire, fichierdate creation);
fichier(fichier const & f);
~fichier();
private:
fichiernom _nom;
fichiertaille _taille;
infofichier * _infosuppl;
};

178
Opérateur d’affectation : Exemple

Rappel, la classe fichier (fichierv3.cc)

#include "fichierv3.hh"
#include <iostream>
fichier::fichier(fichiernom const & nom, fichiertaille taille)
:_nom(nom),_taille(taille), _infosuppl(nullptr) {
}
fichier::fichier(fichiernom const & nom, fichiertaille taille,
fichierproprietaire proprietaire, fichierdate creation)
:fichier(nom, taille) {
_infosuppl = new infofichier {proprietaire, creation};
}
fichier::fichier(fichier const & f)
:fichier(f._nom, f._taille) {
if (f._infosuppl)
_infosuppl = new infofichier(*f._infosuppl);
}
fichier::~fichier() {
delete _infosuppl;
179
}
Opérateur d’affectation

Par défaut, l’opérateur d’affectation copie la valeur de tous les


attributs.
f4._nom = f1._nom; f4._taille = f1._taille;
f4._infosuppl = f1._infosuppl;

Ici, il copie donc la valeur de _infosuppl, c’est à dire le pointeur.


⇒ Problème à la destruction ou à la modification.
Attention
Définir un constructeur par recopie ne règle pas le problème.
« Faire une affectation (sur un objet existant) » est différent de
« Construire un nouvel objet par copie ».

180
Surcharge d’opérateurs

Il faut donc (re)définir le code devant être exécuté lors d’une


affectation.
En C++, la plupart des opérateurs pouvant être appliqués à des
instances d’une classe peuvent être redéfinis : =, ==, [], <, <<, >=, >>,
etc.
Syntaxe Redéfinition d’opérateur
La redéfinition d’un opérateur se fait en déclarant et définissant une
méthode ayant pour nom operator suivi de l’opérateur.

181
Surcharge d’opérateurs

La méthode operator est appelée sur l’objet présent en partie


gauche de l’opérateur, et reçoit comme paramètre l’objet présent en
partie droite de l’opérateur : f4 = f1; est un appel de la méthode
operator= sur l’objet f4, en prenant comme paramètre f1 :
f4.operator=(f1);.

Exemple (Signatures habituellement utilisées)


class C {
...
bool operator==(C const & c) const;
bool operator<(C const & c) const;
C & operator=(C const & c);
};

182
Opérateur d’affectation

Déclaration (fichierv4.hh)

class fichier {
public:
fichier(fichiernom const & nom, fichiertaille taille);
fichier(fichiernom const & nom, fichiertaille taille,
fichierproprietaire proprietaire, fichierdate creation);
fichier(fichier const & f);
~fichier();
fichier & operator=(fichier const & f);

183
Opérateur d’affectation

Définition (fichierv4.cc)

fichier & fichier::operator=(fichier const & f) {


if (this != &f) {
_nom = f._nom;
_taille = f._taille;
delete _infosuppl;
if (f._infosuppl)
_infosuppl = new infofichier(*(f._infosuppl));
else
_infosuppl = nullptr;
}
return *this;
}

La classe fichier est un exemple de l’application de la règle des 3 :


constructeurs par recopie, destructeur, opérateur d’affectation étaient
tous les trois nécessaires.
184
Opérateur d’affectation et constructeur virtuel

L’opérateur d’affectation de stock doit être défini pour respecter la


règle des 3.
operateur d’affectation (constrvirtuel/stock.hh)

stock & operator=(stock const & s) {


if (this != &s) {
for (auto p : _prod)
delete p;
_prod.clear();
for (auto p : s._prod)
_prod.push_back(p->clone());
}
return *this;
}

185
Opérateur de sortie

<< est un opérateur « comme les autres », il peut être redéfini.

Problème.
std::cout << c est un appel de l’opérateur << sur cout.
Cela est d’ailleurs équivalent à std::cout.operator<<(c).
Il faudrait donc redéfinir cet opérateur dans std::ostream !

Solution.
Les opérateurs >> et << (comme les autres opérateurs) peuvent être
définis comme des fonctions avec la signature suivante :
std::ostream & operator<<(std::ostream & os, C const & c);
std::istream & operator>>(std::istream & os, C & c);

186
Opérateur de sortie - Exemple

Déclaration (fichierv5.hh)

class fichier {
public:
fichier(fichiernom const & nom, fichiertaille taille);
fichier(fichiernom const & nom, fichiertaille taille,
fichierproprietaire proprietaire, fichierdate creation);
fichier(fichier const & f);
~fichier();
fichiernom const & nom() const { return _nom; }
fichiertaille taille() const { return _taille; }
fichier & operator=(fichier const & f);
private:
fichiernom _nom;
fichiertaille _taille;
infofichier * _infosuppl;
};
std::ostream & operator<<(std::ostream & os, fichier const & f);
187
Opérateur de sortie - Exemple

Le code de la fonction doit être donné dans le fichier cc, pas le fichier
hh. Sinon, problème d’édition de liens : fonction définies plusieurs
fois.
Définition (fichierv5.cc)

std::ostream & operator<<(std::ostream & os, fichier const & f) {


os << f.nom() << "␣" << f.taille();
return os;
}

188
Opérateur de sortie et hiérarchie de classes

L’opérateur de sortie est une fonction, il ne peut donc pas avoir le


comportement d’une méthode virtuelle.
Exemple. Écrire l’opérateur de sortie de stock qui sort les
informations sur les produits.
opérateur de sortie (constrvirtuel/stock.hh)

std::ostream & operator<<(std::ostream & os, stock const & s);

On suppose que les classes produit définissent un opérateur de


sortie :
std::ostream & operator<<(std::ostream & os, produit const & s);
std::ostream & operator<<(std::ostream & os, produitperissable const
& s);
std::ostream & operator<<(std::ostream & os, produitmultimedia const
& s);
...
189
Opérateur de sortie et hiérarchie de classes

opérateur de sortie (constrvirtuel/stock.cc)

std::ostream & operator<<(std::ostream & os, stock const & s) {


for (auto p : s._prod)
os << (*p);
return os;
}

(à condition que _prod soit public, ce qui n’est certainement pas une
bonne idée)
Pas d’erreur de compilation, mais ne fait pas ce qui est attendu :
appelle systématiquement l’opérateur de produit (car *p est de type
produit pour le compilateur).

Il faut appeler une méthode virtuelle.

190
Opérateur de sortie et hiérarchie de classes

méthode virtuelle de sortie (constrvirtuel/produit.hh)

virtual void sortie(std::ostream & os) const;

méthode virtuelle de sortie (constrvirtuel/produit.cc)

void produit::sortie(std::ostream & os) const {


os << _ref << "␣" << _nom << "\n";
os << "prix␣HT␣:␣" << _prixht << "\n";
os << "prix␣TTC␣:␣" << prixttc() << "\n";
}

191
Opérateur de sortie et hiérarchie de classes

Et la redéfinir dans les sous-classes :


méthode virtuelle de sortie (constrvirtuel/produitperissable.hh)

void sortie(std::ostream & os) const override;

méthode virtuelle de sortie (constrvirtuel/produitperissable.cc)

void produitperissable::sortie(std::ostream & os) const {


produit::sortie(os);
os <<"peremption␣:␣" << _peremption << "\n";
}

192
Opérateur de sortie et hiérarchie de classes

opérateur de sortie (constrvirtuel/stock.cc)

std::ostream & operator<<(std::ostream & os, stock const & s) {


for (auto p : s._prod)
p->sortie(os);
return os;
}

On peut ainsi écrire :


stock s;
s.ajouterproduit( ... );
std::cout << s;

Pour éviter de rendre _prod public, il suffit d’écrire le code de sortie


de stock dans une méthode, appelée depuis l’opérateur de sortie.

193
Opérateur de sortie et hiérarchie de classes

Un stock peut être envoyé dans un flux mais pas un produit


produitperissable pp1( ... );
produit * p = new produitperissable( ... );
std::cout << pp1;
std::cout << (*p);

Erreurs de compilation.

194
Opérateur de sortie et hiérarchie de classes

Rien n’empêche de définir l’opérateur de sortie de produit pour


appeler sortie.
opérateur de sortie (constrvirtuel/produit.cc)

std::ostream & operator<<(std::ostream & os, produit const & p) {


p.sortie(os);
return os;
}

Et il est inutile de définir cet opérateur sur les sous-classes de


produit car l’opérateur de sortie de produit peut recevoir (par
référence constante) une instance de produit (ou de n’ importe
quelle sous-classe) et appelle une méthode virtuelle.

195
Chapitre 5
Pointeurs intelligents

Allocation de ressources
Pointeur intelligent unique
Pointeur intelligent partagé
Divers
Allocation de ressources

Allocation de ressources
Pointeur intelligent unique
Pointeur intelligent partagé
Divers
Valeurs, Références, Pointeurs

En C++, les objets peuvent être manipulés par valeur, référence,


pointeur.
personne p1(...);
personne & p2(p1);
personne * p3(nullptr);
personne * p4(&p1);
personne * p5(new personne(...));

Une valeur et une référence identifient obligatoirement un objet.


Un pointeur peut être nullptr : on ne doit pas appeler de méthode à
partir de ce pointeur.
Syntaxiquement p4 et p5 sont de même type mais p5 doit être libéré
(delete p5;) et surtout pas p4.

196
Valeurs, Références, Pointeurs

Quand on peut éviter d’utiliser des pointeurs, on les évite :

▶ Cause de fuites de mémoire (si on oublie delete p5;)


▶ Cause de plantages (delete p4;)

Même chose pour le passage de paramètres :


bool estplusjeuneque(personne const * p2) const // ou
bool estplusjeuneque(personne const & p2) const

La deuxième solution est préférable, car un pointeur peut être


nullptr.

197
Valeurs, Références, Pointeurs

Même chose pour le type des attributs d’une classe.


class personne{
...
private:
std::string * _nom;
unsigned int * _age;
};

Faire ceci est une mauvaise idée : il faut allouer dynamiquement


std::string et unsigned int dans le constructeur, et les libérer
dans le destructeur.

198
Pointeurs

Quels sont les avantages des pointeurs, alors ?

▶ Ils peuvent être nullptr : Donnée optionnelle


▶ Ils permettent de repérer des objets de différentes classes
(polymorphisme)
▶ Ils sont facilement copiables (partage d’objets)

Le partage d’objets est aussi une difficulté : qui doit libérer l’objet
pointé par le pointeur si plusieurs classes/fonctions ont accès au
même objet par pointeur ? quand le faire ?

199
Pointeurs

Dès qu’on utilise des pointeurs, se pose la question de la propriété de


l’objet pointé.
Bonne pratique (RAII). L’objet pointé par un pointeur attribut est créé
dans le constructeur de la classe propriétaire, détruit dans le
destructeur de la même classe. Une classe robuste ne devrait jamais
donner accès à ce pointeur.
Problème. Comment respecter la règle des 0 quand un attribut d’une
classe est un pointeur.

200
Pointeurs - Information optionnelle

class fichier {
...
private:
fichiernom _nom;
fichiertaille _taille;
infofichier * _infosuppl;
};

_infosuppl est une information optionnelle : certaines instances de


fichier auront une valeur nullptr , d’autres une valeur non nulle
(pointant sur un objet alloué dynamiquement dans le constructeur,
libéré dans le destructeur).
fichier respecte la bonne pratique RAII mais requiert d’écrire un
destructeur. Comment faire pour s’en passer ?

201
Pointeurs - Conteneur et polymorphisme

Dans la classe stock, les objets doivent être manipulés par pointeur.

▶ Pointeurs sur des objets créés dynamiquement dans stock


▶ Objets libérés dans stock
▶ Les pointeurs sur ces objets ne doivent être visibles nulle par
ailleurs.

⇒ Un constructeur virtuel a été défini, ce qui est mémorisé dans


stock lors d’un ajout est une copie de produit dont le stock est
propriétaire.
stock respecte la bonne pratique RAII mais requiert l’écriture d’un
destructeur. Comment faire pour s’en passer ?

202
Pointeur intelligent unique

Allocation de ressources
Pointeur intelligent unique
Pointeur intelligent partagé
Divers
Pointeurs

stock est robuste car respecte le principe RAII et la règle des 3 mais…

Requiert de gérer la mémoire, avec un constructeur par recopie


particulier, un destructeur particulier, un opérateur d’affectation
particulier, si on en oublie un, code incorrect.
Un produit d’un stock a comme (unique) propriétaire le stock et
n’est accessible qu’à ce stock.
→ Pour un produit il existe un unique pointeur pointant sur cette
instance, ce pointeur étant stocké dans une instance de stock.

203
unique_ptr

Un pointeur intelligent unique est un pointeur qui représente une


relation de propriété unique d’un objet pointé.

▶ Unique point d’accès à l’objet propriété du pointeur.


▶ Peut être vide (n’est propriétaire d’aucun objet).
▶ Sa valeur ne peut être copiée (cela créerait un deuxième accès à
l’objet pointé). Vérifié à la compilation.
▶ Libère automatiquement l’objet propriété quand le pointeur
cesse d’exister.
▶ Coût en mémoire ou en temps d’exécution : 0.

204
unique_ptr

▶ #include <memory>
▶ Type : std::unique_ptr<type>
▶ Constructeur par défaut : Pas d’objet propriété
▶ Allocation dynamique d’un objet propriété par
std::make_unique<type>(parametrescontructeur)
▶ Conversion en bool : true si possède un objet, false sinon
▶ Le constructeur par recopie et l’opérateur d’affectation sont
=delete
▶ std::move permet de transférer la propriété de l’objet pointé
▶ reset permet libérer l’objet propriété

205
unique_ptr - déclaration

stock : déclaration de l’attribut _prod (ressources/stock.hh)

std::vector<std::unique_ptr<produit>> _prod;

stock : destructeur (ressources/stock.hh)

~stock() =default;

std::unique_ptr s’utilise comme un pointeur : utilisation de -> pour


accéder à un membre, opérateur * pour accéder à l’objet pointé.

206
unique_ptr - accès

stock : parcours - Erreur (ressources/stock.cc)

void stock::afficher() const {


for (auto p : _prod)
p->afficher();
}

Erreur de compilation : p est résolu comme un


std::unique_ptr<produit>, qui a chaque itération reçoit une copie
d’un std::unique_ptr<produit> de _prod
error: use of deleted function
stock : parcours - Correct (ressources/stock.cc)

void stock::afficher() const {


for (auto const & p : _prod)
p->afficher();
}
207
unique_ptr - dynamic_cast

Un dynamic_cast ne peut être fait directement sur un


std::unique_ptr, mais uniquement sur un pointeur (brut).

La méthode get de std::unique_ptr permet d’accéder au pointeur


(brut) sur l’objet.
À utiliser avec précaution. Normalement, uniquement pour faire une
conversion telle que dynamic_cast.
stock : dynamic_cast (ressources/stock.cc)

void stock::afficherperemptions() const {


for (auto const & p : _prod) {
auto pp = dynamic_cast<produitperissable const*>(p.get());
if (pp != nullptr)
std::cout << pp->peremption();
}
}
208
unique_ptr - passage en paramètre

stock : ajout - Erreur (ressources/stock.cc)

void stock::ajouterproduit(std::unique_ptr<produit> p) {
_prod.push_back(p);
}

Erreur de compilation : ajoute à _prod une copie de p. Il faut


transférer la propriété de l’objet au std::unique_ptr qui sera dans
_prod en utilisant std::move.
stock : ajout (ressources/stock.cc)

void stock::ajouterproduit(std::unique_ptr<produit> p) {
_prod.push_back(std::move(p));
}

Après un appel à std::move, le std::unique_ptr p ne pointe plus


sur un objet, sa conversion en bool est false.
209
unique_ptr - passage en paramètre

stock : ajout (ressources/stockmain.cc)

stock s;
auto pp = std::make_unique<produitperissable>("pp1", 100, "10/10")
;
/* Erreur , copie impossible .
s . ajouterproduit (pp) ;
*/
s.ajouterproduit(std::move(pp));

Il n’a pas été nécessaire de définir un constructeur virtuel pour écrire


une classe robuste.
La classe stock est plus simple que précédemment : pas de gestion
d’allocations dynamiques (aucun new, aucun delete), pas de
destructeur.

210
unique_ptr - constructeur par recopie

Toute classe dispose d’un constructeur par recopie implicite qui


appelle les constructeurs par recopie de ses attributs.
→ Le constructeur par recopie de stock appelle le constructeur par
recopie de std::vector<std::unique_ptr<produit>>
qui appelle le constructeur par recopie de
std::unique_ptr<produit>
qui ne peut être appelé.

211
unique_ptr - constructeur par recopie

Mais alors… pourquoi pas d’erreur de compilation ?


Parce que le constructeur par recopie implicite n’est « généré » que
s’il est appelé. Il suffit de l’appeler pour obtenir une erreur…un peu
incomprehensible car elle est dans un code qu’on n’a pas écrit.
Il est donc conseillé d’interdire explicitement la copie (constructeur
par recopie, opérateur d’affectation) de stock (idem operator=).
stock : interdire la copie (ressources/stock.hh)

stock(stock const & s) =delete;

Ainsi l’appel au constructeur par recopie provoquera l’erreur error:


use of deleted function 'stock::stock(const stock&)'.

212
unique_ptr - constructeur par recopie

Mais si on veut copier des stock ? Rien n’empêche de définir le


constructeur par recopie (et l’opérateur d’affectation) en utilisant le
constructeur virtuel de produit.
produit : constructeur virtuel (ressources/produit.hh)

virtual std::unique_ptr<produit> clone() const =0;

produit périssable : constructeur virtuel (ressources/produitperissable.hh)

std::unique_ptr<produit> clone() const override {


return std::make_unique<produitperissable>(*this);
}

stock : constructeur par recopie (ressources/stock.hh)

stock(stock const & s) {


for (auto const & p : s._prod)
_prod.push_back(p->clone());
} 213
Pointeur intelligent partagé

Allocation de ressources
Pointeur intelligent unique
Pointeur intelligent partagé
Divers
unique_ptr et propriété partagée

Dans certains cas, std::unique_ptr n’est pas adapté : Quand la


propriété d’un objet doit être partagée dans plusieurs instances.
Exemple. On veut rajouter à produit, stock… la possibilité de
représenter des commandes composées de produits, et on veut
stocker à l’intérieur d’une commande des pointeurs vers les produits
du stock (et non des copies des produits).
Premier problème. Donner accès aux produits d’une instance de
stock.

214
Propriété partagée

recherche d’un produit dans stock - Erreur (ressources/partage/stock.hh)

produit rechercher1(reference r) const {


for (auto & p : _prod)
if (p->ref() == r)
return *p;
}

Erreur de compilation : la méthode retourne une instance de produit


(construite par le constructeur par recopie de produit), or produit
est abstraite.
Mais même si elle n’était pas abstraite, ce serait faux : Problème de
slicing.

215
Propriété partagée

recherche d’un produit dans stock - Fragile (ressources/partage/stock.hh)

produit const * rechercher2(reference r) const {


for (auto & p : _prod)
if (p->ref() == r)
return p.get();
return nullptr;
}

Fonctionne, mais dangereux : stock expose un pointeur sur un objet


dont il devrait être le seul propriétaire.
Démonstration de la fragilité (ressources/partage/stockmain.cc)

auto prod = s.rechercher2(ref);


std::cout << *prod << "\n";
delete prod;

Erreur de segmentation dans le destructeur de stock.


216
Propriété partagée

recherche d’un produit dans stock - Moyen (ressources/partage/stock.hh)

produit const & rechercher3(reference r) const {


for (auto & p : _prod)
if (p->ref() == r)
return *p;
}

Fonctionne, mais génère un avertissement : la méthode peut se


terminer sans rien retourner.
(Peut être géré par une levée d’exception).

217
Propriété partagée

Attention à l’utilisation :
Retour de référence - Incorrect (ressources/partage/stockmain.cc)

produit prod1 = s.rechercher3(ref);


auto prod2 = s.rechercher3(ref);

Ces deux lignes provoquent une erreur de compilation : Ce sont des


appels au constructeur par recopie de produit (initialisation d’un
produit à partir d’une référence de produit) : instanciation d’une
classe abstraite.
Retour de référence - Correct (ressources/partage/stockmain.cc)

auto const & prod = s.rechercher3(ref);


std::cout << prod << "\n";

218
Propriété partagée

La classe commande - fragile (ressources/partage/commandev1.hh)

class commande {
public:
commande(std::string const & client)
:_client(client), _produits()
{}
void ajouter(produit const * p) {
_produits.push_back(p);
}
private:
std::string _client;
std::vector<produit const *> _produits;
};

Utilisation (ressources/partage/stockmain.cc)

auto const & prod = s.rechercher3(ref);


std::cout << prod << "\n";
commande c1("nom"); 219
c1.ajouter(&prod);
Propriété partagée

Ça marche, mais…

▶ Les produits sont manipulés par référence et par pointeurs


(bruts).
▶ Des pointeurs bruts sur les produits de stock sont utilisés
ailleurs que dans stock.
▶ Ne pas libérer les pointeurs de _produits.
▶ Aucune garantie dans commande que les pointeurs mémorisés
pointent sur des objets qui existent encore → Fragilité aux
évolutions de stock.

220
shared_ptr

Un pointeur intelligent partagé est un pointeur qui représente une


relation de propriété d’un objet pointé. La propriété peut être
partagée à partir de plusieurs pointeurs.

▶ Peut être vide (n’est propriétaire d’aucun objet).


▶ Sa valeur peut être copiée : Cela partage la propriété avec un
nouveau pointeur partagé.
▶ Libère automatiquement l’objet propriété quand le « dernier »
pointeur partagé est détruit ou est modifié pour pointer vers un
autre objet.
▶ Coût en mémoire (compteur de référence) et en temps
d’exécution (incrémentation / décrémentation du compteur).

221
shared_ptr - Déclaration

Stock (ressources/partage/stockv2.hh)

class stock {
public:
stock() =default;
stock(stock const & s) =delete;
~stock() =default;
stock & operator=(stock const & s) =delete;
void afficher() const;
float tvamoyenne() const;
void afficherperemptions() const;
void ajouterproduit(std::shared_ptr<produit> p);
std::shared_ptr<const produit> rechercher(reference r) const;
private:
std::vector<std::shared_ptr<produit>> _prod;

222
shared_ptr - Utlisation

Stock (ressources/partage/stockv2.cc)

void stock::ajouterproduit(std::shared_ptr<produit> p) {
_prod.push_back(std::move(p));
}
std::shared_ptr<const produit> stock::rechercher(reference r) const{
for (auto const & p : _prod)
if (p->ref() == r)
return p;
return nullptr;
}

▶ L’appel à std::move dans ajouterproduit est optionnel.


▶ rechercher permet de partager la propriété d’un produit.
▶ nullptr peut être utilisé pour construire un std::shared_ptr.
▶ Préférer le passage par valeur des std::shared_ptr, sauf si on
sait ce qu’on fait.
223
shared_ptr - Utilisation

Commande (ressources/partage/commandev2.hh)

class commande {
public:
commande(std::string const & client)
:_client(client), _produits()
{}
void ajouter(std::shared_ptr<const produit> p) {
_produits.push_back(std::move(p));
}
private:
std::string _client;
std::vector<std::shared_ptr<const produit>> _produits;
};

Garantie. Les pointeurs mémorisés dans commande pointent sur des


objets qui ne peuvent être détruits ailleurs (même si le stock est
modifié / supprimé).
224
shared_ptr - Exemple

main (ressources/partage/stockmainv2.cc)

int main() {
stock s;
auto pp = std::make_shared<produitperissable>("pp1", 100, "10/10")
;
auto ref = pp->ref();
s.ajouterproduit(std::move(pp));

auto prod = s.rechercher(ref);


commande c1("nom");
c1.ajouter(std::move(prod));
return 0;
}

225
shared_ptr - Opérateurs de conversion

La méthode get retourne le pointeur brut d’un std::shared_ptr


mais doit être évitée.
Il est inutile de l’appeler pour faire appel à un opérateur de
conversion : std::static_pointer_cast,
std::const_pointer_cast et std::dynamic_pointer_cast.

std::dynamic_pointer_cast (ressources/partage/stockv2.cc)

void stock::afficherperemptions() const {


for (auto const & p : _prod) {
auto pp = std::dynamic_pointer_cast<produitperissable>(p);
if (pp)
std::cout << pp->peremption();
}
}

226
shared_ptr

▶ Utiliser systématiquement std::make_shared<>.


▶ Un constructeur de std::shared_ptr prend comme paramètre
un std::unique_ptr.
▶ std::shared_ptr<>::use_count() permet d’accéder au
compteur de références.
▶ std::shared_ptr libère l’objet pointé quand le compteur de
références atteint 0. Totalement déterministe.
▶ Attention aux références cycliques. Résolues par std::weak_ptr.

227
Pointeurs intelligents - Résumé

▶ Éviter autant que possible les pointeurs bruts.


Causes de fuites de mémoire, Cause d’erreurs d’exécution si mal
utilisés, Complique le code : Requiert destructeur, constructeur
par recopie, opérateur d’affectation (règle des 3).
▶ Préférer les pointeurs intelligents.
Permettent souvent d’appliquer la règle des 0.

▶ Préférer les std::unique_ptr aux std::shared_ptr.


Gestion « automatique » de la mémoire, sans aucun coût
(mémoire, cpu).
▶ Réserver les std::shared_ptr à la propriété partagée de
ressources entre différentes instances pour lesquelles on ne peut
isoler un propriétaire qui aura une durée de vie supérieure aux
autres et qui contiendra toutes les ressources.
228
Divers

Allocation de ressources
Pointeur intelligent unique
Pointeur intelligent partagé
Divers
Déclaration avancée

Habituellement, une classe est déclarée dans un fichier .hh et définie


dans un fichier .cc de même nom.

▶ Pour hériter d’une classe C, pour déclarer un attribut de type C,


pour utiliser C comme paramètre (passé par valeur) d’une
déclaration de méthode ou valeur de retour (par valeur) d’une
déclaration de méthode, pour appeler une méthode de C…
Le compilateur a besoin de la déclaration de la classe ⇒ Inclure
le fichier .hh
▶ Pour déclarer un pointeur (ou une référence) sur un C…
Le compilateur a besoin de savoir que C est une classe.
⇒ Il n’est pas nécessaire d’inclure le fichier .hh
Une déclaration avancée suffit.

229
Déclaration avancée

Précédemment, nous avions fait un #include "produit.hh" dans


stock.hh pour déclarer l’attribut std::vector<produit *> _prod.
(ou un std::vector<std::unique_ptr<produit>>)
En utilisant une déclaration avancée, on peut écrire :
Déclaration de stock (ressources/stockv2.hh)

#pragma once
#include <vector>
#include <iostream>
#include <memory>
class produit;

class stock {

230
Déclaration avancée

Par contre, pour appeler des méthodes de produit, le compilateur a


besoin de connaître la déclaration complète de la classe.
Ici stockv2.hh ne contient pas de code de méthodes appelant des
méthodes de produit donc une déclaration avancée est possible.
Les méthodes de produit sont appelées dans stockv2.cc, il faut
donc une déclaration complète de la classe :
Définition de stock (ressources/stockv2.cc)

#include "stockv2.hh"
#include "produit.hh"
#include "produitperissable.hh"
#include <iostream>
void stock::afficher() const {
for (auto const & p : _prod)
p->afficher();
}
231
friend

Problème. Rendre visibles à une fonction f ou aux méthodes d’une


classe D les membres privés d’une classe C… sans les montrer aux
autres fonctions/classes de l’application.
Exemple. Montrer les attributs de fichier dans son opérateur de
sortie.
Syntaxe friend
La déclaration dans une classe D de :

▶ friend nomclasse;
rend les membres privés de D visibles par toutes les méthodes de
nomclasse.
▶ friend typeretour nomfonction(arguments);
rend les membres privés de D visibles par la fonction
nomfonction.
232
friend

Opérateur de sortie de stock (ressources/stockv2.hh)

void ajouterproduit(std::unique_ptr<produit> p);


private:
std::vector<std::unique_ptr<produit>> _prod;
friend std::ostream & operator<<(std::ostream & os, stock const &
s);
};

Opérateur de sortie de stock (ressources/stockv2.cc)

std::ostream & operator<<(std::ostream & os, stock const & s) {


for (auto const & p : s._prod)
p->sortie(os);
return os;
}

233
Chapitre 6
Exceptions

Définition et syntaxe
Exemple
Détails
Comment gérer les erreurs ?

▶ En C, les fonctions retournent habituellement un int qui est égal


à 0 si la fonction a été exécutée correctement ou un code d’erreur
si une erreur s’est produite…mais pas toujours : fopen, malloc,
read retournent une valeur qui a un sens, et non un code d’erreur.
▶ Pour écrire un programme robuste il faut (faudrait) toujours
tester le code de retour des fonctions appelées et agir en
conséquence. ⇒ Lourd à écrire ⇒ On ne le fait pas toujours.

Pour gérer les erreurs en C++, on utilise le mécanisme des exceptions,


nettement plus agréable à utiliser.
Les fonctions peuvent retourner une valeur, et les erreurs sont
« remontées » par un canal différent de la valeur retournée par une
fonction.
234
Définition et syntaxe

Définition et syntaxe
Exemple
Détails
Définition

Une exception est un objet qui est créé (« lever une exception ») et
qui :

▶ Interrompt l’exécution du bloc d’instructions qui a levé


l’exception.
▶ Remonte la pile d’appels des fonctions/méthodes…
▶ jusqu’à trouver un bloc de gestion de l’exception.
▶ Si aucun bloc de gestion n’est trouvé, l’exception « sort » du main
et le programme se termine.

235
Usage

▶ Si une fonction se termine sans erreur…


▶ Elle peut retourner une valeur.
▶ Si une fonction provoque une erreur…
▶ Elle ne se termine pas.
Les instructions qui suivent la levée de l’exception ne sont pas
exécutées.
▶ Elle ne retourne aucune valeur.
▶ Elle lève une exception.
▶ Cette exception peut être gérée dans un bloc de gestion
d’exceptions
▶ Qui peut gérer différents types d’exceptions
▶ Un seul bloc peut gérer les erreurs provoquées par plusieurs appels
de fonctions.
▶ Ce bloc n’est pas forcément dans le bloc qui a appelé la fonction

236
Syntaxe

Syntaxe Exceptions
▶ Levée d’exception
throw variable;
▶ Bloc de gestion d’exceptions
try {
// Instructions à protéger
}
catch (typeexception1 e1) {
// Gestion de l ’ exception de type typeexception1
}
catch (typeexception2 e2) {
// Gestion de l ’ exception de type TypeException2
}

237
Exemple

Définition et syntaxe
Exemple
Détails
Exemple

Levée et gestion d’exception (exceptions.cc)

#include <iostream>
float division(int a, int b) {
if (b == 0) throw 1;
else return static_cast<float>(a) / b;
}
void test() {
int a, b;
std::cin >> a; std::cin >> b;
std::cout << division(a, b) << "\n";
std::cout << "Calcul␣fini\n";
}

238
Exemple

Levée et gestion d’exception (exceptions.cc)

int main() {
try {
test();
std::cout << "Test␣exécuté\n";
}
catch (int i) {
std::cout << "Erreur␣détectée␣" << i << "\n";
}
return 0;
}

239
Exemple

Exemples d’exécution :
▶ Sans exception
2 3
0.666667
Calcul fini
Test exécuté
▶ Avec exception
2 0
Erreur détectée 1
▶ Sans le bloc try du main
2 0
terminate called after throwing an instance of
'int'
Abandon

240
Remarques et conseils

▶ En C++, tout type (primitif et classe) peut être utilisé comme


exception.
▶ Attention lors de la levée d’une exception dans un constructeur :
Si l’objet est à moitié construit, problème.
▶ Il est conseillé de lever une exception par valeur.
Par contre, on peut gérer l’exception dans un catch en la prenant
par référence (constante).
▶ Dans la plupart des cas, on n’utilisera pas de types primitifs pour
les exceptions mais des types spécialement créés à cet effet.

241
Détails

Définition et syntaxe
Exemple
Détails
Ordre des catch

S’il y a plusieurs blocs catch, le premier capable de gérer l’exception


est exécuté.
⇒ Dans le cas de classes exceptions avec relation d’héritage, l’ordre
des catch est important.
⇒ Écrire les blocs catch de l’exception la plus spécifique à la plus
générique.

242
Ordre des catch - Exemple

Classes exception (exceptions2.cc)

#include <string>
#include <iostream>
class monexception {
public:
monexception(int g)
:_gravite(g) {}
int _gravite;
};
class monexceptionfichier: public monexception {
public:
monexceptionfichier(int g, std::string const & nf)
:monexception(g), _nomfichier(nf) {};
std::string _nomfichier;
};

243
Ordre des catch - Exemple

Classes exception (exceptions2.cc)

int main() {
try {
throw monexceptionfichier(3, "f.txt");
}
catch (monexception const & e) {
std::cout << "monexception\n";
}
catch (monexceptionfichier const & e) {
std::cout << "monexceptionfichier\n";
}
return 0;
}

warning: exception of type ‘monexceptionfichier’


will be caught by earlier handler for ‘monexception’
Résultat de l’exécution. monexception 244
throw

▶ Dans un bloc catch, il est possible de lever une exception.


▶ Il est possible aussi de « lever à nouveau » l’exception qui a
entraîné l’exécution du bloc catch par un simple throw sans
argument.

Ceci permet d’effectuer des traitements particuliers en cas


d’erreur… sans gérer l’erreur.
Ou de tester en fonction de la valeur de l’exception si celle-ci peut
être traitée… si elle ne peut pas l’être, elle est levée à nouveau.
Exemple
catch (monexceptionfichier const & e) {
if (e._gravite < 5)
std::cerr << "Attention␣à␣" << e._nomfichier;
else throw;
}
245
Bloc de gestion par défaut

catch(...) permet de gérer tous les types d’exceptions qui n’ont pas
été gérés par les blocs catch précédents.

▶ Il est optionnel.
▶ Il est toujours utilisé comme dernier catch associé à un try.

246
Levée d’exception dans une fonction/méthode

Il n’est pas nécessaire (comme en Java avec throws) de déclarer les


exceptions pouvant être levées dans une fonction/méthode.

▶ La signature d’une fonction méthode peut déclarer les exceptions


susceptibles d’être levées (avec la clause throw (exceptions)).
On le fait rarement.
▶ Par défaut, une méthode/fonction peut lever n’ importe quel type
d’exception.
▶ Pour exprimer qu’une méthode/fonction ne lève pas d’exception,
on utilisera le mot clef noexcept dans la signature de la
méthode/fonction.
▶ Les destructeurs sont noexcept par défaut.

247
Levée d’exception dans une fonction/méthode

Lever une exception dans une méthode noexcept ne provoque pas


une erreur de compilation mais un avertissement si la levée est
directement dans la fonction :
warning: throw will always call terminate().
noexcept (comportement incorrect) (exceptions3.cc)

float division(int a, int b) {


if (b == 0) throw 1;
else return static_cast<float>(a) / b;
}
void test() noexcept {
int a, b;
std::cin >> a; std::cin >> b;
std::cout << division(a, b) << "\n";
std::cout << "Calcul␣fini\n";
}

248
Levée d’exception dans une fonction/méthode

Une exception levée dans une méthode noexcept provoque la fin de


l’exécution
terminate called after throwing an instance of 'int'
même si des blocs de gestion d’exception étaient présents.

249
Classes d’exceptions de la bibliothèque standard

http://en.cppreference.com/w/cpp/error/exception
▶ std::exception racine des classes d’exceptions.
Il est conseillé de définir les nouvelles classes exceptions comme
sous-classes de std::exception.
Pour cela, on redéfinira (au moins) la méthode
const char * what()const noexcept.
Cette méthode est appelée dans le cas d’une exception qui sort
du main pour afficher le message d’erreur.
▶ std::bad_alloc levée lors d’une erreur rencontrée par new.
▶ std::bad_cast levée lors d’un dynamic_cast impossible vers
une référence d’une sous-classe.
▶ std::out_of_range levée lors d’un appel incorrect à
std::vector::at, etc. std::logic_error,
std::invalid_argument…(déclarées dans <stdexcept>) 250
Chapitre 7
C++ avancé

Héritage multiple
Espaces de noms
Fonctions anonymes
Classes emboîtées
Modèle de classes
Héritage multiple

Héritage multiple
Espaces de noms
Fonctions anonymes
Classes emboîtées
Modèle de classes
Héritage multiple

Une classe peut avoir plusieurs classes-mères.


Syntaxe Héritage multiple
class clfille: public clmere1, public clmere2 ... {
...
};

Problème
Que se passe-t-il si les classes-mères définissent des attributs de
même nom (éventuellement de types différents) ou des méthodes
de même nom et de même signature ?

⇒ L’héritage multiple peut conduire à la définition de classes dont le


comportement est peu intuitif.
⇒ Il est conseillé (à moins de très bien connaître C++) d’utiliser
l’héritage multiple uniquement dans des cas où il n’y a pas de
251
problèmes.
Héritage multiple - Quand l’utiliser ?

Principalement pour implémenter des interfaces.

▶ Une interface est une classe abstraite définissant uniquement


des méthodes virtuelles pures (et un destructeur virtuel).
▶ Une classe peut implémenter plusieurs interfaces et être, en plus,
une sous-classe d’une classe « classique ».
▶ En C++, on utilise l’héritage multiple pour représenter le fait que
la classe implémente des interfaces.

252
Héritage multiple - Exemple

Déclaration d’une interface (heritagemultiple/pile.hh)

class debug {
public:
virtual ~debug() =default;
virtual unsigned int tailleoctets() const =0;
};

253
Héritage multiple - Exemple

Déclaration d’une interface (heritagemultiple/pile.hh)

class pile {
public:
virtual ~pile() =default;
virtual void empiler(int i) =0;
virtual bool vide() const =0;
virtual void depiler() =0;
virtual int sommet() const =0;
};

254
Héritage multiple - Exemple

Classe implémentant 2 interfaces (heritagemultiple/pile.hh)

class pilevector: public debug, public pile {


public:
unsigned int tailleoctets() const override;
void empiler(int i) override;
bool vide() const override;
void depiler() override;
int sommet() const override;
public:
std::vector<int> _contenu;
};

255
Héritage multiple - Exemple

Définition de la classe (heritagemultiple/pile.cc)

unsigned int pilevector::tailleoctets() const {


return _contenu.size() * sizeof(int);
}
void pilevector::empiler(int i) {
_contenu.push_back(i);
}
bool pilevector::vide() const {
return _contenu.empty();
}
void pilevector::depiler() {
_contenu.pop_back();
}
int pilevector::sommet() const {
return _contenu.back();
}

256
Héritage multiple - Exemple

▶ pilevector implémente les deux interfaces debug et pile.


▶ Toute instance de pilevector peut être passée comme
paramètre à une fonction/méthode qui attend un debug ou un
pile.

Utilisation d’une interface (heritagemultiple/pile.cc)

void remplirpile(pile & p) {


for (int i=0; i<10; ++i)
p.empiler(i);
}
void viderpile(pile & p) {
while (!p.vide())
p.depiler();
}
void affichertaille(debug const & o) {
std::cout << "Taille␣de␣l'objet␣" << o.tailleoctets() << "\n";
} 257
Héritage multiple - Un cas à problème

pilevector est un cas sans problèmes : les super-classes n’ont pas


de membres ayant le même nom.
Un cas à problème (heritagemultiple/probleme.cc)

class A {
public:
int attribut;
void meth() {}
};

class B {
public:
float attribut;
int meth() {return 0;}
};

class C: public A, public B {


};
258
Héritage multiple - Un cas à problème

Exemple

C c;
c.attribut = 0;
c.meth();

Erreur de compilation :

request for member 'attribut' is ambiguous


candidates are: float B::attribut int A::attribut
request for member 'meth' is ambiguous
candidates are: int B::meth() void A::meth()

Attention
La classe C a hérité de deux attributs nommés attribut et de deux
méthodes nommées meth. 259
Héritage multiple

Il faut préciser quel membre doit être utilisé…


En préfixant son nom par le nom de la classe dont il est issu.
Exemple

c.A::attribut = 0;
c.B::meth();

Cela peut conduire à du code difficile à mettre au point.

260
Espaces de noms

Héritage multiple
Espaces de noms
Fonctions anonymes
Classes emboîtées
Modèle de classes
Espaces de noms

▶ Les espaces de noms permettent de « ranger » des classes dans


des ensembles.
▶ Chaque ensemble a un nom, et deux ensembles différents
peuvent contenir une classe de même nom.
Le nom complet d’une classe est en fait nomespace::nomclasse.
▶ Par exemple, l’espace de nom std peut contenir une classe
string, et l’espace de nom mabibliotheque une classe string.
▶ Les espaces de noms sont habituellement utilisés lors du
développement d’un code prévu pour être réutilisable, et importé
dans d’autres projets (bibliothèque de classes)
Si le projet qui importe le code contient une classe de même
nom, erreur de compilation.
Avec l’utilisation d’espaces de noms, pas d’ambiguïté.

261
Espaces de noms

Syntaxe Espaces de noms

▶ Déclaration d’une classe (dans un .hh)


namespace nomespace {
class nomclasse { ... };
}
▶ Définition d’une classe (dans un .cc)
namespace nomespace {
nomclasse::nomclasse() { ... }
}

262
Espaces de noms

Syntaxe Espaces de noms


▶ Utilisation d’une classe
nomespace::nomclasse
▶ Recherche des classes dans un espace de noms (import)
using namespace nomespace;
À éviter dans un fichier .hh
▶ Import d’un élément d’un espace
using nomespace::nomelement;
(exemple : using std::cout;)

263
Fonctions anonymes

Héritage multiple
Espaces de noms
Fonctions anonymes
Classes emboîtées
Modèle de classes
Fonction anonyme

Une fonction anonyme est une fonction… qui n’a pas de nom.

▶ Ne peut être appelée ailleurs qu’à l’endroit où elle a été définie.


▶ Fonction courte à usage unique.
▶ Peut être passée comme paramètre à une fonction/méthode.
▶ Peut accéder à des variables de la fonction/méthode dans
laquelle elle est déclarée.

264
Fonction anonyme

Syntaxe Fonction anonyme


▶ [captures](parametres)->typederetour{corps}
▶ [captures](parametres){corps}

Si le type de retour n’est pas présent, il est déterminé


automatiquement par le compilateur à partir des types des valeurs
retournées par les return de la fonction (qui doivent être de même
type…idéalement un seul return).

265
Fonction anonyme - Exemple

La bibliothèque standard C++ fournit un certain nombre de services


qui prennent comme paramètre une fonction qui sera exécutée sur
les éléments d’un conteneur.
Certains de ces services sont des méthodes du conteneur
(std::list::remove_if, std::list::sort…) d’autres sont des
fonctions pour la plupart disponibles dans la bibliothèque
d’algorithmes
http://en.cppreference.com/w/cpp/algorithm.
Par exemple std::for_each exécute une fonction sur tous les
éléments d’un conteneur compris dans un intervalle défini par couple
d’itérateurs.

266
Fonction anonyme - Exemple

Soit v un std::vector<int>.
Fonction anonyme - Déclaration complète (lambdas/lambdas.cc)

std::for_each(v.begin(), v.end(),
[](int i)->void{std::cout << i << '␣';});

Fonction anonyme - Simplification (lambdas/lambdas.cc)

std::for_each(v.begin(), v.end(),
[](int i){std::cout << i << '␣';});

Fonction anonyme - Simplification (lambdas/lambdas.cc)

std::for_each(v.begin(), v.end(),
[](auto i){std::cout << i << '␣';});

267
Fonction anonyme - Exemple

Le paramètre peut aussi être passé par référence.


Fonction anonyme (lambdas/lambdas.cc)

void doubler(std::vector<int> & v) {


std::for_each(v.begin(), v.end(), [](auto & i){i*=2;});
}

268
Fonction anonyme - Exemple

Utilisation du type de retour de la fonction


std::all_of retourne un booléen valant true si tous les éléments
repérés par un couple d’ itérateurs vérifient le prédicat passé en
troisième paramètre (la fonction prédicat retourne true), false sinon.
Fonction anonyme (lambdas/lambdas.cc)

bool touspositifs(std::vector<int> const & v) {


return std::all_of(v.begin(), v.end(),
[](auto i){ return i >= 0; });
}

Il y a aussi std::any_of et std::none_of.

269
Fonction anonyme - Capture

La capture permet de fournir à une fonction anonyme des données


supplémentaires, quand la signature est fixée.
Exemple. Utiliser std::any_of pour déterminer si un vector<int>
contient (au moins) une valeur supérieure à une valeur borne.Utiliser
un paramètre de la fonction ? Non . La signature du prédicat est fixée
par std::any_of : ce doit être (int)->bool.

270
Fonction anonyme - Capture

La capture est composée d’une liste de variables accessibles dans le


bloc dans laquelle la fonction anonyme est déclarée, séparées par
des virgules, passées par valeur ou par référence, dans ce cas, elles
sont préfixées par &.
Les variables de la capture sont accessibles dans la fonction anonyme.
Fonction anonyme - Capture (lambdas/lambdas.cc)

bool contientsuperieur(std::vector<int> const & v, int borne) {


return std::any_of(v.begin(), v.end(),
[borne](auto i){ return i >= borne; });
}

271
Fonction anonyme - Capture

Fonction anonyme - Capture (lambdas/lambdas.cc)

int somme(std::vector<int> const & v) {


int result(0);
std::for_each(v.begin(), v.end(), [&result](auto i){result+=i;});
return result;
}

On ne peut pas capturer un attribut d’une classe, mais on peut


capturer this, qui donne accès aux attributs privés.

272
Classes emboîtées

Héritage multiple
Espaces de noms
Fonctions anonymes
Classes emboîtées
Modèle de classes
Classes emboîtées

▶ Il est possible de définir des classes (ou types : struct, union,


enum) à l’ intérieur d’une classe.
▶ La visibilité (encapsulation) s’applique à ces types.
▶ Cela est utilisé pour définir une classe qui n’a de sens qu’« à
l’intérieur » d’une autre classe.

Exemple. Un graphe (orienté) est composé de sommets, chaque


sommet contient un ensemble d’arcs sortants.
Les sommets n’ont de sens qu’à l’intérieur d’un graphe. Les arcs
sortants n’ont de sens qu’à l’intérieur d’un sommet.

273
Classes emboîtées

Déclarations emboîtées (emboites/graphe.hh)

class graphe {
public:
class sommet {
public:
using identifiant = unsigned int;
private:
struct arcsortant {
identifiant _extremite;
std::string _etiquette;
};
public:
sommet(std::string const & et)
:_etiquette(et), _id(++_compteur) {}
identifiant id() const { return _id; }
std::vector<identifiant> voisins() const;
std::string const & etiquette() const { return _etiquette; }
void ajouterarc(std::string const & et,identifiant extremite);
274
Classes emboîtées

Déclarations emboîtées (suite sommet) (emboites/graphe.hh)

private:
std::string _etiquette;
identifiant _id;
std::list<arcsortant> _arcs;
static identifiant _compteur;
};
public:
sommet::identifiant ajoutersommet(std::string const & et);
sommet const & accessommet(sommet::identifiant id) const;
void ajouterarc(sommet::identifiant origine, sommet::identifiant
extremite, std::string const & etiquette);
private:
std::list<sommet>::iterator chercher(sommet::identifiant id);
std::list<sommet>::const_iterator chercher(sommet::identifiant
id) const;
private:
std::list<sommet> _sommets;
}; 275
Classes emboîtées

Définition des classes (emboites/graphe.cc)

graphe::sommet::identifiant graphe::sommet::_compteur(0);

void graphe::sommet::ajouterarc(std::string const & et, identifiant


extremite) {
auto f(std::find_if(_arcs.begin(), _arcs.end(), [extremite](auto
const & a){return a._extremite==extremite;}));
if (f == _arcs.end())
_arcs.push_back(arcsortant {extremite, et});
else
*f = arcsortant {extremite, et};
}

std::vector<graphe::sommet::identifiant> graphe::sommet::voisins()
const {
std::vector<identifiant> result;
for (auto const & av : _arcs)
result.push_back(av._extremite);
return result; 276
}
Classes emboîtées

Définition des classes (emboites/graphe.cc)

graphe::sommet::identifiant graphe::ajoutersommet(std::string const


& et) {
sommet nouv(et);
_sommets.push_back(nouv);
return nouv.id();
}

graphe::sommet const & graphe::accessommet(sommet::identifiant id)


const {
return *chercher(id);
}

void graphe::ajouterarc(sommet::identifiant origine, sommet::


identifiant extremite, std::string const & etiquette) {
auto io(chercher(origine));
auto ie(chercher(extremite));
io->ajouterarc(etiquette, extremite);
} 277
Classes emboîtées

Définition des classes (emboites/graphe.cc)

std::list<graphe::sommet>::iterator graphe::chercher(sommet::
identifiant id) {
auto f = std::find_if(_sommets.begin(), _sommets.end(), [id](auto
const & a){return a.id() == id;});
if (f == _sommets.end())
throw std::invalid_argument(std::to_string(id));
else
return f;
}

std::list<graphe::sommet>::const_iterator graphe::chercher(sommet::
identifiant id) const {
auto f = std::find_if(_sommets.begin(), _sommets.end(), [id](auto
const & a){return a.id() == id;});
if (f == _sommets.end())
throw std::invalid_argument(std::to_string(id));
else
return f; 278
Classes emboîtées

Exemple d’utilisation (emboites/graphe.cc)

int main() {
graphe g;
graphe::sommet::identifiant paris(g.ajoutersommet("Paris"));
auto angers(g.ajoutersommet("Angers"));
auto nantes(g.ajoutersommet("Nantes"));
g.ajouterarc(angers, nantes, "Bus");
g.ajouterarc(nantes, angers, "Bus");
g.ajouterarc(angers, paris, "Train");
g.ajouterarc(paris, nantes, "Avion");
std::cout << "Voisins␣de␣" << g.accessommet(angers).etiquette() <<
"\n";
auto voisinsangers(g.accessommet(angers).voisins());
for (auto v : voisinsangers)
std::cout << g.accessommet(v).etiquette() << "␣";
return 0;
}
279
Modèle de classes

Héritage multiple
Espaces de noms
Fonctions anonymes
Classes emboîtées
Modèle de classes
Modèle de classes

▶ Un modèle de classes (ou patron de classes) permet de


construire plusieurs classes qui diffèrent par l’utilisation d’un
type de données.
▶ std::vector est un modèle qui permet de définir les classes
std::vector<int>, std::vector<std::string>, …

Attention
Un modèle de classes n’est pas une classe.

Il ne peut donc pas être instancié ou utilisé comme un type.


Exemple (Erroné)
std::vector v;
v.push_back(???);

Les conteneurs de la STL sont des modèles de classes. Il est possible


280
de définir ses propres modèles.
Modèle de classes

Déclaration modèle de classes (template/pile.hh)

#pragma once
#include <vector>

template <typename t>


class pile {
public:
bool vide() const {
return _contenu.empty(); }
void empiler(t v) {
_contenu.push_back(v); }
void depiler() {
_contenu.pop_back(); }
t sommet() const;
private:
std::vector<t> _contenu;
};

281
Modèle de classes

Déclaration modèle de classes (template/pile.hh)

template <typename t>


t pile<t>::sommet() const {
t s = _contenu.back();
return s;
}

282
Modèle de classes

Utilisation modèle de classes (template/pilemain.cc)

#include "pile.hh"
#include <string>

int main() {
pile<int> pint; pile<std::string> pstring;
pint.empiler(2);
pstring.empiler("essai");
return 0;
}

283
Modèle de classes

▶ Les modèles de classes sont souvent utilisés pour définir des


classes conteneurs.
▶ Un modèle peut avoir plusieurs arguments.
▶ La définition des méthodes est habituellement donnée dans le
fichier .hh
▶ dans la déclaration de la classe.
▶ après la déclaration de la classe.
▶ dans un fichier d’ implémentation du modèle, inclus dans le fichier
.hh.

284
Chapitre 8
Initiation au développement d’interfaces
graphiques avec Qt

Arborescence de composants
Signaux et slots
Placement des composants
Interfaces graphiques et C++

Le langage C++ et la bibliothèque standard ne fournissent pas d’outils


pour développer des interfaces graphiques.
Plusieurs bibliothèques peuvent être utilisées wxWidgets, GTK, CEGUI,
etc.
Qt https://www.qt.io est une bibliothèque C++ permettant de
développer des applications disposant d’une interface graphique (et
bien plus).
▶ Desktop, Mobile, IOT
▶ Portable, Apparence native, Efficace
▶ Bindings dans différents langages
▶ Très utilisé (Dreamworks, Lucasfilm, Philips, Samsung, Blizzard,
AMD, Valve, etc). KDE, VLC, Photoshop (Album/Elements),
Telegram, Virtualbox, etc.
285
Arborescence de composants

Arborescence de composants
Signaux et slots
Placement des composants
Principe de Qt

▶ Une fenêtre d’ interface graphique est composée d’une


arborescence de widgets.
▶ Une action de l’utilisateur est représentée par un signal reçu par
l’application.
▶ Un signal peut être connecté à un slot qui exécutera une action
en réponse au signal.
▶ Signaux et slots ne sont pas accessibles directement depuis le
compilateur C++ : Qt fournit un compilateur de meta-objets (moc)
qui génère du code C++ gérant slots et signaux.

286
QObject : Arborescence d’objets

▶ QObject est la racine d’héritage de la plupart des classes


fournies par Qt.
▶ QObject gère une arborescence d’objets parent(), children().
La destruction d’un objet détruit sa descendance.
→ un objet qui a un parent doit être alloué dynamiquement, et
jamais détruit.
→ un objet qui n’a pas de parent doit être détruit après
utilisation… Pas de raison d’utiliser l’allocation dynamique.

287
QWidget : Arborescence de composants

▶ QWidget (sous-classe de QObject) est la racine des composants


d’interface graphique.
▶ Qt fournit un grand nombre de widgets… on peut définir les
nôtres.
▶ Un QWidget sans parent est affiché dans une fenêtre. Un QWidget
ayant un parent est affiché dans son parent.

288
QApplication : Application

Le main ne fait « rien »… si ce n’est…

▶ instancier QApplication (avec les arguments reçus en ligne de


commande) ;
▶ créer le composant de la fenêtre principale de l’application (et sa
descendance) ;
▶ afficher ce composant (méthode show) ;
▶ appeller la méthode run de QApplication qui se charge de gérer
toutes les interactions avec l’utilisateur.

Quand la méthode QApplication::run se termine, l’application se


termine.

289
Exemple d’application Qt minimale

Application minimale : main (qt/premier/main.cc)

#include <QtWidgets>

int main(int argc, char *argv[]) {


QApplication app(argc, argv);
QWidget window;
window.resize(320, 240);
window.setWindowTitle("Ma␣première␣fenêtre");
window.show();
return app.exec();
}

290
Exemple d’application Qt minimale

Construction de l’exemple (qt/premier/CMakeLists.txt)

1 cmake_minimum_required(VERSION 3.1.0)
2 project(qtpremier CXX)
3

4 set(CMAKE_CXX_STANDARD 14)
5 set(CMAKE_CXX_STANDARD_REQUIRED on)
6 set(CMAKE_CXX_EXTENSIONS off)
7

8 find_package(Qt5Widgets REQUIRED)
9 set(CMAKE_INCLUDE_CURRENT_DIR ON)
10 set(CMAKE_AUTOMOC ON)
11 set(CMAKE_AUTOUIC ON)
12

13 if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")


14 add_compile_options(-Wall -Wpedantic)
15 endif()
16

17 add_executable(qtpremier main.cc)
18 target_link_libraries(qtpremier Qt5::Widgets) 291
Construction d’une application Qt

Ce qui est nouveau par rapport à un CMakeLists.txt C++14

▶ Lignes 8-11. Recherche de la bibliothèque Qt5Widgets (8) ;


Utilisation simplifiée du moc (9-10) et de l’uic (11) (non utilisés
dans cet exemple).
▶ Ligne 18. L’exécutable est lié à la bibliothèque Qt5Widgets.

292
Quelques composants

▶ QLabel Texte fixe.


▶ QPushButton Bouton.
▶ QCheckBox Case à cocher.
▶ QLineEdit Champ d’édition.
▶ QComboBox Liste déroulante.
▶ QRadioButton Bouton radio.

Et bien d’autres
http://doc.qt.io/qt-5/widget-classes.html

293
Arborescence de composants

Quelques composants (qt/widgets/main.cc)

#include <QtWidgets>

int main(int argc, char *argv[]) {


QApplication app(argc, argv);
QWidget window; window.resize(320, 200);
QLabel* monlabel(new QLabel("Un␣<i>QLabel</i>.", &window));
monlabel->move(0,0);
QCheckBox * macheckbox(new QCheckBox("QCheckBox", &window));
macheckbox->move(10,30);
QPushButton* monbutton(new QPushButton("QPushButton", &window));
monbutton->setGeometry(10,90,300,100);
QLabel* autrelabel(new QLabel("labeldansbouton", monbutton));
autrelabel->move(200,80);
window.show();
return app.exec();
}
294
Arborescence de composants

295
Arborescence de composants

▶ Un composant ayant un parent ne doit pas être détruit.


▶ Les coordonnées passées à move(x,y) sont celles du point
supérieur gauche du composant et sont relatives au composant
parent.
▶ Un composant de tout type peut avoir comme parent un
composant de tout type.
▶ Un composant ne peut pas sortir de son parent.
▶ QLabel comprend un sous-ensemble d’HTML.

296
Signaux et slots

Arborescence de composants
Signaux et slots
Placement des composants
Signaux et slots - Principe

▶ Dans une interface graphique (mais pas uniquement),


l’application doit réagir à des événements externes : actions de
l’utilisateur.
▶ Un événement sur un composant est représenté dans Qt par
l’émission d’un signal. Exemple : clic sur un bouton.
▶ Un slot est une méthode d’un QObject effectuant un traitement.
Exemple : fermer la fenêtre.
▶ Un slot peut être connecté à un signal (d’un autre objet ou du
même). Quand le signal est émis, le slot est déclenché. Exemple :
un clic sur un bouton (signal) provoque la fermeture d’une
fenêtre (slot).

297
Signaux - Principe

Mécanisme de communication entre objets avec couplage faible :


Quand un signal est émis, l’émetteur ne sait pas « qui » traitera ce
signal.

▶ Un signal est émis quand l’état de l’émetteur change, ce qui peut


intéresser d’autres objets.
▶ Un signal a une signature correspondant aux paramètres qu’ il
porte.

298
Slots - Principe

▶ Un slot est une méthode. Un slot a une signature. Un slot peut


être appelé comme une méthode.
▶ Un signal d’une signature donnée ne peut être connecté qu’à des
slots qui ont une signature compatible. (vérification à la
compilation).
▶ Un slot peut être connecté à plusieurs signaux, un signal peut
être connecté à plusieurs slots.

299
Signaux - Fonctionnement

Une classe qui émet des signaux doit :

▶ Être une sous classe de QObject.


▶ Utiliser la macro Q_OBJECT au début de sa déclaration.
▶ Déclarer les signaux émis dans une section signals:
Un signal a la syntaxe d’une méthode (le type de retour est
souvent void).
▶ Utiliser l’ instruction emit pour émettre un signal.

Compiler simplement cette classe par un compilateur C++ n’est pas


suffisant.

300
Signaux - Le Meta-Object Compiler

Le moc gère des extensions au langage C++ propres à Qt, dont les
signaux et les slots.

▶ Quand un fichier source contient une déclaration classe utilisant


la macro Q_OBJECT, le moc génère un fichier définissant le code
de méthodes devant être compilées et liées à l’exécutable (entre
autres pour la gestion des signaux et slots).
▶ Cela est fait automatiquement par CMake à condition que les
lignes suivantes soient présentes dans le CMakeLists.txt.

CMakeLists et moc (qt/slotssignals/CMakeLists.txt)

set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)

301
Signaux - Exemple

Classe émettrice de signaux (qt/slotssignals/setofstring.hh)

#pragma once
#include <QObject>
#include <vector>
#include <string>
class setofstring: public QObject {
Q_OBJECT
public:
void ajoutervaleur(std::string const &);
void supprimervaleur(std::string const &);
bool contientvaleur(std::string const &) const;
signals:
void valeurajoutee(std::string);
void valeursupprimee(std::string);
void taillemodifiee(std::size_t);
private:
std::vector<std::string> _contenu;
};
302
Signaux - Exemple

Classe émettrice de signaux (qt/slotssignals/setofstring.cc)

#include "setofstring.hh"
#include <algorithm>

void setofstring::ajoutervaleur(std::string const & v) {


if (!contientvaleur(v)) {
_contenu.push_back(v);
emit valeurajoutee(v);
emit taillemodifiee(_contenu.size());
}
}

303
Signaux - Exemple

Classe émettrice de signaux (qt/slotssignals/setofstring.cc)

void setofstring::supprimervaleur(std::string const & v) {


auto i(std::find(_contenu.begin(), _contenu.end(), v));
if (i != _contenu.end()) {
_contenu.erase(i);
emit valeursupprimee(v);
emit taillemodifiee(_contenu.size());
}
}

bool setofstring::contientvaleur(std::string const & v) const {


return std::find(_contenu.begin(), _contenu.end(), v) != _contenu.
end();
}

304
Slots

Un slot est une méthode qui répond à l’émission d’un signal.


Dans quelle classe déclarer une telle méthode ?
Ça dépend…
Habituellement on définit une classe pour représenter une fenêtre
d’interface graphique (contrairement à l’exemple widgets donné
précédemment).
Cette classe peut être utilisée pour définir des méthodes slots.

305
Slots

306
Slots

Classe fenêtre (qt/slotssignals/fenetrev1.hh)

#include <QtWidgets>

class fenetre: public QWidget {


Q_OBJECT
public:
fenetre();
public:
QLineEdit * _saisie;
QPushButton * _ajout;
QLabel * _taille;
QPushButton * _quitter;
};

307
Slots

Classe fenêtre (qt/slotssignals/fenetrev1.cc)

#include "fenetrev1.hh"

fenetre::fenetre()
:QWidget() {
resize(320,200);
_saisie = new QLineEdit("", this);
_ajout = new QPushButton("Ajouter", this);
_taille = new QLabel("taille", this);
_quitter = new QPushButton("Quitter", this);
_saisie->setGeometry(10,10,300,40);
_ajout->setGeometry(10,50,300,50);
_taille->move(10,100);
_quitter->setGeometry(10,140,300,50);
}

308
Connexion slot/signal

Connexion
La méthode (de classe) QObject::connect permet de connecter un
signal d’un objet à un slot d’un objet.
Paramètres : Pointeur sur l’objet émetteur, Signal émis, Pointeur sur
l’objet receveur, Slot déclenché.

▶ Un slot, comme un signal, est identifié par un pointeur sur son


code.
▶ Les classes de Qt fournissent des slots qui peuvent être appelés
comme méthodes classiques ou connectées à un signal.

309
Connexion slot/signal

Par exemple, QApplication dispose d’une méthode quit() qui


termine l’application.
Connexion à un slot existant (qt/slotssignals/fenetrev1.cc)

int main(int argc, char *argv[]) {


QApplication app(argc, argv);
fenetre f;
QObject::connect(f._quitter, &QPushButton::clicked, &app, &
QApplication::quit);
f.show();
return app.exec();
}

310
Déclaration de slots

Dans une section public slots: d’une classe (sous-classe de


QObject, etc.)
Déclaration de slots (qt/slotssignals/fenetrev2.hh)

#include <QtWidgets>
#include "setofstring.hh"
class fenetre: public QWidget {
Q_OBJECT
public:
fenetre(setofstring& sos);
public slots:
void onclicajout();
void ontaillemodifiee(std::size_t);
void onajoutervaleur(std::string const &);
private:
setofstring& _sos;
QLineEdit * _saisie;
QPushButton * _ajout;
311
Connexions

Connexions (qt/slotssignals/fenetrev2.cc)

fenetre::fenetre(setofstring& sos)
:QWidget(), _sos(sos) {
resize(320,200);
_saisie = new QLineEdit("", this);
_ajout = new QPushButton("Ajouter", this);
_taille = new QLabel("taille", this);
_quitter = new QPushButton("Quitter", this);
_saisie->setGeometry(10,10,300,40);
_ajout->setGeometry(10,50,300,50);
_taille->move(10,100);
_quitter->setGeometry(10,140,300,50);
connect(_ajout, &QPushButton::clicked, this, &fenetre::onclicajout
);
connect(&_sos, &setofstring::taillemodifiee, this, &fenetre::
ontaillemodifiee);
connect(&_sos, &setofstring::valeurajoutee, this, &fenetre::
onajoutervaleur); 312
}
Définition de slots

Définition de slots (qt/slotssignals/fenetrev2.cc)

void fenetre::onclicajout() {
_sos.ajoutervaleur(_saisie->text().toStdString());
}

void fenetre::ontaillemodifiee(std::size_t ns) {


_taille->setText(QString::number(ns));
}

void fenetre::onajoutervaleur(std::string const & n) {


std::cout << "Ajout␣de␣" << n << "\n";
}

Un clic sur le bouton d’ajout émet le signal QPushButton::clicked


ce qui provoque l’exécution du slot fenetre::onclicajout. Cette
méthode appelle setofstring::ajoutervaleur qui peut émettre les
signaux setofstring::valeurajoutee et taillemodifiee …
313
Placement des composants

Arborescence de composants
Signaux et slots
Placement des composants
Placement des composants

Positionner et dimensionner les composants de façon absolue a deux


défauts :

▶ Fastidieux.
▶ Ne gère pas l’agrandissement des fenêtres.

Qt propose d’utiliser des gestionnaires de placements, permettant de


ranger (et redimensionner) automatiquement les composants.
QLayout est la classe mère des gestionnaires de placements de Qt.

314
Principe d’un gestionnaire de placement

▶ Instancier une sous-classe de QLayout correspondant au type de


« rangement » désiré.
▶ Rattacher ce gestionnaire au composant contenant les
composants à ranger.
▶ Rattacher les composants à ranger au gestionnaire.

315
Principaux gestionnaires de placement

http://doc.qt.io/qt-5/layout.html

▶ QHBoxLayout

▶ QVBoxLayout

▶ QGridLayout
▶ … 316
QHBoxLayout

QHBoxLayout (qt/layout/main.cc)

QWidget window1; window1.resize(400, 400);


QPushButton* b11=new QPushButton("1");
QPushButton* b12=new QPushButton("2");
QPushButton* b13=new QPushButton("3");
QPushButton* b14=new QPushButton("4");
QHBoxLayout * layout1 = new QHBoxLayout;
layout1->addWidget(b11); layout1->addWidget(b12);
layout1->addWidget(b13); layout1->addWidget(b14);
window1.setLayout(layout1);
window1.show();

L’ordre des appels à addWidget est important. Les appels au


constructeur de QPushButton qui ne précisent pas le composant
parent : en étant ajouté au QLayout, les composants prennent comme
parent le composant rattaché au QLayout.
317
QHBoxLayout

318
QVBoxLayout

QVBoxLayout (qt/layout/main.cc)

QWidget window2; window2.resize(400, 400);


QPushButton* b21=new QPushButton("1");
QPushButton* b22=new QPushButton("2");
QPushButton* b23=new QPushButton("3");
QPushButton* b24=new QPushButton("4");
QVBoxLayout * layout2 = new QVBoxLayout;
layout2->addWidget(b21); layout2->addWidget(b22);
layout2->addWidget(b23); layout2->addWidget(b24);
window2.setLayout(layout2);
window2.show();

319
QGridLayout

QGridLayout (qt/layout/main.cc)

QWidget window3; window3.resize(400, 400);


QPushButton* b31=new QPushButton("1");
QPushButton* b32=new QPushButton("2");
QPushButton* b33=new QPushButton("3");
QPushButton* b34=new QPushButton("4");
QGridLayout * layout3 = new QGridLayout;
layout3->addWidget(b31,0,0); layout3->addWidget(b32,0,1);
layout3->addWidget(b33,1,0,1,2); layout3->addWidget(b34,2,0);
window3.setLayout(layout3);
window3.show();

320
QGridLayout

QGridLayout::addWidget prend 5 paramètres

▶ Le composant à ajouter
▶ Les coordonnées de la case (ligne, colonne, en commençant à 0)
▶ Le nombre de cases (verticalement, horizontalement) (par défaut
1,1)

Le nombre de colonnes/lignes est déterminé automatiquement.

321
QGridLayout - Étirement

Une colonne/ligne peut être « vide » pour gérer l’espacement entre


cases.
QGridLayout::setColumnMinimumWidth (setRowMinimumHeight)
permettent de fixer la taille minimale d’une colonne/ligne.
QGridLayout::setColumnStretch (setRowStretch) permet de
choisir le facteur d’étirement de la colonne/ligne.
Une colonne/ligne ayant un facteur d’étirement de 0 a une taille fixée.
Sauf si toutes les colonnes/lignes ont un facteur d’étirement de 0.

322
QGridLayout

QGridLayout (qt/layout/main.cc)

QGridLayout * layout4 = new QGridLayout;


layout4->addWidget(b41,0,0);
layout4->setColumnStretch(0,4);
layout4->setColumnMinimumWidth(1, 20);
layout4->addWidget(b42,0,2);
layout4->setColumnMinimumWidth(3, 20);
layout4->addWidget(b43,0,4);
layout4->addWidget(b44,0,5);
layout4->setColumnStretch(5,2);
window4.setLayout(layout4);

323
QGridLayout - Étirement

▶ Colonne 0 : Bouton 1, Étirement 4


▶ Colonne 1 : Rien, Largeur minimale 20 (constante)
▶ Colonne 2 : Bouton 2, Largeur constante
▶ Colonne 3 : Rien, Largeur minimale 20 (constante)
▶ Colonne 4 : Bouton 3, Largeur constante
▶ Colonne 5 : Bouton 4, Étirement 2

324
QGridLayout - Séparateurs

▶ Entre les colonnes 4 et 5, espacement standard.


▶ Changer l’espacement entre widgets à l’ intérieur du layout par
QGridLayout::setHorizontalSpacing(int)
(setVerticalSpacing(int))
▶ Changer les marges (espace autour des widgets du layout) par
QLayout::setContentsMargins(int left, int top, int
right, int bottom).

325

Vous aimerez peut-être aussi