Service d’Informatique

118
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

Transcript of Service d’Informatique

Page 1: 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

Page 2: Service d’Informatique

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

Page 3: Service d’Informatique

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

Page 4: Service d’Informatique

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

Page 5: Service d’Informatique

M.BENJELLOUN & G. LIBERT S. Informatique

PARTIE 1 :

INTRODUCTION A LA METHODOLOGIE OBJET

Page 6: Service d’Informatique

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.

Page 7: Service d’Informatique

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).

Page 8: Service d’Informatique

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

Page 9: Service d’Informatique

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.

Page 10: Service d’Informatique

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.

Page 11: Service d’Informatique

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.

Page 12: Service d’Informatique

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

Page 13: Service d’Informatique

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.

Page 14: Service d’Informatique

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.

Page 15: Service d’Informatique

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).

Page 16: Service d’Informatique

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,

Page 17: Service d’Informatique

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.

Page 18: Service d’Informatique

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.

Page 19: Service d’Informatique

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.

Page 20: Service d’Informatique

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.

Page 21: Service d’Informatique

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.

Page 22: Service d’Informatique

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

Page 23: Service d’Informatique

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

Page 24: Service d’Informatique

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.

Page 25: Service d’Informatique

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

Page 26: Service d’Informatique

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.

Page 27: Service d’Informatique

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.

Page 28: Service d’Informatique

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:

Page 29: Service d’Informatique

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:

Page 30: Service d’Informatique

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

Page 31: Service d’Informatique

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.

Page 32: Service d’Informatique

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:

Page 33: Service d’Informatique

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

Page 34: Service d’Informatique

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.

Page 35: Service d’Informatique

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.

Page 36: Service d’Informatique

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.

Page 37: Service d’Informatique

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.

Page 38: Service d’Informatique

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

Page 39: Service d’Informatique

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

Page 40: Service d’Informatique

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

Page 41: Service d’Informatique

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

Page 42: Service d’Informatique

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.

Page 43: Service d’Informatique

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.

Page 44: Service d’Informatique

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).

Page 45: Service d’Informatique

PARTIE 2 :

ET PROGRAMMATION OBJET

Page 46: Service d’Informatique

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.

Page 47: Service d’Informatique

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

Page 48: Service d’Informatique

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

Page 49: Service d’Informatique

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;

}

Page 50: Service d’Informatique

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.

Page 51: Service d’Informatique

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.

Page 52: Service d’Informatique

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

Page 53: Service d’Informatique

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

};

Page 54: Service d’Informatique

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.

Page 55: Service d’Informatique

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

Page 56: Service d’Informatique

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.

Page 57: Service d’Informatique

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 :

Page 58: Service d’Informatique

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 :

Page 59: Service d’Informatique

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.

Page 60: Service d’Informatique

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

Page 61: Service d’Informatique

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

Page 62: Service d’Informatique

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.

Page 63: Service d’Informatique

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

Page 64: Service d’Informatique

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" ;

}

Page 65: Service d’Informatique

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;

Page 66: Service d’Informatique

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 ;

}

Page 67: Service d’Informatique

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.

Page 68: Service d’Informatique

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

Page 69: Service d’Informatique

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;

}

Page 70: Service d’Informatique

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.

Page 71: Service d’Informatique

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.

Page 72: Service d’Informatique

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); }

};

Page 73: Service d’Informatique

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] ;

Page 74: Service d’Informatique

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

Page 75: Service d’Informatique

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 : ") ;

Page 76: Service d’Informatique

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.

Page 77: Service d’Informatique

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();

Page 78: Service d’Informatique

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";

}

Page 79: Service d’Informatique

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>

Page 80: Service d’Informatique

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.

Page 81: Service d’Informatique

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 :

Page 82: Service d’Informatique

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 ()

Page 83: Service d’Informatique

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

Page 84: Service d’Informatique

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

Page 85: Service d’Informatique

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>.

Page 86: Service d’Informatique

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];

Page 87: Service d’Informatique

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

Page 88: Service d’Informatique

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 ?

Page 89: Service d’Informatique

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

Page 90: Service d’Informatique

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 :

Page 91: Service d’Informatique

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.

Page 92: Service d’Informatique

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

Page 93: Service d’Informatique

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 :

Page 94: Service d’Informatique

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

Page 95: Service d’Informatique

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 ~~~~

Page 96: Service d’Informatique

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 :

Page 97: Service d’Informatique

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 ; }

};

Page 98: Service d’Informatique

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.

Page 99: Service d’Informatique

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 ~~~~

Page 100: Service d’Informatique

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 ( ) ;

}

Page 101: Service d’Informatique

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.

Page 102: Service d’Informatique

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();

..

Page 103: Service d’Informatique

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

Page 104: Service d’Informatique

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);

Page 105: Service d’Informatique

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

Page 106: Service d’Informatique

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;

}

Page 107: Service d’Informatique

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();

Page 108: Service d’Informatique

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.

Page 109: Service d’Informatique

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;

Page 110: Service d’Informatique

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");

Page 111: Service d’Informatique

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

Page 112: Service d’Informatique

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 .... :

Page 113: Service d’Informatique

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.

Page 114: Service d’Informatique

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();

Page 115: Service d’Informatique

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.

Page 116: Service d’Informatique

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)

!

Page 117: Service d’Informatique

PARTIE 3 :

LA SÉCURITÉ INFORMATIQUE

Page 118: Service d’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é ?