Service d’Informatique
Transcript of Service d’Informatique
Service d’Informatique
Méthodologie Orientée Objet
M. Benjelloun
G. Libert
2ème Bachelier en sciences de l'ingénieur
Année académique 2015-2016
FACULTÉ POLYTECHNIQUE DE MONS
II
Avant-propos
L'enseignement de Méthodologie Orientée Objet, destiné aux étudiants de la deuxième année
de bachelier en Ingénieur civil de la Faculté Polytechnique de l'Université de Mons (UMons)
est organisé, en deux parties :
1. Le cours magistral qui comprend
- dans sa première partie, l'étude des principaux concepts nécessaires pour le
développement de logiciel de qualité et la description de la méthodologie orientée
objet permettant d'obtenir cette qualité, en suivant, le livre "Conception et
programmation orientées objet" de Bertrand Meyer, Éditions Eyrolles, Paris, 2000,
1223 pages;
- dans sa deuxième partie, les notions fondamentales et les possibilités de la
programmation orientée objet. Cette partie est consacrée à l'étude de la syntaxe du
langage de programmation objet C++ et à l'écriture de programmes illustratifs
permettant à l’étudiant d’assimiler les principales notions présentées;
- dans sa troisième partie, une initiation et conscientisation à la sécurité informatique.
2. Les séances de travaux pratiques qui permettent de mettre en œuvre sur ordinateur des
programmes écrits dans un langage de programmation objet. Un document est dédicacé à cela.
Les objectifs de cet enseignement sont donc:
A. La connaissance des principes de base pour développer du logiciel de qualité.
B. La maîtrise d'un langage de programmation objet, C++.
C. La sensibilisation et la conscientisation à la sécurité informatique.
Le présent syllabus ne peut être considérer comme complet. Lors des séances de cours et de
travaux pratiques, un complément de matière sera abordé.
M. Benjelloun et G. Libert
16 juin 2015
III
TABLE DES MATIERES
PARTIE 1 : INTRODUCTION A LA METHODOLOGIE OBJET
Chap1 : LA QUALITÉ DU LOGICIEL 1 1.1 LE CONTRAT 1
1.2 LA NOTION D'UTILISATEUR PARFAIT 3
1.3 L'UTILISATION D'INVARIANTS 4
1.4 QU'EST CE QU'UN TEST? 4
1.5 QU'EST-CE QU'UNE EXCEPTION? 6
1.6 CAPITALISER 6
1.7 DOCUMENTER 8
Chap2 : LA MÉTHODOLOGIE OBJET : OUTIL DE QUALITÉ 10 2.1 LE CONCEPT D'OBJET 10
2.2 LE CONCEPT DE CLASSE 13
Chap3: LA QUALITÉ DU LOGICIEL PAR LES CLASSES 22 3.1 INTRODUCTION 22
3.2 LA MODULARITÉ 23
3.3 LA RÉUTILISABILITÉ 25
3.4 PRINCIPES DE CONCEPTION OBJET 27
3.5 TERMINOLOGIE 31
Chap4 : POUR DES CLASSES DE QUALITÉ 33 4.1 PRINCIPES GÉNÉRAUX DE DÉTERMINATION DES CLASSES 33
4.2 CRITÈRES DE QUALITÉ DES CLASSES 35
4.3 LA RÉUTILISABILITÉ: QUALITÉ ESSENTIELLE DES CLASSES 38
PARTIE 2 : C++ ET PROGRAMMATION OBJET 40
Chap5: LES CLASSES EN PRATIQUE 43 5.1 PRINCIPES DES STRUCTURES 43
5.2 PRINCIPES DES CLASSES 47
5.3 CONSTRUCTEUR ET DESTRUCTEUR 57
5.4 TABLEAU D’OBJETS 68
5.5 SURCHARGE DES OPÉRATEURS 70
Exercices 72
IV
Chap6 : LES PATRONS ET AMIS, FONCTIONS ET CLASSES 75 6.1 PATRONS DE FONCTIONS 75
6.2 CLASSE TEMPLATE : PATRON DE CLASSES 76
6.3 UTILISATION D’UN PATRON DE CLASSES 78
6.4 FONCTIONS ET CLASSES AMIES 81
Exercices 83
Chap7 : L'HÉRITAGE 84 7.1 CLASSES DE BASE ET CLASSES DÉRIVÉES 84
7.2 MODE DE DÉRIVATION 84
7.3 HÉRITAGE PUBLIC 85
7.4 REDÉFINITION DE MEMBRES DANS LA CLASSE DÉRIVÉE 88
7.5 HÉRITAGE ET CONSTRUCTEURS/DESTRUCTEURS 90
7.6 POLYMORPHISME 92
Exercices 94
Chap8 : LA BIBLIOTHÈQUE STL 98 8.1 INTRODUCTION 98
8.2 LES CONTENEURS 98
8.3 LES ITÉRATEURS 100
8.4 LES ALGORITHMES 104
Exercices 108
PARTIE 3 : LA SÉCURITÉ INFORMATIQUE
Chap9 : LA SÉCURITÉ INFORMATIQUE 113
M.BENJELLOUN & G. LIBERT S. Informatique
PARTIE 1 :
INTRODUCTION A LA METHODOLOGIE OBJET
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
1
Chap1: LA QUALITÉ du LOGICIEL
Le génie logiciel est une discipline qui vise à structurer et organiser l'ensemble des
activités liées à la réalisation de logiciels et à promouvoir des niveaux de qualité croissants.
L'expérience des succès et échecs de l'industrie informatique a permis de dégager des
concepts assez fédérateurs et de mesurer leur efficacité. La programmation orientée
objet en est un exemple avec son langage phare qui est C++.
Ce cours a pour objectif de donner les bases de techniques de travail reconnues comme
nécessaires pendant tout le cycle de vie du logiciel et d'apprendre les méthodes qui
permettent d'affronter systématiquement et de résoudre les problèmes avec un objectif
de qualité. C'est pourquoi, dans cette introduction, sont abordés les concepts
fondamentaux de qualité logicielle et notamment les notions de contrats, d'invariants et
une discussion sur la notion de test.
1.1 LE CONTRAT
Principe fondamental dans toutes les activités industrielles ou plus généralement
économiques, le contrat est aussi un fondement de toutes les étapes de l'élaboration d'un
logiciel, de sa spécification jusqu'à son abandon. Bien sûr, en fonction du point de vue auquel
on se situe, la nature et les effets des différents contrats seront variables. Toutefois, on
peut reconnaître parmi les fonctions du contrat les éléments fondamentaux suivants:
le contrat garantit une communication sans défaut par un examen exhaustif de tous
les cas nécessaires. Un bon contrat lève toute ambiguïté entre ses parties.
le contrat est un support fondamental de la simplicité des solutions mises en oeuvre
pour le respecter. En effet, il permet de travailler en monde clos, et de ne pas
anticiper des évolutions improbables.
En matière de génie logiciel, les documents contractuels sont nombreux. L'ensemble de ces
pièces décrit les réponses aux questions suivantes:
"quoi": quelles sont les fonctionnalités à implanter,
"comment": comment accéder à ces fonctionnalités,
"quand": quelles sont les limites hors desquelles ces fonctionnalités ne sont pas
accessibles (pas implantées).
Ces contrats lient dans tous les cas des "clients" et des "fournisseurs". Trois niveaux
fondamentaux de contrat sont usuels en informatique et lient des parties distinctes. Le
plus haut niveau permet la communication entre acheteur du logiciel et prestataire, c'est le
cahier des charges (spécification). Le niveau suivant lie les membres d'une équipe
d'informaticiens, c'est la conception. Enfin, avec la granularité nécessaire, le dernier niveau
lie les programmes entre eux.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
2
1.1.1 La spécification: contrat entre client et fournisseur
Un contrat fondamental lie le prestataire d'une solution informatique et son client: la
spécification. Son document de référence est le cahier des charges. Une fois accepté, il
constitue un engagement des deux parties. Le client admet que ses besoins y sont exprimés
de façon correcte et complète et donc accepte d'avance toute solution conforme. Le
prestataire sait de son côté que les solutions qu'il envisage sont réalisables (dans les délais,
ceux-ci pouvant apparaître comme un besoin dans la spécification). Il a la garantie qu'aucun
besoin nouveau ne viendra briser l'édifice qu'il va construire.
L'acceptation du cahier des charges a donc pour effet de figer la partie initiale de l'étude,
permettant à la conception, puis au développement, de se faire sur des bases solides. Il ne
faudrait pas croire pour autant que la spécification est réalisée indépendamment de ces
phases ultérieures. L'engagement de réalisation rend implicite la validation technique des
solutions envisagées. Cette validation peut aller jusqu'à la programmation de certains
modules pour tester une faisabilité. Par ailleurs, l'expression des besoins par le client est
parfois floue et nécessite des interactions entre les parties sur la base d'un prototype.
Dans ce cas, des itérations successives conduiront par exemple à l'acceptation d'un
prototype qui sera jugé adéquat.
Dans de nombreux cas, la pression exercée par l'extérieur sur le prestataire pour faire
démarrer les choses le plus vite possible aura pour effet de réduire le temps passé sur la
rédaction du cahier des charges, sinon sur la réflexion associée. Le prestataire doit avoir la
force de lutter pour écarter les points d'ombre et éviter de laisser des bombes à
retardement dans la spécification.
Au niveau de la spécification, le "quoi" reflète l'ensemble des fonctionnalités du produit, le
"comment" sera souvent la spécification de l'interface homme machine, et le "quand"
correspondra à la mention explicite rigoureuse des différentes limites de fonctionnement
du système (pas plus de 3 utilisateurs, pas de facture à zéro, etc…).
1.1.2 La conception: contrat liant les membres de l'équipe
Le contrat qui lie l'ensemble des développeurs entre eux et avec le chef de projet est la
conception. La conception décrit de manière détaillée l'ensemble des solutions techniques
devant être mises en oeuvre pour implanter ce qui a été spécifié. Il est formellement
impossible de faire plus, ou moins, que ce que la conception a décrit. Aucune initiative
individuelle n'est possible dans ce cadre. Toutes les influences et toutes les contraintes ont
été prises en compte pour concevoir et l'initiative d'un membre de l'équipe n'ayant pas de
vision d'ensemble est à proscrire (personne n'a jamais une vision d'ensemble suffisamment
détaillée d'un grand projet pour prendre une décision technique très pointue).
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
3
La conception détaillée reflète évidemment tous les éléments annoncés dans la
spécification. Elle y ajoute de façon aussi détaillée que souhaitable les conditions "quoi,
comment, quand" des différents modules qu'elle décrit.
1.1.3 La réalisation: contrat entre programmes
Un logiciel est découpé en éléments logiques indépendants (en général des modules et
fonctions). Il est bon de décrire comment chaque élément s'engage sur les points suivants:
- quoi: quelle est la fonctionnalité implantée par ce programme. Bien sûr, dire ce que
fait une fonction est une condition incontournable.
- comment: quelles sont les règles de nommage (noms de fonction, types des
arguments, types en général) pour obtenir cette fonctionnalité.
- quand: quelles sont les données de base pour lesquelles son fonctionnement est
garanti (contraintes de domaine).
Le corollaire de ces engagements est une facilité de développement accrue. En effet, on ne
peut exiger d'une fonction de prévoir, voire de traiter un cas exceptionnel. Hors de son
domaine de définition officiel, on doit considérer qu'une fonction produit des résultats
indéterminés (il est bon de penser que le programme s'interrompra). En d'autres termes, on
ne doit pas considérer qu'une fonction traite une exception. Traiter une exception, c'est-à-
dire s'en sortir honorablement, revient à ajouter l'exception au domaine de définition,
même si le résultat du calcul sur cette valeur est sujet à caution ou même illogique.
1.2 LA NOTION D'UTILISATEUR PARFAIT
La conception et la programmation gagnent considérablement au respect du "principe de
l'utilisateur parfait" où tout programme s'engage à se comporter comme un utilisateur
parfait de ses ressources (les fonctions qu'il appelle), c'est-à-dire à ne jamais appeler une
fonction dans des conditions qui la mettent hors de son domaine de définition. Autrement
dit: l'utilisateur d'un programme garantit qu'il n'activera jamais ce programme dans des
conditions inacceptables. Ce principe est le réciproque du contrat signé par chaque
programme: Tout programme qui fournit un service au système s'engage à fonctionner
correctement lorsque ses éléments de calculs sont valides. Autrement dit: la responsabilité
d'un élément de programme est limitée à son domaine de fonctionnement, mais elle est
entière dans ce domaine.
Aucune fonction ne doit s'attendre à gérer un cas où un autre élément de
programme fournirait des données incorrectes.
Aucune fonction ne doit tester les arguments de son calcul pour leur correction.
En termes plus mathématiques, on considère qu'une fonction possède un domaine de
définition et un domaine d'application. Le programme qui implémente la fonction ne doit pas
tester l'appartenance de ses opérandes au domaine de définition de la fonction. Par contre,
il doit garantir sa correction (résultats justes) et sa complétude (couverture de tout le
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
4
domaine de définition). Bien entendu, il est nécessaire de fournir de manière séparée des
fonctions qui testent l'appartenance d'arguments au domaine.
1.3 L'UTILISATION D'INVARIANTS
Le principe d'utilisateur parfait stipule qu'un programme ne teste pas l'appartenance de
ses arguments au domaine de définition. Pourtant, du point de vue du développeur, ce test
doit être effectué pour des raisons évidentes de détection d'erreurs pendant le
développement. Les tests de domaine, mais ce ne sont pas les seuls, figurent parmi les
invariants des programmes. Un invariant est une propriété logique qui est toujours vraie
pendant l'exécution d'une partie d'un programme, soit qu'elle est un prérequis (c'est alors
bien un invariant de domaine de définition), soit qu'elle est implicite du fait de la nature du
programme (la définition et la vérification des invariants implicites est un outil de base
dans la preuve formelle de programmes).
Les invariants de domaine sont appelés des préconditions et les invariants implicites
portant sur la valeur que doit retourner une fonction s'appellent les postconditions. Ce ne
sont pas les seuls. Par exemple, les paramètres intervenant dans une boucle peuvent être
soumis à un certain nombre d'invariants (le test de continuation de la boucle en est un).
Dans tous les cas, on gagne à ajouter au programme des opérations supplémentaires
permettant la vérification des invariants. Comme cette vérification n'est utile que pendant
le développement, et plus lorsque le produit aura été testé et ne violera plus aucun
invariant, on doit utiliser un macro-processeur pour que le test puisse être retiré dans tous
les programmes par une simple option de compilation.
1.4 QU'EST CE QU'UN TEST?
Les tests réalisés lors de l'exécution d'un programme relèvent de deux problématiques
complémentaires. Certains décident de l'appartenance d'un jeu de données au domaine de
définition d'une fonction, d'autres identifient l'appartenance à une partie d'un tel
ensemble. Par exemple, imaginons une fonction définie par intervalles sur le domaine des
nombres réels strictement positifs:
- x ≤ 0 fonction non définie test de domaine
- x > 0 et x < 3 f(x) = 1/x test de partie
- x ≥ 3 f(x) = 1/3 test de partie
Principe des tests de domaine: une fonction qui implante un algorithme ne teste jamais
l'appartenance de ses arguments au domaine de définition. Ce test s'il s'avère cependant
utile est implanté par une fonction spécialisée à valeur booléenne. Ce principe sert trois
objectifs fondamentaux de concision des programmes, de facilité de programmation, et de
performance.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
5
- Concision: certains tests étant supprimés en tête de fonctions qui implantent des
algorithmes, le texte du programme se trouve donc réduit à l'expression simple
des algorithmes.
- Facilité: le programmeur d'un algorithme ne s'attend pas à gérer des situations
d'exceptions puisqu'il n'en écrit pas les tests. La réflexion qui aboutit au
programme est donc débarrassée de ces considérations auxiliaires.
- Performance: l'économie provenant de la non réalisation de certains tests est
réelle.
Par exemple, soit f une fonction à valeur entière prenant deux arguments entiers non nuls.
Voici le programme que l'on est tenté d'écrire:
int f(int x, int y){
if (x == 0) {
cout << "f :: erreur : argument x nul";
return 0; //par exemple, mais quelle valeur conviendrait?
} else if (y == 0) {
cout << "f :: erreur : argument y nul";
return 0; //par exemple, mais quelle valeur conviendrait?
} else {
… algorithmes de f
}
}
alors que ceci devrait être fait:
int f(int x, int y) {
… algorithmes de f
}
Cette approche garantit de meilleures performances. En effet, lors de l'exécution d'un
programme, il est fréquent qu'une condition d'appartenance à un domaine soit toujours
vérifiée de manière implicite lors d'un fonctionnement correct à tous égards. Par exemple,
un programme qui construt une liste en y ajoutant des éléments ne génère jamais de
situation où la liste serait vide. Toutes les fonctions susceptibles d'être appelées sur cette
liste qui testeraient sa non nullité le feraient donc inutilement. Cette approche est
confortée par un autre argument. S'il est vrai dans un cas particulier que le test de
domaine d'une fonction doive être effectué de façon quasiment systématique, il sera
rarement le cas que les erreurs soient prises en compte de manière uniforme par tous les
programmes, ni qu'une valeur de retour discriminante de la condition d'erreur puisse être
choisie qui convienne dans tous les cas.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
6
Le principe des tests peut alors être reformulé de la façon suivante par le principe de
séparation des algorithmes et des conditions: une fonction qui implémente un algorithme ne
teste jamais que les conditions de mise en oeuvre de l'algorithme sont vérifiées.
Enfin, les programmes qui prennent abusivement en compte le contrôle des situations hors
domaine sont les moins susceptibles d'être réutilisés pour d'autres projets. En effet, la
manière effective de traiter une exception est très spécifique d'un projet (tel
programmeur sait que dans le projet en cours une fonction ne renverra jamais la valeur
1000 et utilise donc cette valeur comme témoin d'erreur) alors que l'algorithme
proprement dit possède une portée beaucoup plus générale. Les programmes qui ne testent
pas leurs conditions d'exécution ont donc plus de chances d'être réutilisés que les autres.
1.5 QU'EST-CE QU'UNE EXCEPTION?
Les langages orientés objet modernes (C++, Java, C#,...) permettent la prise en compte
d'exceptions. Le lancement d'une exception par un programme permet d'interrompre de
façon brutale la pile d'appels en cours et de transférer le contrôle à une section alternative
du programme. La mise en œuvre des exceptions traduit une évolution, au sein des langages
de programmation, des moyens d'interruption brutale mais contrôlée de l'exécution
obtenue pour le langage C avec les fonctions "setjump()" et "longjump()". Les exceptions
permettent de détecter la présence d'une situation ne permettant pas l'exécution normale
d'un programme, tout en laissant au programme client la possibilité de survivre, de la
manière qu'il souhaite, à la situation, ou de laisser passer l'exception, en délégant ainsi son
(absence de) traitement au monde extérieur (un autre programme, ou le système).
Le choix de définir et de mettre en œuvre certaines exceptions est un choix de conception
important, normalement complémentaire de la mise en œuvre d'invariants. En particulier, il
est déraisonnable de définir une hiérarchie d'exceptions pour la totalité des préconditions
et invariants d'un programme. On met en œuvre des exceptions par exemple dans un
programme devant manipuler le contenu d'un fichier. Si l'ouverture du fichier échoue (pour
des raisons qui sont extérieures au programme), l'exécution ne peut continuer. Toutefois,
cette erreur doit pouvoir être récupérée à plus haut niveau. On lui associe donc une
exception. Dans le même cadre, on pourra associer des exceptions à la présence dans le
fichier d'éléments non reconnus ou de données incompatibles avec le traitement. On ne met
pas en œuvre d'exceptions spécifiques pour tester qu'une fonction est appelée avec des
arguments situés dans son domaine de définition.
1.6 CAPITALISER
L'activité de programmation est coûteuse et, autant que possible, les organisations doivent
mettre en place une forme de recyclage du travail. Les efforts passés doivent être
capitalisés et pouvoir resservir. Traduit en termes de programmes, on dira que les
programmes écrits doivent être réutilisables.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
7
Il est vrai que de nombreux outils logiciels de base se rencontrent dans tous les projets.
Citons par exemple les listes, les tableaux réallouables, les arbres. On observe souvent
qu'une équipe de programmeurs ne s'entende pas sur la spécification de ces éléments de
base et qu'un projet intègre des versions légèrement différentes des mêmes programmes.
A fortiori, on conçoit que de projet en projet les outils conçus antérieurement puissent
être perdus.
Permettre la réutilisabilité suppose d'avoir une approche "produit" dans la réalisation des
outils de base, c'est-à-dire qu'il faut de manière adaptée spécifier, concevoir, implanter,
documenter, livrer et maintenir la bibliothèque qui implante une fonctionnalité d'usage
général.
spécifier: les besoins auxquels répond une gestion de liste par exemple doivent être
clairement définis. Pour garantir leur réutilisabilité, ils doivent être universels, donc
indépendants de toute spécificité applicative. Si de telles spécificités existent cependant,
elles sont décrites à part en s'appuyant sur le noyau fondamental.
concevoir: pour garantir la réutilisabilité, la conception doit prévoir une interface de
portabilité (indépendance des architectures matérielle et système) et doit obéir aux
différents principes généraux, dont celui de l'utilisateur parfait. La conception de
l'interface homme machine et de ses relations avec l'applicatif présente à cet égard un
caractère saillant dans l'ensemble de la conception. C'est souvent elle qui conditionnera le
niveau de modularité et de réutilisabilité de l'ensemble.
programmer: les outils de base font l'objet de modules indépendants, groupés dans une
ou plusieurs bibliothèques. Ces bibliothèques sont accessibles à l'ensemble des membres de
l'équipe et font l'objet d'une rigoureuse gestion de versions. Les programmes eux-mêmes
sont protégés des malversations par des contrôles d'invariants dans leur version de
développement. Un tel programme ne doit jamais "planter" s'il est mal utilisé mais doit se
comporter comme une boîte noire. On ne doit jamais conduire un utilisateur à utiliser un
débugger pour rechercher dans des sources inconnues l'origine d'une erreur.
documenter: de bons documents de spécification et de conception constituent déjà une
documentation adéquate. Il faut la compléter par un "manuel de l'utilisateur" qui décrit
notamment des cas d'utilisation détaillés et des exemples.
livrer: un utilitaire doit être disponible aux développeurs sous deux versions. La
première, de développement, effectue tous les tests de domaines nécessaires et garantit
de ne jamais "planter" sans dire pourquoi elle plante. La seconde version est utilisée lorsque
le programme a été intégralement testé. Elle ne fait plus aucune vérification de domaine. En
cas de "plantage", il est alors conseillé de refaire une édition de liens avec la bibliothèque
de tests plutôt que de courir après l'erreur via un débugger.
Les algorithmes utiles dans les cas les plus courants de gestion de structures de données
comme les arbres balancés ou les listes sont tellement connus que de nombreuses versions
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
8
en existent dans le domaine public. Il est possible de partir d'un des sources disponibles
sur le marché plutôt que de zéro. Il est conseillé aux organisations de concevoir et
d'implanter leurs propres outils de base. Ainsi, elles possèdent la maîtrise d'un élément qui
s'avère central dans leurs projets. Toutefois, il faut encourager tous les membres des
équipes de développement à connaître et à faire vivre les outils qu'ils utilisent. La
compétence sur ces programmes ne doit pas rester le fait d'un individu isolé. De même, en
amont, il faut que les étapes de conception et de spécification de ces outils à caractère
général voient la participation de tous leurs futurs utilisateurs. Lorsqu'en un point
éventuellement avancé du projet, un individu identifie une fonctionnalité pouvant
raisonnablement être considérée comme d'usage public, et réutilisable, les décisions
relatives à sa spécification et à sa conception doivent être prises par l'équipe au complet.
1.7 DOCUMENTER
La documentation des programmes est un art difficile. Il y a trois niveaux fondamentaux de
documentation:
- 1. la documentation interne du programme,
- 2. le manuel de référence,
- 3. le manuel d'utilisateur et d'exemples.
1.7.1 La documentation interne
La plus grande difficulté en matière de documentation interne est de trouver la juste limite
entre trop en faire ou pas assez. En fait, aucune information nécessaire à la compréhension
rapide d'un programme ne devrait manquer à son lecteur. On doit considérer que la
structure logique d'un programme (les tests et les boucles) parle d'elle-même au lecteur
lui-même programmeur. Une documentation intégrée strictement redondante avec cette
logique est inutile. Ainsi, l'algorithme ne doit pas être expliqué dans le détail. Peut-être que
quelques lignes d'explication initiales sur les principes généraux suivis seront suffisantes.
Par contre, quand une expression évaluée dans un test n'est pas suffisament explicite pour
le comprendre, une ligne de commentaire est nécessaire. Dans le cas où un test complexe
est réalisé, il est parfois utile de reporter ce test dans une fonction à valeur booléenne
dont le nom sera explicite de ce qui est testé. Ainsi, le corps du programme aura une
expression proche de l'algorithme. De même, lorsqu'un programme possède un effet de
bord non évident, cela doit être documenté.
Observons maintenant que les invariants mentionnés explicitement dans les programmes, et
testés, constituent une documentation fondamentale. En effet, leur conformité avec les
algorithmes est garantie (justement parce qu'ils sont testés). La pratique du
développement avec contrôle d'invariants montre que très souvent, leur présence éclaire
considérablement le sens et la fonction des programmes ou ils apparaissent. Les invariants
constituent le fondement de la documentation interne.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
9
1.7.2 Le manuel de référence
Ce document décrit dans le détail exactement ce que fait chaque fonction, sous quelles
conditions et avec quels paramètres. Aucune spécificité non implicite ne doit y manquer afin
de permettre à tout nouveau programmeur ou utilisateur de trouver toutes les informations
nécessaires à son activité. Le manuel de référence comporte une partie destinée à
l'utilisateur final. Cette partie doit être strictement conforme à ce qui est mentionné dans
le cahier des charges. L'autre partie du manuel de référence peut être obtenue par
extraction de parties de programmes lorsque la documentation interne est faite
rigoureusement. Automatiser l'extraction du manuel de référence est un moyen de
conduire à une documentation interne de qualité. De plus, cette automatisation peut aussi
permettre de contrôler plus facilement (sinon automatiquement) la conformité de ce qui est
implanté effectivement avec le cahier des charges.
1.7.3 Le manuel d'exemples
Ce manuel donne des exemples d'utilisation de l'outil et, en général, guide de façon
progressive vers la maîtrise de ses différents concepts. C'est donc un cours sur papier
avec, fréquemment, une implication assez forte de l'utilisateur par le biais de l'ordinateur
(écriture de programmes). Ce document n'est pas forcément rédigé par les programmeurs
eux-mêmes.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
10
Chap2: LA METHODOLOGIE OBJET : OUTIL DE QUALITE
2.1 LE CONCEPT D'OBJET
L'émergence de la notion d'objet en informatique est liée aux progrès réalisés quant à la
description des systèmes informatiques eux-mêmes donc aux techniques de spécification.
Les programmeurs ont voulu réaliser des systèmes concrets pouvant être qualifiés de
modèles acceptables d'une réalité, ayant donc:
- les mêmes réactions que la réalité représentée,
- la même modularité que le monde réel.
Pour éclairer le sens du terme "modularité" employé ci-dessus, disons qu'un système
informatique supposé simuler un monde à 1,2,…N cailloux doit pouvoir évoluer de N à N+1
aussi facilement que le monde réel: par ajout d'un "caillou informatique". L'objet
informatique est donc une projection de l'objet du monde réel, qui n'en reproduit bien sûr
jamais tous les attributs, mais qui possède, au regard de l'objectif informatique défini, le
même degré d'individualité que l'objet réel. Il faut noter que le terme "réalité" est ici
ambigu car il s'agit d'une réalité perçue. Certains systèmes ont donc mis l'accent sur une
simulation des structures et des mécanismes de la perception (les frames de l'école
américaine) plus que sur une simulation réduite à ce qui est effectivement perçu. Cette
généralisation a conduit à étendre les concepts fondamentaux explorés ici. Pour garantir
qu'un système informatique fini aura les mêmes comportements que le monde réel, on peut
choisir de faire en sorte que chaque objet se comporte "comme" son homologue réel en
termes de:
- persistance de son état,
- réactions aux perturbations externes,
- communication avec les autres objets.
2.1.1 Persistance
La persistance d’un état est obtenue de façon élémentaire par stockage de données
pertinentes dans une structure (les types struct en C++). La structure est donc l'élément
fondamental dans la représentation informatique de l'objet: un espace clos et contigu où
figurent toutes les informations relatives à un objet. La démarche consistant à décrire un
tel espace est nommée encapsulation des données. Une structure est également décrite
comme un agrégat de données hétérogènes. Le type "tableau" au contraire constitue un
agrégat de données d’un même type (homogène).
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
11
2.1.2 Réactions
La réaction aux perturbations externes est simulée par des fonctions que l'on peut
appliquer à l'objet. Par exemple, un caillou peut bouger de trois centimètres vers la gauche.
On imagine bien alors d'appeler une fonction particulière du programme nommée par
exemple "bouge_caillou (mon_caillou, 3, gauche)" pour le simuler. Toutefois, l'existence
chez différents objets de fonctionnalités sémantiquement voisines, et pour autant
différentes dans leur réalisation, conduit à rejeter un modèle informatique de premier
niveau dans lequel les fonctions sont distinguées par leur seul nom et sont globales (On
entend par globales le fait que les fonctions sont appelables en tout point du programme,
par n'importe quelle fonction) comme c'est le cas dans les langages traditionnels non
orientés objets. On veut lutter contre deux difficultés (au moins):
- le nommage des fonctions globales nécessite de donner des noms différents à des
fonctions homologues, et donc à multiplier dans un programme le nombre des
symboles. Or, on voudrait nommer "bouge" toutes les fonctions permettant de
"bouger" un objet quel qu'il soit, sachant que la nature connue de l'objet permet
de choisir (automatiquement) la fonction à appeler pour réaliser cette simulation.
- les fonctions globales ne permettent pas avec une grande finesse le contrôle
d'éventuelles restrictions au graphe d'appel du programme. On souhaite en général
décrire les possibilités d'interactions de façon précise (une mouche ne peut pas
faire bouger un caillou par exemple).
2.1.2.1 Le polymorphisme
La difficulté liée au nommage des fonctions est contournée par le mécanisme du
polymorphisme: deux fonctions différentes par leurs arguments ou par le type de la valeur
retournée peuvent porter le même nom. La description complète de la fonction: nom, type
de la donnée retournée, types et séquence des arguments est appelée sa signature ou son
prototype. Le polymorphisme permet donc de distinguer et d'employer des fonctions de
même nom mais ayant des signatures distinctes. Notons que ni le langage C++, pour des
raisons historiques (en C, on peut ignorer la valeur retournée par une fonction), ni le langage
Java, n’intègrent dans la signature le type de la donnée retournée. Deux fonctions ne
peuvent donc pas différer par cette seule information. Ainsi, n'est-il pas nécessaire de
nommer "bouge_caillou" une fonction qui ne peut être appliquée qu'à un "caillou" et les
descriptions de l’exemple suivant peuvent cohabiter dans le même programme:
void bouge (caillou obj);
void bouge (pomme obj);
L'économie réalisée grâce au polymorphisme seul est considérable:
- les programmes gagnent en abstraction car des concepts non pertinents en sont
retirés,
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
12
- il n'est pas nécessaire de définir une règle de nommage des fonctions homologues
(par exemple "bouge_caillou", ou bien "CaillouBouge"). Le programmeur ne vit donc
plus cette contrainte et ne risque pas d'y faillir,
- l'écriture des programmes est facilitée.
2.1.2.2 Encapsulation des fonctions
Deuxièmement, la solution retenue dans le concept d'objet au problème du contrôle des
appels est l'encapsulation des fonctions. Dans ce modèle, sont associées logiquement à la
structure (de données) représentant un objet les seules fonctions qui simulent (ou
implantent) des réactions de cet objet au monde extérieur. Ces fonctions sont alors
appelées des méthodes. Bien entendu, le polymorphisme joue son rôle ici et l'on peut faire
lors de leur nommage l'économie d'informations permettant de les distinguer de méthodes
homologues applicables à d'autres objets. Ainsi les cailloux ont une méthode "bouge", et les
troncs d'arbres (coupés!) aussi, sans que cela ne génère d'ambiguïté. Par définition, toutes
les méthodes de même nom constituent des implantations appropriées du même concept, qui
porte le nom d'opération.
2.1.3 Communication
La communication entre objets requiert un transfert d'informations d'un objet à un autre.
Au sens large, on peut dire que ces informations sont transmises sous forme de messages.
La structure plus ou moins variable du contenu de ces messages, qui peut donner lieu à des
implantations d'élégance variable suivant le langage utilisé, ne doit pas faire oublier que
leur transmission se fait dans tous les cas par l'intermédiaire d'une méthode spécialisée. Il
n'y a donc rien de plus dans l'envoi de messages que n'en permet l'encapsulation de
fonctions. Dans le cas synchrone, la réponse est fournie par la valeur de retour de la
fonction appelée. Dans le cas asynchrone, la réponse est fournie par un message en retour,
donc un appel de fonction de l'objet source, pouvant avoir lieu avant la fin du premier
message ou après.
L'envoi d'un message d'un objet à un autre suppose qu'une méthode du premier appelle une
méthode du deuxième. Cela suggère deux remarques, l'une de principe, l'autre technique:
- l'encapsulation permet de contrôler les possibilités de communications inter
objets,
- l'appel réciproque et récursif de méthodes entre deux (ou plus) objets peut
conduire à des situations de bouclage qui devront être soigneusement écartées.
Enfin, le concept d'envoi de messages évoque la simulation d'une souplesse presque
conviviale dans l'échange d'informations entre objets. Il a d'ailleurs été réalisé des
systèmes informatiques dans lesquels des objets élaborent de véritables négociations pour
construire un état cohérent.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
13
2.2 LE CONCEPT DE CLASSE
On observe aisément que la partie "structure" de l'objet, contenant ses données, doit être
dupliquée pour chaque objet similaire. (Tous les cailloux possèdent une position dans
l'espace notamment). Par contre, les méthodes de ces objets, pour être applicables à tous,
ne nécessitent pas d'être décrites plusieurs fois. On distingue donc entre la réalisation
particulière d'un objet, qui sera appelée instance, et l'ensemble des informations
nécessaires pour construire et "animer" ces instances: leur classe. Le modèle utilisé pour
générer pour chaque objet une structure physique de données est appelé le prototype (des
instances de la classe).
Ainsi, la base sur laquelle repose l'ensemble de la technologie orientée objet est la classe
et les textes logiciels qui servent à produire des systèmes informatiques sont les classes.
Les objets sont une notion n'ayant un sens qu'à l'exécution: ils sont créés et manipulés par
le logiciel durant son exécution. La classe est la généralisation du type de données à savoir
une description statique de certains objets dynamiques: les divers éléments de données qui
seront traités durant l'exécution d'un système logiciel. La classe est un concept sémantique
puisque chaque classe influence directement l'exécution d'un système logiciel en
définissant la forme des objets que le système créera et manipulera lors de l'exécution.
2.2.1 Le type abstrait: un point de vue formel sur la classe
Le concept de type abstrait est introduit pour la spécification de systèmes et possède un
prolongement naturel dans la conception. Un type abstrait consiste en une description
formelle (i.e. logique) des états accessibles aux objets de ce type et des transitions qui
peuvent survenir entre états. Ces transitions correspondent à des fonctions membres qui
modifient les instances. Toutes les propriétés des instances d’un type abstrait sont
démontrables comme on démontre un théorème. Une telle description spécifie donc sans
ambiguïté la sémantique d’un ensemble d’objets, indépendamment des perspectives de sa
réalisation informatique.
Le type abstrait est un modèle formel des objets qu'il décrit et peut être appelé une
classe. Cette classe comporte assez d'informations pour générer un prototype. Mais elle ne
possède pas de réalisation physique en soi. Elle n'est qu'un cadre général de réalisation.
Certaines écoles des langages orientés objet ont privilégié cette approche. La classe y est
un descripteur de type ne possédant pas (au moins pour l'utilisateur du langage) de
réalisation physique. En C++, les classes sont en fait des types abstraits utilisés par le
compilateur pour générer le code mais ne sont jamais accessibles en tant que données des
objets eux-mêmes.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
14
2.2.2 Un point de vue opérationnel sur la classe
Une classe décrit essentiellement un prototype des objets de cette classe et les méthodes
applicables aux objets de cette classe. Pour des raisons techniques (notamment la liaison
dynamique décrite plus loin), l'implantation des langages orientés objet nécessite que
certaines classes engendrent une structure de données permettant de stocker des
pointeurs vers des méthodes de la classe. Ces méthodes sont alors appelées virtuelles.
Chaque objet de la classe comporte dans ce cas un pointeur vers ce conteneur de méthodes
qui apparaît clairement comme une réalisation physique de la classe. On se trouve dans une
situation où la classe "existe" et où des informations liées à la classe sont partagées par
toutes les instances de cette classe.
Instance 1 de A
Instance 2 de A
Instance 3 de A
Méthodes de A
L'école américaine des langages à objets a pris acte de cet état de fait et a produit des
langages, tel Smalltalk, où le statut "physique" de la classe est acquis, ce qui permet des
extensions intéressantes du concept d'objet, comme souvent acquises via une définition
récurrente des notions. En voici quelques exemples:
- une classe est elle-même un objet, instance d'une métaclasse, et ce à l'infini
- tout objet peut servir de prototype à la construction d'un autre objet
- tous les types de données sont des objets (en Smalltalk, même un caractère est un
objet, qui communique par envoi de messages avec ses voisins)
- l'objet classe peut contenir des informations partagées par toutes les instances
de la classe (cela existe en Java, et partiellement en C++)
Les langages de l'école américaine sont appelés langages à base de frames (un "frame" est
un cadre en français, au sens "encadrement" d'un tableau). L'objectif des chercheurs était
dans ce domaine autant de donner un modèle informatique satisfaisant de la représentation
humaine (psychocognitive) de l'information que de la réalité à simuler elle-même.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
15
2.2.3 L'héritage
Les logiciens, et après eux les naturalistes, géologues, géographes et tous les descripteurs
du monde réel ont forgé dans la pensée occidentale un modèle du monde par lequel les
propriétés des objets découlent logiquement de la place qu'ils occupent dans une
classification patiemment construite, dont l'adéquation est mesurée au faible nombre
d'exceptions qu'elle engendre. Ce modèle s'intéresse avant tout aux propriétés des objets
(appelées propositions en logique) plus qu'à leurs éventuelles relations.
Par exemple: si on sait qu'un "objet" est un mammifère, on sait alors qu'il allaite ses petits
et qu'il ne vole pas (en général). Une classification des concepts nous indique également que
tous les mammifères et tous les oiseaux sont des animaux. On dispose donc de deux sortes
de "règles":
- A : type → propriété1 propriété2 …
- B : type0 → type1( type2 …)
-
Les règles de format "A" sont perçues comme des règles décrivant la classe "type". Les
règles "B" sont quand à elles des règles d'héritage.
Les cailloux par exemple sont des objets minéraux au même titre que le verre. Les langages
à objets permettent de décrire une classe d'objets minéraux, comportant tous les
éléments communs à tous les objets minéraux, et deux autres classes, l'une pour le verre,
l'autre pour les cailloux, qui hériteront de la première.
// Voici un exemple des caractéristiques logiques de l'héritage :
// la classification
caillou → objet_minéral
verre → objet_minéral
(caillou → ¬verre) (verre → ¬caillou)
// la description des objets minéraux
objet_minéral → imputrescible
// les deux variétés "verre" et "caillou"
verre → transparent plat
caillou → opaque rond
La classe qui hérite dispose par défaut de
toutes les propriétés et fonctionnalités de
ce qu'on appelle sa superclasse. Ces
données par défaut peuvent être, ou non,
comme dans la réalité, modifiables par les
sous-classes où par les instances des
classes. Voici un exemple de hiérarchie
d'héritage.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
16
2.2.4 Utilisation de l'héritage
Une difficulté surgit lors de l'utilisation effective de l'héritage en informatique, due en
partie à ce que la notion d'héritage de classes ne couvre qu'imparfaitement les concepts
logiques que l'on souhaite décrire. Les progrès en matière d'outils logiques ont été trop
lents pour que l'on puisse les utiliser directement pour la programmation.
La difficulté rencontrée est la suivante: lorsque B hérite de A, la structure de données
décrite par A sera présente d'office dans toute structure de type B. La relation d'héritage
de classes présente donc un caractère incrémental qui peut être utilisé comme une facilité
d'écriture. (Définissant B comme héritant de A, on réalise l'économie de la description de
A dans B au prix peut-être de quelques retouches permises par le langage).
Pour des raisons techniques et de normalisation, certains langages, comme C++, ne
permettent pas à travers l'héritage de supprimer dans une sous-classe des informations de
la superclasse qui sont inutiles. Cela conduit parfois à vouloir renverser la relation
d'héritage, pour profiter au mieux des caractéristiques incrémentales du système
(essentiellement pour éviter de consommer de la mémoire inutile), au détriment de la
relation logique naturelle qui unit les deux classes.
Exemple:
Soient les classes graphiques "Point" et "Cercle"
Un point peut être vu comme un cercle dégénéré de diamètre nul. La seule relation logique
qui peut unir ces deux classes est Point → Cercle.
Toutes les instances de Point, si elles héritent de Cercle, vont comporter un champ appelé
"diamètre" qui leur est inutile. Utiliser la relation d'héritage inverse permet au contraire
de construire Cercle en ajoutant les informations de diamètre à la classe Point.
Cette difficulté est partiellement responsable du label de "mauvais langage objet" attribué
par certains à C++ (ou Java). Toutefois, il est essentiel de noter que cela est dû au fait que
la spécification de ce langage respecte des contraintes techniques (d'alignement de
structures notamment, fondamentales en communication, et de performance) qui le rendent
meilleur que tout autre langage objet, même en se plaçant dans le pire des cas (ci-dessus:
Point hérite de Cercle).
Par ailleurs, il est clair que le problème peut être contourné en ne faisant pas hériter le
Point du Cercle mais en rendant ces deux classes sous-classes d'une classe commune. La
règle en matière d'implantation de l'héritage est donc:
S'il est loisible de dire que "tout A est un B", alors B ne doit pas hériter de A, et A peut, le
cas échéant, hériter de B.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
17
De même, pour identifier les champs:
S'il est loisible de dire que tout A peut posséder un B, alors B est une donnée de A.
Un cas laisse souvent une impression ambiguë qui ne peut être tranchée en théorie, celui où
on peut dire "tout B possède exactement un A". Cette situation peut clairement être
traduite informatiquement par "B hérite de A" mais il est préférable de ne pas choisir
cette solution qui possède l'inconvénient de masquer, via l'héritage, A dans B. Voici un
exemple graphique des trois alternatives envisageables:
héritage donnée donnée pointeur
A
B
A
B
A
B
A
Le choix entre donnée et pointeur relève surtout de considérations opérationnelles. En
effet, la performance de nombreux programmes est conditionnée par une bonne utilisation
de la mémoire cache du processeur, rarement possible avec les structures de données
accédées par des pointeurs. Le langage Java rend cette discussion obsolète car il masque la
véritable nature des données (toujours accédées via un pointeur). Par contre, le choix de
l'héritage viole conceptuellement le lien véritable qui unit A et B. De plus, si l'on se trouve
dans une situation telle que A possède exactement un B et un C, on voit que l'on est conduit
soit à choisir un héritage simple de B ou de C, soit un héritage multiple qu'il est préférable
d'éviter lorsqu'il n'est pas motivé par une relation logique authentique entre les classes. On
décide donc dans ce cas: s'il est vrai que tout B possède exactement un A, alors A est une
donnée de B.
2.2.5 La liaison dynamique
Supposons avoir le projet de réaliser un éditeur graphique interactif permettant la création
et la manipulation d'objets à l'écran. Ces objets, une fois créés, sont accessibles par leurs
pointeurs via une structure particulière, par exemple, une liste (de pointeurs) pour faire
simple.
On aura avantage à ce que la structure choisie manipule des objets abstraits, en ignorant le
fait que ce soient des ronds, des carrés, des triangles, pour invoquer sur eux des méthodes
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
18
comme "déplace", "retaille", "détruit". Pourtant, il est nécessaire que la bonne fonction "de
déplacement de carré" soit invoquée sur un pointeur "de carré".
Dans ce cas, on utilise à ce qu'on appelle la liaison dynamique: la fonction devant être
appelée sur chaque objet est choisie à l'exécution car cette décision ne peut être prise
lors de la compilation, le type exact de l'objet étant alors inconnu. Le langage C++ propose
par défaut la liaison statique des méthodes. La liaison dynamique est obtenue par
l'utilisation du mot clef virtual. A contrario, le langage Java ne permet pas d'utiliser la
liaison statique: toutes les méthodes sont virtuelles.
Techniquement, mettre en œuvre la liaison dynamique se fait en conservant au sein de la
structure de l'objet l'adresse d'une table des méthodes virtuelles (C++) ou bien l'accès à
une représentation plus complète de la classe (cas de Java). Dans les deux cas, cette
indirection permet de procéder à l'appel effectif de la bonne méthode sur chaque objet, le
compilateur ayant juste à connaître l'emplacement dans cette table de la méthode à
appeler (C++) ou la signature de la méthode (Java). Les mécanismes d'introspection Java
permettent même à un programme d'interroger dynamiquement la classe d'un objet afin de
connaître ses fonctions et données membres.
2.2.6 Champs, accesseurs et aspects
La pratique de la programmation orientée objet a conduit à reconsidérer le rapport
existant entre données internes des objets (les champs) et les méthodes permettant de
calculer un état de l'objet (que l'on appelle des accesseurs). Il y a à cela au moins trois
raisons:
D'une part, on peut souvent implanter la même classe d'objets de plusieurs façons
équivalentes, certaines données étant représentées par des champs dans une version et
calculées dans une autre. Une classe "Cercle" par exemple sera fonctionnelle aussi bien avec
un rayon implanté en donnée membre et un diamètre calculé qu'avec l'inverse. Il vaut donc
mieux ne pas exposer au monde extérieur des données traduisant une mise en œuvre des
services de la classe qui est susceptible de variations. Ce principe de conception s'appelle le
"masquage de données".
D'autre part, la spécialisation de classes par héritage peut conduire à disposer de
données dans une classe qui seront redéfinies pour être calculées dans une autre. Par
exemple, des objets "Cercle", "Carré" et "Point" disposent de fonctions appropriées pour
déterminer leur surface. Dans les cas du cercle et du carré, ce calcul peut se faire à partir
des données intrinsèques de l'objet (diamètre, longueur de côté). Dans les deux premiers
cas, c'est bien l'appel d'une méthode qui fournira la réponse. Par contre, le point possède
une surface constante nulle. Sa surface serait donc idéalement représentée par un champ
constant (une donnée constante de la classe pour tous les points), ne consommant pas
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
19
d'espace de stockage pour chaque objet. Là encore, la bonne approche consiste à ne pas
exposer de donnée membre.
Enfin, des considérations d'efficacité peuvent parfois à stocker au sein d'un champ
membre auxiliaire une donnée calculée une fois pour toutes pour chaque instance, afin
d'éviter notamment un calcul fréquent et coûteux. Donner accès à ce champ est délicat car
c'est trompeur sur la nature de la donnée.
Toutes ces considérations sont liées à l'implantation effective et sans rapport avec les
propriétés abstraites de l'objet. Elles devraient être ignorées par un utilisateur. Par
ailleurs, le programmeur d'une classe est le garant notamment de sa compatibilité
ascendante c'est-à-dire que toutes les versions ultérieures d'une classe devront pouvoir
être utilisées pour compiler des programmes clients anciens. Certains langages objets
proposent des notations qui résolvent cette difficulté de façon plus ou moins complète. Par
contre, le langage C++ n'apporte ici aucune aide puisque l'accès à un champ membre se fait
de façon totalement différente de l'accès à un champ calculé. Pour s'en convaincre, voici
des exemples d'interfaces illustrant les cas énoncés plus haut. Pour commencer, voici deux
(fragments d') interfaces alternatives pour une classe Cercle:
class Cercle1 {..
public :
double rayon;
double diamètre () { return 2*rayon; }
};
class Cercle2 {
…
public :
double rayon() { return diamètre/2; };
double diamètre;
};
En l'absence de précaution supplémentaire, le choix de définir Cercle1 plutôt que Cercle2
fige donc une interface de programmation qui ne pourra plus être modifiée, sauf aux dépens
des utilisateurs, ce qui doit absolument être évité. On observe notamment que dans Cercle1,
un programme client autorisé pourra modifier explicitement le rayon (grâce à la déclaration
public), mais pas le diamètre car c'est une fonction et réciproquement pour Cercle2.
Voici maintenant un exemple de classes C++ décrivant de façon naïve et inadéquate des
types Point, Cercle et Carré. On s'intéresse ici au calcul de la surface des instances de ces
types.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
20
class Carre {
int tailleCote;
int surface (){return tailleCote * tailleCote;}
};
const double pi = 3.14159;
class Cercle {
int diamètre;
int surface (){return diamètre * diamètre * pi / 4;}
};
class Point {
const int surface = 0; // surface n'est plus une méthode
// ( int surface(){return 0;} serait bien préférable)
};
Cet exemple peut paraître un peu artificiel mais la situation concrète se présente souvent.
Ce programme est inadéquat car l'accès à une information de nature comparable pour des
objets de types "proches" est réalisé de différentes façons suivant les classes. Une telle
représentation ne traduit donc pas l'existence d'une abstraction utile dans ce cas.
L'héritage de classes permet de manipuler des objets virtuellement, à partir d'un type de
base, le langage garantissant l'appel de la bonne méthode sur le bon objet. Il est donc
souhaitable (et homogène) d'avoir un accès uniforme à des informations similaires. Voici
deux approches traditionnelles de ce problème:
- le langage objet donne une syntaxe fonctionnelle à la lecture d'un champ d'un
objet. Dans l'exemple ci-dessus, la lecture du champ surface de la classe Point se
fait par surface ();, comme s'il s'agissait une fonction,
- le langage permet d'implanter tous les accesseurs d'un objet sous la forme de
champs fictifs calculés. L'accès à un tel champ se fait comme s'il était une donnée,
mais il est calculé de façon transparente.
Aucune de ces deux approches ne peut facilement être simulée en C++. Dans le deuxième
cas, les champs correspondants peuvent être qualifiés de champs fictifs, ce qui n'est
réellement le cas que lorsqu'ils peuvent recevoir une valeur. On se trouve alors en présence
d'une nouvelle difficulté. Peut-on rendre tout à fait invisible à l'utilisateur d'une classe la
différence qui existe entre le champ réel rayon de la classe Cercle1 ci-avant et le champ
diamètre correspondant? Peut-on définir un langage où l'on pourrait écrire "diamètre = 12"
même si le champ diamètre, "fictif", n'est obtenu qu'au travers de la donnée membre
rayon? Oui! Dès lors que l'on dispose de ce que l'on appelle des aspects.
A tout champ d'un objet sont associées deux fonctions, l'aspect de lecture et l'aspect
d'écriture, appelées automatiquement lors des accès correspondants au champ sans que
l'utilisateur de la classe ne s'en rende compte. Par cette technique, le champ diamètre de la
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
21
classe Cercle1 peut se comporter exactement comme un champ réel, sa lecture se faisant,
par l'aspect de lecture, via un calcul où intervient rayon, et son écriture appelant de façon
invisible l'aspect d'écriture, qui modifie indirectement le champ rayon. Cette technique
permet de modifier la classe pour rendre le champ rayon fictif et le champ diamètre
concret sans aucun effet sur les programmes clients. D'un point de vue anthropomorphique,
sans considérer les aspects comme un dispositif permettant de garantir la compatibilité
ascendante, un tel mécanisme permet à chaque objet d'être informé de toute opération de
lecture où d'écriture. Cette approche est de loin la plus élégante à l'ensemble des questions
dont il est question dans cette partie.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
22
Chap3: LA QUALITE DU LOGICIEL PAR LES CLASSES
3.1 INTRODUCTION
Ce chapitre a pour but d’introduire la notion même de conception objet d’un point de vue
théorique. Il n’a pas la prétention d’expliquer en détail ce type de conception mais se
propose d’en rappeler les idées maîtresses. La philosophie du langage C++, comme celle des
autres langages à objets, est en effet directement inspirée de ce type de conception.
La conception par objet trouve ses fondements dans une réflexion menée autour de la vie
du logiciel. D’une part, le développement de logiciels de plus en plus importants nécessite
l’utilisation de règles permettant d’assurer une certaine qualité de réalisation. D’autre part,
la réalisation même de logiciel est composée de plusieurs phases, dont le développement ne
constitue que la première partie. Elle est suivie dans la plupart des cas d’une phase dite de
maintenance qui consiste à corriger le logiciel et à le faire évoluer. On estime que cette
dernière phase représente 70 % du coût total d’un logiciel, ce qui exige, plus encore que la
phase de développement, de produire du logiciel de qualité.
La conception objet est issue des réflexions effectuées autour de cette qualité. Celle-ci
peut-être atteinte à travers certains critères:
- la correction ou la validité: c’est-à-dire le fait qu’un logiciel effectue exactement les
tâches pour lesquelles il a été conçu;
- l’extensibilité: c’est-à-dire la capacité à intégrer facilement de nouvelles
spécifications, qu’elles soient demandées par les utilisateurs ou imposées par un
événement extérieur;
- la réutilisabilité: les logiciels écrits doivent pouvoir être réutilisables, complètement
ou en partie. Ceci impose lors de la conception une attention particulière à
l’organisation du logiciel et à la définition de ses composantes;
- la robustesse: c’est-à-dire l’aptitude d’un logiciel à fonctionner même dans des
conditions anormales. Bien que ce critère soit plus difficile à respecter, les
conditions anormales étant par définition non spécifiées lors de la conception d’un
logiciel, il peut être atteint si le logiciel est capable de détecter qu’il se trouve dans
une situation anormale.
On peut aussi citer
- la compatibilité: facilité avec laquelle des éléments logiciels peuvent être combinés à
d'autres.
- l'efficacité: capacité d'un système logiciel à utiliser le minimum de ressources
matérielles, que ce soit le temps machine, l'espace occupé en mémoire externe et
interne, ou la bande passante des moyens de communication.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
23
- la portabilité: facilité avec laquelle des produits logiciels peuvent être transférés
d'un environnement logiciel ou matériel à un autre.
- la facilité d'utilisation: facilité avec laquelle des personnes présentant des
formations et des compétences différentes peuvent apprendre à utiliser les
produits logiciels et à s'en servir pour résoudre des problèmes. C'est aussi la
facilité d'installation, d'opération et de contrôle.
- la fonctionnalité: étendue des possibilités offertes par un système.
- la ponctualité: capacité d'un système logiciel à être livré au moment désiré ou avant.
- la vérifiabilité: la facilité à préparer les procédures de recette, en particulier les
jeux de tests, ainsi que les procédures permettant de détecter les erreurs et de les
faire remonter, lors des phases de validation et d'opération, aux défauts dont elles
proviennent.
- l'intégrité: la capacité que présentent certains systèmes logiciels à protéger leurs
divers composants (programmes, données) contre les accès et modifications non
autorisés.
- la réparabilité: la capacité à faciliter la réparation des défauts.
- l'économie, cousine de la ponctualité: la capacité d'un système à être terminé dans
les limites de son budget, ou en-deçà.
3.2 LA MODULARITE
Les critères énoncés au paragraphe précédent influent sur la façon de concevoir un logiciel,
et en particulier sur l’architecture logicielle. En effet, beaucoup de ces critères ne sont pas
respectés lorsque l’architecture d’un logiciel est obscure, monolithique. Dans ces conditions,
le moindre changement de spécification peut avoir des répercutions très importantes sur le
logiciel, imposant une lourde charge de travail pour effectuer les mises à jour.
On adopte généralement une architecture assez flexible pour parer ce genre de problèmes,
basée sur les modules. Ceux-ci sont des entités indépendantes intégrées dans une
architecture pour produire un logiciel. L’ensemble des modules utilisés, ainsi que les
relations qu’ils entretiennent entre eux, est appelé système. L’intérêt de ce type de
conception est de concentrer les connaissances liées à une entité logique à l’intérieur d’un
module qui est seul habilité à exploiter ces connaissances. L’une des conséquences
immédiates est que lorsqu’une maintenance est à effectuer sur une entité logique, celle-ci
ne doit concerner qu’un seul module, ce qui confine la maintenance.
3.2.1 Deux méthodes de conception de modules
Si la définition de modules est une approche communément admise, il faut également une
méthode de construction de systèmes qui permette de déduire quels sont les bons modules.
Il existe deux grandes familles de méthodes modulaires:
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
24
- Les méthodes descendantes qui procèdent par décomposition de problème. Un
problème est ainsi divisé en un certain nombre de sous-problèmes, chacun de
complexité moindre. Cette division est ensuite appliquée aux sous-problèmes
générés, et ainsi de suite, jusqu’à ce que chacun des sous-problèmes soit trivial.
- Les méthodes ascendantes qui procèdent par composition de briques logicielles
simples, pour obtenir des systèmes complets. C’est en particulier le cas des
bibliothèques de sous-programmes disponibles avec tous les systèmes, langages,
environnements, ...
Les deux méthodes ne sont pas automatiquement à opposer et sont souvent utilisées en
même temps lors de la conception d’un logiciel. On peut cependant noter que l’approche
descendante ne favorise pas toujours la réutilisabilité des modules produits.
3.2.2 Quelques critères de qualité
En dehors de la démarche même menant aux modules, il est bon de préciser quelques
critères de qualité à respecter lors de la définition des modules:
- Compréhensibilité modulaire: les modules doivent être clairs et organisés de manière
compréhensible dans le système. Ceci implique que les modules doivent communiquer
avec peu de modules, ce qui permet de les "situer" plus facilement. De même,
l’enchaînement des différents modules doit être logique et on ne doit pas avoir, par
exemple, à utiliser plusieurs fois de suite un module pour produire une action
atomique.
- Continuité modulaire: ce critère est respecté si une petite modification des
spécifications n’entraîne qu’un nombre limité de modifications au sein d’un petit
nombre de modules, sans remettre en cause les relations qui les lient.
- Protection modulaire: ce critère signifie que toute action lancée au niveau d’un
module doit être confinée à ce module, et éventuellement à un nombre restreint de
modules. Ce critère ne permet pas de corriger les erreurs introduites mais de
confiner autant que possible les erreurs dans les modules où elles sont apparues.
Ces notions, qui sont assez intuitives, et qui découlent des réflexions menées autour de la
vie du logiciel, doivent être considérées lors de la définition et de la maintenance des
modules, même si elles ne sont pas accompagnées d’une méthodologie précise permettant
d’y arriver. C’est à travers elles que la qualité globale d’un logiciel peut être atteinte.
3.2.3 Les principes de définition
À partir des critères exposés ci-dessus, quelques principes de conception ont été retenus
pour la réalisation de modules:
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
25
- Interface limitée: le but n’est pas de borner les actions associées à un module, mais
de se restreindre à un nombre limité d’actions bien définies, ce qui supprime une
part des erreurs liées à l’utilisation de modules.
- Communications limitées: les communications entre modules, réalisées via leur
interface, doivent être limitées de façon quantitative. Ceci est une conséquence du
principe de modularité, qui est d’autant mieux respecté que les modules jouent leur
rôle. Si les échanges sont trop importants, la notion même de module devient floue,
limitant l’intérêt de cette technique.
- Interface explicite: les communications entre modules doivent ressortir
explicitement.
- Masquage de l’information: toutes les informations contenues dans un module doivent
être privées au module, à l’exception de celles explicitement définies publiques. Les
communications autorisées sont ainsi celles explicitement définies dans l’interface
du module, via les services qu’il propose.
- Les modules définis lors de la conception doivent correspondre à des unités
modulaires syntaxiques liées au langage de programmation. En clair, le module
spécifié ne doit pas s’adapter au langage de programmation, mais au contraire le
langage de programmation doit proposer une structure permettant d’implanter le
module tel qu’il a été spécifié. Par exemple, si le langage de programmation ne
permet pas d’effectuer le masquage de l’information (comme le langage C), il n’est
pas adéquat pour implanter les modules de manière satisfaisante selon les critères
de la conception objet.
Ce genre de critères proscrit ainsi des comportements tels que l’utilisation de variables
globales par exemple, qui va à l’encontre des principes énoncés. En effet, les variables
globales peuvent être utilisées et modifiées par n’importe quelle composante d’un
programme, ce qui complique d’autant la maintenance autour de ce genre de variables.
3.3 LA REUTILISABILITE
La réutilisabilité n’est pas un concept nouveau en informatique et a été utilisée dès les
balbutiements. En effet, les types de données à stocker sont toujours construits autour
des mêmes bases (tables, listes, ensembles) et la plupart des traitements comportent des
actions atomiques telles que l’insertion, la recherche, le tri, ... qui sont des problèmes
résolus en informatique. Il existe une bibliographie assez abondante décrivant des solutions
optimales à chacun de ces problèmes. La résolution des problèmes actuels passe par la
composition des solutions de chacun de ces problèmes basiques.
Les bibliothèques (systèmes, mathématiques, etc.) sont des bons exemples de réutilisabilité
et sont couramment utilisées par les programmeurs. Elles montrent cependant parfois leurs
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
26
limites. En effet, les fonctions qu’elles comportent ne sont pas capables de s’adapter aux
changements de types ou d’implantation.
La solution dans ce cas est de fournir une multitude de fonctions, chacune adaptée à un cas
particulier, ou d’écrire une fonction prenant tous les cas en considération. Dans un cas
comme dans l’autre, ce n’est que peu satisfaisant. C’est pourquoi la conception objet se
propose de formaliser un peu plus cette notion de réutilisabilité et de proposer de nouvelles
techniques pour l’atteindre pleinement.
3.3.1 Les principes de la réutilisabilité
Le paragraphe précédent a rappelé le notion de module, en insistant sur les avantages de la
conception modulaire, mais n’a pas donné de détails sur la conception même d’un module. On
conviendra ici qu’un "bon" module est un module réutilisable, c’est-à-dire conçu dans
l’optique d’être placé dans une bibliothèque à des fins de réutilisation.
Afin de marier modularité et réutilisabilité, quelques conditions nécessaires à la conception
de bons modules ont été définies:
- un module doit pouvoir manipuler plusieurs types différents. Un module de listes par
exemple doit pouvoir manipuler aussi bien des entiers que des types composites;
- de même, un module doit pouvoir s’adapter aux différentes structures de données
manipulées dotées de méthodes spécifiques. Il devra ainsi par exemple pouvoir
rechercher de la même manière une information contenue dans un tableau, une liste,
un fichier;
- un module doit pouvoir offrir des opérations aux clients qui l’utilisent sans que ceux-
ci connaissent l’implantation de l’opération. Ceci est une conséquence directe du
masquage de l’information préconisé. C’est une condition essentielle pour développer
de grands systèmes: les clients d’un module sont ainsi protégés de tout changement
de spécifications relatif à un module;
- les opérations communes à un groupe de modules doivent pouvoir être factorisées
dans un même module. Ainsi par exemple, les modules effectuant du stockage de
données, tels que les listes, les tables, etc. doivent être dotés d’opérations de même
nom permettant d’accéder à des éléments, d’effectuer un parcours, de tester la
présence d’éléments. Ceci peut permettre entre autres de définir des algorithmes
communs, tels que la recherche, quelle que soit la structure de données utilisée pour
stocker les données.
3.3.2 De nouvelles techniques
L’idée, afin de faire cohabiter les principes inhérents à la modularité et à la réutilisabilité,
est d’utiliser la notion de paquetage, introduite par des langages tels que Ada ou Modula-2.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
27
Un paquetage correspond à un regroupement, au sein d’un même module, d’une structure de
données et des opérations qui lui sont propres. Ceci satisfait en particulier les critères de
modularité, en isolant chaque entité d’un système, ce qui la rend plus facile à maintenir et à
utiliser. En ce qui concerne les critères de réutilisabilité, il est possible d’aller encore un
peu plus loin. Nous introduisons ici de nouvelles notions qui apparaissent avec les paquetages
et vont permettre de franchir ce pas:
- La surcharge: cette notion prévoit que des opérations appartenant à des modules
différents peuvent être associées au même nom. Les opérations ne sont donc plus
indépendantes, elles prennent leur signification contextuellement en fonction du
cadre dans lequel elles sont utilisées. Parmi ces opérations, on trouve les fonctions
mais également les opérateurs. Cela peut permettre, par exemple, de définir une
fonction insérer dans chaque module de stockage, permettant d’écrire de manière
uniforme: insérer(élément, container) quelque soit le type de container (liste,
tableau, fichier, ...).
- La généricité: cette notion permet de définir des modules paramétrés par le type
qu’ils manipulent. Un module générique n’est alors pas directement utilisable: c’est
plutôt un patron de module qui sera "instancié" par les types paramètres qu’il
accepte. Cette notion est très intéressante, car elle va permettre la définition de
méthodes (façon de travailler) plus que de fonctions (plus formelles). Ces définitions
et ces nouveaux outils vont nous permettre de définir de nouvelles manières de
concevoir des systèmes informatiques.
3.4 PRINCIPES DE CONCEPTION OBJET
3.4.1 Introduction
Après avoir énuméré les qualités souhaitables nécessaires à l’élaboration d’un système de
qualité, il nous reste maintenant à déterminer les règles de construction de tels systèmes.
D’un point de vue général, la construction d’un système informatique se résume par la
formule:
Algorithmes + Structures de données = Programme
Le concepteur d’un système informatique a donc deux grandes options pour l’architecture
d’un système: orienter sa conception en se basant sur les données ou sur les traitements.
Dans les méthodes de conception par traitements, qui constituent l’approche traditionnelle,
la base de la réflexion est effectuée autour des traitements. Le concepteur considère ainsi
les tâches que doit accomplir le programme, en décomposant celui-ci en une série de tâches
simples (approche descendante) ou en le construisant par composition de traitements
(fonctions) disponibles (approche ascendante). On peut faire les remarques suivantes sur ce
type d’approche:
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
28
- Les modules trouvés par cette approche se trouvent souvent être des modules ad
hoc, adaptés au type de problème posé au départ et ne permettant que peu
d’extensibilité du système obtenu et que peu de réutilisabilité.
- Les traitements définis ne prennent pas assez en considération les structures de
données sous-jacentes, qui se retrouvent partagées entre plusieurs modules. Il
devient difficile dans ces conditions de maintenir et de protéger ces structures de
données.
- Il n’est pas toujours évident d’identifier les traitements, ou leur enchaînement,
impliqués dans un système informatique. Sur des cas simples, cela reste aisé, mais
sur des cas plus compliqués (un système d’exploitation par exemple) cela ne permet
pas de déduire une architecture naturelle.
- Les traitements sont généralement beaucoup moins stables que les données. En
effet, un programme une fois terminé se verra souvent étendre par de nouvelles
fonctionnalités, parfois en concordance avec les objectifs premiers du programme,
mais pas toujours. Par exemple, n’importe quel programme devant initialement
calculer des fiches de paie ou archiver des données devra par la suite effectuer des
statistiques ou être capable de répondre interactivement alors qu’il était prévu
initialement pour fonctionner chaque nuit ou chaque mois. Les données en revanche
sont beaucoup plus stables, et si des modifications interviennent dans la
représentation des données, elles ne changent pas radicalement la représentation
des données.
Bien sûr, les approches basées sur les traitements ont quand même quelques avantages,
dont celui d’être relativement intuitives et facilement applicables. Elles peuvent à ce titre
être utilisées pour la réalisation d’applications de taille raisonnable. Elles se révèlent
cependant rapidement inadaptées lors de la réalisation de systèmes plus conséquents ou
sensibles aux changements de spécification.
3.4.2 La conception par objets
Afin d’établir de façon stable et robuste l’architecture d’un système, il semble maintenant
plus logique de s’organiser autour des données manipulées, les objets. En effet, les données
étant de par leur nature plus stables que les traitements, la conception en est simplifiée.
De plus, il apparaît que la conception par traitement ne favorise pas l’utilisation des
principes de qualité mis en évidence, tels que la modularité ou la réutilisabilité. Il reste
maintenant à éclaircir les fondements mêmes de la conception par objets. Dans cette
optique, voici une première définition de la conception par objets (B. Meyer):
"La conception par objet est la méthode qui conduit à des architectures logicielles fondées
sur les OBJETS que tout système ou sous-système manipule"
ou encore
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
29
"Ne commencez pas par demander ce que fait le système, demandez À QUOI il le fait !"
La spécification d’un système va donc maintenant s’axer principalement sur la détermination
des objets manipulés. Une fois cette étape réalisée, le concepteur n’aura plus qu’à réaliser
les fonctions de haut-niveau qui s’appuient sur les objets et les familles d’objets définis.
C’est cette approche, préconisant de considérer d’abord les objets (c’est-à-dire en
première approche les données) avant l’objectif premier du système à réaliser, qui permet
d’appliquer les principes de réutilisabilité et d’extensibilité. Reste maintenant à décrire
comment trouver, décrire et exploiter les objets !
3.4.3 Détermination des objets
Pour la détermination même des objets, il faut simplement se rattacher aux objets
physiques ou abstraits qui nous entourent. Typiquement, dans un programme de fiches de
paie, le bulletin de salaire, l’employé, l’employeur, la date d’émission, etc. sont des objets.
Les objets vont couvrir tous les types d’entités, des plus simples aux plus complexes, et
sont à ce titre partiellement similaires aux structures utilisées dans des langages tels que C
ou Pascal. Mais ils sont également dotés de nombreuses propriétés supplémentaires très
intéressantes.
Pour représenter ou décrire de tels objets, nous allons nous intéresser non pas aux objets
directement mais aux classes qui les représentent. Les classes vont constituer le modèle
dont seront issus chacun des objets, appelés aussi instances de classes. Et pour décrire ces
classes, nous n’allons pas faire intervenir des champs (ou attributs — des données) comme
cela pouvait être le cas auparavant, nous allons les spécifier formellement en fonction des
services (ou fonctions) qu’elles offrent. Ceci apporte l’avantage de se détacher de la
représentation physique des données et de raisonner uniquement sur les services qu’elles
doivent offrir. À titre d’exemple, voici la description simplifiée d’une PILE:
- La partie Type indique le type de données décrit, paramétré ici par le type T. Ceci
permet de définir un type générique de pile, qui sera ensuite instancié lors de son
utilisation par le type d’objets que l’on souhaite empiler: des entiers, des caractères,
d’autres types...
- La partie Fonctions décrit les services disponibles sur les piles. Ces fonctions sont
exprimées sous une forme mathématique, décrivant les paramètres nécessaires à
l’utilisation de la fonction et les résultats fournis.
- La partie Préconditions précise les conditions d’utilisation des fonctions définies à
l’aide de /→, qui ne sont pas définies pour toutes les valeurs de paramètres
possibles.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
30
TYPE
PILE [T]
FONCTIONS
- créer: PILE [T]
- vide: PILE [T] → BOOLEAN
- sommet: PILE [T] /→ T
- empiler: PILE [T] x T → PILE [T]
- dépiler: PILE [T] /→ PILE [T]
PRECONDITIONS
Pour tout p : PILE [T]
- dépiler (p: PILE [T]) défini ssi not vide (p)
- sommet (p: PILE [T]) défini ssi not vide (p)
AXIOMES
Pour tout x: T, p: PILE [T],
- A1 : sommet (empiler (p, x)) = x
- A2 : dépiler (empiler (p, x)) = p
- A3 : vide (créer)
- A4 : not vide (empiler (p, x))
D’autres parties peuvent être ajoutées:
- une partie Postconditions qui s’utilise comme la partie Préconditions, et qui précise
les propriétés des résultats qui seront fournis par les différentes fonctions de la
classe;
- une partie Axiomes qui précise les propriétés que chacun des objets de type PILE
doit respecter à tout moment.
Cette méthode est appliquée sur chacun des types d’objets recensés nécessaires à
l’élaboration du système. L’approche est conforme à la philosophie énoncée car le module
est bien défini en fonction des services qu’il offre, c’est-à-dire à partir de son interface.
Les principes de masquage d’information sont notamment bien respectés: l’utilisateur du
module ne sait pas quelle est la représentation physique des données. Il connaît juste les
services offerts par la classe. Cette démarche s’avère nécessaire pour le développement
d’un système conséquent et respecte les règles de qualités établies auparavant. Un des
intérêts de ce type de démarche est par ailleurs de permettre d’améliorer les principes de
réutilisabilité.
Cette étape de spécification de type devance celle de l’implantation de la classe
correspondant au type. Et toute classe définie pourra être potentiellement étendue par une
nouvelle classe qui la spécialisera. Ce mécanisme, appelé héritage, est étudié en détails dans
la deuxième partie au chapitre 7.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
31
3.4.4 Conclusion
Pour conclure, faisons le point sur les objectifs souhaités pour obtenir du logiciel de qualité
et les techniques introduites pour atteindre ce but:
- modularité: cette technique permet de découper un système complet en un ensemble
de modules qui sont indépendants;
- réutilisabilité: les classes produites peuvent être regroupées en bibliothèques et
être réutilisées. L’héritage permet également de réutiliser des classes en les
spécialisant;
- abstraction de données et masquage de l’information: les classes n’indiquent pas la
représentation physique des données qu’elles utilisent, mais se contentent de
présenter les services qu’elles offrent. Le concept de généricité permet encore
d’accroître cette abstraction, en proposant des classes qui sont paramétrées par
des types de données;
- extensibilité: les classes sont définies en terme de services. Dès lors, un
changement de représentation interne de données ou une modification de celles-ci
n’altère pas la façon dont les autres classes les utilisent.
- lisibilité: l’interface (documentée) permet d’avoir un mode d’emploi clair et précis de
l’utilisation d’une classe, qui est d’autant plus clair que l’implantation des classes est
cachée.
3.5 TERMINOLOGIE
Ce paragraphe se propose de faire le point sur toutes les notions abordées et d’introduire
le vocabulaire spécifique à la conception objet.
- De part la prédominance de la maintenance dans la conception d’un logiciel, il est
essentiel d’utiliser des règles permettant de construire du logiciel de qualité.
- Dans ce contexte, certains critères tels que la réutilisabilité et l’extensibilité ont
été définis, répondant en partie à cette exigence.
- La modularité est une approche qui permet de parvenir à ces objectifs. Ce concept
s’applique à l’implantation, mais surtout à la conception.
- La programmation est une tâche qui s’avère assez répétitive et qui nécessite de se
doter de techniques favorisant la réutilisabilité. Deux de ces techniques ont été
présentées: la généricité et la surcharge.
- L’architecture d’un système peut se concevoir autour des traitements ou autour des
données. Si la première approche est assez intuitive et facile d’utilisation, elle ne
convient que pour des systèmes de taille limitée. En revanche, la deuxième approche
permet de construire des systèmes plus stables et répond mieux aux objectifs de
qualité définis.
- La conception par objets se propose de décrire un système à travers les classes
d’objets qui sont manipulées. Ces classes sont regroupées en familles de classes qui
peuvent être implantées à travers l’héritage.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
32
- Une classe est une structure regroupant des données appelées attributs ou variables
d’instance et des fonctions disponibles, appelées méthodes.
- Une instance de classe est appelée objet.
- La classe dont est issu un objet est appelée type de l’objet.
- Une classe A est appelée client d’une classe B si A contient un attribut de type B. La
classe B est alors appelée fournisseur de A.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
33
Chap4 : POUR DES CLASSES DE QUALITE
4.1 PRINCIPES GENERAUX DE DETERMINATION DES CLASSES
4.1.1 Introduction
Les classes correspondent fondamentalement à des objets réels du système que l'on
modélise. Ce système peut n'avoir qu'un rapport lointain avec toute réalité physique, et
dans ce cas les classes représentent des abstractions de conceptions intellectuelles. On ne
s'étonnera donc pas de ce que le choix des classes effectives lors de la réalisation d'un
système donné laisse beaucoup de liberté au programmeur. Les recommandations qui suivent
sont celles données par Bertrand Meyer dans sa présentation du langage Eiffel.
Dans ce cadre, on peut distinguer entre les classes représentant effectivement des objets
concrets du système (l'imprimante, l'utilisateur, par exemple), et celles qui servent
d'intermédiaire à la conception, en apportant des fonctionnalités auxiliaires. On observe
trois niveaux fondamentaux de classes:
- les classes externes, partie émergée de l'iceberg, fournies à l'utilisateur du
système qui le font apparaître précisément comme un modèle d'une réalité, fût-elle
hypothétique,
- les classes représentant des ressources de haut niveau nécessaires au
fonctionnement du système (structure d'arbre pour la forme intermédiaire d'une
compilation par exemple),
- et enfin celles décrivant les types de données fondamentaux, qui peuvent être
comparées à un langage de programmation "personnalisé".
4.1.2 L'accès équitable aux services
Une classe implante un ensemble de services offerts indistinctement par toutes ses
instances. Dans un langage comme C++, l'encapsulation des fonctions dans les classes est
effectivement réalisée par les classes et non par les instances, puisqu'aucun mécanisme ne
contrôle ("dynamiquement") les droits d'accès des instances aux services offerts par la
classe.
Il est donc utile de voir une classe comme un concept offrant ses services de manière
indifférenciée ("équitable") au travers de toutes ses instances. En particulier, aucun
séquencement dans l'utilisation des services d'une classe n'est contraint a priori, chaque
opération étant considérée comme autonome et indépendante des autres. Cela conduit à une
simplification par rapport à l'approche fonctionnelle classique ("de haut en bas"). Ici, il
n’est pas besoin de se concentrer sur une fonction principale ("main") à implanter. De plus,
les changements dans la spécification de séquences étant très fréquents, on évite le coût
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
34
associé qui peut être grand dans un système complexe conçu de manière fonctionnelle. La
spécification de l'ordre dans lequel les services sont rendus par une classe est donc
séparée de l'implantation des mécanismes fondamentaux.
4.1.3 L'ajout (théoriquement) illimité de services
Une fois acquise l'équité des services, il n'existe plus de limite théorique (autre que le bon
sens) au nombre de services qu'une classe peut fournir. C'est d'ailleurs observé
pratiquement, dans la mesure où la conception de bibliothèques de programmes C++ produit
en général des classes avec de très nombreuses fonctionnalités.
L'évolution naturelle de l'interface de programmation d'une classe est donc d'incorporer
de façon progressive un nombre croissant de méthodes - autant de services - que la classe
est décemment en mesure de fournir. Noter toutefois qu'il est rigoureusement interdit au
programmeur d'une classe d'implanter un service non requis par la spécification. Par
exemple, une classe "Liste" peut, mais en aucun cas ne doit, implanter le service d'ajout
d'élément en queue. Si ce n'est pas requis, le programmeur de la liste doit impérativement
se résoudre à ne pas l'implanter.
De plus, les principes modernes de conception recommandent que les interfaces de
programmation respectent une propriété de "forte cohésion". Selon ce principe, les
services offerts par une classe doivent être fortement reliés, une classe ne pouvant pas
offrir des fonctionnalités sans rapport les unes avec les autres.
4.1.4 Conception de bas en haut
Une conception n'est jamais totalement réalisée de bas en haut bien sûr, mais la
réutilisation de composants renforce progressivement l'impression par laquelle un système
est conçu à partir de briques de base nouvelles ou non. Ces briques s’appellent aujourd’hui
des composants logiciels, en anglais "software components". Penser en termes de
composants devient rapidement une habitude pour le programmeur et le concepteur objet.
Cela n'est possible toutefois que si les composants logiciels sont authentiquement
réutilisables, ce qui suppose le respect de certaines règles lors de leur définition. Si tel
n'est pas le cas, on retrouve un schéma plus classique, car une application nouvelle doit
malgré tout redéfinir ses éléments de bas niveau, dont les spécificités dépendent du cadre
courant. Le savoir faire du concepteur s’exprime donc pour une bonne part dans sa capacité
à produire des programmes acceptés par les autres comme tels.
4.1.5 Influence de l'utilisation des objets sur la conception
L'utilisation de classes a pour effet de répartir les décisions tout au long du cycle de
conception. Des composants indépendants (réutilisables ou non) peuvent être conçus très
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
35
tôt dans le processus sans nécessiter d'abstraction "fumeuse", ni obérer le processus
complet ou la performance du résultat final.
Des classes connues, et fiables, augmentent au fil du temps l'arsenal de programmation
disponible. Ces classes sont connues de façon abstraite par les concepteurs et les
programmeurs, un peu comme un mécanicien sait qu'il peut placer une pompe à essence de
tel ou tel modèle sur un moteur, sans pour autant connaître sa structure interne. Ainsi, il
est possible de se concentrer sur des concepts de plus haut niveau, sachant les éléments
dont on dispose.
L'habitude de la conception de classes "outil" conduit également à reporter l'effort de
conception de certains composants (spécifiques) du système final, sachant que l'on connaît
d'avance la forme que l'on pourra leur donner.
4.2 CRITERES DE QUALITE DES CLASSES
D'après Bertrand Meyer, la valeur d'une méthode de spécification / conception de
programmes orientés objet est mesurée par les critères suivantes:
- facilité de décomposition du problème en classes,
- facilité de composition des classes,
- facilité de compréhension des composants,
- modularité constructive,
- protection modulaire.
4.2.1 La décomposition en classes
Les méthodes modernes de modélisation orientée objet (UML) conduisent à une
présentation structurée qui peut être convertie de façon très directe en classes, méthodes
et fonctions d'un langage de programmation objet. A un niveau élevé du système, la
conception ne pose donc pas trop de difficultés en ce qui concerne la décomposition en
classes.
4.2.2 La composition des classes
Une classe interagit avec certaines classes qui constituent des ressources dont elle est
cliente. Bien sûr, pour permettre la réutilisation facile d'une classe dans de nouveaux
projets, comme ressource de nouvelles classes, il faut qu'elle soit aussi autonome que
possible, et notamment qu'elle ne requière pas de ressources apparemment sans objet avec
sa signification. Les différentes classes auxquelles une conception a abouti doivent pouvoir
être utilisées au besoin de façon combinée ou séparée, et doivent donc interagir aussi peu
que strictement nécessaire. Ce jugement qualitatif peut être prolongé de façon
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
36
quantitative. En particulier, l'utilisation d'une classe ne doit pas nécessiter l'utilisation
d'un trop grand nombre d'autres classes. Lorsque c'est le cas, il y a signe d'un défaut
potentiel de conception du système.
Notons qu'un système, de même qu'une bibliothèque de composants, décrit en général une
classe dite d'environnement (on appelle aussi ces classes des "utility classes"), dont le rôle
est de servir de conteneur pour toutes les variables qui se seraient trouvées globales en
programmation classique. Cette classe viole par essence le principe précédent, mais ce n'est
pas dommageable car elle n'est, par essence, pas réutilisable, du moins pas au même titre
que les classes "outil" définies par la bibliothèque ou le système. Le programmeur ne doit
plus recourir à des variables et fonctions globales!
4.2.3 La lisibilité
Le fonctionnement de chaque composant doit être intelligible sans nécessiter de
comprendre le mécanisme de trop nombreux autres. Cela exclut en particulier toute forme
de programmation séquentielle, où un résultat n'est atteint qu'après l'exécution
consécutive de plusieurs modules, ayant chacun des effets de bord sur le système. Chacun
des programmes à exécuter n'est pas dans ce cas indépendant de ses partenaires, ce qui
est dommageable à leur compréhension.
4.2.4 La modularité
L'extension des fonctionnalités d'un système doit être modulaire c'est-à-dire que l'ajout
d'une fonctionnalité requiert de modifier un petit nombre de programmes et, en aucun cas,
une refonte totale du système.
Un exemple traditionnel et très simple de cette considération est l'utilisation de fichiers
(externes) de définition de constantes. Les constantes sont définies de manière
symboliques et utilisées comme tel, mais ne figurent jamais en clair dans les programmes.
Ainsi, changer une constante ne demande pas de modifier tous les programmes. Atteindre la
modularité dans les programmes se fait en poussant cette logique à son terme.
4.2.5 La protection modulaire
Une méthode de conception satisfait le critère de protection modulaire lorsqu'une
éventuelle erreur se produisant lors de l'exécution au niveau d'un composant ne se propage
pas à d'autres composants ou seulement à peu d'entre eux. Une erreur doit être détectée
et traitée par le composant ou provoquer l'arrêt du programme dans le composant.
Dans la pratique, on ne peut empêcher par exemple un composant d'activer une de ses
ressources dans des conditions erronées (l'erreur n'est alors pas détectée par le
composant fautif). Mais on peut détecter cette erreur avant l'exécution de la méthode
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
37
voulue de la ressource, par un jeu complet de tests de préconditions. Cette technique
permet de détecter précocement les conditions d'erreur et d'être capable d'incriminer de
façon immédiate le responsable.
4.2.6 Les règles à suivre
Les principes suivants n'ont pas valeur de méthode, mais peuvent guider dans le travail de
conception d'ensembles de classes de qualité:
- chaque classe doit communiquer avec aussi peu d'autres classes que possible. En
particulier, une classe ne doit pas communiquer avec une autre non nécessaire à
l'atteinte de sa sémantique.
- chaque classe doit communiquer à l'aide de messages aussi abstraits que possible. En
termes de fonctions, cela signifie que l'on n'implante pas de fonctions comportant
plus qu'un nombre raisonnable d'arguments (4 ?). Si ce n'est pas le cas, on doit
pouvoir grouper des paramètres dans des classes qui seront elles passées en
paramètres.
- lorsque deux classes communiquent, cela doit être explicite à la lecture des codes de
chacune. Notamment, un partage d'information ne devrait pas se faire par un effet
de bord invisible (partage d'un pointeur sur un tableau de caractères par exemple).
Si un tel processus est nécessaire, le source décrivant la classe doit faire clairement
état de cette information.
- toute information d'une classe qui ne nécessite pas explicitement d'être
communiquée doit être privée.
4.2.7 Évaluation des décompositions
Les situations qui permettent de découvrir des défauts dans la conception sont les
suivantes:
- des classes trop fortement couplées (pas indépendantes),
- des classes interconnectées avec trop d'autres classes,
- des classes échangeant des informations trop nombreuses, ou insuffisamment
structurées avec d'autres classes,
- des méthodes ayant trop de paramètres.
Bien souvent, l'existence de classes échangeant de grandes quantités d'informations est un
signe de l'absence dans le système d'une classe réalisant une abstraction de ces messages.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
38
4.3 LA REUTILISABILITE: QUALITE ESSENTIELLE DES CLASSES
4.3.1 Programmer "utilisable"
Avant de parler de réutilisabilité, il faut bien sûr garantir qu’une classe soit simplement
utilisable. Les critères d’usabilité sont nombreux et en appellent pour la plupart au bon
sens. Les points d’achoppement les plus importants sont les suivants:
- une mauvaise documentation,
- l’existence d’effets de bord,
- une interface de programmation non homogène, mal conçue,
- l’existence de mécanismes inutiles non "débranchables" (la classe en fait trop)
Il faut qu’un programmeur qui n’a pas écrit ce programme trouve un avantage à l’utiliser tel
quel si le besoin se présente.
4.3.2 Qu’entend on par réutilisabilité? ADAPTABILITE!
Un programme est rarement satisfaisant comme tel. Plus il sera apte à se plier aux
exigences d’un grand nombre de contextes, plus il sera effectivement réutilisable. En gros,
il en va des programmes comme des êtres vivants: s’adapter est une condition de survie. La
question posée au concepteur de logiciel est alors de déterminer le niveau de réutilisabilité
requis par le projet. La réutilisabilité n’est pas soudain rendue possible par l’émergence des
objets. Tout au plus, celle-ci rend-elle certaines formes de réutilisabilité plus faciles à
mettre en oeuvre.
4.3.3 Deux techniques de réutilisation
4.3.3.1 L’utilisation de pointeurs vers des abstractions
Une classe peut offrir une capacité de stockage et de manipulation d’objets inconnus par
elle au moyen de pointeurs vers des abstractions:
- éventuellement du type le plus général: void * en C++
- habituellement vers une classe de base abstraite dont l’interface de programmation
virtuelle est connue: GraphicObject *.
Cette réutilisabilité est dite dynamique. Le compilateur ne peut fournir que peu de
diagnostics sur la correction des programmes. C’est à l’exécution tout au plus que certaines
vérifications pourront être faites. Dans le premier cas, pour interagir avec l’objet pointé, il
faut en général permettre également d’enregistrer un pointeur vers une fonction adéquate.
Le second cas est en fin de compte une utilisation élégante de C++ pour faire la même
chose.
INTRODUCTION A LA METHODOLOGIE OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
39
4.3.3.2 Réutiliser par héritage
Le cas précédent est celui de la réutilisabilité dynamique: on adapte une instance à un
besoin en lui faisant transporter des informations sur des objets inconnus par elle.
L’héritage permet de mettre en oeuvre une forme de réutilisabilité statique: on adapte un
type en lui faisant transporter des informations nouvelles ou en contraignant son
comportement ou les deux. Ce nouveau type est alors connu du compilateur qui fait toutes
les vérifications possibles. Dans tous les cas, l'utilisation de l’héritage n’est envisageable
que si la règle fondamentale est respectée: A hérite de B si et seulement si on peut dire
que tout A est un B. On distingue deux modalités fondamentales d’héritage, qui sont
souvent combinées.
Héritage pour extension
Dans ce cas, la classe d’origine est adaptée par ajout d’informations (par exemple par ajout
d’un membre, mais aussi héritage multiple si c’est permis par le langage). Normalement, ces
informations nouvelles sont indépendantes de la classe d’origine (et inconnues de celle-ci).
En tout cas, elles ne doivent pas interférer avec ces dernières.
Héritage pour restriction
Ici, la classe d’origine décrit un sur-ensemble des objets que l’on veut manipuler. On
souhaite en fait réduire les valeurs admissibles pour les données membres ou bien limiter
les possibilités d’arguments à des fonctions membres, en général les deux. Le fait de
définir un nouveau type permet de le manipuler directement et d’éviter de multiplier les
vérifications dans les programmes qui l’utilisent.
4.3.4 Quoi utiliser ?
Comme toujours, nécessité fait loi. En pratique on doit respecter les deux règles suivantes:
- une bibliothèque extensible doit toujours proposer les classes de base qui peuvent
être adaptées par héritage et donc ne pas contraindre à réutiliser obligatoirement
de façon dynamique. De toutes façons, ces classes sont les plus générales et
découlent normalement d’une bonne conception.
- si nécessaire, les classes génériques dynamiques sont implantées sur la base des
précédentes. Même si elles sont absentes, il est en général facile au programmeur
client de réaliser celles dont il a besoin (alors qu’il lui aurait été impossible de
supprimer un membre void* d’une classe existante).
PARTIE 2 :
ET PROGRAMMATION OBJET
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
41
Cette partie constitue le support illustrant la programmation objet avec le langage de
programmation C++. Ce support sera utilisé dans le cours magistral et les séances de
travaux pratiques. Lors des séances de cours et de travaux pratiques, un complément de
matière sera abordé. Donc le présent document ne peut être considéré comme complet.
Ce cours s’appuie sur des notions préliminaires de programmation procédurale. La syntaxe
du langage C++ ainsi que les notions fondamentales de programmation ont été étudiées
durant le cours de structure de données et algorithme en Ba1 et sont supposées acquises.
Durant ce cours, les bases de la programmation orientée objet sont abordées. Nous
commençons par un rappel sur les structures auxquelles nous ajoutons les fonctions
membres. Avec le chapitre sur les classes, généralisation de la notion de type défini par
l'utilisateur, nous abordons véritablement les possibilités de "programmation (orientée)
objets". Nous introduisons la définition d’une classe, la notion de méthode, l’encapsulation
et le type de membre (publique, privé, protégé), ainsi que les notions très importantes de
constructeur et de destructeur indispensables à ce type de programmation. Nous montrons
aussi comment définir une famille de fonctions ou de classes paramétrées par un ou
plusieurs types grâce à la notion de patron de fonctions et de classes.
Par la suite nous proposons un principe propre à la programmation orientée objet :
l'héritage qui permet de créer une nouvelle classe à partir d'une classe existante. Un autre
concept des langages objet qui découle directement de l'héritage est le polymorphisme.
Pour terminer, nous esquissons la bibliothèque STL (Standard Template Library :
Bibliothèque standard générique.) qui est certainement l’un des atouts de C++ et qui peut
être considérée comme un outil très puissant.
Comme la programmation par l’exemple vaut mieux que les beaux discours, nous avons
illustré et agrémenté ces notes, comme les Slides du cours et des séances d'exercices, de
nombreux programmes permettant à l’étudiant d’assimiler les principales notions
présentées. Tous les programmes sont fournis complets avec le résultat de leur exécution.
Le code source de ces programmes est accessible sur le site Web de la Faculté. Nous ne
pouvons qu’insister sur la quasi-nécessité de les tester, et mieux encore de les comprendre
et de les modifier. En ce qui concerne les exercices, nous encourageons l’étudiant à les
programmer, car la programmation est un art qui s’apprend en pratiquant.
Vous l’aurez compris, ces notes n’ont pas la prétention d’expliquer en détail tous les
concepts du langage C++, mais se proposent d'être une bonne introduction à la
programmation orientée objet. Cette partie du syllabus ne peut donc être considéré comme
autosuffisante pour apprendre tout le langage C++. En effet, la matière est assez vaste et
le temps attribué est court. Mais la programmation orientée objet, sera approfondie, pour
certain, durant le cours de 3ème année en préparation au master.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
42
Nous renvoyons, néanmoins le lecteur désireux d'en savoir plus à l'un des très nombreux
ouvrages de référence sur le C++, notamment à:
Claude Delannoy Programmer en langage C++, Eyrolles 2007
Très bon livre pour commencer le C++, livre très pédagogique et assez complet.
Plusieurs chapitres sont dédiés aux composants de la bibliothèque standard du C++.
John R. Hubbard Programmer en C++, Collection Schaum‘s Ediscience, Dunod 2002.
Un ouvrage expliquant les concepts de base de la programmation C++ à l'aide
d'exemples. On y trouve entre autre: Classes, Surcharge d'opérateurs, Composition et
héritage, Modèles et itérateurs, C++ standard et les vecteurs, Classes conteneur.
John R. Hubbard Structures de données en C++, Collection Schaum‘s Ediscience,
Dunod 2003.
Ce volume est complémentaire au précédent. Près de 455 exercices et problèmes
résolus sont décortiqués pour une meilleure compréhension. On y trouve entre autres :
Classes, Listes, Tables, Arbres, classes conteneurs standard, algorithmes génériques.
Sur le Web : http://cpp.developpez.com/cours/cpp/
Cours, tutoriels, livres électroniques et Docs sur C++ :
http://www.developpez.com/c/megacours/book1.html/
Mega Cours C++ en français
Si vous notez la moindre erreur ou si vous souhaitez me proposer vos suggestions, n'hésitez
pas à le faire à l’adresse E-mail suivante :
[email protected] www.ig.fpms.ac.be/~benjellounm
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
43
Chap5: LES CLASSES EN PRATIQUE
Le but de ce chapitre est d’introduire les concepts de base des objets. Ainsi, les termes de
"programmation (orientée) objets", de classe, d'objet, de méthode, d’encapsulation et type
de membre (publique, privé) vont être définis. Ce chapitre n’a pas la prétention d’expliquer
en détail ce type de conception, mais se propose d’en introduire les idées maîtresses en
préambule à l’étude du langage C++. Afin d’illustrer par la programmation ces concepts, ce
chapitre est agrémenté de nombreux programmes permettant à l’étudiant d’assimiler les
principales notions et de comprendre quelques subtilités de cette matière. Pour plus de
précisions, le lecteur pourra se reporter aux références.
Des notions comme l'héritage, le polymorphisme, … seront abordées dans d’autres chapitres
de ce syllabus.
Mais avant d’aborder les classes, nous rappelons dans cette section l'utilisation des
structures en C++ vu en 1ère année de bachelier. Certaines notions vont être rappelées et
d'autres complètement nouvelles comme les fonctions membres, seront introduites.
5.1 PRINCIPES DES STRUCTURES
Une structure est un ensemble de variables (de types éventuellement différents),
définissant un nouveau type sous un seul nom, adapté à une gestion spécifique et à une
manipulation facile des données. Elle se définit par un identificateur suivant le mot-clé
struct, ainsi que par une liste de champs ou membres définis par un identificateur d'un type
donné. Par exemple, pour gérer les coordonnées d'un point (abscisse et ordonnée) ou des
étudiants, on pourra définir les types suivants : struct point {
int x;
int y;
};
struct Etudiant {
int Id;
string Nom;
};
x et y sont des champs ou des membres de la structure point; Nom et Id sont des champs
de la structure Etudiant.
On déclare ensuite des variables du type point (Etudiant) par des instructions telles que :
struct point a, b; // a et b deux variables de type structure point
struct Etudiant Etud1, Etud2;
Etudiant Etud[10] ; // Etud est un tableau de 10 éléments de type structure Etudiant
Ces déclarations réservent l'emplacement pour des structures nommées a et b de type
point, Etud1 et Etud2 de type Etudiant et finalement un tableau de 10 éléments de type
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
44
Etudiant. L'accès aux membres de a, b ou de Etud1 et Etud2 se fait à l'aide de
l'opérateur point (.). Par exemple, a.y désigne le membre y de la structure a et Etud1.Nom
désigne le nom du Etudiant Etud1.
Rq
En C++ le mot clé struct n'est pas nécessaire devant la structure lors de la déclaration.
En C++, nous allons pouvoir, dans une structure, associer aux données constituées par ses
membres des méthodes qu'on nommera "fonctions membres".
Supposons qu’en plus de la déclaration des données Id et Nom, nous souhaitions associer à
la structure Etudiant deux fonctions :
void Saisie() : pour saisir l’identifiant et le nom d’un seul Etudiant ;
void Affiche() : pour afficher les informations d’un seul Etudiant.
Voici comment nous pourrions éventuellement déclarer la structure Etudiant :
struct Etudiant {
int Id;
string Nom;
void Saisie() ;
void Affiche() ;
};
Dans la déclaration d'une structure, il est permis (mais généralement peu conseillé)
d'introduire les données et les fonctions dans un ordre quelconque. (Nous recommandons de
placer systématiquement les données avant les fonctions).
Le programme suivant reprend la déclaration du type Etudiant, la définition de ses
fonctions membres et un exemple d’utilisation dans la fonction main :
#include <iostream>
#include <string>
using namespace std ;
struct Etudiant {
int Id;
string Nom;
void Saisie() ; // Saisir un élément
void Affiche() ;
};
// ----- Définition des fonctions membres du type Etudiant ----
void Etudiant::Saisie() {
cout << "donnez un identifiant : ";
cin >> Id;
cout << "donnez un nom : " ;
cin >> Nom;
}
void Etudiant::Affiche() {
cout << " Identifiant ="<< Id << endl;
cout << " Son nom : "<< Nom << endl;
}
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
45
void main(){
Etudiant Etud3, Etud[3];
cout << "----> Etud012 ---->"<< endl;
for(int i=0 ; i<3 ; i++){
Etud[i].Saisie();
Etud[i].Affiche();
}
cout << "----> Etud3 ---->"<< endl;
Etud3.Saisie(); // accès à la fonction Saisie du Etudiant Etud1
Etud3.Affiche();
}
Programme 5.1.
SOLUTION
----> Etud012 ---->
donnez un identifiant : 0
donnez un nom : Etud0
Identifiant =0
Son nom : Etud0
donnez un identifiant : 1
donnez un nom : Etud1
Identifiant =1
Son nom : Etud1
donnez un identifiant : 2
donnez un nom : Etud222
Identifiant =2
Son nom : Etud222
----> Etud3 ---->
donnez un identifiant : 33
donnez un nom : ETUD33
Identifiant =33
Son nom : ETUD33
Dans l'en-tête, le nom de la fonction Etudiant::Saisie(), signifie que la fonction Saisie() est
celle définie dans la structure Etudiant. En l'absence de ce "préfixe" (Etudiant::), nous
définirions effectivement une fonction nommée Saisie(), mais celle-ci ne serait plus
associée à Etudiant; il s'agirait d'une fonction "ordinaire" et non plus de la fonction
membre de la structure Etudiant.
Rq Dans les fonctions Saisie() et Affiche(), il faut remarquer l’utilisation de Id et du Nom qui ne sont ni des arguments de fonctions ni des variables locales. En fait, ils désignent les membres Id et Nom de la structure Etudiant. L’association est réalisée par Etudiant:: de l’en-tête des fonctions.
Voici un autre exemple utilisant cette fois ci la structure point.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
46
#include <iostream>
using namespace std ;
struct point {
int x ;
int y ;
// Déclaration des fonctions membres (méthodes)
void initialise(int, int) ;
void affiche() ;
} ;
void Affiche2(point A, point B){ // fonction non membre
cout << "\n ....Je suis dans Affiche2 ...."<<"\n" ;
A.affiche();
B.affiche();
}
// --- Définition des fonctions membres du type point ---
void point::initialise (int abs, int ord) {
x = abs ; y = ord ;
}
void point::affiche () {
cout << "Je suis en " << x << " " << y << "\n" ;
}
void main() {
point a, b, TabPoint[3];
a.initialise (3, 7) ; a.affiche () ;
b.x = 10; b.y = 20 ; b.affiche ();
for(int i=0 ; i<3 ; i++){
TabPoint[i].initialise(i, i+4);
TabPoint[i].affiche();
}
Affiche2(a, b);
}
Programme 5.2.
SOLUTION
Je suis en 3 7
Je suis en 10 20
Je suis en 0 4
Je suis en 1 5
Je suis en 2 6
....Je suis dans Affiche2 ....
Je suis en 3 7
Je suis en 10 20
Rqs Dans le cas des structures, un appel tel que a.initialise (3,7) ; pourrait être
remplacé par: a.x = 3 ; a.y = 7 ;
Dans un programme, nous pouvons évidemment mélanger fonctions membres et fonctions non membres.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
47
5.2 PRINCIPES DES CLASSES
Une classe définit un regroupement de données et de fonctions ou méthodes. D'un point de
vue syntaxique, une classe ressemble beaucoup à la définition d’une structure, il suffit de
remplacer le mot clé struct par le mot réservé class;
préciser quels sont les membres publics (fonctions ou données) et les membres
privés en utilisant les mots clés public et private.
struct Etudiant {
int Id;
string Nom;
void Saisie();
void Affiche();
};
Class Etudiant {
private : // Cachées aux fonctions externes
int Id;
string Nom;
public : // Accessibles depuis l'extérieur de la classe
void Saisie();
void Affiche();
};
La déclaration d’une classe commence par le mot-clé class suivi du nom de la classe et se
termine par le point-virgule obligatoire. Le nom de la classe dans cet exemple est Etudiant.
On appelle
objet, une donnée d'un type class, c’est une instanciation d’une classe.
Class Etudiant Etud1 ; // Etudiant est une classe, Etud1 est une instance d’un objet.
fonction membre ou méthode, un membre d'une classe qui est une fonction (Saisie),
donnée membre, un membre qui est une variable (Id et Nom).
Le C++ introduit des concepts de programmation orientée objet. Un des concepts majeurs
est l'encapsulation des données.
5.2.1 Concept d’encapsulation
Dans la classe Etudiant, toutes les fonctions membres sont désignées comme public et
toutes les données membres comme private; les premières étant accessibles depuis
l’extérieur de la classe, les secondes l’étant seulement depuis la classe.
L'association de membres et fonctions au sein d'une classe, avec la possibilité de rendre
privées certains d'entre eux, s'appelle l'encapsulation des données, un des concepts
majeurs de la programmation orientée objet. Intérêt de la démarche: puisqu'elles ont été
déclarées privées, les données membres Id et Nom ne peuvent être modifiées autrement
que par un appel de la fonction Saisie().
En effet, l'encapsulation est un mécanisme consistant à rassembler les données et les
méthodes au sein d'une classe en cachant l'implémentation de l'objet: on cache
l'information contenue dans un objet et on ne propose que des méthodes ou des fonctions
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
48
de manipulation de cet objet. Ainsi les propriétés contenues dans l'objet seront
assurées/validées par les méthodes de l'objet et ne seront plus de la responsabilité de
l'utilisateur extérieur. Les fonctions membres doivent pouvoir servir d'interface pour
manipuler les données membres.
L'utilisateur extérieur ne pourra pas modifier directement l'information et risquer de
mettre en péril les propriétés comportementales de l'objet.
L'encapsulation permet donc de garantir l'intégrité des données contenues dans l'objet. On
place l'étiquette public devant les fonctions membres dédiées à la manipulation des données
membres. En effet, si l'utilisateur de la classe ne peut pas modifier les données membres
directement, il est obligé d'utiliser l'interface (les fonctions membres) pour les modifier,
ce qui peut permettre au créateur de la classe d'effectuer des contrôles...
Le C++ implémente l'encapsulation en permettant de déclarer les membres d'une classe
avec le mot réservé public, private ou protected. Ainsi, lorsqu'un membre est déclaré :
public, il sera accessible depuis n'importe quelle classe ou fonction.
private, il sera uniquement accessible d'une part, depuis les fonctions qui sont
membres de la classe et, d'autre part, depuis les fonctions autorisées explicitement
par la classe (par l'intermédiaire du mot réservé friend (voir plus loin) ).
protected, il aura les mêmes restrictions que s'il était déclaré private, mais il sera
en revanche accessible par les classes héritières (voir plus loin).
Dans la classe Etudiant, les membres nommés Id et Nom sont privés, tandis que les
fonctions membres nommées Saisie() et Affiche() sont publiques.
Les expressions public: et private: peuvent apparaître un nombre quelconque de fois dans
une classe. Les membres déclarés après private: (resp. public:) sont privés (resp. publics)
jusqu'à la fin de la classe, ou jusqu'à la rencontre d'une expression public: (resp. private:). class XY {
private :
...
public :
...
private :
...
} ;
Dans l’exemple suivant, seule la fonction ft() pourra être appelée à partir de n’importe
quelle fonction du programme. Par contre il n’est possible d’accéder aux champs x et y que
par l’intermédiaire de la fonction membre public ft(). Ainsi l’accès aux différents champs
de la classe est contrôlé. class point {
private :
int x, y; // champs membres privés de la classe
void gt( ); // fonction membre privée
public :
void ft( ) ; // fonction membre public
};
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
49
Le C++ n'impose pas l'encapsulation des membres dans leurs classes. On pourrait donc
déclarer tous les membres publics, mais en perdant une partie des bénéfices apportés par
la programmation orientée objet. Il est de bon usage de déclarer toutes les données
privées, ou au moins protégées, et de rendre publiques les méthodes agissant sur ces
données. Ceci permet de cacher les détails de l'implémentation de la classe.
Si l'on rend publics tous les membres d'une classe, on obtient l'équivalent d'une structure.
Ainsi, ces deux déclarations définissent le même type point :
struct point {
int x, y;
void gt( );
void ft( );
};
class point {
public :
int x, y;
void gt( );
void ft( );
};
Dans une classe, tout ce qui n’est pas déclaré public est privé par défaut. Ainsi, ces deux
déclarations sont équivalentes :
class point {
private :
int x, y;
public :
void gt( );
void ft( );
};
class point {
int x, y;
public :
void gt( );
void ft( );
};
5.2.2 Création d'objets
En C++, il existe deux façons de créer des objets, c'est-à-dire d'instancier une classe :
de façon statique
de façon dynamique
La création statique d'objets consiste à créer un objet en lui affectant un nom, de la même
façon qu'avec une variable :
Nom_de_la_classe Nom_de_l_objet;
Etudiant Etud1 ; // Etudiant est une classe, Etud1 est une instance de la classe Etudiant.
Ainsi, l'objet est accessible grâce à son nom...
La création dynamique d'objet est une création d'objet par le programme lui-même en
fonction de ses « besoins » en objets. La gestion dynamique des objets se fait de la même
manière que la gestion dynamique de la mémoire pour des variables simples (int, float, …) ou
des structures par la manipulation, comme nous l’avons vu en 1ère année Bachelier, des
pointeurs en utilisant les opérateurs spécifiques : new, delete, new[] et delete[].
L’opérateur new permet d'allouer de la mémoire, alors que l’opérateur delete la restitue.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
50
La syntaxe de new est très simple, il suffit de faire suivre le mot clé new du type de la
variable à allouer, et l'opérateur renvoie directement un pointeur sur cette variable avec le
bon type. Par exemple, l'allocation d'un objet Etudiant se fait comme suit :
Etudiant *Etud; // pointeur vers la classe Etudiant
Etud = new Etudiant; // création de l'objet « dynamique » grâce au mot clé new
L’accès aux "membre public" se fait de la manière suivante : EtudSaisie(); ou (*Etud).Saisie();
EtudAffiche();
EtudNom ;
La syntaxe de delete est plus simple, puisqu'il suffit de faire suivre le mot clé delete du
pointeur sur la zone mémoire à libérer :
delete Etud;
Les opérateurs new[] et delete[] sont utilisés pour allouer et restituer la mémoire pour les
types tableaux. L'emploi de l'opérateur new[] nécessite de donner la taille du tableau à
allouer. Ainsi, on pourra créer un tableau de 500 objets de la manière suivante :
Etudiant *Tableau=new Etudiant[500];
et détruire ce tableau de la manière suivante :
delete[] Tableau;
Tout objet créé dynamiquement, c'est-à-dire avec le mot-clé new devra impérativement être détruit à la fin de son utilisation grâce au mot clé delete. Dans le cas contraire, une partie de la mémoire (celle utilisée par les objets créés dynamiquement) ne sera pas libérée à la fin de l'exécution du programme...
5.2.3 Affectation d’objets
On a déjà vu que l’on peut affecter à une structure la valeur d'une autre structure de
même type. Ainsi, avec les déclarations suivantes : struct point {
int x, y;
};
struct point A, B;
nous pouvons écrire sans problème : A = B ;
Cette instruction recopie l'ensemble des valeurs des champs de B dans ceux de A. Elle joue
le même rôle que : A.x = B.x ;
A.y = B.y ;
En C++, cette possibilité d’affectation globale s'étend aux objets de même type. Elle
correspond à une recopie des valeurs des membres données, que ceux-ci soient publics ou
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
51
non. Ainsi, avec ces déclarations (notez qu'ici nous avons prévu, artificiellement, x privé et
y public) : class point {
int x ;
public :
int y ;
};
point A, B ;
L’instruction : B = A ;
provoquera la recopie des valeurs des membres x et y de A dans les membres
correspondants de B. Contrairement à ce qui a été dit pour les structures, il n'est plus
possible ici de remplacer cette instruction par : B.x = A.x ;
B.y = A.y ;
En effet, si la deuxième affectation est légale, puisque ici y est public, la première ne l'est
pas, car x est privé.
Notez bien que l'affectation B = A est toujours légale, quel que soit le statut (public ou
privé) des membres données. On peut considérer qu'elle ne viole pas le principe
d'encapsulation, dans la mesure où les données privées de B (c'est-à-dire les copies de
celles de A) restent toujours inaccessibles de manière directe.
5.2.4 Déclaration d’une fonction membre
En ce qui concerne la définition des fonctions membres d'une classe, elle se fait de la même
manière que celle des fonctions membres d'une structure (qu'il s'agisse de fonctions
publiques ou privées). Il existe deux façons de définir ces fonctions membres, en
définissant :
le prototype et le corps de la fonction à l'intérieur de la classe en une opération ; le prototype de la fonction à l'intérieur de la classe et le corps de la fonction à l'extérieur.
La seconde solution est généralement celle la plus utilisée. Ainsi, puisque l'on définit la
fonction membre à l'extérieur de sa classe, il est nécessaire de préciser à quelle classe
cette dernière fait partie. On utilise pour cela l'opérateur de résolution de portée, noté ::.
Il suffit ainsi de faire précéder le nom de la fonction par ::, suivi du nom de la classe pour
lever toute ambiguïté (deux classes peuvent avoir des fonctions membres différentes portant le
même nom...).
Le programme 5.3 montre ce que devient le programme 5.1 lorsque l'on remplace la
structure Etudiant par la classe Etudiant. Ce programme permet aussi d’illustrer un
exemple de définitions de fonctions à l’intérieur et à l’extérieur de la classe. La fonction
Affiche() est définie à l’intérieur de la classe Etudiant, tandis que la déclaration de la
fonction Saisie() est à l’intérieur et sa définition est à l’extérieur de la classe.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
52
#include <iostream>
using namespace std ; class Etudiant {
int Id; // déclaration des membres privés
string Nom;
public:
void Saisie() ;
void Affiche(){ // Définir une fonction membre dans la classe
cout << "identifiant ="<< Id << endl;
cout << "Son nom : "<< Nom << endl;
} };
// Définition de la fonction membre à l’extérieur de la classe
void Etudiant::Saisie() {
cout << "donnez un identifiant"<< endl;
cin >> Id;
cout << "donnez un nom"<< endl;
cin >> Nom;
}
void main(){
Etudiant Etud1, Etud2; // 2 objets de la classe Etudiant
Etud1.Saisie();
Etud1.Affiche();
Etud2.Saisie();
Etud2.Affiche();
}
Programme 5.3.
Dans le programme 5.3, Etud1 et Etud2 sont déclarés comme objets de la classe Etudiant.
C’est pourquoi ils possèdent leurs propres données membres Id et Nom, et la capacité
d’appeler les deux fonctions membres de la classe : Saisie() et Affiche(). Il faut noter
qu’une fonction comme Saisie()est appelée en préfixant son nom du nom de son
propriétaire : Etud1.Saisie().
Une fonction membre peut être définie dans la classe (Affiche() ) ou à l’extérieur
(Saisie()). Dans la plupart des cas, il est préférable de définir ces fonctions hors de la
déclaration de la classe à l’aide de l’opérateur de résolution de portée (Etudiant::Saisie()).
Ceci permet de séparer physiquement les déclarations de fonctions de leurs définitions et
donc une meilleure lisibilité du programme.
Rq
Il faut remarquer que les données membres privés " x et y " sont accessibles dans les fonctions membres (Saisie()et Affiche()) sans qu'elles passent comme arguments de ces fonctions.
A titre d’exemple, voici ce que devient le programme 5.2 lorsque l'on remplace la structure
point par la classe point :
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
53
#include <iostream>
using namespace std ;
class point {
private :
int x, y;
public :
// Déclaration des fonctions membres (méthodes) void initialise(int, int) ;
void affiche() ;
} ;
void point::initialise (int abs, int ord) {
x = abs ; y = ord ;
}
void point::affiche () {
cout << "Je suis en " << x << " " << y << "\n" ;
}
void main() {
point a, TabPoint[3];
a.initialise (3, 7) ; a.affiche () ;
for(int i=0 ; i<3 ; i++){
TabPoint[i].initialise(i, i+4);
TabPoint[i].affiche();
}
}
Programme 5.4.
SOLUTION
Je suis en 3 7
Je suis en 0 4
Je suis en 1 5
Je suis en 2 6
Rqs
1. Si pour les structures (programme 5.2) un appel tel que a.initialise(3,7) ; pouvait être remplacé par : a.x = 3 ; a.y = 7 ;
cela n’est plus possible pour les classes. En effet, comme les données membres de point sont privées, on ne peut pas y accéder directement. Il faut absolument passer par une fonction membre publique. Une tentative d'utilisation directe comme a.x = 3; a.y = 7; au sein de la fonction main, conduirait à une erreur de compilation dont le genre de message ressemblerait à :
error C2248: 'x' : cannot access private member declared in class
'point', see declaration of 'x'
error C2248: 'y' : cannot access private member declared in class
'point', see declaration of 'y'
2. Dans l’exemple du Programme 5.4, les deux fonctions membres sont publiques. Il est tout à fait possible d'en rendre l’une ou l’autre, voire les deux, privées. Dans ce cas, de telles fonctions ne seront plus accessibles de l’"extérieur" de la classe. Elles ne pourront être appelées que par d'autres fonctions membres.
Voici encore deux exemples pour illustrer le fonctionnement et l’accès aux membres privés :
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
54
#include <iostream>
using namespace std;
class CRec {
private:
int Long, Larg;
public:
void initialise(int Lo, int La){
Long = Lo;
Larg = La;
}
int Surf() {
return (Long*Larg);
}
};
1
2
3
4
5
6
7
8
9
10-
11-
12
13
14
void main()
{
CRec Rc1, Rc2, Rc3;
Rc1.initialise(10,20) ;
cout <<"\n Surface rectangle 1 = "<< Rc1.Surf();
cout <<"\n Surface rectangle 2 = "<< Rc2.Surf();
// Rc3.Long = 30;
// Rc3.Larg = 24;
// cout <<"\n Surface rectangle 3 = "<< Rc3.Surf(); }
Programme 5.5.
L’ajout des lignes 10 et 11 provoquerait des erreurs à la compilation. En effet, comme Long
et Larg sont des membres privés de la classe, on ne peut y accéder de l’extérieur qu’à
travers les fonctions publiques de la classe. Donc si l’on désire modifier ces valeurs pour
Rc3, il faut faire appel à la fonction initialise().
L’exemple suivant attire l’attention sur le mode d’utilisation des membres privés et l’accès à
un objet lorsque nous avons plus d’une classe.
class A {
private : int a ;
public : void fonction_a();
};
class B {
private : int b ;
public : void fonction_b();
};
void A:: fonction_a(){
A a0;
B b0;
a=5; //OK: a et fonction_a() dans la classe A
a0.a = 1 //OK : a0 est un objet de la classe A
b0.b = 3 // OK : b est private dans B, donc pas accessible
// dans une fonction de A.
}
Le principe d’encapsulation interdit à une fonction membre d’une classe d’accéder à des
données privées d’une autre classe. Nous verrons plus loin comment contourner cela sans
perdre le bénéfice de la protection des objets.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
55
5.2.5 Le mot-clé this
On a parfois besoin de désigner à l’intérieur d’une fonction membre l’objet qui est manipulé
par la méthode. Comment le désigner alors qu’il n’existe aucune variable le représentant
dans la fonction membre? Les fonctions membres travaillent en effet directement sur les
attributs de classes et ceux qui sont atteints correspondent alors à ceux de l’objet
courant. C++ apporte une solution à ce problème en introduisant le mot-clé this qui permet à
tout moment dans une fonction membre d’accéder à un pointeur sur l’objet manipulé. this
est passée en tant que paramètre caché de chaque fonction membre. Voici deux exemples
d’utilisation qui seront discutés au cours.
Exemple 1 :
class point {
int x, y;
public :
void initialise (int x, int y){
this->x = x ; this-> y = y ;
}
…
} ;
void main(){
point Obj;
int Som;
…
}
Exemple 2 :
Le pointeur this permet d’obtenir une référence sur l’instance, plutôt qu’un pointeur. Cela
permet d’enchaîner plusieurs appels de méthodes.
#include<iostream>
using namespace std;
class KLa {
private: int x;
public :
KLa & set (int i) { x=i; return *this ; }
KLa & add (int i) { x+=i; return *this ; }
KLa & sub (int i) { x-=i; return *this ; }
int get_x (){ return x; }
};
void main() {
KLa A;
cout<< A.set(10).add(15).sub(3).get_x() << endl;
}
Ce programme affichera la Solution : 10+15-3 : 22
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
56
5.2.6 Membres Statiques : static
Jusqu’à présent, les attributs et méthodes tels que nous les avons définis ont toujours été
liés aux objets. En effet, dans le cas d’un attribut, une valeur différente de cet attribut
peut être associée à chaque objet. De même, les méthodes sont toujours invoquées par
rapport à un objet et ne peuvent pas être appelées en dehors de tout contexte.
Dans certains cas, ce comportement n’est cependant pas souhaité. On peut en effet avoir
envie de rattacher un attribut non pas à chaque instance d’une classe, mais à la classe elle-
même. De la même manière, il peut parfois être utile de pouvoir invoquer une méthode dans
le contexte d’une classe, et non dans celui d’un objet. Cette fonctionnalité existe en C++
grâce aux membres statiques, appelés également variables et méthodes de classe (par
opposition aux variables et méthodes d’instance).
Par exemple, si nous souhaitons attribuer automatiquement un identifiant d’étudiant à
chaque nouvel élément que nous créons, il est possible de définir un compteur statique au
niveau de la classe Etudiant. Ce compteur n’aura plus alors qu’à être incrémenté à chaque
nouvelle création d’objet Etudiant. La modification s’opère donc au niveau de l’interface de
la classe (pour la définition de ce nouvel attribut) ainsi que dans le ou les constructeurs de
cette classe [Programme 5.6]. #include <iostream>
using namespace std ;
class Etudiant {
private : int Id ; char code ;
public:
Etudiant() { // Constructeur
cout << "Dans constructeur"<<endl;
static i=1; // compteur du nombre d'objets créés
static char K='A';
Id = i;
code = K;
i++;
K++;
}
void Affiche(){
cout << " Identifiant ="<< Id << endl;
cout << " Son code : "<< code << endl;
}
} ;
void main(){
Etudiant E1; E1.Affiche();
Etudiant E2; E2.Affiche();
Etudiant E3; E3.Affiche();
}
Programme 5.6.
Solution
Dans constructeur
Identifiant =1 Son code : A
Dans constructeur
Identifiant =2 Son code : B
Dans constructeur
Identifiant =3 Son code : C
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
57
Nous verrons la durée de vie d’une variable statique et nous discuterons des améliorations
de ce programme dans les séances d’exercices et nous les appliquerons dans les séances de
travaux pratiques.
Attention: une méthode statique pouvant être appelée en dehors du contexte d’un objet, il
est impossible d’y intégrer des accès aux membres (attributs ou méthodes) non-statiques
de la classe. Il est également impossible, pour les mêmes raisons, de faire référence au
pointeur this à l’intérieur d’une méthode statique. La définition de membres statiques est
très utile pour définir et utiliser des variables dont le comportement est proche de
variables globales. Il existe cependant des avantages à définir des membres statiques
plutôt que de vraies variables globales:
- Les règles de masquage de l’information sont respectées. En particulier, une variable
statique peut être publique ou privée alors qu’une variable globale ne peut être que
publique.
- Une variable membre statique est définie à l’intérieur d’une classe et ne peut donc
pas interférer avec les membres définis dans les autres classes (pas de conflit de
noms).
5.3 CONSTRUCTEUR ET DESTRUCTEUR
Dans la version actuelle des programmes 5.4 et 5.5, les données membres x, y (class
point) et Long, Larg (class CRec) sont initialisées par l'intermédiaire de la fonction
membre initialise(). Dans une classe, cette initialisation peut être simplifiée. En effet, lors
de la création d'un objet, une fonction membre particulière est exécutée, si elle existe.
Cette fonction est appelée constructeur.
Le constructeur est une fonction membre d’initialisation (définie comme les autres
fonctions membres) qui sera appelée et exécutée automatiquement à chaque création d'un
objet. Ceci a pour effet de simplifier la programmation et assurer l’initialisation des
données membres.
Parmi les méthodes d'un objet se distinguent deux types de méthodes bien particulières et
remplissant un rôle précis dans sa gestion : les constructeurs et les destructeurs.
Le constructeur :
porte le nom de sa classe, définit l'initialisation d'une instance, notamment en initialisant ses données membres,
est appelé implicitement à toute création d'instance,
ne peut retourner aucune valeur, (fonction membre non typée même pas void ),
peut admettre des arguments qui sont en fait les valeurs d’initialisation des
différents champs de la variable.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
58
La définition d'un constructeur n'est pas obligatoire lorsqu'il n'est pas nécessaire. Un objet qui n’a pas de constructeur explicite, le compilateur se charge de créer de manière statique les liens entre champs et méthodes. Un objet peut avoir autant de constructeurs que l’on veut (tant qu’ils diffèrent par leur nombre et types d’arguments), ce qui est très intéressant pour initialiser les variables avec différents types. Dans ce cas, c’est l’utilisateur qui décidera du constructeur à appeler.
De même, le destructeur est une fonction membre appelée automatiquement au moment de
la destruction de l'objet, il :
est une fonction membre non typée et sans paramètre,
porte le nom de sa classe précédé d'un tilde (~),
n'a pas de type de retour (même pas void), définit la "désinitialisation" d'une instance, il se charge de détruire l'instance de l'objet,
est appelé implicitement à toute disparition d'instance.
Attention, il n’y a qu’un seul destructeur par classe !
5.3.1 Constructeurs par défaut
A) Constructeurs par défaut sans arguments
Le premier type de constructeur par défaut est de type : point::point(). C’est un
constructeur qui peut être appelé sans paramètre. Son rôle est de créer une instance "non
initialisée" quand aucun autre constructeur fourni n’est applicable. Donc il est appelé chaque
fois qu'un objet est créé sans qu'il y ait appel explicite d'un constructeur. Comme les
autres fonctions membres, les constructeurs peuvent être déclarés dans la classe et
définis soit à l’intérieur ou ailleurs. Le programme ci-dessous est un programme simple
mettant en évidence les moments où sont appelés respectivement le constructeur et le
destructeur d'une classe. Pour garder une trace de leur appel, nous y affichons un message.
Le programme 5.7 montre comment on peut définir les fonctions membres, y compris
constructeur et destructeur, à l'intérieur de la définition de la classe. Son exécution
affichera à l’écran :
Dans constructeur
Identifiant =-1
Son nom : Vide
Dans destructeur
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
59
#include <iostream>
#include <string>
using namespace std ;
class Etudiant {
private : int Id ; string Nom ;
public:
Etudiant() { // Constructeur
cout << "Dans constructeur"<<endl;
Id = -1 ; Nom = "Vide";
}
~Etudiant() { // Destructeur
cout << "Dans destructeur";
}
void Affiche(){
cout << " Identifiant ="<< Id << endl;
cout << " Son nom : "<< Nom << endl;
}
};
void main(){
Etudiant Etud; // Création d’un objet Etud
Etud.Affiche();
}
Programme 5.7.
En effet, lors de la création de l’objet Etud de la classe Etudiant, le constructeur est
appelé, d’où le commentaire "Dans constructeur". Ensuite on rencontre la fonction Affiche()
qui montre les valeurs d’initialisation des données membres. Enfin, comme le programme n’a
plus d’instructions à exécuter, il détruit l’objet avant de quitter la fonction main() en
appelant le destructeur.
B) Constructeurs par paramètres
Le constructeur peut posséder des paramètres. Pour illustrer le fonctionnement d’un tel
constructeur, le programme 5.4 peut être transformé comme suit :
#include <iostream>
using namespace std ;
class point {
private :
int x, y;
public :
// Déclaration des fonctions membres (méthodes)
point(int, int) ; // Constructeur
void affiche() ;
} ;
point::point (int abs, int ord) {
x = abs ; y = ord ;
}
void point::affiche () {
cout << " Je suis en " << x << " " << y << "\n" ;
}
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
60
void main() {
point a(3,7); // création d'objet a et initialisation des données
cout << "Objet a : " << endl;
a.affiche() ;
for(int i=0 ; i<3 ; i++){
point b(i, i+4);
cout << "Objet b (" << i << ") : "<< endl;
b.affiche();
}
cout << "Objet c : " << endl
point c = point(5, 6);
c.affiche() ;
cout << "Objet *Pt: " << endl;
point *Pt;
Pt = new point(8, 9);
Pt->affiche();
delete Pt;
}
Programme 5.8.
Solution
Objet a :
Je suis en 3 7
Objet b (0) :
Je suis en 0 4
Objet b (1) :
Je suis en 1 5
Objet b (2) :
Je suis en 2 6
Objet c :
Je suis en 5 6
Objet *Pt :
Je suis en 8 9
Un constructeur est toujours appelé lorsqu'un objet est créé, soit explicitement, soit
implicitement. Les appels explicites peuvent être écrits sous deux formes :
point a(3, 4); // Création de l’objet « a » + appel du constructeur avec des arguments 3 et 4 point c = point(5, 6);
Dans le cas d'un constructeur avec un seul paramètre, on peut aussi adopter une forme qui
appelle l'initialisation des variables de types primitifs :
point e = 7; // équivaut à : point e = point(7) ;
Un objet alloué dynamiquement est lui aussi toujours initialisé, au mois implicitement. Dans
beaucoup de cas il peut, ou doit, être initialisé explicitement. Cela s'écrit :
point *Pt;
Pt = new point(8, 9);
…
delete Pt;
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
61
Voici deux autres exemples illustrant la définition du constructeur (initialiser, voire créer
dynamiquement un objet) par paramètres et le destructeur.
class CRec {
public:
int Long;
int Larg;
CRec(int Lo, int La){
Long = Lo;
Larg = La;
}
~CRec() ;
};
~CRec()::CRec(){
Long = 0; Larg=0;
}
class Etudiant {
private :
int Id ;
char *Nom ;
public:
Etudiant (int id, string Name) {
Id = id ;
Nom = new char[Name.size()+1];
}
~ Etudiant() { // Destructeur
if (Nom!=NULL)
delete [] Nom ;
}
};
Cependant, à partir du moment où un constructeur est défini, il doit pouvoir être appelé
(automatiquement) lors de la création de l'objet. Dans le programme 5.8 ou dans les deux
exemples, les constructeurs point et CRec ont besoin de deux arguments alors que Tab en
a besoin d’un. Ceux-ci doivent obligatoirement être fournis dans notre déclaration.
Dans cet exemple :
CRec R1(10, 20) ; // Création d’un objet R1, donc lancement du constructeur
// R1.Long=10 etR1.Larg=20 ;
Tab T(10) ;// Création d’un objet T : donc réservation d’un espace mémoire pour 10 entiers
CRec R2 ; // Erreur
Tab T1, T2[10]; // Erreur
point Pt ; // Erreur
Les déclarations de R2, T1, T2[10] et Pt provoquent des erreurs à la compilation car il ne
possèdent pas le nombre nécessaire d’arguments. Ceci montre un exemple où il est
nécessaire de fournir à la classe point un constructeur par défaut de type point::point().
La classe peut dès lors être déclarée comme suit:
class point { int x, y ; // déclaration des membres privés
public : // déclaration des membres publics
point () ; // constructeur
point (int, int) ; // constructeur
~point() ; // destructeur
} ;
Il ne reste qu’à définir le destructeur et les différents constructeurs, par exemple :
point::point() { //constructeur par défaut sans paramètre x = 3 ; y = 4 ;
}
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
62
point::point (int abs, int ord) {
x = abs ; y = ord ;
}
point :: ~point () {
cout << "Dans destructeur au revoir \n" ;
x=0; y = 0;
}
Dans cette version, la classe point a deux constructeurs. Le premier n’a pas de paramètre
et initialise l’objet déclaré à l’aide des valeurs par défaut 3 et 4. Le deuxième a des
paramètres que l’utilisateur peut modifier au moment de la déclaration, mais pas de valeurs
par défaut. Les deux constructeurs permettent d’initialiser les objets dès leurs
déclarations.
point Pt(10, 20); // Création d’un objet Pt, avec initialisation
// Pt.x=10 et Pt.y=20 ;
point T; // Création d’un point T, donc lancement du
// Constructeur par défaut T.x=3 et T.y=4
point T[5]; // produit 5 appels de point()
point *pt = new point[10]; // produit 10 appels de point()
Le programme suivant montre l’utilisation du destructeur et des deux types de
constructeurs.
#include <iostream>
using namespace std;
class CRec {
public:
int Long, Larg;
CRec (int Lo, int La){
cout << "In Constructeur Param"<< endl ;
Long = Lo; Larg = La;
}
CRec (){
cout << "Dans Constructeur 0000"<< endl ;
}
~CRec (){
cout << "Lancer Destructeur" << endl;
}
int CalcSurf() { return (Long*Larg); }
};
void main() {
CRec Rc1(10,20), Rc2, Rc3;
cout <<"\n Surface rectangle 1 = "<< Rc1.CalcSurf();
cout <<"\n Surface rectangle 2 = "<< Rc2.CalcSurf();
Rc3.Long = 30;
Rc3.Larg = 24;
cout <<"\n Surface rectangle 3 = "<< Rc3.CalcSurf()<< endl;
}
Programme 5.9.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
63
Solution
Dans Constructeur Param
Dans Constructeur 0000
Dans Constructeur 0000
Surface rectangle 1 = 200
Surface rectangle 2 = 0
Surface rectangle 3 = 720
Lancer Destructeur
Lancer Destructeur
Lancer Destructeur
Rq
Il faut remarquer que dans ce programme, les données Long et Larg sont définies comme public dans la classe CRec. C’est pour cette raison que les initialisations :
Rc3.Long = 30; Rc3.Larg = 24;
sont autorisées.
Si la classe est déclarée comme ceci : class CRec {
private:
int Long, Larg;
public :
CRec (int Lo, int La){
….
Les assignations suivantes: Rc3.Long = 30;
Rc3.Larg = 24;
provoqueraient des erreurs à la compilation, du genre :
error C2248: 'Long' : cannot access private member declared in class 'CRec'
error C2248: 'Larg' : cannot access private member declared in class 'CRec'
Par souci de compréhension et pour mieux maîtriser l’enclenchement du constructeur et du
destructeur, voici comment pourrait être adapté le programme 5.8 afin de tracer les
appels de ces fonctions:
#include <iostream>
using namespace std ;
class point {
int x, y ;
public :
point (int abs, int ord){
x = abs ; y = ord ;
cout << "++ Construction d'un point :" << x << " " << y << "\n" ;
}
~point(){
cout << "-- Destruction du point : " << x << " " << y << "\n" ;
}
} ;
point a(1,1) ; // instanciation "Globale" d’un objet de classe point
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
64
void main(){
cout << "****** Debut main *****\n" ;
point b(10,10) ; // un objet automatique de classe point
point c(3,3) ;
int i ;
for (i=1 ; i<=3 ; i++) {
cout << "** Boucle tour numero " << i << "\n" ;
point b(i,2*i) ; // objets créés dans un bloc
}
cout << "****** Fin main ******\n" ;
}
Programme 5.10.
Solution
++ Construction d'un point : 1 1
****** Debut main *****
++ Construction d'un point : 10 10
++ Construction d'un point : 3 3
** Boucle tour numero 1
++ Construction d'un point : 1 2
-- Destruction du point : 1 2
** Boucle tour numero 2
++ Construction d'un point : 2 4
-- Destruction du point : 2 4
** Boucle tour numero 3
++ Construction d'un point : 3 6
-- Destruction du point : 3 6
****** Fin main ******
-- Destruction du point : 3 3
-- Destruction du point : 10 10
-- Destruction du point : 1 1
Nous demandons aux étudiants de bien comprendre cette solution. Une discussion à ce
sujet sera abordée durant les séances d’exercices. Nous traiterons aussi d’autres exemples
afin d’illustrer au mieux la trace des appels des constructeurs et destructeurs.
C) Constructeurs par défaut et par paramètres
Le constructeur par défaut est un constructeur qui peut être appelé sans paramètres : ou
bien il n'en a pas, ou bien tous ses paramètres ont des valeurs par défaut. Il joue un rôle
remarquable, car il est appelé chaque fois qu'un objet est créé sans qu'il y ait appel
explicite d'un constructeur. Voici un exemple de constructeur par défaut pour la classe
CRec. Il possède des paramètres avec des valeurs de défaut pour l’initialisation.
CRec :: CRec (int Lo = 0, int La = 0){
Long = Lo;
Larg = La;
}
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
65
Dans ce cas, la déclaration d’un objet peut se faire avec deux, un ou aucun argument.
L’exemple suivant illustre des créations valides pour des objets de type CRec :
CRec X; // OK Equiv : Z.Long=0 ; Z.Larg = 0
CRec Y(1); // OK Equiv : Z.Long=1 ; Z.Larg = 0
CRec Z(1,2); // OK : Equiv : Z.Long=1 ; Z.Larg = 2
En effet, de la même façon qu'une fonction, un constructeur peut être surchargé (voir
Informatique I). Dans l'exemple suivant, un objet de type CRec, peut être créé en passant
0, 1 ou 2 arguments. Si aucun argument n'est passé, Long et Larg sont initialisés à la valeur
0. Si un argument est passé Y(1), cela correspondra à Long=1 et Larg=0. Si 2 arguments
sont passés, ils sont utilisés pour initialiser les données membres.
5.3.2 Constructeur de copie
Le constructeur de copie (ou de recopie) est spécialisé dans la création et l’initialisation
d’un objet à partir d’un autre objet pris comme modèle. Il est donc appelé lorsqu’on doit
créer une instance d’un objet qui est la copie d’un autre objet.
Toute classe dispose d'un constructeur de copie par défaut généré automatiquement par le
compilateur, dont le seul but est de copier les champs de l'objet à copier un à un dans les
champs de l'objet à instancier. Toutefois, ce constructeur par défaut ne suffira pas
toujours, et le programmeur devra parfois en fournir un explicitement. Ce sera notamment
le cas lorsque certaines données des objets auront été allouées dynamiquement.
Le constructeur de copie sert à créer un objet identique à l’objet reçu en paramètre. C’est
un constructeur dont le premier paramètre est de type « C & » (référence sur un C) ou
« const C &» (référence sur un C constant) et dont les autres paramètres, s'ils existent,
ont des valeurs par défaut.
La syntaxe habituelle d’un constructeur de copie est la suivante :
point::point(const point& Pt) // Pt est l'objet à copier
point(const point& Pt) // Si la définition est dans la classe
Rqs Les attributs de l’objet passé en paramètre sont recopiés dans les attributs de l’objet à
créer, en prenant garde d’allouer un nouvel espace mémoire pour stocker le nom de l’objet (Pt, RC1, …) ;
Le constructeur de copie est aussi appelé lorsqu’une fonction retourne un objet (attention: le constructeur de copie n’est pas appelé lorsqu’une fonction retourne une référence à un objet);
S’il existe un constructeur au moins, toute instance qui naît est soit construite par un constructeur appelé implicitement, soit initialisée par une instance déjà construite.
Pour illustrer l’utilisation de constructeur par copie, nous reprenons la classe Etudiant qui
nous permettra par la même occasion de manipuler les pointeurs. Dans ce programme, nous
avons effectué quelques modifications afin d’enrichir cette classe.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
66
#include <iostream>
#include <string>
using namespace std ;
class Etudiant {
private :
int Id ;
char *Nom ;
public:
Etudiant (int id, string Name) { // Constructeur
cout << “Constructeur \n”;
Id = id ;
Nom = new char[Name.size()+1];
for (int i=0; i< Name.size(); i++)
Nom[i]=Name[i];
Nom[i]='\0'; // pour fin de chaîne de caractères
}
Etudiant(const Etudiant& ET) {
cout << “Constructeur de copie\n”;
if (ET.Nom) {
Nom = new char[strlen(ET.Nom) + 1];
strcpy(Nom, ET.Nom);
Id = ET.Id;
}
else{
Nom = 0;
Id = 0;
}
}
~ Etudiant() { // Destructeur if (Nom!=NULL) delete [] Nom ;
}
void Affiche(){
cout << " Identifiant ="<< Id << endl;
cout << " Son nom : "<< Nom << endl;
}
};
void main(){
string Name;
cout << " Name ....: ";
cin >> Name;
Etudiant E1(100, Name);
E1.Affiche();
Etudiant E2(E1); // copie de E1 dans E2 (1)
E2.Affiche();
Etudiant E3=E1; // copie de E1 dans E3 (2)
E3.Affiche();
}
Programme 5.11.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
67
Solution
Name ....: Ghandi
Constructeur
Identifiant =100
Son nom : Ghandi
Constructeur de copie
Identifiant =100
Son nom : Ghandi
Constructeur de copie
Identifiant =100
Son nom : Ghandi
Comme le montre la solution, dans les deux exemples (1) et (2), c'est le constructeur de
copie qui est appelé et pas le constructeur normal.
5.3.3 Listes d’initialisation des constructeurs
La plupart des constructeurs ne font qu’initialiser les données membres de l’objet. Le C++
propose un dispositif appelé liste d’initialisation. L’exemple suivant illustre comment on peut
réécrire les constructeurs des différents exemples à l’aide d’une liste d’initialisation :
Etudiant() {
Id = -1 ; Nom = "Vide"; }
Etudiant() : Id (-1) , Nom ("Vide") {}
point (int abs, int ord){
x = abs ; y = ord ; } point (int abs, int ord) :
x (abs) , y (ord) {} CRec (int Lo = 0, int La = 0){
Long = Lo; Larg = La; } CRec (int Lo = 0, int La = 0) :
Long (Lo) , Larg (La) {}
Les déclarations d’affectation se trouvant dans les corps des fonctions sont supprimées.
Leur action est gérée par la liste d’initialisation en gras dans les exemples. Remarquez que
la liste commence par deux points et précède le corps des fonctions qui sont maintenant
vides.
5.3.4 Affectation d’un objet par un autre de la même classe
Une partie sur l’affectation d’objets a déjà été traitée dans le paragraphe 5.2.3 (Affectation d’objets) de ce chapitre. Cependant, maintenant que les constructeurs ont été
vus, voici un programme véhiculant un complément d’informations sur le sujet.
#include <iostream>
using namespace std;
class CRec {
int Long, Larg; // membres privés
public:
CRec (int Lo, int La){
cout << "\n Dans Constructeur Param"<< endl ;
Long = Lo; Larg = La;
}
CRec (){
cout << "\n Dans Constructeur 0000"<< endl ;
}
int CalcSurf() { return (Long*Larg); }
};
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
68
void main() {
CRec Rc1(10,20), Rc2;
int Surface = 0;
Rc2 = Rc1;
Surface = Rc1.CalcSurf();
cout << "\n Surface rectangle 1 = " << Surface;
cout << "\n Surface rectangle 2 = " << Rc2.CalcSurf();
Rc1=CRec(5,15);
cout << "\n Surface rectangle 1 = " << Rc1.CalcSurf();
CRec Rc3=Rc1; // initialisation
CRec Rc4(Rc1); // idem (syntaxe équivalente)
cout << "\n Surface rectangle 3 = " << Rc3.CalcSurf();
cout << "\n Surface rectangle 4 = " << Rc4.CalcSurf();
}
Programme 5.12.
Solution
Dans Constructeur Param
Dans Constructeur 0000
Surface rectangle 1 = 200
Surface rectangle 2 = 200
Dans Constructeur Param
Surface rectangle 1 = 75
Surface rectangle 3 = 75
Surface rectangle 4 = 75
Rq Remarquez que dans cette solution, le constructeur ne s’est pas enclenché pour :
CRec Rc3=Rc1; CRec Rc4(Rc1);
5.4 TABLEAU D’OBJETS
Un tableau d’objets est défini et manipulé de la même façon qu’un tableau contenant des
éléments issus des types fondamentaux. Lors de la création d’un tableau d’objets, un
constructeur est appelé pour chaque élément du tableau (implicitement, c’est celui sans
paramètre). Lorsque la classe ne définit pas un tel constructeur, il faut nécessairement
initialiser chaque élément en indiquant le constructeur à appeler.
Si nous considérons la classe CRec définit comme:
class CRec {
int Long, Larg;
public:
CRec (int Lo, int La){
Long = Lo; Larg = La;
}
int CalcSurf() { return (Long*Larg); }
};
la déclaration d’un tableau de trois objets de type CRec par: CRec Tab[3] ;
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
69
conduira à une erreur de compilation. En effet, dans ce cas le constructeur sans paramètre
sera appelé successivement pour chacun des éléments. Or, dans cet exemple la classe n’en
possède pas. Dans ce cas, il faut éventuellement déclarer le tableau comme suit :
CRec Tab[3] = { CRec(1,2),CRec(5,7),CRec(3,9) );
Une autre solution est d’ajouter un constructeur par défaut sans paramètre ou encore
remplacer le constructeur de cette classe par un constructeur par défaut et par
paramètres.
CRec (int Lo=0, int La = 0){ // constructeur (0, 1 ou 2 arguments)
Long = Lo; Larg = La;
}
Dans ce cas la déclaration CRec Tab[3]; permet d’initialiser à zéro Long et Larg de chaque
élément du tableau. Voici un programme illustrant la construction et l’initialisation d’un
tableau d’objets.
#include <iostream>
using namespace std ;
class CRec {
int Long, Larg;
public:
CRec (int Lo=0, int La=0){
Long = Lo; Larg = La;
cout <<"++ Constructeur ---> : " << Long << " et "<< Larg<<"\n";
}
~CRec () {
cout <<"-- Destructeur : --->: " << Long << " et "<< Larg<<"\n";
}
};
void main() {
int n = 2 ;
CRec Tab[5] = { CRec(3,4), 9, n, 5*n+2 } ; (1)
cout << "*** fin du programme ***\n" ;
}
Programme 5.13.
Solution
++ Constructeur ---> : 3 et 4
++ Constructeur ---> : 9 et 0
++ Constructeur ---> : 2 et 0
++ Constructeur ---> : 12 et 0
++ Constructeur ---> : 0 et 0
*** fin du programme ***
-- Destructeur : --->: 0 et 0
-- Destructeur : --->: 12 et 0
-- Destructeur : --->: 2 et 0
-- Destructeur : --->: 9 et 0
-- Destructeur : --->: 3 et 4
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
70
On peut remarquer que la liste d’initialisation (1) peut comporter moins de valeurs que le
tableau n’a d’éléments. Chaque fois qu’un argument d’initialisation est manquant, il est
remplacé par la valeur par défaut (zéro dans cet exemple).
5.5 SURCHARGE DES OPÉRATEURS
Avec les outils du langage étudiés jusqu'à présent nous pouvons définir des nouvelles
classes, créer des objets de ces classes et les manipuler presque comme les objets des
types primitifs.
Néanmoins, avec des objets des types primitifs on peut écrire int i, j;
if (i == j) { .….
}
Autrement dit, on peut utiliser les opérateurs arithmétiques et logiques qui sont prédéfinis
par le langage.
C++ offre au programmeur la possibilité de définir la plupart des opérateurs pour une classe
quelconque. Pour illustrer cela, reprenons un programme ou une partie de programme facile
comme le programme 5.4. On désire savoir si 2 objets de la classe point sont identiques.
Pour répondre à cette question on peut ajouter une fonction membre qui permet de
comparer 2 objets de cette classe.
#include <iostream>
#include <string>
using namespace std ;
class point {
private :
int x, y;
public :
// Déclaration des fonctions membres (méthodes)
point(int, int) ; // Constructeur void affiche(string ) ;
int EstEgal(point ) ;
};
point::point (int abs, int ord) {
x = abs ; y = ord ;
}
void point::affiche (string T) {
cout << T << "Je suis en " << x << " " << y << "\n" ;
}
int point::EstEgal (point Pt){
if ( (x==Pt.x) && (y==Pt.y) ) return 1 ;
else return 0;
}
void main() {
point a(3,7); // création de l'objet a et initialisation des données
a.affiche("Objet a : ") ;
point b(4, 4);
b.affiche("Objet b : ") ;
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
71
if (a.EstEgal(b)==1)
cout << " Oui EGALITE "<< endl;
else
cout << " Non EGALITE "<< endl;
point c(3, 7);
c.affiche("Objet c : ") ;
if (a.EstEgal(c)==1)
cout << " Oui EGALITE "<< endl;
else
cout << " Pas d EGALITE "<< endl;
}
Programme 5.14.
Solution
Objet a : Je suis en 3 7
Objet b : Je suis en 4 4
Non EGALITE
Objet c : Je suis en 3 7
Oui EGALITE
Mais il est souhaitable de pouvoir comparer ces 2 objets de la même façon que l'on compare
2 objets d'un type primitif. if ( a == b) ….
else .…
Nous allons donc redéfinir l'opérateur d'égalité "==" pour les objets de la classe point.
int point::operator==(point Pt) { if ( (x==Pt.x) && (y==Pt.y) ) return 1 ;
else return 0;
}
Pour comparer 2 objets de la classe point, on peut utiliser la fonction operator== à la
place de la fonction membre EstEgal : a.operator==(b)
Mais on peut aussi l'utiliser comme dans le cas d'un objet de type primitif : if (a == b).
Les opérateurs qui peuvent être surchargés sont : + - * / % ^ & | ~ ! ' = < > <= ++ - - << >>
== != && || += -= /= %= ^= &= |= *=
<<= >>= [] () -> ->* new delete
La surcharge des opérateurs les plus utilisés vous sera demandée comme exercices.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
72
Exercices
5.1. Ecrire un programme qui gère un ensemble de Personnes (Nmax=10). Chaque Personne aura
une structure avec un identifiant Id de type entier et un nom de type string dont la déclaration est:
struct Personne {
int Id;
string nom;
};
Ce programme doit gérer en boucle le menu suivant :
1 : Saisie et Affichage
2 : Ajouter au (début, milieu ou fin) et Affichage
3 : Supprimer le début et Affichage
4 : Tri selon NOM et Affichage
5 : Tri selon Id et Affichage
6 : Quitter
5.2. Transformez l’exercice 5.1. en remplaçant la structure Personne par une classe Personne.
5.3. Ecrivez la classe Etudiant. Le programme doit gérer en boucle
le menu suivant :
a) Saisie d’1 tableau Tab1 d’Etudiant de 1ere + Affichage
b) Saisie d’1 tableau Tab2 d’Etudiant de 2ere + Affichage
c) Fusion dans un tableau Dynamique les tableaux Tab1 et Tab2 +Affichage
5.4. Comment concevoir le type classe CLAS de sorte que le programme : void main() {
CLAS x;
cout << " Salut \n" ;
}
fournisse les résultats suivants : Creation Objet
Salut
Destruction Objet.
5.5. Saisissez le programme ci-dessous et exécutez-le. Quelles sont les valeurs des champs de
chaque variable? Que remarquez-vous au sujet de l’initialisation des champs ? Quand avez-vous
appelé les constructeurs et destructeurs de chaque variable? Quelles conclusions en tirez-vous sur
l’apport des constructeurs et destructeurs dans la programmation ?
#include <iostream>
using namespace std ;
class CLS {
int x ;
float y ;
public :
char z ;
Etudiant
string Nom;
public : void saisie();
void afficher();
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
73
CLS( int a = 1 , float b = 2.2 , char c = 'F' );
~CLS( );
} ;
CLS :: CLS( int a , float b , char c ) {
//...
x = a ; y = b ; z = c ;
}
CLS :: ~CLS( ){
//...
}
void main( ) {
CLS var1 ;
CLS var2( 20 ) ;
CLS var3( 30 , 31.23 ) ;
CLS var4( 40 , 43.21, 'D' );
}
5.6. Reprenez l’exercice 5.5. et ajoutez une fonction fct permettant de modifier la valeur des
champs et une fonction Affichage permettant l’affichage des champs de l’objet. Utilisez ensuite
deux variables, l’une statique et l’autre dynamique, en leurs appliquant les appels de fonctions.
Quand avez-vous appelé les constructeurs et destructeurs de chaque variable? Quelles conclusions
en tirez-vous sur l’utilisation des variables dynamiques ?
5.7. Ecrire un programme utilisant une classe Vecteur3d permettant de manipuler des vecteurs à
3 composantes (x, y, z) de types float. On y prévoira :
un constructeur, avec des valeurs par défaut (0),
une fonction d'affichage des 3 composantes du vecteur sous la forme :
< composante1, composante2, composante3>
une fonction permettant d'obtenir la somme de deux vecteurs, une fonction membre coincide(??) permettant de vérifier si deux vecteurs ont les mêmes composantes,
la définition d’un opérateur == afin de tester la coïncidence ou pas de deux vecteurs,
la définition d’un opérateur + afin d'additionner deux vecteurs,
la définition d’un opérateur - afin d'additionner deux vecteurs,
Utilisez la fonction main() suivante comme programme d'essai de ce problème et vérifier que les
résultats sont conformes à la solution fournie.
void main() {
vecteur3d v1 (1,2,3), v2 (4,5), w ;
cout << "v1 = " ; v1.affiche () ; cout << "\n" ;
cout << "v2 = " ; v2.affiche () ; cout << "\n" ;
cout << "w = " ; w.affiche () ; cout << "\n" ;
w = v1.somme (v2) ;
cout << "w = " ; w.affiche () ; cout << "\n" ;
w = v1 + v1 ; cout << "w = " ; w.affiche () ; cout << "\n" ;
v1.coincide(v2);
v1.coincide(v1);
if (v1==w) cout << "\n <<< v1 = w";
else cout << "\n >>> v1 != w \n";
w = v1 - v2 ; cout << "w = " ; w.affiche () ; cout << "\n";
w = v1 - v1 ; cout << "w = " ; w.affiche () ; cout << "\n";
}
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
74
Affichage de la solution : v1 = < 1, 2, 3> v2 = < 4, 5, 0>
w = < 0, 0, 0>
w = < 5, 7, 3>
w = < 2, 4, 6>
---> === Pas EGALITE ===
---> === EGALITE ===
>>> v1 != w
w = < -3, -3, 3>
w = < 0, 0, 0>
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
75
Chap6 : LES PATRONS ET AMIS, FONCTIONS ET CLASSES
Parmi les techniques pour améliorer la réutilisabilité des morceaux de code, nous trouvons
la notion de généricité. Cette notion permet d’écrire du code générique en paramétrant des
fonctions et des classes par un type de données. Un module générique n’est alors pas
directement utilisable : c’est plutôt un modèle, patron (template) de module qui sera
«instancié » par les types de paramètres qu’il accepte. Dans la suite nous allons montrer
comment C++ permet, grâce à la notion de patron de fonctions, de définir une famille de
fonctions paramétrées par un ou plusieurs types, et éventuellement des expressions. D'une
manière comparable, C++ permet de définir des "patrons de classes". Là encore, il suffira
d’écrire une seule fois la définition de la classe pour que le compilateur puisse
automatiquement l'adapter à différents types.
6.1 PATRONS DE FONCTIONS
Pour illustrer les patrons de fonctions, prenons un exemple concret : une fonction min qui
accepte deux paramètres et qui renvoie la plus petite des deux valeurs qui lui est fournie.
On désire bénéficier de cette fonction pour certains types simples disponibles en C++ (int,
char, float). Les notions que nous avons vu en C++ jusqu’à maintenant ne nous permettent
pas d'obtenir cela avec une seule implémentation. Nous devons définir trois fonctions min,
une pour chacun des types considérés.
int min (int a, int b) { if ( a < b) return a ; // return ((a < b)? a : b);
else return b ;
}
float min (float a, float b) {
if ( a < b) return a ;
else return b ;
}
char min (char a, char b) {
if ( a < b) return a ;
else return b ;
}
Définition des fonctions min grâce à la surcharge
Lors d’un appel à la fonction min, le type des paramètres est alors considéré et
l’implémentation correspondante est appelée. Ceci présente cependant quelques
inconvénients :
– La définition des 3 fonctions (perte de temps, source d’erreur) mène à des instructions
identiques, qui ne sont différenciées que par le type des variables qu’elles manipulent.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
76
– Si on souhaite étendre la définition de cette fonction à de nouveaux types, il faut définir
une nouvelle implémentation de la fonction min par type considéré.
Une autre solution est de définir une fonction template, c’est-à-dire générique. Cette
définition définit en fait un patron de fonction, qui est instancié par un type de données (ici
le type T) pour produire une fonction par type manipulé. template <class T> // T est le paramètre de modèle
T min (T a, T b) {
if ( a < b) return a
else return b;
}
void main(){
int a = min(1, 7); // int min(int, int)
float b = min(10.0, 25.0); // float min(float, float)
char c = min(’z’, ’c’); // char min(char, char) }
Définition de la fonction min générique
Il n’est donc plus nécessaire de définir une implémentation par type de données. De plus, la
fonction min est valide avec tous les types de données dotés de l’opérateur <. On définit
donc bien plus qu’une fonction, on définit une méthode permettant d’obtenir une certaine
abstraction en s’affranchissant des problèmes de type.
Rqs – Il est possible de définir des fonctions template acceptant plusieurs types de données en paramètre. Chaque paramètre désignant une classe est alors précédé du mot-clé class, comme dans l’exemple : template <class T, class U> .... – Chaque type de données paramètre d’une fonction template doit être utilisé dans la définition de cette fonction. – Pour que cette fonctionnalité soit disponible, les fonctions génériques doivent être définies au début du programme ou dans des fichiers d’interface (fichiers .h).
6.2 CLASSE TEMPLATE : PATRON DE CLASSES
Il est possible, comme pour les fonctions, de définir des classes template, c’est-à-dire
paramétrées par un type de données. Cette technique évite ainsi de définir plusieurs
classes similaires pour décrire un même concept appliqué à plusieurs types de données
différents. Elle est largement utilisée pour définir tous les types de containers (comme les
listes, les tables, les piles, etc.), mais aussi des algorithmes génériques par exemple. La
bibliothèque STL (chapitre 6) en particulier propose une implémentation d’un bon nombre de
types abstraits et d’algorithmes génériques.
La syntaxe permettant de définir une classe template est similaire à celle qui permet de
définir des fonctions template.
Pour illustrer cela, nous reprenons notre exemple de création de la classe point. Nous avons
souvent été amenés à créer une classe point de ce genre :
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
77
class point {
int x, y ;
public :
point (int abs=0, int ord=0) ;
void affiche () ;
// .....
} ;
Lorsque nous procédons ainsi, nous imposons que les coordonnées d'un point soient des
valeurs de type int. Si nous souhaitons disposer de points à coordonnées d'un autre type
(float, double, long ...), nous devons définir une autre classe en remplaçant simplement, dans
la classe précédente, le mot clé int par le nom de type voulu.
Ici encore, nous pouvons simplifier considérablement les choses en définissant un seul
patron de classe de cette façon :
template <class T> class point {
T x , y ;
public :
point (T abs=0, T ord=0) ;
void affiche () ;
} ;
Comme dans le cas des patrons de fonctions, la mention template <class T> précise que l'on
a affaire à un patron (template) dans lequel apparaît un paramètre de type nommé T ;
rappelons qu’en C++ le mot clé class précise que T est un argument de type (pas forcément
classe...).
Bien entendu, la définition de notre patron de classes n'est pas encore complète puisqu'il y
manque la définition des fonctions membres, à savoir le constructeur point et la fonction
affiche().
Pour ce faire, la démarche va légèrement différer selon que la fonction concernée est
définie dans ou en dehors de la définition de la classe. Pour une définition dans la classe,
voici par exemple comment pourrait être défini notre constructeur:
point (T abs=0, T ord=0) {
x = abs ; y = ord ;
}
En revanche, lorsque la fonction est définie en dehors de la définition de la classe, il est
nécessaire de rappeler au compilateur :
• que, dans la définition de cette fonction, vont apparaître des paramètres de type ;
pour ce faire, on fournira à nouveau la liste de paramètre sous la forme : template <class T>
• le nom du patron concerné (de même qu'avec une classe "ordinaire", il fallait
préfixer le nom de la fonction du nom de la classe...) ; par exemple, si nous
définissons ainsi la fonction affiche, son nom sera:
point<T>::affiche ()
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
78
En définitive, voici comment se présenterait l'en-tête de la fonction affiche si nous le
définissions ainsi en dehors de la classe :
template <class T>
void point<T>::affiche ()
En toute rigueur, le rappel du paramètre T à la suite du nom de patron (point) est
redondant puisqu’il a déjà été spécifié dans la liste de paramètres suivant le mot clé
template.
Voici ce que pourrait être finalement la définition de notre patron de classe point :
template <class T> class point { // Création d'un patron de classe
T x , y ;
public :
point (T abs=0, T ord=0) {
x = abs ; y = ord ;
}
void affiche () ;
} ;
template <class T> void point<T>::affiche () {
cout << "Paire : " << x << " " << y << "\n" ;
} Création d'un patron de classes
6.3 UTILISATION D’UN PATRON DE CLASSES
Comme pour les patrons de fonctions, l’instanciation de tels patrons est effectuée
automatiquement par le compilateur selon les déclarations rencontrées. Après avoir créé ce
patron, une déclaration telle que : point <int> ai ;
conduit le compilateur à instancier la définition d'une classe point dans laquelle le
paramètre T prend la valeur int. Autrement dit, tout se passe comme si nous avions fourni
une définition complète de cette classe. Si nous déclarons : point <float> ad ;
le compilateur instancie la définition d'une classe point dans laquelle le paramètre T prend
la valeur float, exactement comme si nous avions fourni une autre définition complète de
cette classe.
Si nous avons besoin de fournir des arguments au constructeur, nous procéderons de façon
classique comme dans : point <int> ai (3, 5) ;
point <float> ad (1.5, 9.6) ;
Si nous faisons abstraction de la signification des paramètres (coordonnées d’un point) et
que nous les déclarons comme des caractères ou chaînes de caractères, le compilateur ne
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
79
signalera pas d’incohérence et la fonction affiche() remplacera x et y par les arguments,
comme le montre l’exemple récapitulatif.
Exemple récapitulatif :
Voici un programme complet comportant :
• la création d'un patron de classes point dotée d’un constructeur en ligne et d’une fonction
membre (affiche) non en ligne,
• la création d’un patron de fonctions min (en ligne dans la patron de classes) qui retourne le
minimum des arguments ,
• un exemple d'utilisation (main).
#include <iostream>
#include <string>
using namespace std ;
template <class T> class point { // Création d'un patron de classe T x ;
T y ;
public :
point (T abs=0, T ord=0) {
x = abs ; y = ord ;
}
void affiche () ;
T min () {
if ( x < y) return x;
else return y;
}
};
template <class T> void point<T>::affiche () {
cout << "Paire : " << x << " " << y << "\n" ;
}
void main () {
point <int> ai (3, 5) ; // T prend la valeur int pour la classe point ai.affiche() ;
cout << " Min : ... " << ai.min() << endl;
point <char> ac ('z', 't') ; ac.affiche() ;
cout << " Min : ... " << ac.min() << endl;
point <double> ad (1.5, 9.6) ; ad.affiche() ;
cout << " Min : ... " << ad.min() << endl;
point <string> as ("Salut", " A vous") ; as.affiche() ;
cout << " Min : ... " << as.min() << endl;
}
Programme 6.1.
Solution
Paire : 3 5
Min : ... 3
Paire : z t
Min : ... t
Paire : 1.5 9.6
Min : ... 1.5
Paire : Salut A vous
Min : ... A vous
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
80
Il faut noter aussi que le nombre de paramètres n'est pas limité à 1, on peut en avoir
plusieurs :
template < class A, class B, class C, ... >
class MaClasse
{
// ...
public:
// ...
};
L’exemple suivant montre comment implémenter les fonctions de la classe template.
template <class T, class T1, class T2> class Etudiant{
T Nom, Prenom;
T1 Id;
T2 Age;
public:
//...
T getNom(){return Nom;}
T1 getId(){return Id;}
T2 getAge();
void Saisie();
void affiche();
//...
};
template <class T, class T1, class T2> T2 Etudiant<T,T1,T2>::getAge(){
return Age;
}
template <class T, class T1, class T2> void Etudiant<T,T1,T2>::affiche () {
cout << "Data : " << Nom << " " << Id << " " << Age <<"\n" ;
}
La suite de cet exemple sera traitée en séances d’exercices.
Pour de plus amples informations et plus de détails sur les templates, le lecteur peut se
reporter par exemple à l’ouvrage référencé au début de ce syllabus : Claude Delannoy
Programmer en langage C++, Eyrolles, 2007. Chapitre 12.
Rq
Le comportement de point<char> est satisfaisant si nous souhaitons effectivement disposer de points repérés par de vrais caractères. En revanche, si nous avons utilisé le type char pour disposer de "petits entiers", le résultat est moins satisfaisant. En effet, nous pourrons toujours déclarer un point de cette façon : point <char> pc (4, 9) ; Mais le comportement de la fonction affiche ne nous conviendra plus (nous obtiendrons les caractères ayant pour code les coordonnées du point !). Il est toujours possible de modifier cela en "spécialisant" notre classe point pour le type char ou encore en spécialisant la fonction affiche pour la classe point<char>.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
81
6.4 FONCTIONS ET CLASSES AMIES
Nous avons vu qu’une classe avait généralement des membres privés et que ceux-ci n’étaient
pas accessibles par des fonctions non membres. Cette restriction peut sembler lourde, mais
elle est à la base même de la protection des données qui fait une grande partie de la
puissance de la programmation par objets en général, et de C++ en particulier.
Dans certains cas, cependant, il peut être intéressant d’accorder des accès aux attributs
ou aux méthodes à certaines classes clientes, tout en protégeant ce même accès vis-à-vis
des autres classes. Comment faire alors si par exemple on souhaite qu’une fonction f()
et/ou une classe B puisse accéder aux données membres privées d’une classe A?
La solution est de déclarer dans la classe A que f() et/ou B sont amies de A. Voici
comment.
class A {
private:
int i; … // tout ce qui est nécessaire à A…
friend class B; // B est autorisée :classe amie
friend void f(..); // f() est autorisée : fonction amie; pas membre de la classe A
};
class B {
… // tout ce qui appartient à B, et qui peut se servir des données membres privées de A …
};
void f(..) { ..; }
6.4.1 Fonctions amies
Une fonction amie d'une classe est une fonction qui, sans être membre de cette classe, a le
droit d'accéder à tous ses membres, aussi bien publics que privés.
Une fonction amie doit être déclarée ou définie dans la classe qui accorde le droit d'accès,
précédée du mot réservé friend. Cette déclaration doit être écrite indifféremment parmi
les membres publics ou parmi les membres privés :
class Tableau {
int tab[20], nbr;
friend void afficher(Tableau t);
public: Tableau(int nbrElements);
...
};
et, plus loin:
void afficher(Tableau t) {
for (int i = 0; i < t.nbr; i++)
cout << ' ' << t.tab[i];
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
82
}
Notez que bien que déclarée à l'intérieur de la classe Tableau, la fonction afficher n'est
pas membre de cette classe. À part le fait qu’elle est amie de la classe Tableau, la fonction
est parfaitement ordinaire, et peut être déclarée et définie de la même façon que toute
autre.
L’emploi de friend est une relaxation de la règle de portée textuelle des langages à
structure de blocs, car il donne aux fonctions amies les mêmes privilèges que ceux des
fonctions membres.
6.4.2 Méthodes amies
Dans la plupart des cas, une fonction amie (4.4.1.) peut avantageusement être remplacée par
une fonction membre. Si l’on souhaite qu’une méthode d’une classe puisse accéder aux
parties privées d’une autre classe, il suffit de déclarer la méthode friend également, en
utilisant son nom complet (nom de classe suivi de :: et du nom de la méthode).
Par exemple : class Tableau; // déclaration avant celle de la classe Fenetre sinon ERROR
class Fenetre {
...
public:
void afficher(Tableau t); ...
};
class Tableau {
int tab[20], nbr;
public:
friend void Fenetre::afficher(Tableau t); ...
};
Maintenant, la fonction afficher est membre de la classe Fenetre et amie de la classe
Tableau : elle a tous les droits sur les membres privés de ces deux classes. Elle s'écrit :
void Fenetre::afficher( Tableau t) {
for (int i = 0; i < t.nbr; i++)
cout << ' ' << t.tab[i];
}
6.4.3 Classes amies
Lorsqu’on souhaite que tous les membres d’une classe puissent accéder aux parties privées
d’une autre classe, on peut déclarer « amie » une classe entière :
class B ; // déclaration avant celle de la classe A qui contient friend B sinon ERROR
class A {
private:
int i; … // tout ce qui est nécessaire à A…
friend B; // B est autorisée :classe amie
};
class B {
… // tout ce qui appartient à B, et qui peut se servir des données membres privées de A
…
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
83
};
Les membres de la classe B peuvent tous modifier les parties privées des instances de A. La
déclaration de B avant celle de A est obligatoire, sinon on obtient
Error : Undefined symbol 'B' (symbole 'B' non défini).
Pour l’éviter, on peut éventuellement changer l’ordre de définition, mais il suffit en fait de
préciser le sélecteur class derrière friend :
class A {
private:
int i; … // tout ce qui est nécessaire à A…
friend class B; // B est autorisée :classe amie
};
Exercices
6.1. Considérons les deux fonctions suivantes : void swap(int& a,int& b) {
int tmp=a; a=b; b=tmp;
}
void swap(string& a,string& b) {
string tmp=a; a=b; b=tmp;
}
Elles utilisent le même principe, seul le type des deux paramètres (et celui de tmp)
changent. Ecrire une seule fois la fonction échange de manière à ce qu’elle soit utilisable
pour échanger toute paire de variables d’un même type.
6.2. Créez deux patrons de fonctions, l’un permettant d’afficher les éléments et l’autre
de calculer la somme d’un tableau d’éléments de type quelconque. Le nom et le nombre
d’éléments du tableau sont fournis en paramètres.
6.3. Soit la déclaration de la classe vecteur3d de l’exercice 5.6. class vecteur3d {
float x, y, z ;
public :
vecteur3d (float c1=0, float c2=0, float c3=0) {
x=c1 ; y=c2 ; z=c3;
}
…
} ;
Ecrire une fonction indépendante coincide( ??), amie de la classe vecteur3d, permettant de
vérifier si deux vecteurs ont les mêmes composantes. Si v1 et v2 désignent deux vecteurs
de type vecteur3d, comment s’écrit le test de coïncidence de ces deux vecteurs ?
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
84
Chap7 : L'HÉRITAGE
7.1 CLASSES DE BASE ET CLASSES DÉRIVÉES
L'héritage est un principe propre à la programmation orientée objet, permettant de créer
une nouvelle classe à partir d'une classe existante. C’est un principe qui permet l’extension
d'une classe de base afin de lui ajouter des fonctionnalités particulières tout en conservant
les fonctionnalités déjà définies dans la classe de base. En fait c’est une technique de
réutilisation de composants (classes) dont l’objectif est la spécialisation de types existants.
On parle aussi de la dérivation de classe qui consiste à faire hériter une classe d’une autre.
Donc une classe B est dérivée d’une classe A si elle en hérite. La classe A est alors appelée
superclasse ou classe de base ou classe mère de la classe B. On dit aussi que B est la classe
fille ou B dérive de A ou encore que B est dérivée de A. Quelque soit le terme choisi, si la
classe B hérite de la classe A, alors B possède toutes les propriétés de A; la classe B peut
bien sûr avoir des propriétés (données membres, méthodes, …) supplémentaires qui
reflètent sa spécificité.
En C++, il existe l’héritage simple (une classe hérite d’une seule classe) et l’héritage multiple
(une classe peut être dérivée de plusieurs classes). Dans ce cours, nous ne nous
intéresserons qu’à l'exposé de l’héritage simple.
7.2 MODE DE DÉRIVATION
La syntaxe pour la définition d'une classe dérivée est :
class ClasseDerivee : < MODE DE DERIVATION > ClasseBase {
...
};
Dans la définition de la classe dérivée, afin d’utiliser l’héritage, on ajoute le symbole :
après le nom de la classe en précisant par la suite quelle est la classe de base.
< MODE DE DERIVATION > permet de spécifier le mode de dérivation et les types de
protection par l'emploi d'un des mots-clé suivants : public, protected ou encore
private. Le mode de dérivation détermine quels membres de la classe de base sont
accessibles dans la classe dérivée. Au cas où aucun mode de dérivation n'est spécifié, le
compilateur C++ prend par défaut le mot-clé private pour une classe.
Si B hérite de A, B hérite des attributs et des "méthodes" de A. Cependant, la classe B ne
peut pas accéder aux membres privés (déclarés private) de la classe A. Faut-il pour autant
les déclarer public, au risque de violer le principe d'encapsulation ? Le C++ permet de
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
85
déclarer des membres protégés uniquement accessibles par les objets de la même classe et
par tous les objets des classes dérivées. On utilise pour cela le mot clé protected.
Si le mode de dérivation est :
public : le contrôle d’accès est sans changement ; les données publiques de la
classeBase restent publiques dans la classeDerivee, protégées restent protégées,
privées restent privées ;
protected: c'est-à-dire accessibles aux membres de la classe et à ses classes
dérivées (par héritage). Dans ce cas, les données publiques deviennent protégées,
les données protégées restent protégées et les privées restent privées,
private : les données publiques et protégées deviennent privées alors que les
données privées restent privées. Le tableau suivant récapitule les propriétés des différentes situations selon le mode de
dérivation :
mode de
dérivation
Statut dans la
classe de base
Statut dans la
classe dérivée
public
public public
protected protected
private inaccessible
protected
public protected
protected protected
private inaccessible
private
public private
protected private
private inaccessible
Une classe dérivée peut accéder aux membres hérités publics ou protégés mais ne peut
accéder aux membres privés hérités.
En général, on choisit le mode de dérivation public qui est la forme la plus courante
d'héritage. C'est d’ailleurs le seul mode que nous traitons dans ce syllabus. Les autres
modes seront illustrés par quelques transparents lors des séances d’exercices.
7.3 HÉRITAGE PUBLIC
Ce mode, comme signalé plus haut, donne aux membres publics et protégés de la classe de
base le même statut dans la classe dérivée.
Pour spécifier en C++ qu’une classe B hérite d’une classe A et que le mode de dérivation est
public, on déclare la classe B de la manière suivante :
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
86
class B : public A {
// Champs et méthodes };
Plusieurs classes peuvent dériver de la même classe de base. Il est donc possible de
représenter sous forme de hiérarchie de classes, parfois appelée arborescence de classes,
la relation de parenté qui existe entre ces différentes classes. L'arborescence commence
par la superclasse (classe de base, classe parent, classe mère ou père). Puis les classes
dérivées (classe fille ou sous-classe) deviennent de plus en plus spécialisées. Ainsi, on peut
généralement exprimer la relation qui lie une classe dérivée à sa classe de base par la
phrase "est un". Personne
Classes dérivées
Agent
Enseignant
Etudiant
Un Etudiant (Enseignant, Agent) est une personne mais pas forcément l’inverse.
Les déclarations correspondantes à cet arbre peuvent être sous cette forme :
class Personne {
…
} ;
class Agent : public Personne {
…
};
class Enseignant : public Personne{
…
};
…
…
Pour vous présenter la mise en œuvre de l’héritage nous commençons à partir d’un exemple
simple ne faisant intervenir ni constructeur ni destructeur. Nous reprenons le programme
5.2. que nous modifions afin d’illustrer les possibilités de l’héritage.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
87
#include <iostream>
#include <string>
using namespace std ;
class point {
private :
int x, y;
public :
void initialise(int, int) ;
void affiche(string) ;
};
class Pixel : public point { // Pixel dérive de point
int couleur ;
public :
void colore (int cl) {
couleur = cl;
cout << "\n --- couleur = " << couleur << "\n" ;
}
};
void point::initialise (int abs, int ord) {
x = abs ; y = ord ;
}
void point::affiche (string S) {
cout << S << ":- Je suis en " << x << " " << y << "\n" ;
}
void main() {
point a; // objet de type class point
Pixel PC ; // objet de type class Pixel
a.initialise (3, 7) ; a.affiche ("point") ;
PC.initialise (10,20) ; PC.colore (5) ;
PC.affiche("Pixel") ;
}
Programme 7.1.
SOLUTION
point:- Je suis en 3 7
--- couleur = 5
Pixel:- Je suis en 10 20
La classe Pixel dérive de la classe point. Le mode de dérivation étant public, les membres
publics de la classe de base point sont également publics, donc accessibles et réutilisables
dans la classe dérivée Pixel. C’est pour cette raison, que dans la fonction main(), les
fonctions membres de la classe point, initialise et affiche, sont accessibles par l’objet PC
de la classe Pixel. Cet objet, comme tout objet de la classe Pixel, peut accéder à sa
fonction membre colore contrairement aux objets appartenant à la classe point.
La classe Pixel telle qu’elle est définie ne permet pas d’afficher la couleur d’un objet. Une
amélioration possible est de créer une fonction qui permet d’afficher les coordonnées du
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
88
point ainsi que sa couleur. Pour afficher les coordonnées on peut réutiliser éventuellement
la fonction public affiche de la classe point. Une définition possible de cette fonction est :
void Pixel ::AfficheCol (string S){
affiche(S); // membre de la classe de base point
cout << "\n --- couleur = " << couleur << "\n" ;
}
Il n’est pas possible de remplacer la fonction affiche(S) par son code dans la fonction AfficheCol(S). Autrement dit, la fonction AfficheCol(S) ne peut être définie comme :
void Pixel ::AfficheCol (string S){ cout << S << ":- Je suis en " << x << " " << y << "\n" ;
cout << "\n --- couleur = " << couleur << "\n" ;
}
En effet, x et y sont des membres privés de la classe point, et ne sont nullement accessibles par une fonction membre de la classe dérivée Pixel. C’est le principe de l’encapsulation.
7.4 REDÉFINITION DE MEMBRES DANS LA CLASSE DÉRIVÉE
7.4.1 Redéfinition des fonctions membres
Il est possible, pour une raison ou une autre, de redéfinir une fonction dans une classe
dérivée si on lui donne le même nom que dans la classe de base. C’est comme si dans
l’exemple précédent de la classe Pixel, on donne le même nom pour les fonctions affiche et
AfficheCol . Dans ce cas, cette dernière deviendrait :
void Pixel ::affiche (string S){ // membre de la classe Pixel
affiche(S); // membre de la classe de base point
cout << "\n --- couleur = " << couleur << "\n" ;
}
L’exécution de cette fonction provoquerait des erreurs. En effet, pour le compilateur qui ne
fait pas la différence entre les fonctions affiche() de Pixel et de point, c’est un appel
récursif de cette fonction. Il faut donc faire appel à l’opérateur de résolution de portée
(::) afin de localiser convenablement la méthode voulue. La fonction sera définie comme
suit :
void Pixel ::affiche (string S){ // membre de la classe Pixel
point :: affiche(S); // membre de la classe de base point
cout << "\n --- couleur = " << couleur << "\n" ;
}
Le programme 7.1. peut être transformé de la façon suivante :
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
89
#include <iostream>
#include <string>
using namespace std ;
class point {
private :
int x, y;
public :
void initialise(int, int) ;
void affiche(string) ;
};
class Pixel : public point { // Pixel dérive de point
int couleur ;
public :
void colore (int cl) {
couleur = cl;
cout << "\n --- couleur = " << couleur << "\n" ;
}
void affiche (string);
};
void point::initialise (int abs, int ord) {
x = abs ; y = ord ;
}
void point::affiche (string S) {
cout << S << ":- Je suis en " << x << " " << y << "\n" ;
}
void Pixel ::affiche (string S){ // membre de la classe Pixel
point :: affiche(S); // membre de la classe de base point
cout << "\n +++ couleur = " << couleur << "\n" ;
}
void main() {
point a; // objet de type class point
Pixel PC ; // objet de type class Pixel
a.initialise (3, 7) ; a.affiche ("point") ;
PC.initialise (10,20) ; PC.colore (5) ; PC.affiche("Pixel") ;
}
Programme 7.2.
SOLUTION
point:- Je suis en 3 7
--- couleur = 5
Pixel:- Je suis en 10 20
+++ couleur = 5
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
90
7.4.2 Redéfinition des données membres
C’est beaucoup moins courant de redéfinir les données membres. Cependant, ce que nous
avons dit à propos de la redéfinition des fonctions membres s’applique tout aussi bien aux
membres données.
class A {
int X, Y ;
…
};
class B: public A {
float X, Y ;
…
};
7.5 HÉRITAGE ET CONSTRUCTEURS/DESTRUCTEURS
Les constructeurs, constructeurs de copie, destructeurs et opérateurs d'affectation ne
sont jamais hérités. En fait les constructeurs par défaut des classes de bases sont
automatiquement appelés avant le constructeur de la classe dérivée. Afin d’illustrer ces
propos, le programme suivant en donne un exemple. On remarquera que l'appel des
destructeurs se fait dans l'ordre inverse des constructeurs.
#include <iostream>
using namespace std ;
class A {
public:
A() {
cout<< "++++ A ++++" << endl;
}
~A() {
cout<< "~~~~ A ~~~~" << endl;
}
};
class B : public A {
public:
B() {
cout<< "++++ B ++++" << endl;
}
~B() {
cout<< "~~~~ B ~~~~ " << endl;
}
};
void main() {
B *ObjB = new B;
// ...
delete ObjB;
}
Programme 7.3.
SOLUTION
++++ A ++++
++++ B ++++
~~~~ B ~~~~
~~~~ A ~~~~
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
91
Si l’on ne désire pas appeler les constructeurs par défaut, mais des constructeurs avec des
paramètres, on doit utiliser une liste d'initialisation. Le programme qui nous servira
d’exemple est une adaptation du programme 7.2.
#include <iostream>
using namespace std ;
class point {
int x, y ;
public :
point (int abs=0, int ord=0) { // constructeur de point ("inline")
cout << "++ constr. point :" << abs << " " << ord << "\n" ;
x = abs ; y =ord ;
}
~point(){
cout<< "~~~~ point ~~~~" << endl;
}
} ;
class Pixel : public point {
int couleur ;
public :
Pixel (int, int, int) ; // déclaration constructeur Pixel
~Pixel(){
cout<< "~~~~ Pixel ~~~~ :" << couleur << endl;
}
} ;
Pixel::Pixel (int abs, int ord, int cl) {
cout << "++ constr. Pixel : " << abs << " " << ord << " " << cl << "\n";
couleur = cl ;
}
void main() {
Pixel a(10,15,3);
}
Programme 7.4.
SOLUTION
++ constr. point : 0 0
++ constr. Pixel : 10 15 3
~~~~ Pixel ~~~~ : 3
~~~~ point ~~~~
La déclaration : Pixel a(10, 15, 3) ;
entraînera :
l’appel de point qui recevra les arguments les arguments par défaut 0 et 0,
l’appel de Pixel qui recevra les arguments 10, 15 et 3.
Si maintenant le constructeur de la classe point n’avait pas d’arguments par défaut, donc
déclaré comme ceci : point (int abs, int ord) { …
voici l’erreur de compilation que l’on peut avoir :
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
92
error C2512: 'point' : no appropriate default constructor available
Si par contre on souhaite que Pixel retransmette à point les deux premières informations
reçues, on écrira son en-tête de cette manière :
Pixel::Pixel (int abs, int ord, int cl) : point (abs, ord)
Le compilateur mettra en place la transmission au constructeur de point des informations
abs et ord correspondant aux deux premiers arguments de Pixel. Ainsi la déclaration : Pixel a (10, 15, 3) ;
entraînera :
l’appel de point qui recevra les arguments 10 et 15,
l’appel de Pixel qui recevra les arguments 10, 15 et 3.
En revanche, la déclaration : Pixel b (1, 7) ;
provoquera une erreur puisqu’il n’existe pas de constructeur Pixel à deux arguments. Bien
entendu, il reste la possibilité d’initialiser les arguments par défaut, par exemple : Pixel::Pixel (int abs=0, int ord=0, int cl=9) : point (abs, ord)
Dans ces conditions, la déclaration : Pixel c (2) ;
entraînera :
l’appel de point qui recevra les arguments 2 et 0,
l’appel de Pixel qui recevra les arguments 2, 0 et 9. Notez que la présence éventuelle d’arguments par défaut dans point n’a aucune incidence ici.
La déclaration : Pixel * adr ; adr = new Pixel (20,30) ; // objet dynamique
entraînera :
l’appel de point qui recevra les arguments 20 et 30,
l’appel de Pixel qui recevra les arguments 20, 30 et 9.
7.6 POLYMORPHISME
Considérons les deux classes CLA et CLB ; class CLA {
public :
void Affiche() {
cout << " Affichage:CLA " << endl ; }
} ;
class CLB : public CLA {
public :
void Affiche() {
cout << " Affichage:CLB " << endl ; }
};
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
93
La classe CLB dérive de la classe CLA et redéfinit (surcharge) la fonction Affiche(). Voyons
un exemple d’utilisation simple.
CLA *ptr_a = new CLA ;
CLB *ptr_b = new CLB ;
CLA *ptr ;
ptr = ptr_a;
ptrAffiche(); // Affichage:CLA
ptr = ptr_b;
ptrAffiche(); // Affichage:CLA
Le problème réside dans le deuxième appel de la méthode Affiche(). En effet, bien que le
pointeur ptr fasse référence à une instance de type CLB, c’est la méthode Affiche() de CLA
(le type initial de ptr) qui est appelée. Ce comportement n’est généralement pas celui
souhaité ; on préfère que la méthode appelée soit celle du type de l’objet auquel le pointeur
fait référence. On voudrait que le pointeur puisse être polymorphe (change de forme, de
nature).
Le polymorphisme est un concept des langages objet qui découle directement de l'héritage.
Si ce dernier permet de réutiliser le code écrit pour la classe de base dans les classes
dérivées, le polymorphisme rendra possible l'utilisation d'une même instruction pour
appeler dynamiquement des méthodes différentes dans la hiérarchie des classes. Le
polymorphisme réside donc dans la capacité d'un objet à modifier son comportement propre
et celui de ses descendants au cours de l'exécution.
En C++, il faut indiquer au compilateur qu'il a affaire à une fonction polymorphe, sinon il
serait tenté d'utiliser la fonction de la classe de base. Le polymorphisme est mis en oeuvre
par l'utilisation des fonctions virtuelles.
Une fonction virtuelle est une fonction qui possède la capacité de "changer de forme" dans
les classes dérivées de la classe de base définissant la méthode virtuelle. Un mot clé est
alors introduit : virtual. Ce mot clé est placé devant la déclaration de la méthode.
Dans notre exemple il suffit d’ajouter virtual dans les classes devant les fonctions
concernées. Les déclarations des classes deviennent alors :
class CLA {
public :
virtual void Affiche() {
cout << " Affichage:CLA " << endl ;
}
} ;
class CLB : public CLA {
public :
virtual void Affiche() {
cout << " Affichage:CLB " << endl ;
}
} ;
Il n’est pas obligatoire de répéter le mot-clef virtual dans CLB mais cela améliore la
lisibilité du code.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
94
Le code qui posait problème a maintenant le comportement voulu, sans rien y changer et le
compilateur choisit la bonne méthode, celle qui doit être appelée lors de l'exécution :
ptr = ptr_a;
ptrAffiche(); // Affichage:CLA
ptr = ptr_b;
ptrAffiche(); // Affichage:CLB
Rq
Il est généralement conseillé d'utiliser le mot clé virtual devant la déclaration du destructeur de la classe de base, afin que celui des sous-classes soit appelé également lorsque le programme utilise un pointeur d'instance de la classe de base au lieu d'un pointeur d'instance de la classe dérivée.
Exercices 7.1. Remplacez dans le code du programme 5.4.
Pixel::Pixel (int abs, int ord, int cl) { …
par Pixel::Pixel (int abs=0, int ord=0, int cl=9) : point (abs, ord){…
puis, donnez un exemple d’instructions dans la fonction main() qui aboutira à la solution suivante : ++ constr. point : 10 15
++ constr. Pixel : 10 15 3
++ constr. point : 2 3
++ constr. Pixel : 2 3 9
++ constr. point : 12 0
++ constr. Pixel : 12 0 9
++ constr. point : 12 25
++ constr. Pixel : 12 25 9
~~~~ Pixel ~~~~ :9
~~~~ point ~~~~
~~~~ Pixel ~~~~ :9
~~~~ point ~~~~
~~~~ Pixel ~~~~ :9
~~~~ point ~~~~
~~~~ Pixel ~~~~ :3
~~~~ point ~~~~
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
95
7.2. Saisissez le programme et exécutez-le. Quand est appelé le constructeur de A, de B ?
Quelles sont les valeurs des champs de la variable b à la fin du programme ?
Comment l’expliquez-vous ? Que faut-il faire pour modifier uniquement la valeur du champ x? Quel
est l’intérêt du mot protected ?
#include <iostream>
using namespace std ;
class ClsA {
protected : int x ;
void f ( int ) ;
public :ClsA ( int a = 3 ) ;
};
ClsA :: ClsA ( int a ) {
x = a ;
}
void ClsA :: f ( int i ) {
x = i;
}
class ClsB : public ClsA {
int y ;
public :ClsB ( int a ) ;
void g ( ) ;
};
ClsB ::ClsB ( int a ) : ClsA ( 2*a ) {
y = a ;
}
void ClsB :: g ( ) {
cout << "Les valeurs sont x = " << x << "\ny = " << y << "\n" ;
}
void main ( ) {
ClsB b ( 10 ) ;
b.g ( ) ;
}
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
96
7.3. Le poulailler
Implémentez la hiérarchie de classes ci-contre : la classe
de base Volaille et ses classes dérivées : Poule, Coq,
Œuf ainsi que les différentes fonctions membres. On
considère que le poids d’un œuf = 0,1 poids de la poule.
Il est possible d’ajouter des paramètres aux fonctions
et/ou d’ajouter d’autres fonctions.
Ce programme doit gérer en boucle le Menu suivant :
1- Créer un Tableau Dynamique de poules + Affichage
2- Créer un coq + Affichage
3- Le coq fait cocorico et toutes les poules pondent
4- Le coq dit CoCo et une poule sur 2 pond 5- Fin (on tue tout le poulailler !!)
1- Créer Tableau Dynamique de Poules et Affichage:
Il est constitué au moins de deux fonctions : SaisiePoule (…….) ;
Dans cette fonction on demandera et on testera le nombre de
Poules (NP<Max=10) à saisir et on effectuera la saisie dans un
Tableau Dynamique.
Voici un exemple d’exécution du programme
Menu :
*******
1. Créer un Tb Dyn poule + Af.
2. Créer un coq + Affichage
3. Le coq fait cocorico et
toutes les poules pondent
4. Le coq dit CoCo poule/2
5. Fin
Choix : 1
Combien de poules ? : 4
Le Nom ? : Poule1
Le poids ? : 200
Le Nom ? : Poule2
Le poids ? : 180
Le Nom ? : Poule3
Le poids ? : 1200
Le Nom ? : Poule4
Le poids ? : 1000
Poule4 (1000)
Poule3 (1200)
Poule2 (180)
Poule1 (200)
Menu :
*******
Choix : 2
Le Nom ? : M_CoQ
Description ? : BeauCoQ
Son Nom : M_CoQ
Description : BeauCoQ
Menu :
*******
Choix : 3
L'oeuf a bien ete pondu et pese : 100(gr)
Qualite de l'oeuf (A,B ou C)? : A
L'oeuf a bien ete pondu et pese : 120(gr)
Qualite de l'oeuf (A,B ou C)? : B
L'oeuf a bien ete pondu et pese : 18(gr)
Qualite de l'oeuf (A,B ou C)? : A
L'oeuf a bien ete pondu et pese : 20(gr)
Qualite de l'oeuf (A,B ou C)? : C
Menu :
*******
Choix : 4 (Une poule sur 2)
L'oeuf a bien ete pondu et pese : 100(gr)
Qualite de l'oeuf (A,B ou C)? : A
L'oeuf a bien ete pondu et pese : 18(gr)
Qualite de l'oeuf (A,B ou C)? : A
Volaille
string Nom ;
public :
void saisie();
void affiche();
Poule int Poids ;
public :
void saisie();
void affiche();
void pondre(??);
Coq string Description ;
public :
void saisie();
void affiche(); void cocorico( ??);
void CoCo( ??);
Oeuf char Qualite;
public :
void saisie();
void affiche();
Rqs : Normalement pas de Classe
amie.
Utilisez les règles de bonnes pratiques
telles que l’indentation syntaxique, la
programmation modulaire, la
libération de mémoire.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
97
7.4. Implémentez la hiérarchie de classes suivante :
Ensuite, implémentez un programme qui doit gérer en boucle le Menu suivant :
1. Saisie et/ou Ajout Etudiants et affichage
2. Saisie et/ou Ajout Profs et affichage
3. Doyen membre de Direction Calcul Moyenne étudiant et affichage
4. Cherche un Prof selon son nom
5. Cherche un Etudiant selon son nom
6. Quitter;
Dans 1. et 2. on utilisera des tableaux dynamiques. Optimisez votre code et utilisez des
templates quand c’est nécessaire.
Personne
string nom, prenom;
public :
void saisie();
void afficher();
Etudiant
int NumCarte;
int AnEtud;
int Note[3];
public :
void saisie();
void afficher()
Prof
string Service;
float Salaire;
public :
void saisie();
void afficher();
Direction
string Titre;
public :
void saisieDir();
void afficherDir();
..
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
98
Chap8 : LA BIBLIOTHÈQUE STL
8.1 INTRODUCTION
La bibliothèque STL (Standard Template Library : Bibliothèque standard générique.) est
certainement l’un des atouts de C++ et peut être considérée comme un outil très puissant.
Il s’agit d’un ensemble de structures de données (des conteneurs) et d’algorithmes
suffisamment performants pour répondre aux besoins usuels des programmeurs et leur
faciliter la tâche. Afin de rendre ces composants génériques, ils sont implémentés pour la
plupart à l'aide de patrons (templates) de classe ou de fonction.
Cette bibliothèque fournit ces composants :
un ensemble de classes conteneurs (collections d'objets), telles que les vecteurs,
les tableaux associatifs, les listes chaînées, qui peuvent être utilisées pour contenir
n'importe quel type de données à condition qu'il supporte certaines opérations
comme la copie et l'assignation.
une abstraction des pointeurs : les iterators. Ceux-ci fournissent un moyen simple et
élégant de parcourir des séquences d'objets et permettent la description
d'algorithmes indépendamment de toute structure de données.
des algorithmes génériques tels que des algorithmes d'insertion/suppression,
recherche et tri.
une classe string (déjà utilisée) permettant de gérer efficacement et de manière
sûre les chaînes de caractères.
L’objectif de ce chapitre n’est pas de présenter tous les détails de STL, vu le temps imparti
à ce cours, mais simplement de fournir quelques notions sur le sujet. Des compléments
d’informations peuvent se trouver par exemple sur
http://www.sgi.com/tech/stl/ : manuel d’utilisation de STL en anglais.
http://www.mochima.com/tutorials/STL.html : Un excellent tutorial en anglais.
ou un certain nombre d’ouvrages, en plus des références données au début du syllabus,
comme :
La bibliothèque standard STL du C++ Alain Bernard Fontaine chez MASSON
Pour mieux développer avec C++ : design patterns, STL, RTTI et smart pointers
Aurélien Geron & Fatmé Tawbi chez DUNOD
8.2 LES CONTENEURS
Les containers ou conteneurs sont des objets décrits par des classes génériques
représentant les structures de données logiques les plus couramment utilisées : les listes
(list), les vecteurs (vector), les ensembles (set), les listes d'associations (map),... Ces
classes sont dotées de méthodes permettant d'organiser un ensemble de données en
séquence, puis de parcourir ces données. Ces méthodes permettent de créer, de copier, de
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
99
détruire ces containers, d’y insérer, de rechercher ou de supprimer des éléments. La
gestion de la mémoire, l’allocation et la libération, est contrôlée directement par les
containers, ce qui facilite leur utilisation.
Tous les conteneurs proposent les méthodes suivantes :
empty() : teste si le conteneur est vide ,
max_size() : retourne taille limite ,
size() : retourne taille actuelle ,
operator=() : affectation d'un conteneur dans un autre ,
operator<(), operator<=(), operator>() operator>=(), operator==(), operator!=()
opérateurs de comparaison ,
swap() : échange les éléments de deux conteneurs ,
erase() : supprime un ou plusieurs éléments ,
clear() : supprime tous les éléments ,
begin() : retourne un itérateur qui pointe sur le 1er élément du conteneur ;
end() : retourne un itérateur qui pointe après le dernier élément du conteneur ;
rbegin() : retourne un itérateur qui pointe sur le dernier élément du conteneur (pour
parcours du conteneur en sens inverse)
rend() : retourne un itérateur qui pointe avant le 1er élément du conteneur (pour parcours
du conteneur en sens inverse)
Dans ce chapitre nous nous intéressons uniquement aux conteneurs list et vector. Après
l’introduction de la notion d’itérateur nous donnons un programme exemple dont le but est
d’illustrer un certain nombre de méthodes disponibles sur ces conteneurs. Pour les utiliser,
il suffit d’inclure dans le programme <list> et <vector>.
8.2.1 Vector
Le but de cette classe est de généraliser la notion de tableau. Sa structure interne est un
tableau dynamique qui est redimensionnable, automatiquement ou manuellement. A tout
moment, il est possible d'ajouter un élément, il n'y a pas de limitation (hormis l'espace
mémoire). Ainsi, si le tableau interne est plein au moment de l'ajout d'un nouvel élément,
alors il est réalloué avec une taille plus importante et tous ses éléments sont recopiés dans
le nouvel espace.
vector<int> v;
for (int i=0; i<100; i++)
v.push_back(i); // insérer un nouvel élément à la fin du container
Cependant, il est possible de contrôler à l'avance la taille du tableau interne (grâce à la
méthode reserve), ce qui permet d'éviter la réallocation du tableau.
vector<int> v;
v.reserve(10);
for (int i=0; i<10; i++) v.push_back(i);
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
100
Il est aussi possible d'initialiser un vecteur rempli d'un certain nombre d'éléments. Ces
derniers sont créés à partir du constructeur par défaut de leur classe.
vector<int> v(10); // v est construite avec 10 éléments
for (int i=0; i<10; ++i) v[i]=i;
Il est possible d'accéder directement à un élément d'un vecteur à partir de son index, en utilisant
l'opérateur [ ]. La numérotation des éléments débute à zéro, comme pour un tableau classique.
8.2.2 List
La liste propose un ajout et une suppression en début de liste. La structure interne du
conteneur est une liste dynamique doublement chaînée dédiée à la représentation
séquentielle de données. Si l’opérateur [ ] qui permet d’accéder directement à un élément
est disponible sur les objets de type vector, il ne l’est pas sur ceux de type list. L’accès aux
éléments ainsi que l'ajout et la suppression en milieu de liste sont rendus possibles par
l'intermédiaire des itérateurs. Grâce à la structure de liste chaînée, ces opérations sont
très efficaces. Il existe également des fonctions spécifiques à la classe générique list.
Voici par exemple comment créer une liste d’entiers : list<int> Liste1, Liste2; // liste d'entiers vide
Il est aussi possible d'initialiser une liste avec la même valeur. Le premier argument du
constructeur fournit le nombre d’éléments de la liste (5), le second la valeur (77) de chaque
nœud. list<int> Liste(5, 77); // construite avec 5 éléments de type int ayant tous la valeur 77
Pour accéder à la valeur de tête ou de queue : Liste.front(); // Retourne la valeur en tête de liste
Liste.back(); // Retourne la valeur en queue de liste
Ces 2 fonctions retournent des références, c'est-à-dire qu'on peut s'en servir pour
modifier le premier où le dernier élément d'une liste. Par exemple Liste.front() = 5 ;
8.3. LES ITÉRATEURS
Les itérateurs sont des pointeurs spécialisés qui accèdent aux éléments d'un conteneur. Un
itérateur sur un conteneur ne peut être créé que par le conteneur même. Chaque conteneur
fournit un itérateur portant le nom iterator (même const_iterator, reverse_iterator …) Il
est doté d’une méthode begin qui renvoie un itérateur sur le premier de ses éléments, et
d’une méthode end qui renvoie un itérateur sur une place se trouvant juste après le dernier
élément. Un itérateur peut être incrémenté en utilisant l’opérateur ++ (attention, ne pas
utiliser -- pour reculer, ça ne marche pas toujours), et accéder à l'élément pointé par
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
101
l'opérateur *. Les opérateurs == et != sont également disponibles pour vérifier si deux
itérateurs pointent au même endroit.
L'exemple suivant montre comment effectuer les parcours direct et inverse
séquentiellement d’un conteneur de son début à sa fin :
list<int> Liste ;
…
list<int> ::iterator it ; // itérateur direct sur une liste de int
list<int> ::reverse_iterator rit ; // itérateur inverse sur une liste de int
for (it= Liste.begin() ; it != Liste.end() ; it++) {
cout << *it << endl; // *it désigne l’élément courant de la liste }
for (rit= Liste.rbegin() ; rit != Liste.rend() ; rit++) {
cout << *rit << endl; // *rit désigne l’élément courant de la liste
}
Le programme suivant présente une application de deux conteneurs de séquence: vector
(vecteur) et list (liste). Nous exposons un certain nombre de méthodes qui sont disponibles
sur tous les types de conteneurs. Pour le reste des méthodes nous invitons le lecteur
motivé à consulter les références ou faire une petite balade sur le net.
#include <iostream>
#include <string>
#include <vector>
#include <list>
using namespace std;
void Affiche_V(vector<int> V){
cout << "VECTOR" << " : ";
for (int i=0; i< V.size() ; i++) {
cout <<V[i] <<" ";
}
cout << endl;
}
void Affiche_L(list<int>::iterator it, list<int> Liste, string S){
cout << S << " : ";
if(Liste.empty()) cout<<"La liste est vide "<<endl;
else
for (it= Liste.begin() ; it != Liste.end() ; it++) {
cout << *it << " "; // *it désigne l'élément courant de la liste
}
cout << endl;
}
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
102
void R_Affiche_L(list<int>::reverse_iterator it, list<int> Liste, string S){
cout << S << " : ";
for (it= Liste.rbegin() ; it != Liste.rend() ; it++) {
cout << *it << " "; // *it désigne l'élément courant de la liste
}
cout << endl;
}
void main() {
// appel de constructeurs sans arguments et construit un conteneur vide
vector<int> Tab; // Crée un tableau d'entiers vide sans dim
list<int> Liste1, Liste2; // Crée deux listes d'entiers vides
list<int> ::iterator it ; // itérateur direct sur une liste de int
list<int> ::reverse_iterator rit ; // itérateur inverse sur une liste de int
int i;
// Saisie des entiers cout << "Saisir le prochain entier (-1 pour finir) : ";
cin >> i;
while (i != -1) {
Liste1.push_back(i);
Liste2.push_front(i);
Tab.push_back(i); // pas de Tab.push_front(i);
cout << "Saisir le prochain entier (-1 pour finir) : ";
cin >> i;
}
// Affichage des données Affiche_V(Tab);
Affiche_L( it, Liste1, "LISTE1");
Affiche_L( it, Liste2, "LISTE2");
R_Affiche_L( rit, Liste1, "LISTE1 Inverse");
// Nombre d'éléments des containers cout << "Il y a " << Tab.size()
<< " Elements dans le tableau" << endl;
cout << "Il y a " << Liste1.size()
<< " Elements dans la liste1" << endl;
// Accès à des éléments cout << "Premier et dernier element du tableau : "
<< Tab.front() << " ; "<< Tab.back()<< endl;
cout << "Premier et dernier element de la liste1 : "
<< Liste1.front() << " ; "<< Liste1.back() << endl;
int milieu = Tab.size()/2;
cout << "Element de milieu de tableau : "
<< Tab[milieu] << endl;
// supprimer et ajouter des éléments cout << "Sup Dernier " << endl;
Liste1.pop_back();
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
103
Affiche_L( it, Liste1, "LISTE1");
Tab.pop_back();
Affiche_V(Tab);
cout << "Sup premier " << endl;
Liste1.pop_front(); // pas de pop_front() pour Tab Affiche_L( it, Liste1, "LISTE1");
cout << "ERASE premier+2" << endl;
Tab.erase(Tab.begin()+2);
Affiche_V(Tab);
cout << "Inserer dernier-1" << endl;
Tab.insert(Tab.end()-1, 99);
Affiche_V(Tab);
cout << "ERASE Segment entre 1er et dernier" << endl;
Tab.erase(Tab.begin()+1, Tab.end()-1);
Affiche_V(Tab);
cout << "Tout Effacer " << endl;
Tab.clear();
Affiche_V(Tab);
}
Résultats
de
l'exécution
Saisir le prochain entier (-1 pour finir) : 1
Saisir le prochain entier (-1 pour finir) : 2
Saisir le prochain entier (-1 pour finir) : 55
Saisir le prochain entier (-1 pour finir) : 33
Saisir le prochain entier (-1 pour finir) : 4
Saisir le prochain entier (-1 pour finir) : 9
Saisir le prochain entier (-1 pour finir) : 7
Saisir le prochain entier (-1 pour finir) : -1
VECTOR : 1 2 55 33 4 9 7
LISTE1 : 1 2 55 33 4 9 7
LISTE2 : 7 9 4 33 55 2 1
LISTE1 Inverse : 7 9 4 33 55 2 1
Il y a 7 Elements dans le tableau
Il y a 7 Elements dans la liste1
Premier et dernier element du tableau : 1 ; 7
Premier et dernier element de la liste1 : 1 ; 7
Element de milieu de tableau : 33
Sup Dernier
LISTE1 : 1 2 55 33 4 9
VECTOR : 1 2 55 33 4 9
Sup premier
LISTE1 : 2 55 33 4 9
ERASE premier+2
VECTOR : 1 2 33 4 9
Inserer dernier-1
VECTOR : 1 2 33 4 99 9
ERASE Segment entre 1er et dernier
VECTOR : 1 9
Tout Effacer
VECTOR :
Programme 8.1.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
104
Rqs
Le parcours de la liste ne peut se faire qu’à l’aide d’un itérateur et ne peut se faire comme pour vector à l’aide de : for (i=0; i< N ; i++).
L’utilisateur n’a pas à se soucier de l’allocation ou de la libération de la mémoire. C’est vrai lors de l’insertion d’éléments (.push_back(i); .push_front(i); .insert) et aussi à la fin du programme : aucune instruction particulière n’est nécessaire pour restituer la mémoire occupée par les conteneurs. À la sortie du bloc dans lequel ils sont définis, leur destructeur se charge de libérer toutes les ressources occupées.
8.4 LES ALGORITHMES
Les algorithmes sont des patrons de fonction dans la STL permettant d’effectuer des
opérations et des manipulations des données sur les conteneurs. Afin de pouvoir s’appliquer
à plusieurs types de conteneurs, les algorithmes ne prennent pas de conteneurs en
arguments, mais des itérateurs qui permettent de désigner une partie ou tout un conteneur.
Nous n'allons pas détailler ici tous les algorithmes, nous invitons le lecteur à consulter la
documentation pour plus d'informations. Nous signalons juste qu’il existe :
sort() : Trier par ordre croissant les éléments entre début et fin à l’intérieur d’un conteneur
sort (Itérateur début, Itérateur fin);
Exemple : sort(v.begin(),v.end());
find() : Trouver la position de Valeur entre début et fin. Retourne comme résultat
un itérateur sur cet élément. find(Itérateur début, Itérateur fin, const Type&Valeur);
Exemple : find(v.begin(),v.end(), 15);
search() : Trouver la position de la sous-séquence comprise entre début2 et fin2 à
l’intérieur de la séquence comprise entre début1 et fin2. search(Itér début1, Itér fin1, Itér début2, Itér fin2);
Exemple : search(v1.begin(), v1.end(), v2.begin(), v2.end());
unique() : Supprimer les doublons dans la séquence source (appliquer aux couples
d'éléments successifs).
replace() : Remplacer tous les val1 par val2 entre début et fin. replace(Itér début, Itér fin, const Type&Va11, const Type&Va12);
Exemple:replace(v.begin(),v.end(), 2, 4); // Remplace tous les 2 par des 4
reverse() : Inverser l'ordre des éléments d'une séquence. Exemple : reverse(v.begin(),v.end()) ;
min_element() et max_element(): Retournent un itérateur référençant
respectivement le plus petit et le plus grand des éléments de la séquence. Exemple : cout << *min_element(v.begin(),v.end()) << endl;
cout << *max_element(t, t+10) << endl;
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
105
lower_bound() et upper_bound() : Recherche binaire déterminant la première et la
dernière position à laquelle Val peut être insérée dans la séquence ordonnée
spécifiée par les itérateurs début et fin sans en briser l'ordre. lower_bound (Itér début, Itér fin, const Type&Va1); upper_bound (Itér début, Itér fin, const Type&Va1);
Exemple : lower_bound(v.begin(),v.end(), 5) ;
merge() : Fusionner deux listes d’éléments ordonnées. Exemple : Liste3.merge(Liste2);
// Liste3 = Liste3 + Liste2 en s'appuyant sur l'opérateur >; à la fin Liste2 est vide
copy() : Recopier l’intervalle [v.begin(), v.end()] à partir de la position L.begin(). Copy(v.begin(), v.end(), L.begin() ) ;
remove() : Supprimer les éléments valant Val de la séquence [v.begin(), v.end()]. remove(v.begin(), v.end(), Val);
/* En réalité, remove() place les éléments égaux à la fin du conteneur ; la suppression se
fera ultérieurement par la méthode erase du conteneur */
Pour utiliser ces algorithmes, il suffit d’inclure dans le programme <algorithm>. Voici un
programme d’utilisation de certains algorithmes.
#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;
void Affiche_V(vector<int> V, string S){
cout << S << " : ";
for (int i=0; i< V.size() ; i++) {
cout <<V[i] <<" ";
}
cout << endl;
}
void Affiche_L(list<int>::iterator it, list<int> Liste, string S){
cout << S << " : ";
for (it= Liste.begin() ; it != Liste.end() ; it++) {
cout << *it << " ";
}
cout << endl;
}
void main() {
int T1[7] = { 3, 5, 9, 1, 4, 18, 5};
int T2[5] = { 9, 1, 7, 2, 5};
int TT[15]={0};
vector<int> V1(T1, T1+7), V2(T2, T2+5), VV(TT, TT+15);
Affiche_V(V1, "Vect1 Initial");
Affiche_V(V2, "Vect2 Initial");
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
106
Affiche_V(VV, " VV Initial");
int Seq[2] = {9, 1};
int *p;
p = search(T1, T1+7, Seq, Seq+2);
cout << "........ En Vect1 :{9,1} en position "
<< p - T1 << endl;
p = search(T2, T2+5, Seq, Seq+2);
cout << "........ En Vect2 :{9,1} en position "
<< p - T2 << endl;
sort(V1.begin(), V1.end());
sort(V2.begin(), V2.end());
Affiche_V(V1, "Vect1 Trie ");
Affiche_V(V2, "Vect2 Trie ");
cout << "Min 1 : " << *min_element(V1.begin(),V1.end()) << endl;
cout << "Max It1: " << *max_element(T1, T1+7) << endl;
cout << "Max T1 : " << *max_element(V1.begin(),V1.end()) << endl;
sort(T1, T1+7);
// Détermine les positions possibles d'insertion d'un 5 : cout << "Pour Vect1 Trie: 5 peut etre insere de " <<
lower_bound(T1, T1+7, 5) - T1 <<
" a " <<
upper_bound(T1, T1+7, 5) - T1 << endl;
reverse(V1.begin(), V1.end());
Affiche_V(V1, "Vect1 Trie Reverse");
replace(V1.begin(), V1.end(), 5, 4);
Affiche_V(V1, "Vect1 Trie Reverse Replace 5 par 4");
Affiche_V(VV, "Vect Avant ");
copy(V1.begin(), V1.end(), VV.begin() ) ;
Affiche_V(VV, "Vect + V1 ");
copy(V2.begin(), V2.end(), VV.begin()+7 ) ;
Affiche_V(VV, "Vect +V1+V2 ");
sort(VV.begin(), VV.end());
Affiche_V(VV, "Vect Trie ");
list<int> L1, L2, L3;
list<int> ::iterator it ;
L1.insert(L1.begin(),VV.begin(), VV.end());
Affiche_L(it, L1, "Liste L1 ");
L1.unique();
Affiche_L(it, L1, "L1 Unique ");
L1.remove(4); // supprime tous éléments égaux à 4
Affiche_L(it, L1, "L1 - 4 ");
L2.push_front(5);// L2 : 5
L2.push_front(7);// L2 : 7 5
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
107
L2.push_back(4);// L2 : 7 5 4
L2.push_back(2);// L2 : 7 5 4 2
L2.push_front(3);// L2 : 3 7 5 4 2
Affiche_L(it, L2, "L2 :....... ");
L2.sort();
Affiche_L(it, L2, "L2 Trie ....");
Affiche_L(it, L1, "L1 :....... ");
L2.merge(L1);
Affiche_L(it, L2, "L2 merge L1 ");
Affiche_L(it, L1, "L1 apres merge....");
L2.unique();
Affiche_L(it, L2, "L2 Unique ");
L3.push_front(99);
L3.splice(L3.end(),L2);
Affiche_L(it, L3, "L3 splice L2 : ");
Affiche_L(it, L2, "L2 apres splice .... ");
}
Programme 8.2.
Résultats
de
l'exécution
Vect1 Initial : 3 5 9 1 4 18 5
Vect2 Initial : 9 1 7 2 5
VV Initial : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
........ En Vect1 :{9,1} en position 2
........ En Vect2 :{9,1} en position 0
Vect1 Trie : 1 3 4 5 5 9 18
Vect2 Trie : 1 2 5 7 9
Min 1 : 1
Max It1: 18
Max T1 : 18
Pour Vect1 Trie: 5 peut etre insere de 3 a 5
Vect1 Trie Reverse : 18 9 5 5 4 3 1
Vect1 Trie Reverse Replace 5 par 4 : 18 9 4 4 4 3 1
Vect Avant : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Vect + V1 : 18 9 4 4 4 3 1 0 0 0 0 0 0 0 0
Vect +V1+V2 : 18 9 4 4 4 3 1 1 2 5 7 9 0 0 0
Vect Trie : 0 0 0 1 1 2 3 4 4 4 5 7 9 9 18
Liste L1 : 0 0 0 1 1 2 3 4 4 4 5 7 9 9 18
L1 Unique : 0 1 2 3 4 5 7 9 18
L1 - 4 : 0 1 2 3 5 7 9 18
L2 :....... : 3 7 5 4 2
L2 Trie .... : 2 3 4 5 7
L1 :....... : 0 1 2 3 5 7 9 18
L2 merge L1 : 0 1 2 2 3 3 4 5 5 7 7 9 18
L1 apres merge.... :
L2 Unique : 0 1 2 3 4 5 7 9 18
L3 splice L2 : : 99 0 1 2 3 4 5 7 9 18
L2 apres splice .... :
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
108
Exercices 8.1. Remplacez dans le code de l’exercice 7.3.du poulailler le tableau dynamique par une liste.
8.2. Remplacez dans le code de l’exercice 7.4. les tableaux dynamiques par des listes.
8.3. Complétez la fonction void OrderedList<T> ::add(const T& t) {…
#include <iostream>
#include <list>
#include <string>
using namespace std;
template <class T> class OrderedList : public list<T> {
public:
void add(const T&);
};
void main() {
OrderedList<string> nom;
nom.add(string("Toto"));
nom.add(string("Mimi"));
nom.add(string("Picsou"));
nom.add(string("Aladin"));
nom.add(string("Jasmine"));
list<string>::const_iterator it=nom.begin();
while (it!=nom.end() )
cout << *it++ << endl;
}
template <class T> void OrderedList<T>::add(const T& t) {
...
}
pour que ce programme affiche le résultat suivant sans utiliser la fonction prédéfinie de l’algorithme
sort(). Aladin
Jasmine
Mimi
Picsou
Toto
8.4. Donnez le code d’un programme qui crée une liste L1 contenant les 10 premières lettres de
l’alphabet, crée une nouvelle liste L2 égale à L1 et efface d’un bloc la première moitié de L2.
8.5. Donnez le code d’un programme qui crée un vecteur v1 de 10 float régulièrement répartis
entre 0.0 (inclus) et 1.0 (non compris), crée un nouveau vecteur v2 égal à v1 et efface
élément par élément la deuxième moitié de v2.
8.6. Donnez le code d’un programme qui crée un vecteur de 5 string de longueurs différentes,
affiche ces chaînes, les trie par ordre lexicographique et réaffiche les chaînes triées.
8.7. Donnez le code d’un programme qui crée une liste de string représentant la phrase ”il fait
beau”, trouve la position du mot ”beau” et insère le mot ”tres” à la position précédente.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
109
8.8. En utilisant l’algorithme find de STL et les templates, donnez le code de la fonction Test
pour que ce programme affiche le résultat suivant:
void main() {
vector<string> V(3);
vector<string>::iterator itV;
V[0]="V0";
V[1]="V1";
V[2]="V2";
string St1="V1", St2="V5";
Test(V, itV, St1, "Vecteur");
Test(V, itV, St2, "Vecteur");
list<int> L;
list<int>::iterator itL;
for( int i = 0; i < 5; i++ ) {
L.push_back(i);
}
int x=10, y=2;
Test(L, itL, x, "List");
Test(L, itL, y, "List");
}
8.9. Ecrivez les 2 classes Etudiant et Personne. La classe Etudiant hérite de la classe Personne.
Si nécessaire, des fonctions membres peuvent être ajoutées aux classes.
Ce programme doit gérer en boucle le Menu suivant :
a) Saisie d’1 List d’Etudiant de 1ere (insertion en Fin)
b) Saisie d’1 Vector d’Etudiant de 2d (insertion au début)
c) Calculer et afficher le nombre de filles (F) dans la liste List.
Test Vecteur
V1 est dans Vecteur
Test Vecteur
V5 n'est pas dans Vecteur
Test List
10 n'est pas dans List
Test List
2 est dans List
Etudiant
int Note[2];
char sexe ;
public :
void saisie();
void afficher()
Personne
string Nom;
public :
void saisie();
void afficher();
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
110
8.10. Donnez le code d’un programme qui manipule deux classes :
Une classe Etudiant dont les objets sont des étudiants caractérisés par
int Id;
string nom;
Une seconde classe Prof dont les objets sont des enseignants caractérisés par
int Id;
string nom, Service;
Dans les deux classes (étudiants, enseignants), on déclarera au moins deux
méthodes, l’une pour saisir et l’autre pour afficher chaque objet de la classe (Saisie() et Affichage()
).
La manipulation des étudiants se fera en utilisant un conteneur List alors que les Profs
seront rangés dans un conteneur Vector.
Ce programme doit gérer en boucle le menu suivant :
A : Etudiants ou B : Profs
1 : Saisie et Affichage d’une List (Etud.) ou Vector (Profs)
2 : Ajouter au (début, milieu ou fin) et Affichage de l’ensemble
3 : Créer Dynamiquement 1 Element (Etud ou Prof) et Chercher s’il existe
(Sans utiliser le tri)
4 : Tri selon NOM et Affichage de l’ensemble
5 : Tri selon Id et Affichage de l’ensemble
6 : Quitter
Le menu sera affiché via une fonction, les choix seront traités via l’instruction case. Votre
programme utilisera une série de fonctions permettant de séparer les tâches.
Si nécessaire, des fonctions membres peuvent être ajoutées aux classes. On y prévoira au
moins un constructeur par classe permettant, comme vu aux séances d’exercices,
l’incrémentation automatique de l’Id (Prof1 : Id=1, Prof2 : Id=2, … et Etud1 : Id1=1,
Etud2 : Id2=2, ….).
Optimisez votre code afin de permettre la réutilisation des morceaux de code.
C++ ET PROGRAMMATION OBJET
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
111
8.11. Parcours Vita
Ecrire un programme permettant de manipuler plusieurs listes
dont le nombre est NL< Nmax=15. Il s’agit d’un Parcours
Vita avec NL activités et un nombre de participants
NP<Nmax. Les informations caractérisant chaque participant
dans la liste sont les suivantes: Nom : chaîne de caractères
Code : un caractère (A, B, …)
Créer la (les) classe(s) nécessaire(s) à ce problème.
Les fonctions Saisie(..) et Affichage(..) pour chaque élément
(participant) sont déclarées dans la structure participant.
Ce programme doit gérer en boucle le menu suivant :
1- SaisieL Liste1 et Affichage
2-Liste1 TO Liste2 et Affichage
3- Parcours et Affichage
4- Fin
1- Saisie Liste1 et Affichage:
Est constitué au moins de deux fonctions :
SaisieL (…….) ;
Dans cette fonction on demandera et on testera
le nombre de participants (NP) à saisir et on
effectuera la saisie des participants dans une liste par
insertion en tête. Par exemple si NP=4 et l’utilisateur introduit au clavier :
(NomD D) puis (NomC C) puis (NB B) et enfin (NA A)
La liste est :
Affichage(…….) ;
(NA A) --- (NB B) --- (NomC C) --- (NomD D)
2- Liste1 Liste2 et Affichage:
Ici, comme si les participants sautent, un par un, de la poutre1
(Liste1) à la poutre2 (Liste2). On applique (FIFO), c'est-à-
dire, on retire d’abord le D de Liste1, on insère D dans Liste2
et on affiche liste1 et liste2.
Liste1 : (NA A) --- (NB B) --- (NomC C)
Liste2 : (NomD D)
Puis on transfère (NomC C) de Liste1 vers Liste2. Ce dernier
sera inséré dans Liste2 après (NomD D).
Liste1 : (NA A) --- (NB B)
Liste2 : (NomD D) --- (NomC C)
…
Tant que liste1 n’est pas vide, on continue.
A la fin on aura l’affichage suivant :
Liste1 : Vide
Liste2 : (NomD D) --- (NomC C) --- (NB B) ----(NA A)
Attention : Ici on affiche l’état des deux listes chaque
fois qu’un participant passe de la liste1 à la liste2. Il faut soigner tous les affichages afin de bien suivre et
comprendre les différentes séquences.
3- Parcours et Affichage:
Ici on demandera à l’utilisateur le nombre NL et on y fera
traverser les NP participants comme le montre la figure générale
(début).
On affiche toutes les listes chaque fois que toute l’équipe passe
d’une liste à l’autre. Si par exemple NL=3 on affichera:
Liste1 : (NA A) --- (NB B) --- (NomC C) --- (NomD D)
Liste2 : Vide
Liste3 : Vide - - - - - - - - -
Liste1 : Vide
Liste2 : (NomD D) --- (NomC C) --- (NB B) ----(NA A)
Liste3 : Vide - - - - - - - - -
Liste1 : Vide
Liste2 : Vide
Liste3 : (NA A) --- (NB B) --- (NomC C) --- (NomD D)
!
PARTIE 3 :
LA SÉCURITÉ INFORMATIQUE
M.BENJELLOUN & G. LIBERT M.O.O. S. Informatique
113
Chap9 : LA SÉCURITÉ INFORMATIQUE
Il s’agit d’un domaine qui évolue très vite et il est possible que ce qui est rédigé maintenant,
ne sera plus d’actualité dans 6 mois ou un an. C’est pour cette raison que j’ai décidé de ne
pas rédiger cette partie dans ce syllabus. L’étudiant aura une partie sur ce domaine dans les
transparents du cours et la partie la plus actualisée sera exposée au cours magistral.
Le but de cette partie est de permettre à l’étudiant d’acquérir une initiation et
conscientisation à la sécurité informatique. De lui permettre d’avoir une vision générale de
la problématique de la sécurité et d'acquérir des connaissances de base dans ce domaine.
À cet effet, au cours, nous illustrerons et nous répondrons à des questions comme :
Qu’est-ce que la sécurité informatique ?
Quels sont les principaux objectifs de la sécurité des biens et des services ?
La sécurité informatique - Pourquoi et Pour qui?
Qui est concerné par la sécurité ?
L’attaquant c’est qui ? Motivations ? Types d’attaques
Méthodologie de la sécurité informatique !
Mécanismes de défense ?
Quels sont les conseils de sécurité ?
…