langage orienté objet - c++ - notes de cours -...

53
ENSEIGNEMENT DE PROMOTION SOCIALE —————————————————————— Cours de LANGAGE ORIENTE OBJET - Résolution du projet ABC v1 - —————————————————————— H. Schyns Janvier 2012 (v1.1)

Upload: duongtuyen

Post on 12-Sep-2018

217 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

ENSEIGNEMENT DE PROMOTION SOCIALE

—————————————————————— Cours de

LANGAGE ORIENTE OBJET

- Résolution du projet ABC v1 -

——————————————————————

H. Schyns

Janvier 2012 (v1.1)

Page 2: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 Sommaire

H. Schyns S.1

Sommaire

1. INTRODUCTION

2. ENONCE DU PROBLEME

2.1. Cadre général 2.2. Ce qui est demandé dans la version 1 2.3. Le projet choisi

3. L’IMPORTANT, C’EST DE COMMENCER

3.1. Lire l’énoncé 3.2. La classe Ville 3.3. Tester la classe ! 3.4. Protéger la classe

4. LES ACCESSEURS

4.1. Rôle 4.2. Déclaration 4.3. Implémentation 4.4. Validation 4.5. Accès (incorrect) aux chaînes de caractères et tableaux 4.6. Accès correct aux chaînes de caractères et tableaux 4.7. Validation 4.8. Créer des accesseurs intégraux

5. LES CONSTRUCTEURS

5.1. Rôle 5.2. Concevoir un constructeur élémentaire 5.3. Ajouter un compteur 5.4. Générer automatiquement un nom d'objet 5.5. Concevoir un constructeur avec paramètres

6. LES DESTRUCTEURS

6.1. Rôle 6.2. Créée un destructeur élémentaire

7. LE CONSTRUCTEUR DE RECOPIE

Page 3: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 Sommaire

H. Schyns S.2

7.1. Rôle 7.2. Déclaration et implémentation 7.3. Le pointeur "this" 7.4. Passer des paramètres "par référence" 7.5. Validation

8. L'OPERATEUR D'AFFECTATION

8.1. Rôle 8.2. Déclaration et implémentation 8.3. Validation 8.4. Un opérateur un peu particulier

9. GERER LES ALLOCATIONS DYNAMIQUES

9.1. Rôle 9.2. Syntaxe 9.3. Application 9.4. Validation

10. PROTEGER LES OBJETS PASSES A UNE FONCTION

10.1. Position du problème 10.2. Application

11. VALIDATION GENERALE

12. LA SUITE DU PROGRAMME

12.1. La classe Entreprise 12.2. La classe Chantier 12.3. Observation

13. REFERENCES

13.1. Ouvrages 13.2. Sites web

Page 4: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 1 - Introduction

H. Schyns 1.1

1. Introduction

Ce document présente un schéma de résolution du projet ABC v1 soumis à titre de projet d'examen en octobre 2011.

L’énoncé est disponible sur le site www.notesdecours.info dans la section Langage C++ (Interros et Examens).

Pourquoi ce document ? Un simple listing de la solution ne suffit-il pas ? Il semble que non.

Au fil des années, j’ai pu observer un comportement récurrent chez les étudiants :

La lecture de l’énoncé du projet d’examen provoque une sorte de panique, comme si rien de ce qui a été vu au cours ne pouvait servir à sa résolution.

Ce comportement me fait penser à celui d’un enfant qui, ayant appris à bien nager dans une petite piscine perd tous ses moyens quand il s’agit de nager dans le lac paisible au bord duquel la famille passe ses vacances. Pourtant l’eau du lac est semblable à l’eau de la piscine (au chlore et à la température près) et les mouvements à faire restent les mêmes. Certes, l’étendue d’eau est plus grande, mais dans un cas comme dans l’autre, on part du bord.

J’ai aussi remarqué que l’étudiant a tendance à se concentrer sur ce qu’il pense ne pas pouvoir résoudre plutôt qu’à rechercher les similitudes avec ce qu’il sait résoudre. De même, il prend un malin plaisir à imaginer des cas très particuliers dans lesquels une solution possible ne va pas fonctionner; histoire de se torturer l'esprit et de paralyser la réalisation du projet. Il est beaucoup plus constructif d'implémenter cette solution imparfaite et de signaler dans la documentation quelles sont ses limites.

Les projets demandés chaque année sont sensiblement identiques dans leurs principes et fonctionnalités. Seul le contexte change. Des solutions sont publiées, des exercices ont été réalisés en classe. Pourtant, bien peu d’étudiants prennent la peine d’analyser la solution d’un problème posé antérieurement et à transposer cette solution au problème qu’ils doivent résoudre.

Sans doute pensent-ils que s’inspirer de ce qui a déjà été fait procure moins de mérite. Ils oublient ainsi que ce qu’on demande avant tout à un professionnel est d’être efficace.

Il apparaît également que certains étudiants n’envisagent pas la possibilité d’un développement progressif de la solution. Non, l’application demandée ne doit pas ; comme Minerve qui sortit du crâne de Jupiter casquée et vêtue d’une armure ; se matérialiser instantanément, pourvue de tous ses objets et de toutes ses fonctionnalités. Il est permis (!) de commencer par des choses simples, de développer indépendamment plusieurs classes, de les réunir, de revenir en arrière, de modifier, de simplifier, de laisser tomber certains aspects, de les reprendre par la suite, de créer une classe parente plutôt qu'une classe dérivée…

Enfin, il n'est pas inutile de rappeler que C++ inclut C. En d'autres mots, l'implémentation d'une classe ou d'une fonction ne doit pas uniquement faire appel à des objets C++ standards, il est permis d'utiliser des types C plus simples ou plus familiers. Un bel exemple est l'utilisation d'un type char[ ] au lieu des classes string et strstream pour le traitement des chaînes de caractères.

Page 5: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 1 - Introduction

H. Schyns 1.2

Le but de ce document est de servir de canevas de résolution. Pour tenir compte des remarques des étudiants, la résolution a été très détaillée. De cette manière, ce document peut aussi servir de manuel d'initiation au C++.

Toutefois, pour alléger le texte, le code ne reprend pas tous les détails et, en particulier, il ne reprend pas la liste de tous les #include et autres using namespace. Le code source complet des différentes versions est néanmoins disponible par ailleurs dans un fichier zip.

La solution proposée ne prétend pas être la meilleure ni la plus astucieuse. Elle se veut simplement cohérente d’un point de vue conceptuel et fonctionnel et correcte du point de vue de la syntaxe et de la gestion de la mémoire. C'est également ce qu'on demande à l'étudiant. Toutefois, il serait dommage de ne pas profiter de ce document pour attirer l’attention sur des variantes plus élégantes ou plus subtiles qui n'ont pas toujours été vues au cours.

La version présentée ici peut donc différer par moment de celle construite au cours.

Page 6: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 2 - Enoncé du problème

H. Schyns 2.1

2. Enoncé du problème

2.1. Cadre général

On demande de réaliser un programme qui permet à l'utilisateur de gérer trois concepts quelconques (A, B, C) qui doivent être reliés par une relation de type "un à plusieurs" (1) :

possèdepossèdeA B C

fig. 2.1 Modèle E-R hiérarchique

Les relations s'énoncent de la manière suivante

Chaque A possède un ou plusieurs B Chaque B est possédé par un seul A

Chaque B possède un ou plusieurs C Chaque C est possédé par un seul B

Le verbe "Posséder" peut être remplacer par n'importe quel verbe qui définit une relation. Le caractère unique de la réciproque est capital. Quelques exemples :

A Relation B Relation C Joueurs Possèdent Villes Possèdent Accessoires Clients Possèdent Voitures Subissent Entretiens

Entreprises Comptent Services Emploient Employés Zoos Comprennent Enclos Abritent Animaux

Maisons Abritent Familles Elèvent Enfants

Attention, la relation n'est pas nécessairement hiérarchique. Elle peut faire intervenir une entité pivot. Ainsi dans ces exemples :

A Relation B Relation C Clients Reçoivent Factures Listent Produits Joueur Reçoivent Sélections Produisent Matches

Chaque Facture liste un ou plusieurs Produits (OK) Chaque Produit est listé sur une ou plusieurs Factures

Dans ce premier exemple, il faut encore éclater la relation plusieurs:plusieurs en introduisant l'entité supplémentaire "Ligne de Facture". Le projet comptera donc quatre entités.

Par contre, dans le deuxième projet, c'est l'entité "Sélection" qui joue déjà le rôle de pivot. En effet, le projet initial pourrait s'énoncer :

1 L'analyse Entités-Relations est présentée ici de manière très succincte. Le lecteur qui souhaite en savoir

plus peut consulter le document "Développer une application" et "Analyse Entités-Relations" dans la section "Access" du site www.notesdecours.info

Page 7: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 2 - Enoncé du problème

H. Schyns 2.2

Chaque Joueur joue un ou plusieurs Matches (OK) Chaque Match est joué par un ou plusieurs Joueurs

Après l'introduction de l'entité "Sélection", il devient

Chaque Match produit une ou plusieurs Sélections (OK) Chaque Sélection est produite pour un seul Match (OK)

Chaque Joueur reçoit une ou plusieurs Sélection Chaque Sélection est reçue par un seul Joueur (OK)

produitreçoit B(pivot) CA

fig. 2.2 Modèle E-R avec pivot

Il ne s'agit plus d'une hiérarchie au sens strict car le côté "plusieurs" de la relation est dirigé vers l'entité centrale (pivot). Il faut en être bien conscient de ces aspects au moment où on définit son projet.

2.2. Ce qui est demandé dans la version 1

Définir des classes A, B et C telles que les objets des classes A, B et C soient caractérisé par :

- un code identifiant unique à incrémentation automatique (unsigned long); - un descripteur (nom, marque, définition…) par défaut généré automatiquement

(Joueur0001, Ville001, etc.); - un certain nombre de caractéristiques numériques (âge, prix, durée,…); - un certain nombre de caractéristiques nominales (pseudo, couleur, etc.,…); - d'autres caractéristiques au choix.

De plus, selon les règles de constitution des bases de données :

- la classe B doit contenir un data membre permettant de stocker l'identifiant de l'objet de type A auquel il appartient.

- la classe C doit contenir un data membre permettant de stocker l'identifiant de l'objet de type B auquel il appartient.

Tout nouvel objet, quelle que soit la classe à laquelle il appartient

- voit tous ses data membre initialisés; - est automatiquement nommé "Objet_nnnn", où Objet est le nom de la classe

et nnnn est son code identifiant (utilisation de la fonction sprintf) (cf. supra).

Pour chacune de ces classes, on prévoit obligatoirement

- un constructeur sans paramètres; - un constructeur de recopie - un operator= - un destructeur - des accesseurs de type getXxx et setXxx qui permettent de lire et de

modifier les data membres.

Page 8: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 2 - Enoncé du problème

H. Schyns 2.3

En fonction de leur utilité, on peut aussi définir plusieurs constructeurs avec paramètres.

Il est recommandé d'implémenter un "compteur d'objets en vie" de manière à vérifier la logique de construction et de destruction.

Les trois classes seront testées dans un programme principal (main) qui appelle (de manière brutale et sommaire) les différentes fonctions membres.

2.3. Le projet choisi

Pour illustrer la résolution, nous avons choisi de réaliser un programme qui permet de

gérer les différents chantiers entrepris par différentes villes et réalisés par les entreprises contractantes.

Il y a trois entités en présence :

A Relation B Relation C Villes Entreprennent Chantiers Réalisent Entreprises

Chaque Ville entreprend un ou plusieurs Chantiers (OK) Chaque Chantier est entrepris par une seule Ville (OK)

Chaque Entreprise réalise un ou plusieurs Chantiers (OK) Chaque Chantier est réalisé par une seule Entreprise (OK)

RéaliseEntreprendVilles Chantiers Entre-

prises

fig. 2.3 Modèle E-R du projet servant d'exemple

On voit que ce projet ne permettra pas de gérer les projets entrepris conjointement par plusieurs villes ni les projets réalisés conjointement par plusieurs entreprises. Il faudrait pour cela insérer une entité pivot dans chaque relation.

Nous constaterons cependant que, si la conception et la réalisation ont été menées intelligemment, il est assez facile de faire évoluer le projet sans modifier grand'chose à ce qui a été fait.

Page 9: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 2 - Enoncé du problème

H. Schyns 2.4

Prend Réalise

OrganiseMotiveParticipa-tions Chantiers Inter-

ventions

Villes Entre-prises

fig. 2.4 Modèle E-R plus complet du projet servant d'exemple

Page 10: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 3 - L’important, c’est de commencer

H. Schyns 3.1

3. L’important, c’est de commencer

3.1. Lire l’énoncé

On demande de réaliser un programme qui permet à l'utilisateur de définir des villes qui entreprennent des chantiers grâce à l'intervention d'entreprises.

Nous décidons que chaque Ville sera caractérisée par :

- un code identifiant unique (unsigned long); - son nom (50 char); - son code postal (unsigned int) (1); - son nombre d'habitants (unsigned long);

Pour les autres entités, nous verrons plus tard…

3.2. La classe Ville

A ce stade, nous en savons assez pour commencer l’implémentation d’une classe Ville élémentaire :

class Ville // Ville.h { public: // data membres unsigned long idVille; // identification de la ville char Nom[50]; // chaine pour le nom unsigned int CodePostal; // uint pour CP uns1gned long NbreHabitants; // nombre d'habitants }; // ne pas oublier le ; final

Il est clair que nous ne respectons pas encore le cahier des charges, mais l’important est de commencer avec quelque chose.

Ainsi, tous les data membres sont marqués "public" afin de vérifier leur accessibilité.

3.3. Tester la classe !

A présent, concevons un petit programme de test qui se contente de valider la syntaxe et l'accessibilité des données :

// TestUrbain.cpp #include <iostream> // pour permettre les accès et affichages #include <string.h> // pour strcpy #include "ville.h" using namespace std; // pour qu'il connaisse "cout" void main (void) { Ville v; v.idVille = 1; v.CodePostal = 4000; strcpy(v.Nom, "Liege"); // une chaine doit tjrs etre copiée! v.NbreHabitants = 200000;

1 En toute rigueur, le CP est codé dans une chaîne de caractères car certains pays mélangent les lettres et

les chiffres dans leurs CP.

Page 11: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 3 - L’important, c’est de commencer

H. Schyns 3.2

cout << "Fiche de Ville" << endl ; cout << "Id ville : " << v.idVille << endl; cout << "Nom : " << v.Nom << endl; cout << "Code postal : " << v.CodePostal << endl; cout << "Habitants : " << v.NbreHabitants << endl; }

Il est important de vérifier déjà à ce stade s'il n'y a pas d'erreurs de syntaxe, si les déclarations fonctionnent bien et si nous retrouvons bien ce que nous avons inscrit dans l'objet.

Afin d'économiser le papier, nous ne réécrirons pas chaque fois la liste des #include et using namespace mais il est clair qu'elle reste bien présente.

Voici ce que notre petit programme produit à l'écran :

Fiche de Ville // Affichage à l'écran Id ville : 1 Nom : Liege Code postal : 4000 Habitants : 200000 Press any key to continue

La ligne "Press any key… " n'apparaît pas nécessairement. Cela dépend du compilateur utilisé et du réglage des paramètres de débogage.

3.4. Protéger la classe

Nous protégeons l'accès aux data membres en remplaçant le label public par protected

class Ville // Ville.h { protected: unsigned long idVille; // identification de la ville char Nom[50]; // chaine pour le nom unsigned int CodePostal; // uint pour CP uns1gned long NbreHabitants; // nombre d'habitants }; // ne pas oublier le ; final

Si nous essayons de générer la solution, le compilateur nous délivre un torrent de messages d'erreur ! C'est normal ! Il ne nous reste plus qu'à définir les accesseurs setXxx et getXxx pour chacun des membres.

Page 12: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 4 - Les accesseurs

H. Schyns 4.1

4. Les accesseurs

4.1. Rôle

Lorsque les data membres sont définis comme public, n'importe quelle instruction, fonction ou application peut lire les informations ou les modifier. Nous pourrions ainsi nous retrouver avec une ville dont le nom est effacé ou remplacé par un texte plus long, comme un poème de Victor Hugo ; le code postal pourrait dépasser les valeurs autorisées etc.… (fig. 4.1)

Ville

Instruction

Fonction

Application

IdVilleNom

CodePostalNbreHabitants

Lire

Ecrire

fig. 4.1 Accès aux data membres public

En définissant les data membres comme protected (ou private) nous les rendons absolument inaccessibles depuis l'extérieur (fig. 4.2). Les données sont protégées, certes, mais c'est sans intérêt puisque nous n'y avons pas accès.

Ville

IdVilleNom

CodePostalNbreHabitants

Instruction

Fonction

Application

Lire

Ecrire

fig. 4.2 Accès aux data membres protected

Les accesseurs sont des fonctions qui appartiennent à la classe. On les appelle des fonctions membres ou des méthodes. Elles sont déclarées public ce qui fait qu'elles peuvent être appelées par l'extérieur.

Les accesseurs jouent le rôle de concierge, de gardien ou de magasinier : ils contrôlent si les données que l'on veut inscrire sont acceptables et si les données qu'on veut lire sont disponibles (fig. 4.3). Ils sont le point de passage obligé vers les data membres.

Le mot-clé protected et les accesseurs implémentent le principe d'encapsulation des données.

Page 13: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 4 - Les accesseurs

H. Schyns 4.2

Ville

IdVilleNom

CodePostalNbreHabitants

Instruction

Fonction

Application

setIdVille

getIdVille

fig. 4.3 Accès aux data membres protected via les accesseurs

4.2. Déclaration

Nous devons créer un accesseur setXxxx (écriture) et un accesseur getXxxx (lecture) pour chacun des data membres auquel nous désirons donner accès.

class Ville // Ville.h { protected: // data membres unsigned long idVille; // identification de la ville char Nom[50]; // chaine pour le nom unsigned int CodePostal; // uint pour CP uns1gned long NbreHabitants; // nombre d'habitants public: // fonctions membres void setidVille (unsigned long aidVille); void setCodePostal (unsigned int aCodePostal); void setNbreHabitants (unsigned long aNbreHabitants); unsigned long getidVille (void); unsigned int getCodePostal (void); unsigned long getNbreHabitants (void); };

Le Nom, qui est une chaîne de caractères, nécessite un traitement spécial que nous verrons plus tard.

Notons la symétrie des déclarations :

- les setXxxx prennent un paramètre d'un type donné et ne retournent rien (void);

- les getXxxx ne prennent rien (void) mais renvoient une valeur d'un type donné.

4.3. Implémentation

Traditionnellement, les fonctions prennent le nom du data membre qu'elles contrôlent et le paramètre qui est passé porte aussi le nom du data membre précédé de "a", (ceci pour éviter de se creuser la tête à chercher des noms de fonctions et de paramètres).

Commençons par rédiger quelques accesseurs qui n'effectuent aucun contrôle. Notons que chaque fonction est préfixée par le nom de la classe à laquelle elle appartient (ici Ville::). Ceci signale au compilateur que ces fonctions

Page 14: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 4 - Les accesseurs

H. Schyns 4.3

appartiennent à la classe Ville et donc qu'elles (et elles seules) ont accès aux data membres marqués protected ou private :

#include "ville.h" // Ville.cpp void Ville::setidVille(unsigned long aidVille) { idVille = aidVille; } //-------------------------------------- void Ville::setCodePostal (unsigned int aCodePostal) { CodePostal = aCodePostal; } //-------------------------------------- void Ville::setNbreHabitants (unsigned long aNbreHabitants) { NbreHabitants = aNbreHabitants; } //-------------------------------------- unsigned long Ville::getidVille (void) { return (idVille); } //-------------------------------------- unsigned int Ville::getCodePostal (void) { return (CodePostal); } //-------------------------------------- unsigned long Ville::getNbreHabitants (void) { return (NbreHabitants); }

Nous pourrions aussi définir un accesseur setAll qui passe d'un seul coup toutes les données et un accesseur getAll qui les récupère en une seule fois. Nous les développerons un peu plus loin.

4.4. Validation

Il ne nous reste plus qu'à modifier notre programme de test pour utiliser ces accesseurs à la place des affectations :

void main (void) // TestUrbain.cpp { Ville v; v.setidVille(1); v.setCodePostal(4000); // strcpy(v.Nom, "Liege"); // plus tard v.setNbreHabitants(200000); cout << "Fiche de Ville" << endl ; cout << "Id ville : " << v.getidVille() << endl; // cout << "Nom : " << v.Nom << endl; // plus tard cout << "Code postal : " << v.getCodePostal() << endl; cout << "Habitants : " << v.getNbreHabitants() << endl; }

Puisque les fonctions membres appartiennent à la classe et que la classe se comporte à peu près comme une structure, on les appelle comme si on voulait accéder à un data membre :

Page 15: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 4 - Les accesseurs

H. Schyns 4.4

nomobjet.nomfonction()

Si l'objet est transmis par pointeur, ce que nous verrons plus tard, nous réaliserons l'appel par :

nomobjet->nomfonction()

4.5. Accès (incorrect) aux chaînes de caractères et tableaux

Les chaînes de caractères et tableaux nécessitent quelques précautions car ils sont transmis par pointeurs.

Sauf cas particulier, il ne suffit pas de copier l'adresse de la chaîne ou du tableau dans l'objet, il faut recopier chacun des éléments sinon le principe d'encapsulation est violé.

A première vue, les accesseurs pour le nom pourraient s'écrire comme suit :

class Ville // Ville.h { protected: // data membres x char Nom[50]; // chaine pour le nom public: // fonctions membres x void setNom (char* aNom); char* getNom (void); };

L'implémentation serait

#include "ville.h" // Ville.cpp x void Ville::setNom (char* aNom) { strncpy(Nom, aNom, 50); // copie limitée à n char Nom[49]=0; } char* Ville::getNom (void) { return(Nom); // DANGER !! }

La fonction setNom() est à peu près correcte. Elle utilise la strncpy et non strcpy afin d'éviter un débordement de mémoire si la chaîne fournie par aNom dépasse la taille prévue.

Elle a cependant quelques défauts :

- la longueur de la chaîne Nom est fixée "en dur" (50) au lieu d'utiliser une constante,

- elle n'effectue aucune validation des paramètres transmis.

Telle qu'écrite, la fonction getNom() est terriblement dangereuse !

En effet, en renvoyant le pointeur sur un data membre, elle permet à la routine appelante d'en modifier le contenu sans passer par l'accesseur ! Elle ouvre une brèche énorme dans l'encapsulation.

Petit exemple :

Page 16: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 4 - Les accesseurs

H. Schyns 4.5

void main (void) // TestUrbain.cpp { Ville v; char *ptr; v.setNom("Liege"); // OK écrire en passant par l'accesseur ptr = v.getNom(); // OK lire en passant par l'accesseur cout << "Nom : " << v.getNom() << endl; strcpy(ptr, "Je modifie le nom sans passer par l'accesseur"); cout << "Nom : " << v.getNom() << endl; }

Si vous ne voyez pas immédiatement ce qui se passe, écrivez ce petit programme et testez-le vous-même.

Voici le résultat :

Nom : Liege // Affichage à l'écran Nom : Je modifie le nom sans passer par l'accesseur Press any key to continue

4.6. Accès correct aux chaînes de caractères et tableaux

La conclusion est claire :

l'objet ne doit jamais renvoyer un pointeur sur l'un de ses data membres "protected" ou "private".

Comment faire ? En demandant à la routine appelante de fournir un tableau dans lequel la chaîne de caractères sera recopiée (p.ex. buf). Il faut évidemment qu'elle donne aussi la taille du tableau fourni afin que la recopie ne provoque pas de débordement (p.ex. bufsize). Ceci coûte cher en cycles de calcul mais c'est le prix à payer pour assurer la protection des données.

La sécurité des données est primordiale en POO ! Chaque classe doit veiller à sa sécurité et être sans reproche !

class Ville // Ville.h { protected: // data membres x char Nom[vNOMLEN]; public: x void setNom (char* aNom); x char* getNom (char* buf, unsigned int bufsize); };

Les implémentations deviennent :

#include "ville.h" // Ville.cpp x void Ville::setNom (char* aNom) { if (!aNom) // a-t-on fourni une adresse nulle ? Nom[0]=0; // alors on "efface" le nom

Page 17: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 4 - Les accesseurs

H. Schyns 4.6

else { strncpy(Nom, aNom, vNOMLEN); Nom[vNOMLEN-1]=0; } } char* Ville::getNom (char* buf, unsigned int bufsize) { if (buf && bufsize) // a-t-on fourni un tampon valable ? { // alors on peut recopier strncpy(buf, Nom, bufsize); buf[bufsize-1]=0; } return (buf); }

Notons l'usage de strncpy dans getNom : il ne faut pas que notre classe Ville soit responsable d'un débordement de mémoire dans la routine appelante.

En renvoyant l'adresse de buf sur la stack nous ne violons pas la règle d'encapsulation : buf n'appartient pas à l'objet ; il a été fourni par la routine appelante !

4.7. Validation

Comme de coutume, nous modifions notre programme de test pour vérifier le fonctionnement de ces nouveaux accesseurs :

void main (void) // TestUrbain.cpp { Ville v; char buf[20]; v.setidVille(1); v.setCodePostal(4000); v.setNom("Liege"); v.setNbreHabitants(200000); cout << "Fiche de Ville" << endl ; cout << "Id ville : " << v.getidVille() << endl; cout << "Nom : " << v.getNom(buf, 20) << endl; cout << "Code postal : " << v.getCodePostal() << endl; cout << "Habitants : " << v.getNbreHabitants() << endl; }

C'est parce que getNom renvoie buf sur la stack que nous pouvons l'utiliser comme les autres fonctions getXxxx dans les affichages à l'écran.

Fiche de Ville // Affichage à l'écran Id ville : 1 Nom : Liege Code postal : 4000 Habitants : 200000 Press any key to continue

4.8. Créer des accesseurs intégraux

A côté des accesseurs spécifiques qui accèdent à un et un seul data membre, nous pouvons définir des accesseurs intégraux qui fournissent ou récupèrent l'ensemble

Page 18: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 4 - Les accesseurs

H. Schyns 4.7

des data membres en un seul appel (sauf idVille qui aura un traitement particulier).

class Ville // Ville.h { protected: // data membres x public: x void setAll (char* aNom, unsigned int aCodePostal, unsigned long aNbreHabitants); void getAll (char* buf, unsigned int bufsize, unsigned int *aCodePostal, unsigned long *aNbreHabitants); };

L'implémentation de setAll ne pose pas de problème particulier : il suffit de regrouper le code dispersé dans les fonctions déjà écrites, voire même d'utiliser les accesseurs existants. Cette dernière solution est plus sûre car certains de ces accesseurs procèdent à une validation des données fournies, ce qui nous évite de devoir les réécrire (1) :

#include "ville.h" // Ville.cpp x void Ville::setAll (char* aNom, unsigned int aCodePostal, unsigned long aNbreHabitants) { setNom(aNom); setCodePostal (aCodePostal); setNbreHabitants (aNbreHabitants); }

L'écriture de getAll est un peu plus subtile. En effet, il ne s'agit plus de renvoyer une seule valeur mais bien d'en renvoyer plusieurs dont l'identifiant. Dès lors, il n'est plus possible d'utiliser la technique de la "valeur en retour". Nous devons utiliser un transfert par pointeurs et, pour cela, la fonction appelante doit fournir les adresses des variables.

void Ville::getAll (unsigned long *aidVille, // Ville.cpp char* buf, unsigned int bufsize, unsigned int *aCodePostal, unsigned long *aNbreHabitants) { *aidVille = getidVille(); getNom (buf, bufsize); *aCodePostal = getCodePostal(); *aNbreHabitants = getNbreHabitants(); }

Ici aussi, nous avons très paresseusement réutilisé les accesseurs existants plutôt que de rassembler le code dispersé dans les diverses fonctions getXxxx.

Notre programme de test s'enrichit d'un nouveau paragraphe :

void main (void) // TestUrbain.cpp {

1 Notons au passage que SetAll ne définit par l'identifiant, par contre getAll le récupère. C'est voulu.

Page 19: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 4 - Les accesseurs

H. Schyns 4.8

Ville v; Ville x: char buf[20]; unsigned long id; unsigned int cp; unsigned long nh; v.setidVille(1); v.setCodePostal(4000); v.setNom("Liege"); v.setNbreHabitants(200000); cout << "Fiche de Ville" << endl ; cout << "Id ville : " << v.getidVille() << endl; cout << "Nom : " << v.getNom(buf, 20) << endl; cout << "Code postal : " << v.getCodePostal() << endl; cout << "Habitants : " << v.getNbreHabitants() << endl; x.setAll("Bruxelles", 1000, 900000); x.getAll(&id, buf, 20, &cp, &nh); // adresses pour pointeurs cout << "Fiche de Ville" << endl ; cout << "Id ville : " << id << endl; cout << "Nom : " << buf << endl; cout << "Code postal : " << cp << endl; cout << "Habitants : " << nh << endl; }

Voici ce que nous devons obtenir :

Fiche de Ville // Affichage à l'écran Id ville : 1 Nom : Liege Code postal : 4000 Habitants : 200000 Fiche de Ville Id ville : 1298564421 // n'importe quoi car non initialisé! Nom : Bruxelles Code postal : 1000 Habitants : 900000 Press any key to continue

Page 20: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 5 - Les constructeurs

H. Schyns 5.1

5. Les constructeurs

5.1. Rôle

En programmation procédurale C, nos entités étaient représentées par des structures telle que

typedef struct sVille { char Nom[50]; x } Ville;

Nous avions vu que, pour travailler proprement, ces structures devaient être initialisées par une fonction telle que

VilleInitialiser (Ville* aville) { memset(aville, 0, sizeof(Ville); strcpy (aville->Nom, "<Inconnu>"); x }

Elle se charge, par exemple, de mettre toute la structure "à blanc" puis de définir un nom par défaut.

Le gros problème était de penser à appeler systématiquement cette routine d'initialisation chaque fois qu'une nouvelle structure devait être utilisée.

Ce problème est complètement réglé en C++ par les constructeurs.

Un constructeur (ang.: constructor) est une fonction qui est appellée automatiquement lors de l'instanciation d'un objet issu de la classe donnée.

Cette instanciation a lieu soit lors de la déclaration de la variable soit lors de son allocation dynamique.

Le rôle du constructeur est double :

- initialiser les data membres,

- effectuer toutes les allocations dynamiques internes à la classe.

Toute classe doit obligatoirement posséder un constructeur sans paramètre (void). Toutefois, en cas d'oubli, le compilateur ajoute de lui-même un constructeur… qui ne fait absolument rien (fonction vide) mais qui a le mérite d'exister.

Un constructeur est une fonction publique qui porte le même nom que la classe à laquelle elle appartient. Habituellement, c'est la première fonction déclarée dans le bloc public.

Détail qui a son importance : un constructeur ne renvoie rien, pas même un "void" !

Page 21: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 5 - Les constructeurs

H. Schyns 5.2

5.2. Concevoir un constructeur élémentaire

Pour initialiser les objets, nous définissons donc un constructeur.

class Ville // Ville.h { protected: // data membres x public: Ville(void); //constructeur sans parametres x };

Comme dans le cas des autres fonctions, lors de l'implémentation, le nom du constructeur doit être préfixé par le nom de la classe.

Attention : tous les data membres doivent être initialisés, c'est une règle de bonne pratique.

#include <iostream> // Ville.cpp #include "ville.h" using namspace std // pour cout << x Ville::Ville(void) { cout << "Appel du constructeur" << endl; idVille = 0; memset(Nom,0, vNOMLEN); // mettre le nom à zéro setNom("<Inconnu>"); //définir un nom par défaut CodePostal = 9999; NbreHabitants = 0; }

La première ligne - qui sera supprimée par la suite - nous permet de tracer l'appel du constructeur.

Pour donner un nom par défaut aux objets construits, nous appelons simplement l'accesseur setNom que nous avons défini plus haut. Ce n'est pas obligatoire mais c'est plus propre car nous savons que cette fonction fait tous les tests nécessaires pour éviter les dépassements de mémoire.

Lors de l'appel il n'est pas nécessaire de préfixer le nom de la fonction, pas plus qu'il n'est nécessaire de préfixer le nom des variables (1).

Comme d'habitude, nous testons ce que nous venons de faire (2).

void main (void) // TestUrbain.cpp { Ville v, x; // on définit 2 villes char buf[20]; // contenu "brut" de la ville v cout << "Id ville : " << v.getidVille() << endl; cout << "Nom : " << v.getNom(buf, 20 ) << endl;

1 Certains développeurs préfixent systématiquement les appels (p.ex.: this->setNom() ) dans le but de rendre

le code plus portable sur d'autres compilateurs ou de pouvoir le transposer plus facilement en un autre langage tel que Java ou PHP mais ce n'est pas exigé par la norme C++.

2 Je le répéterai encore cent fois : il est indispensable de tester ce que l'on fait à chaque étape afin d'éliminer les bugs au fur et à mesure de la réalisation du projet.

Page 22: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 5 - Les constructeurs

H. Schyns 5.3

// contenu "brut" de la ville x cout << "Id ville : " << x.getidVille() << endl; cout << "Nom : " << x.getNom(buf, 20 ) << endl; // modifier la ville v v.setidVille(1); v.setCodePostal(4000); v.setNom("Liege"); v.setNbreHabitants(200000); // afficher la ville v cout << "Fiche de Ville" << endl ; cout << "Id ville : " << v.getidVille() << endl; cout << "Nom : " << v.getNom(buf, 20 ) << endl; cout << "Code postal : " << v.getCodePostal() << endl; cout << "Habitants : " << v.getNbreHabitants() << endl; }

Voici le résultat :

Appel du constructeur // Affichage à l'écran Appel du constructeur Id ville : 0 Nom : <Inconnu> Id ville : 0 Nom : <Inconnu> Fiche de Ville Id ville : 1 Nom : Liege Code postal : 4000 Habitants : 200000 Press any key to continue

Les premiers messages qui apparaissent sont ceux écrits par le constructeur. Ils sont bien générés lors de la déclaration des variables ou, plus exactement, lors de leur instanciation. Il y a deux messages car nous avons déclaré deux objets de la classe Ville.

5.3. Ajouter un compteur

L'énoncé demande que les identifiants soient uniques et gérés par une incrémentation automatique. Autrement dit, l'identifiant de la première Ville créée doit être 1 (par exemple), l'identifiant de la Ville suivante doit être 2 et ainsi de suite. Chaque fois qu'un objet de classe Ville est créé, il doit recevoir pour identifiant le numéro qui suit celui de l'objet qui a été créé avant lui.

Le problème peut être facilement résolu à l'aide d'un compteur qu'on incrémente à chaque création d'objet.

La question qui se pose est de savoir où nous allons placer ce compteur. Une première idée (et bonne) idée est de le placer parmi les data membres de la classe.

class Ville // Ville.h { protected: unsigned long Compteur; // ajouter un compteur unsigned long idVille; char Nom[vNOMLEN]; unsigned int CodePostal; unsigned long NbreHabitants; x };

Ensuite, nous activons le compteur dans le constructeur puisque celui-ci est automatiquement appelé à chaque création d'objet :

Page 23: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 5 - Les constructeurs

H. Schyns 5.4

Ville::Ville(void) // Ville.cpp { cout << "Appel du constructeur" << endl; idVille = ++Compteur; // incrementer le compteur memset(Nom,0, vNOMLEN); setNom("<Inconnu>"); CodePostal = 9999; NbreHabitants = 0; }

Le premier problème qui apparaît est qu'en déclarant le compteur de cette manière, nous allons créer un compteur différent dans chacun des objets créés (fig. 5.1).

////

Ville x

compteuridVille

Ville y

compteuridVille

Ville z

compteuridVille

fig. 5.1 Compteurs différents dans chacun des objets

Il n'y a aucune communication entre les compteurs ainsi créés. L'objet Ville y ne connaît absolument pas la valeur du compteur de l'objet Ville x et réciproquement (1).

La solution consiste à déclarer le compteur comme "static" :

class Ville // Ville.h { protected: static unsigned long Compteur; // compteur "static" x };

Déclarer un data membre "static", c'est signaler au compilateur que cette variable est unique et partagée. Elle n'appartient plus à un objet en particulier mais est commune à tous les objets de la classe, aussi bien en lecture qu'en écriture (fig. 5.2).

Ville x

idVille

Ville y

idVille

Ville z

idVille

"static" compteur

fig. 5.2 Compteur "static" commun aux différents objets.

Si l'un des objets incrémente le compteur, tous les autres verront à leur prochaine lecture que le compteur a été incrémenté. Toutefois, l'encapsulation s'applique toujours et personne d'autre en dehors de la classe Ville n'a accès au compteur.

1 Nous pourrions récupérer la valeur du compteur de Ville x par un accesseur x.getCompteur() et le

sauver dans Ville y par un accesseur y.setCompteur() mais cela implique de toujours connaître l'identité du dernier objet créé, ce qui ne fait que déplacer le problème.

Page 24: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 5 - Les constructeurs

H. Schyns 5.5

Il nous reste à initialiser le compteur. Il semble normal d'effectuer cette opération dans le constructeur :

Ville::Ville(void) // Ville.cpp { cout << "Appel du constructeur" << endl; Compteur = 0; idVille = ++Compteur; // incrementer le compteur x };

Le problème apparaît immédiatement : puisque le compteur est commun à tous les objets et que le constructeur est appelé automatiquement à chaque déclaration, il sera systématiquement remis à zéro et tous les identifiants seront identiques.

Il est donc impossible de faire une initialisation d'un data membre statique dans un constructeur.

La solution consiste à faire une initialisation "hors fonction" en plaçant l'instruction avant la première fonction implémentée dans le fichier .cpp :

#include <iostream> // Ville.cpp #include "ville.h" using namespace std; // initialisation "hors fontion" unsigned long Ville::Compteur = 0l; // 0l car "long" et non 01 // constructeur qui l'utilise Ville::Ville(void) { cout << "Appel du constructeur" << endl; idVille = ++Compteur; x }

Notons que la variable initialisée doit être précédée de son type lors de l'initialisation.

A partir de maintenant, la fonction setidVille doit devenir inaccessible de l'extérieur puisque c'est la classe elle-même qui gère ses identifiants. Nous la transférons dans un paragraphe "protected".

class Ville // Ville.h { protected: // acces direct interdit static unsigned long Compteur; x protected: // fonctions cachees void setidVille (unsigned long aidVille); public: // fonctions visibles Ville(void); x };

Testons ces modifications en reprenant le code du test précédent (page 5.2) dans lequel nous supprimons simplement la ligne

v.setidVille(1); // TestUrbain.cpp

Page 25: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 5 - Les constructeurs

H. Schyns 5.6

Nous voyons que cette fois, les objets ont des identifiants différents, générés automatiquement par les constructeurs.

Appel du constructeur // Affichage à l'écran Appel du constructeur Id ville : 1 <<< ici Nom : <Inconnu> Id ville : 2 <<< ici Nom : <Inconnu> Fiche de Ville Id ville : 1 Nom : Liege Code postal : 4000 Habitants : 200000 Press any key to continue

5.4. Générer automatiquement un nom d'objet

Lorsqu'on crée un nouveau document ou une nouvelle feuille de calcul dans une suite bureautique, le logiciel leur donne automatiquement un nom par défaut tel que Document1 ou Feuille1.

Dans un environnement informatique, rien ne peut rester sans nom !

En fonction de ce principe, chacune de nos villes doit recevoir un nom par défaut. La chose est facile à mettre en place puisque chaque ville reçoit déjà un identifiant différent. Il nous suffit de le récupérer et de l'accoler à un nom.

Nous reprendrons simplement la solution déjà utilisée en C. Elle faisait appel à la fonction sprintf. Une seule ligne change dans l'implémentation de notre constructeur :

// Ville.cpp unsigned long Ville::Compteur = 0l; // 0l car "long" et non 01 // constructeur qui l'utilise Ville::Ville(void) { cout << "Appel du constructeur" << endl; idVille = ++Compteur; memset(Nom,0, vNOMLEN); sprintf (Nom, "Ville%04lu", idVille); // nom par défaut CodePostal = 9999; NbreHabitants = 0; }

Le programme de test est inchangé mais cette fois, le nom des deux villes est forgé à partir de leur identifiant. Ca ressemble plus à ce qu'on a l'habitude de voir dans des applications professionnelles :

Appel du constructeur // Affichage à l'écran Appel du constructeur Id ville : 1 Nom : Ville0001 <<< ici Id ville : 2 Nom : Ville0002 <<< ici Fiche de Ville Id ville : 1 Nom : Liege Code postal : 4000 Habitants : 200000 Press any key to continue

Page 26: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 5 - Les constructeurs

H. Schyns 5.7

5.5. Concevoir un constructeur avec paramètres

Dans le chapitre consacré aux accesseurs, nous avons défini une fonction setAll qui permettait de définir tous les data membres en un seul appel.

La même opération est possible avec les constructeurs. Il suffit pour cela de définir un constructeur avec paramètres.

class Ville // Ville.h { protected: // acces direct interdit x public: Ville(void); // constructeur sans param Ville(char* aNom, // constructeur avec param unsigned int aCodePostal, unsigned long aNbreHabitants); x };

Cette nouvelle fonction étant un constructeur, elle doit faire tout ce qu'on attend d'un constructeur. En particulier, elle doit gérer le compteur et mettre la chaîne de caractères du nom à zéro.

Les constructeurs ne peuvent pas s'appeler entre-eux ; ils ne peuvent même pas être appelés explicitement par le développeur.

Par contre, nous pouvons créer une fonction membre "protected" qui, elle, sera appelée par tous les constructeurs. Nous utiliserons cela plus tard.

Ville::Ville(void) // Ville.cpp { x } Ville::Ville(char* aNom, unsigned int aCodePostal, unsigned long aNbreHabitants) { cout << "Appel du constructeur avec parametres" << endl; idVille = ++Compteur; memset(Nom,0, vNOMLEN); setNom(aNom); setCodePostal (aCodePostal); setNbreHabitants (aNbreHabitants); }

Puisque le constructeur est appelé lors de la déclaration de l'objet, c'est lors de la déclaration que nous devons passer les paramètres :

void main (void) // TestUrbain.cpp { Ville v, x; Ville z ("Charleroi", 6000, 150000); x }

Le test montre que le compilateur fait bien la distinction entre les constructeurs qui doivent être appelés :

Page 27: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 5 - Les constructeurs

H. Schyns 5.8

Appel du constructeur // Affichage à l'écran Appel du constructeur Appel du constructeur avec parametres x Fiche de Ville Id ville : 3 Nom : Charleroi Code postal : 6000 Habitants : 150000 Press any key to continue

C++ permet d'écrire plusieurs fonctions qui portent le même nom mais qui diffèrent par le type et/ou le nombre de paramètres. C'est ce qu'on appelle la surcharge des fonctions.

Lors de la génération du programme, le compilateur identifie les paramètres qui sont passés. Il génère ensuite un appel vers la fonction adéquate.

Page 28: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 6 - Les destructeurs

H. Schyns 6.1

6. Les destructeurs

6.1. Rôle

Dans l'énoncé du problème, il nous est demandé de créer un compteur d'objets en vie.

Au point 5.3 nous avons utilisé une variable statique pour compter et numéroter automatiquement les objets créés. Nous pouvons reprendre le même principe à condition de pouvoir décompter les objets lors de leur disparition.

Ce problème - pratiquement insoluble en C - est complètement réglé en C++ par les destructeurs.

Un destructeur (ang.: destructor) est une fonction qui est appellée automatiquement lorsqu'un objet disparaît.

Cette disparition a lieu

- soit lorsque le programme sort du paragraphe dans lequel l'objet a été déclaré (1),

- soit lors de sa libération de mémoire s'il a été créé par allocation dynamique.

Le rôle du destructeur est double :

- faire éventuellement le ménage parmi les data membres (surtout les data membres "static"),

- effectuer toutes les libérations dynamiques de mémoire internes à la classe.

Toute classe doit obligatoirement posséder un destructeur. Toutefois, en cas d'oubli, le compilateur en ajoute un de lui-même… qui ne fait absolument rien (fonction vide) mais qui a le mérite d'exister.

Un destructeur est une fonction publique dont le nom commence par le caractère ~ (tilde) suivi du nom de la classe à laquelle elle appartient. Habituellement, il est placé dans le bloc public, juste après les constructeurs.

Détail qui a son importance : un destructeur ne prend jamais de paramètres et ne renvoie rien !

6.2. Créée un destructeur élémentaire

Pour résoudre le problème du comptage des objets en vie, nous créons donc un autre compteur "static" et nous déclarons un destructeur :

class Ville // Ville.h { protected: // data membres static unsigned long Compteur; static unsigned long NbreEnVie;

1 Rappelons qu'un paragraphe est l'ensemble des instructions comprises entre une accolade ouvrante { et

une accolade fermante }. Ces accolades définissent le "scope" d'une variable qui y a été déclarée, c'est-à-dire la zone dans laquelle cette variable existe et est accessible.

Page 29: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 6 - Les destructeurs

H. Schyns 6.2

x public: Ville(void); //constructeur sans parametres ~Ville(void); //destructeur (toujours sans parametres) x };

Le compteur d'objets en vie est

- initialisé "hors fonction", - incrémenté dans le constructeur, - décrémenté dans le destructeur.

x // Ville.cpp unsigned long Ville::Compteur = 0l; unsigned long Ville::NbreEnVie = 0l; //----------------- constructeur ------------------ Ville::Ville(void) { cout << "Appel du constructeur" << endl; idVille = ++Compteur; NbreEnVie++; memset(Nom,0, vNOMLEN); sprintf (Nom, "Ville%04lu", idVille); CodePostal = 9999; NbreHabitants = 0; cout << NbreEnVie << " villes en vie" << endl; } //----------------- destructeur ------------------ Ville::~Ville(void) { cout << "Appel du destructeur" << endl; NbreEnVie--; cout << NbreEnVie << " villes en vie" << endl; }

Les lignes en début et fin de routine (cout) nous permettent de tracer les appels au constructeur et au destructeur. Elles seront supprimées par la suite.

Le programme de test est inchangé. Toutefois, pendant la phase de débogage, pour bien percevoir les messages générés par les destructeurs avant l'arrêt du programme, nous pouvons placer une deuxième paire d'accolades pour encadrer le code.

void main (void) // TestUrbain.cpp {{ x }}

Ceci crée un niveau de paragraphe supplémentaire qui force l'appel des destructeurs (première accolade fermante) avant de quitter le programme (deuxième accolade fermante).

Voici ce qui doit apparaître à l'écran :

Appel du constructeur // Affichage à l'écran 1 ville(s) en vie <<< ici Appel du constructeur 2 ville(s) en vie <<< ici

Page 30: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 6 - Les destructeurs

H. Schyns 6.3

Id ville : 1 Nom : Ville0001 Id ville : 2 Nom : Ville0002 Fiche de Ville Id ville : 1 Nom : Liege Code postal : 4000 Habitants : 200000 Appel du destructeur <<< ici 1 ville(s) en vie Appel du destructeur <<< ici 0 ville(s) en vie Press any key to continue

Nous voyons bien que tous les objets créés sont détruits à la fin du programme. C'est un signe que le programme termine proprement son exécution.

Lors de la réalisation d'une application, il est vivement recommandé de placer un compteur d'objets en vie dans chacune des classes afin de s'assurer que tous les objets sont bien détruits proprement à la fin du programme.

Page 31: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 7 - Le constructeur de recopie

H. Schyns 7.1

7. Le constructeur de recopie

7.1. Rôle

En C, nous avons vu qu'il est possible d'initialiser une variable lors de sa déclaration. Par exemple :

int i = 5;

En C++, grâce à l'appel au constructeur avec paramètre (1), la même initialisation peut être écrite :

int i(5);

Plus subtilement, nous pourrions avoir

int i = 5; int j = i;

ou, selon ce qui a été dit plus haut

int i(5); int j(i);

Puisqu'une telle syntaxe est admise pour un type prédéfini, elle doit obligatoirement être implémentée dans nos classes !

Un développeur s'attend normalement à pouvoir écrire quelque chose comme

Ville v("Liège", 4000, 200000); Ville x = v; // ou bien Ville x(v);

Les deux dernières lignes font appel à un constructeur appelé constructeur de recopie ou constructeur par recopie.

7.2. Déclaration et implémentation

Un constructeur de recopie est un constructeur :

- qui prend en paramètre un objet de même type que celui qui doit être créé; - qui recopie toutes les valeurs des data membres de l'objet passé en paramètre

(objet source) dans l'objet en cours de création (objet destination).

Attention, ce qui suit n'est correct qu'en apparence. Nous verrons plus loin où est le problème.

class Ville // Ville.h { protected: // data membres x

1 Eh oui ! Tous les types prédéfinis, int, char, double… sont des classes et disposent de constructeurs.

Page 32: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 7 - Le constructeur de recopie

H. Schyns 7.2

public: Ville(void); // constructeur sans param Ville(char *aNom, // constructeur avec param unsigned int aCodePostal, unsigned long aNbreHabitants); Ville(Ville source); // constructeur de recopie x };

Pour implémenter la fonction, nous allons utiliser une propriété importante de la programmation objet sous C++ :

Chaque objet issu d'une classe peut accéder librement et sans contrainte à tous les data membres et fonctions membres private ou protected d'un autre objet issu de la même classe (passé en paramètre d'une fonction).

Il est donc inutile de recourir aux accesseurs.

Par ailleurs, les constructeurs et accesseurs de l'objet source ayant procédé à toutes les vérifications nécessaires, celui-ci est fatalement "propre" et les informations qu'il contient peuvent être recopiées sans souci et sans passer par les accesseurs.

x // Ville.cpp Ville::Ville(Ville source) { cout << "Appel du constructeur de recopie" << endl; NbreEnVie++; idVille = source.idVille; memset(Nom,0, vNOMLEN); strcpy(Nom,source.Nom); CodePostal = source.CodePostal; NbreHabitants = source.NbreHabitants; cout << NbreEnVie << " villes en vie" << endl; }

On attend de ce constructeur qu'il accomplisse toutes les tâches d'un constructeur. Il doit notamment incrémenter le compteur d'objets en vie. Par contre, il ne doit pas générer d'identifiant puisque l'objet créé est une copie à l'identique de l'objet source y compris son identifiant (1).

7.3. Le pointeur "this"

Si, dans une application, on admet une déclaration telle que

Ville v("Liège", 4000, 200000); Ville x = v;

Rien n'empêche un petit rigolo d'écrire une expression syntaxiquement exacte telle que :

Ville x = x;

Ceci ressemble à de la génération spontanée d'objet !

1 Ce point peut être discuté et contesté. Nous verrons toutefois que la manière de faire qui est choisie ici

simplifie considérablement la gestion des entités.

Page 33: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 7 - Le constructeur de recopie

H. Schyns 7.3

Notre constructeur de recopie doit détecter et éviter cette situation mais comment peut-il savoir que l'objet qui lui est passé en paramètre n'est pas lui-même ?

Deux objets sont considérés comme différents si et seulement si leurs adresses sont différentes.

Le problème est que, s'il est aisé de connaître l'adresse de l'objet passé en paramètre, il n'est pas aussi évident de connaître l'adresse de l'objet courant (1). C'est pourquoi chaque classe contient un data membre "caché" nommé this.

this est un pointeur qui renferme l'adresse de l'objet courant. Il est initialisé automatiquement.

Nous pouvons maintenant comparer l'adresse de l'objet passé en paramètre avec celle contenue dans this et, le cas échéant, avorter la construction de l'objet :

x // Ville.cpp Ville::Ville(Ville source) { cout << "Appel du constructeur de recopie" << endl; if (this == &source) return; // avorter si A=A NbreEnVie++; idVille = source.idVille; x };

7.4. Passer des paramètres "par référence"

Il reste un problème de taille à régler. Lorsque nous écrivons l'en-tête ci-dessous ci-dessous

Ville::Ville(Ville source)

nous faisons implicitement appel à un transfert de paramètre par valeur. Ceci signifie que le contenu de l'objet source doit

- 1) être copié sur la stack par la routine appelante (en l'occurrence main), - 2) être copié de la stack vers l'objet courant qui est en cours de construction.

Mais comment les données de la stack peuvent-elles être copiées dans l'objet courant puisque c'est précisément cette fonction de copie que nous sommes en train d'écrire !

Nous pourrions utiliser un transfert par adresse puisque le compilateur connaît la taille d'une adresse et l'adresse de l'objet transmis. Une fois l'adresse connue par la routine appelée (le constructeur en question), ses instructions peuvent accéder directement aux éléments source sans passer par la stack.

Cette solution n'est pas satisfaisante du point de vue syntaxique car nous devrions écrire l'appel de la manière suivante :

Ville v("Liège", 4000, 200000); Ville x = &v; // confusion entre objet et adresse d'objet

1 Par objet courant on entend celui qui exécute la fonction en question.

Page 34: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 7 - Le constructeur de recopie

H. Schyns 7.4

Pour contourner le problème, C++ introduit un troisième type de transfert : le passage de paramètre par référence :

- du point de vue syntaxique, le transfert par référence ressemble furieusement à un transfert par valeur,

- du point de vue fonctionnel, le transfert par référence ressemble furieusement à un transfert par adresse (ou pointeur).

On peut voir une référence comme une sorte de "super pointeur" capable de se dé-référencer tout seul.

La différence est qu'un pointeur peut exister sans contenir d'adresse (on peut définir un tableau de pointeurs nuls) alors que la référence ne peut exister que si l'objet à référencer existe déjà (impossible de définir un tableau de références nulles).

C'est uniquement l'en-tête de la fonction qui définit si le transfert se fait par valeur ou par référence.

void fonction_par_valeur (float f); // transfert par valeur void fonction_par_reference (float& f); // transfert par référence

Notez l'ampersand [ & ] qui définit le transfert par valeur. Il ne faut pas le confondre avec l'opérateur d'adresse.

La syntaxe des deux technique étant similaire, le corps des fonctions reste identique, que l'appel se fasse par valeur ou par référence. Idem pour la syntaxe de l'appel.

Attention, le mécanisme du transfert par référence étant semblable à celui du transfert par pointeur, toute modification effectuée par la fonction appelée sur l'objet passé, se répercute dans l'objet original déclaré dans la routine appelante.

Le transfert par référence est un outil très puissant de C++. Il simplifie grandement la tâche du développeur.

Finalement, nous devons simplement revoir l'en-tête de notre constructeur de recopie.

C'est uniquement l'en-tête de la fonction qui définit si le transfert se fait par valeur ou par référence.

class Ville // Ville.h { x public: Ville(Ville& source); // & car reference x };

Seul l'en-tête change dans l'implémentation :

x // Ville.cpp Ville::Ville(Ville& source) { cout << "Appel du constructeur de recopie" << endl; if (this == &source) return; // avorter si A=A

Page 35: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 7 - Le constructeur de recopie

H. Schyns 7.5

NbreEnVie++; idVille = source.idVille; x };

Le problème est réglé et ce qui précède est correct !

7.5. Validation

Testons rapidement nos trois types des constructeurs :

void main (void) // TestUrbain.cpp {{ Ville v; // constructeur sans param Ville x ("Charleroi", 6000, 150000); // constructeur avec param Ville z=x; // constructeur de recopie char buf[20]; v.setAll("Liege", 4000, 200000); cout << "Fiche de Ville " << 'v' << endl ; cout << "Id ville : " << v.getidVille() << endl; cout << "Nom : " << v.getNom(buf, 20 ) << endl; cout << "Code postal : " << v.getCodePostal() << endl; cout << "Habitants : " << v.getNbreHabitants() << endl << endl; cout << "Fiche de Ville " << 'x' << endl ; cout << "Id ville : " << x.getidVille() << endl; cout << "Nom : " << x.getNom(buf, 20 ) << endl; cout << "Code postal : " << x.getCodePostal() << endl; cout << "Habitants : " << x.getNbreHabitants() << endl << endl; cout << "Fiche de Ville " << 'z' << endl ; cout << "Id ville : " << z.getidVille() << endl; cout << "Nom : " << z.getNom(buf, 20 ) << endl; cout << "Code postal : " << z.getCodePostal() << endl; cout << "Habitants : " << z.getNbreHabitants() << endl << endl; }} // destructeurs

Voici ce que nous devons obtenir à l'écran :

Appel du constructeur // Affichage à l'écran 1 villes en vie Appel du constructeur avec parametres 2 ville(s) en vie Appel du constructeur de recopie // <<< ici 3 villes en vie Fiche de Ville v Id ville : 1 Nom : Liege Code postal : 4000 Habitants : 200000 Fiche de Ville x Id ville : 2 Nom : Charleroi Code postal : 6000 Habitants : 150000

Page 36: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 7 - Le constructeur de recopie

H. Schyns 7.6

Fiche de Ville z // <<< preuve ici Id ville : 2 Nom : Charleroi Code postal : 6000 Habitants : 150000 Appel du destructeur 2 villes en vie Appel du destructeur 1 villes en vie Appel du destructeur 0 villes en vie Press any key to continue

Page 37: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 8 - L'opérateur d'affectation

H. Schyns 8.1

8. L'opérateur d'affectation

8.1. Rôle

Une opération importante en programmation C ou C++ est l'affectation ou copie. Elle est implémentée par l'opérateur =.

int i, j; //constructeur i = 5; // affectation j = i // affectation

En C++ un développeur qui utilise notre classe Ville s'attend normalement à pouvoir écrire quelque chose comme

Ville v("Liège", 4000, 200000); Ville x; x = v; // affectation

Cette instruction ne doit pas être confondue avec ce qui a été dit au sujet du constructeur de recopie :

Ici, les deux objets x et v ont déjà été créés; ils existent avant cette instruction.

En principe, le compilateur implémente de lui-même le code qui effectue cette opération mais il se contente d'une simple copie byte pour byte. Cette technique ne marche évidemment plus dès que les data membres de l'objet sont créés par allocation dynamique de mémoire car il y a écrasement de l'adresse contenue dans le pointeur.

Alloc dyn

Ville v

char* Nom

Nom

Alloc dyn

Ville y

char* Nom

Nom

Alloc dyn

Ville v

char* Nom

Nom

Alloc dyn

Ville y

char* Nom

Nom(Perdu)

ð

fig. 8.1 Effet d'une copie byte pour byte lors d'allocations dynamiques

En programmation procédurale C, nos entités étant représentées par des structures, nous devrions écrire une fonction de recopie telle que

VilleCopier (Ville* vdestination, Ville* vsource) { x }

A nouveau, C++ simplifie considérablement la syntaxe en la rendant semblable à celle que l'on peut utiliser avec les types prédéfinis :

L'affectation (=) est implémentée par une fonction appelée operator=

Page 38: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 8 - L'opérateur d'affectation

H. Schyns 8.2

8.2. Déclaration et implémentation

L'opérateur d'affectation "standard" (operator=) est une fonction :

- qui prend en paramètre un objet de même type que celui qui doit être créé; - qui recopie toutes les valeurs des data membres de l'objet passé en paramètre

(objet source) dans l'objet en cours de création (objet destination).

Cette définition montre que l'opérateur d'affectation est très semblable au contructeur de recopie.

class Ville // Ville.h { x public: Ville& operator=(Ville& source); // & car reference x };

L'opérateur doit renvoyer une référence Ville& sur la stack (valeur en retour) car, en C++, il est permis d'enchaîner les affectations pour écrire :

Ville v("Liège", 4000, 200000); Ville x, y, z; z = y = x = v; // affectations en série

L'implémentation de la fonction s'écrit :

x // Ville.cpp Ville& Ville::operator=(Ville& source) { cout << "Appel de operator=" << endl; if (this == &source) return(source); // by-pass si A=A idVille = source.idVille; strcpy(Nom,source.Nom); CodePostal = source.CodePostal; NbreHabitants = source.NbreHabitants; return(source); }

L'instruction return peut aussi être :

return(*this);

Rappelons-nous que la sytaxe d'un transfert par référence (Ville&) ressemble à une transfert par valeur. Dès lors, ce n'est pas this qu'il faut renvoyer sur la satck car this est un pointeur sur l'objet courant par contre, *this est son contenu, sa valeur.

Au lieu d'écrire une instruction telle que

x = v; // affectation

L'utilisateur peut parfaitement écrire

x.operator=(v);

C'est d'ailleurs ce que fait le compilateur lorsqu'il rencontre une telle instruction.

Page 39: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 8 - L'opérateur d'affectation

H. Schyns 8.3

L'opérateur appelé est toujours celui de l'objet qui le précède.

Ainsi, l'instruction

z = y = x = v; // affectation

devient

z.operator=(y.operator=(x.operator=(v))); // affectation

ce qui montre bien qu'il est nécessaire de remettre une référence sur la stack en valeur de retour de chaque appel.

8.3. Validation

Créeon un petit programme de test pour différencier l'appel de l'operateur d'affectation de celui du constructeur de recopie :

void main (void) // TestUrbain.cpp {{ Ville v ("Charleroi", 6000, 150000); Ville x=v; // appel du constructeur de recopie Ville z; char buf[20]; cout << "Fiche de Ville " << 'v' << endl ; cout << "Id ville : " << v.getidVille() << endl; cout << "Nom : " << v.getNom(buf, 20 ) << endl; cout << "Code postal : " << v.getCodePostal() << endl; cout << "Habitants : " << v.getNbreHabitants() << endl << endl; cout << "Fiche de Ville " << 'x' << endl ; cout << "Id ville : " << x.getidVille() << endl; cout << "Nom : " << x.getNom(buf, 20 ) << endl; cout << "Code postal : " << x.getCodePostal() << endl; cout << "Habitants : " << x.getNbreHabitants() << endl << endl; z = v; // appel de operator= cout << "Fiche de Ville " << 'z' << endl ; cout << "Id ville : " << z.getidVille() << endl; cout << "Nom : " << z.getNom(buf, 20 ) << endl; cout << "Code postal : " << z.getCodePostal() << endl; cout << "Habitants : " << z.getNbreHabitants() << endl << endl; }}

Voici le résultat :

Fiche de Ville v // Affichage à l'écran Id ville : 1 Nom : Charleroi Code postal : 6000 Habitants : 150000 Fiche de Ville x Id ville : 1 Nom : Charleroi Code postal : 6000 Habitants : 150000

Page 40: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 8 - L'opérateur d'affectation

H. Schyns 8.4

Appel de operator= Fiche de Ville z Id ville : 1 Nom : Charleroi Code postal : 6000 Habitants : 150000 Appel du destructeur 2 villes en vie Appel du destructeur 1 villes en vie Appel du destructeur 0 villes en vie Press any key to continue

8.4. Un opérateur un peu particulier

A titre de curiosité, comme C++ autorise la surcharge des fonctions, nous pouvons aussi définir un operator= qui effectue une remise zéro de l'objet courant par une instruction telle que :

Ville v("Liège", 4000, 200000); v = 0; // remise à zéro

Zéro étant un nombre, nous devons définir un nouvel opérateur qui prend un int (ou autre type prédéfini) en paramètre :

class Ville // Ville.h { x public: int operator=(int i); x };

L'implémentation de la fonction s'écrit :

x // Ville.cpp int Ville::operator=(int i) { cout << "Appel de operator=(int)" << endl; idVille = i-i; // juste pour utiliser i memset (Nom, 0, vNOMLEN); CodePostal = 0; NbreHabitants = 0; return(i); }

Notons que l'opérateur efface le contenu de l'objet quelle que soit la valeur transmise.

Page 41: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 9 - Gérer les allocations dynamiques

H. Schyns 9.1

9. Gérer les allocations dynamiques

9.1. Rôle

Nous savons déjà qu'en déclarant une variable, le développeur désigne un espace mémoire dans lequel il conservera tel ou tel type d'information.

float f; // 4 bytes double d[20]; // 160 bytes

Une déclaration standard (statique) telle que ci-dessus réserve un espace de 164 bytes dans le data segment (DS). Toutes les variables déclarées dans le programme, qu'il s'agisse de variables uniques ou de tableaux, trouvent leur place dans ce data segment.

Lors de l'exécution, le programme se voit attribuer en exclusivité une ou plusieurs pages de mémoire : code segment, data segment, stack segment, extra segment.

Malheureusement, lorsqu'il rédige une application, le développeur ignore généralement le nombre de données qu'il devra traiter. Il ignore donc la taille qu'il doit donner à ses tableaux : trop petits, il limite les possibilités de son programme, trop grand, il mobilise inutilement la mémoire de la machine (1). Le problème se complique quand on sait que la taille d'un tableau statique ne peut dépasser la taille d'une page de data segment, soit 64 kB.

Le problème est encore plus pointu en programmation objet car, la plupart du temps, le développeur ignore même la taille des objets qu'il utilise !

La solution consiste à fixer la taille de la mémoire lorsque tous ces éléments sont connus c'est-à-dire lors de l'exécution du programme et non lors de sa conception.

C'est précisément l'intérêt de l'allocation dynamique :

- éviter de monopoliser pendant toute l'exécution du programme, de la mémoire qui ne sera utile que pendant un temps très bref,

- permettre l'accès à une quantité de mémoire plus grande que les limites autorisées par la taille du data segment,

- définir un espace mémoire qui n'est connu que lors de l'utilisation.

L'allocation dynamique de mémoire est très largement utilisée en programmation objet car, comme il a été dit, le concepteur ignore la taille des objets qu'il emploie. Dès lors, au lieu de les déclarer de manière statique et risquer un dépassement du data segment, il préfère les allouer dynamiquement.

Ce raisonnement est parfois poussé à l'extrême par certains auteurs qui mettent "tout" en allocation dynamique sauf un unique pointeur qui retient l'adresse de l'objet qui contient l'ensemble des variables et variables nécessaires au programme.

9.2. Syntaxe

En C++, les allocations dynamiques sont beaucoup plus simples qu'en C :

1 Imaginez un ticket de caisse dont la longueur serait fixée à 1m, quel que soit le nombre d'article achetés …

Page 42: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 9 - Gérer les allocations dynamiques

H. Schyns 9.2

- la fonction malloc dont la syntaxe rébarbative pose souvent des problèmes est remplacée par l'opérateur new.

- new alloue l'espace nécessaire à l'objet créé et l'initialise en appelant le constructeur ad hoc.

- la fonction de libération de mémoire free est remplacée par l'opérateur delete (1).

- la fonction realloc n'existe plus. Il faudra se débrouiller autrement

Voici un exemple de l'évolution de l'usage et de la syntaxe :

- déclaration et allocation statique :

#define vNOMLEN 50 void main (void) { unsigned long unombre; char nom[ vNOMLEN ]; x }

- déclaration et allocation dynamique via pointeurs C :

#define vNOMLEN 50 void main (void) { unsigned long *punombre; // pointeur explicite char *nom; // tableau explicite = pointeurimplicite punombre = (unsigned long*) malloc (sizeof(unsigned long); nom = (char*) malloc (vNOMLEN * sizeof(char)); x free (nom); free (punombre); }

- déclaration et allocation dynamique via pointeurs C++ :

#define vNOMLEN 50 void main (void) { unsigned long *punombre; // pointeur explicite char *nom; // tableau explicite = pointeurimplicite punombre = new unsigned long; nom = new char[vNOMLEN]; x delete [] nom; delete punombre; }

A toute allocation dynamique d'un objet par new doit correspondre une libération dynamique par delete.

A toute allocation dynamique d'un tableau par new...[] doit correspondre une libération dynamique par delete[]...

1 En réalité, new et delete sont plutôt des opérateurs mais cette nuance est sans importance ici.

Page 43: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 9 - Gérer les allocations dynamiques

H. Schyns 9.3

Comme C++ permet de mélanger les déclarations et les instructions, les développeurs couplent très souvent la déclaration et l'allocation :

#define vNOMLEN 50 void main (void) { unsigned long *punombre = new unsigned long; char *nom = new char[vNOMLEN]; x delete [] nom; delete punombre; }

9.3. Application

L'énoncé du problème nous demande d'appliquer l'allocation dynamique à tous les tableaux définis dans la classe.

En fait de tableaux, nous n'avons pour l'instant que le nom de la ville.

Remplaçons ce tableau de caractères par un pointeur dans l'en-tête de la classe. C'est la seule modification à apporter au header :

class Ville // Ville.h { protected: x char* Nom; // un pointeur au lieu d'un tableau public: x };

Evidemment, l'objet Ville doit être "en état de marche" dès les initialisations. Nous devons donc implémenter l'allocation dynamique du tableau dans chacun des constructeurs.

Toute allocation dynamique (new) effectuée dans un constructeur doit être implémentée dans tous les autres constructeurs et exige une libération (delete) dans le destructeur.

Dans le constructeur sans paramètre :

Ville::Ville(void) // Ville.cpp { cout << "Appel du constructeur" << endl; Nom = new char [vNOMLEN]; // allocation dynamique if (!Nom) return; // vérification de l'alloc idVille = ++Compteur; NbreEnVie++; memset(Nom,0, vNOMLEN); sprintf(Nom, "Ville%04lu", idVille); CodePostal = 9999; NbreHabitants = 0; cout << NbreEnVie << " villes en vie" << endl; }

Page 44: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 9 - Gérer les allocations dynamiques

H. Schyns 9.4

Bien que les avis à ce sujet sont très controversés, la documentation de Visual C++ nous affirme que si le système d'exploitation n'a pas pu satisfaire la demande, il renvoie un pointeur nul ou lance une exception.

En attendant de maîtriser la technique des exceptions (1), nous pouvons vérifier si l'allocation s'est bien passée comme en C, en testant l'adresse reçue par le pointeur.

Nous faisons de même dans le constructeur avec paramètres et le constructeur de recopie :

Ville::Ville( char* aNom, // Ville.cpp unsigned int aCodePostal, unsigned long aNbreHabitants) { cout << "Appel du constructeur avec parametres" << endl; Nom = new char [vNOMLEN]; // allocation dynamique if (!Nom) return; // vérification de l'alloc x } Ville::Ville(Ville& source) { cout << "Appel du constructeur de recopie" << endl; if (this == &source) return; // avorter si A=A Nom = new char [vNOMLEN]; // allocation dynamique if (!Nom) return; // vérification de l'alloc x }

La désallocation a lieu dans l'unique destructeur :

Ville::~Ville(void) // Ville.cpp { cout << "Appel du destructeur" << endl; if (Nom) delete [] Nom; // ne pas oublier [] NbreEnVie--; cout << NbreEnVie << " villes en vie" << endl; }

En principe, l'opérateur delete ne fait rien s'il reçoit un pointeur NULL. Mais la règle peut aussi varier d'un compilateur à l'autre.

9.4. Validation

Le programme de test ne change absolument pas. C'est l'un des grands avantages de C++ sur C.

1 Les exceptions seont vues dans un autre chapitre.

Page 45: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 10 - Protéger les objets passés à une fonction

H. Schyns 10.1

10. Protéger les objets passés à une fonction

10.1. Position du problème

Lors de l'appel d'une fonction membre d'une classe nous pouvons transmettre les paramètres par valeurs, par pointeurs ou par références. De même, nous pouvons utiliser des valeurs, des pointeurs ou des références comme "type en retour". Par exemple :

char* Bidule::fonctionA (char *buffer, int buflen); int Bidule::fonctionB (Bidule& source); Bidule& Bidule::fonctionC (Bidule& source);

En principe, l'objet Bidule auquel les fonctions appartiennent prend toutes les précautions nécessaires pour protéger l'intégrité de ses data membres. C'est le principe même de l'encapsulation.

Mais qu'en est-il de la protection des paramètres passés ou des types en retour ? Comment être sûr que les fonctions ne vont pas modifier les paramètres buffer et source ?

Le rôle premier des pointeurs et des références est précisément de permettre la modification des objets pointés et référencés mais dans la pratique de la programmation objet, on les utilise surtout pour éviter de devoir recopier dans la stack la totalité des objets passés. Comment faire la différence entre ces deux situations ?

C++ dispose du mot-clé const qui spécifie que quelque chose doit rester inchangé lors des traitements. Il offre une certaine protection contre de lignes de code qui voudraient modifier la variable ou le paramètre en question.

char* Bidule::fonctionA (const char *buffer, int buflen); int Bidule::fonctionB (const Bidule& source); Bidule& Bidule::fonctionC (const Bidule& source);

En ajoutant const, nous interdisons à fonctionA de modifier le contenu de buffer et aux fonctionB et fonctionC de modifier le contenu de la source même en se servant de ses accesseurs. Dans le cas de buflen, la spécification const est sans intérêt puisque buflen n'est qu'une copie locale (et donc sans rétroaction) de la valeur du paramètre.

Nous en déduisons au passage que la méthode la plus "économique" pour passer un objet en paramètre à une fonction est d'utiliser "const référence". C'est seulement si nous désirons modifier l'objet passé en paramètre que nous omettrons le mot-clé const.

Nous pouvons également utiliser const pour interdire une modification du pointeur ou de la référence renvoyée par la fonction. Un exemple typique est le cas de l'accesseur qui doit renvoyer une chaîne de caractères :

char* Ville::getNom (void) { return (Nom); // dangereux }

Page 46: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 10 - Protéger les objets passés à une fonction

H. Schyns 10.2

Nous avons vu que cette manière de faire présentait un risque majeur pour l'encapsulation des données. Nous pouvons cependant réduire le risque en ajoutant const devant le pointeur en retour (1).

const char* Ville::getNom (void) { return (Nom); }

Enfin, nous pouvons certifier qu'une fonction ne modifie pas l'objet courant en ajoutant const à la fin de sa déclaration. Par exemple :

bool Ville::operator< (const Bidule& source) const { return (stricmp (Nom, source.Nom) < 0); };

Le premier const signifie que l'opérateur ne modifie pas la référence passée en paramètre; le second signifie qu'il ne modifie pas non plus l'objet courant.

10.2. Application

Il nous reste à appliquer ces principes à notre classe Ville.

En règle générale, const se place

- devant les paramètres (pointeurs et références) des constructeurs et des accesseurs set,

- à la fin de la déclaration des accesseurs get, - devant la déclaration et devant le paramètre source de operator=.

class Ville // Ville.h { protected: static unsigned long Compteur; static unsigned long NbreEnVie; unsigned long idVille; char* Nom; unsigned int CodePostal; unsigned long NbreHabitants; protected: void setidVille (unsigned long aidVille); public: Ville (void); Ville (const char* aNom, unsigned int aCodePostal, unsigned long aNbreHabitants); Ville (const Ville& source); ~Ville(void); const Ville& operator= (const Ville& source); int operator= (int i);

1 Réduire seulement, car un développeur expérimenté ne mettra pas longtemps pour trouver une astuce qui

contournera l'interdiction.

Page 47: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 10 - Protéger les objets passés à une fonction

H. Schyns 10.3

void setNom (const char* aNom); void setCodePostal (unsigned int aCodePostal); void setNbreHabitants (unsigned long aNbreHabitants); void setAll (const char* aNom, unsigned int aCodePostal, unsigned long aNbreHabitants); unsigned long getidVille (void) const; char* getNom (char* buf, unsigned int bufsize) const; unsigned int getCodePostal (void) const; unsigned long getNbreHabitants (void) const; void getAll (unsigned long *aidVille, char* buf, unsigned int bufsize, unsigned int *aCodePostal, unsigned long *aNbreHabitants) const; };

Ces modifications doivent évidemment être répercutées dans l'implémentation des fonctions.

Page 48: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 11 - Validation générale

H. Schyns 11.1

11. Validation générale

Nous voici arrivés au bout de la conception de notre première classe.

Voyons à présent comment l'utiliser :

- peut-on créer un objet de classe Ville de manière dynamique (c'est-à-dire à partir d'un pointeur) ?

- peut-on créer un tableau de villes de manière statique ? - peut-on créer un tableau de villes de manière dynamique ? - comment accède-t-on aux fonctions et data membres d'une ville allouée

dynamiquement ?

Ecrivons un petit programme de test qui construit, modifie, et détruit quelques villes :

#include <iostream> // TestUrbain.cpp #include "ville.h" using namespace std; void main (void) {{ Ville VilleA; // deux Villes "statiques" Ville VilleB("Liege", 4000, 200000); Ville TableVille[3]; // un vecteur de 3 Villes "statiques" Ville *pVille; // un pointeur sur Ville "dynamique" Ville *tpVille[3]; // un tableau de 3 pointeurs // sur Villes "dynamiques" int i; char buf[20]; // afficher les Villes "statiques" cout << "Fiche de Ville " << "VilleA" << endl ; cout << "Id ville : " << VilleA.getidVille() << endl; cout << "Nom : " << VilleA.getNom(buf, 20 ) << endl; cout << "Code postal : " << VilleA.getCodePostal() << endl; cout << "Habitants : " << VilleA.getNbreHabitants() << endl << endl; cout << "Fiche de Ville " << "VilleB" << endl ; cout << "Id ville : " << VilleB.getidVille() << endl; cout << "Nom : " << VilleB.getNom(buf, 20 ) << endl; cout << "Code postal : " << VilleB.getCodePostal() << endl; cout << "Habitants : " << VilleB.getNbreHabitants() << endl << endl; // afficher les Villes du tableau "statique" for (i=0; i<3; i++) { cout << "Fiche de Ville " << "Ville[" << i << ']' << endl ; cout << "Id ville : " << TableVille[i].getidVille()<< endl; cout << "Nom : " << TableVille[i].getNom(buf,20) << endl; cout << "Code postal : " << TableVille[i].getCodePostal() << endl; cout << "Habitants : " << TableVille[i].getNbreHabitants() << endl << endl; } // création explicite d'un Ville "dynamique" // affichage puis destruction explicite pVille = new Ville("Bruxelles", 1000, 900000); cout << "Fiche de Ville " << "pVille" << endl ; cout << "Id ville : " << pVille->getidVille() << endl; cout << "Nom : " << pVille->getNom(buf, 20 ) << endl; cout << "Code postal : " << pVille->getCodePostal() << endl;

Page 49: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 11 - Validation générale

H. Schyns 11.2

cout << "Habitants : " << pVille->getNbreHabitants() << endl << endl; delete pVille; // création "dynamique" de 3 Villes for (i=0; i<3; i++) { sprintf (buf, "Bruxelles %d", i+1); tpVille[i] = new Ville(buf, (i+5)*1000, (i+1)*10000); cout << "Fiche de Ville " << "tpVille[" <<i << ']' << endl ; cout << "Id ville : " << tpVille[i]->getidVille() << endl; cout << "Nom : " << tpVille[i]->getNom(buf, 20 ) << endl; cout << "Code postal : " << tpVille[i]->getCodePostal() << endl; cout << "Habitants : " << tpVille[i]->getNbreHabitants() << endl << endl; } // destruction des 3 Villes "dynamiques" for (i=0; i<3; i++) delete tpVille[i]; }}

Notons qu'il n'est pas possible d'utiliser le constructeur avec paramètre pour initialiser les éléments d'un tableau de villes, qu'il soit statique ou dynamique.

Par contre, nous pouvons créer un tableau de pointeurs sur des villes et ensuite allouer dynamiquement chaque ville à l'aide du constructeur avec paramètres.

Tout se passe comme prévu : le compteur joue son rôle ; les noms par défaut sont correctement générés, les codes postaux et autres data membres sont bien définis et les destructeurs sont invoqués comme prévu.

Appel du constructeur // Affichage à l'écran 1 villes en vie Appel du constructeur avec parametres 2 ville(s) en vie Appel du constructeur 3 villes en vie Appel du constructeur 4 villes en vie Appel du constructeur 5 villes en vie Fiche de Ville VilleA Id ville : 1 Nom : Ville0001 Code postal : 9999 Habitants : 0 Fiche de Ville VilleB Id ville : 2 Nom : Liege Code postal : 4000 Habitants : 200000 Fiche de Ville Ville[0] Id ville : 3 Nom : Ville0003 Code postal : 9999 Habitants : 0 Fiche de Ville Ville[1] Id ville : 4 Nom : Ville0004 Code postal : 9999 Habitants : 0

Page 50: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 11 - Validation générale

H. Schyns 11.3

Fiche de Ville Ville[2] Id ville : 5 Nom : Ville0005 Code postal : 9999 Habitants : 0 Appel du constructeur avec parametres 6 ville(s) en vie Fiche de Ville pVille Id ville : 6 Nom : Bruxelles Code postal : 1000 Habitants : 900000 Appel du destructeur 5 villes en vie Appel du constructeur avec parametres 6 ville(s) en vie Fiche de Ville tpVille[0] Id ville : 7 Nom : Bruxelles 1 Code postal : 5000 Habitants : 10000 Appel du constructeur avec parametres 7 ville(s) en vie Fiche de Ville tpVille[1] Id ville : 8 Nom : Bruxelles 2 Code postal : 6000 Habitants : 20000 Appel du constructeur avec parametres 8 ville(s) en vie Fiche de Ville tpVille[2] Id ville : 9 Nom : Bruxelles 3 Code postal : 7000 Habitants : 30000 Appel du destructeur 7 villes en vie Appel du destructeur 6 villes en vie Appel du destructeur 5 villes en vie Appel du destructeur 4 villes en vie Appel du destructeur 3 villes en vie Appel du destructeur 2 villes en vie Appel du destructeur 1 villes en vie Appel du destructeur 0 villes en vie Press any key to continue

Le plus dur est fait : nous avons un point de départ ! (voir fichiers du classeur Urbain15).

Il ne nous reste plus qu'à faire la même démarche avec les Entreprises et les Chantiers. Inutile de dire que nous allons utiliser à fond les possibilités du copier/coller et et du rechercher/remplacer !

Page 51: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 12 - La suite du programme

H. Schyns 12.1

12. La suite du programme

12.1. La classe Entreprise

La fiche signalétique de l'entreprise est reprise par les data membres.

#define eNOMLEN 20 // Entreprise.h #define eADRESSELEN 50 #define eLOCALITELEN 50 #define eTVALEN 20 class Entreprise { protected: static unsigned long Compteur; static unsigned long NbreEnVie; unsigned long idEntreprise; char Nom[eNOMLEN]; char Adresse[eADRESSELEN]; char Localite[eLOCALITELEN]; unsigned int CodePostal; char NumTVA[eTVALEN]; unsigned long NbreEmployes; double ChiffreAffairesEUR; public: x };

12.2. La classe Chantier

La fiche signalétique du chantier est reprise par les data membres.

#define cDENOMLEN 20 // Chantier.h #define cADRESSELEN 50 #define cLOCALITELEN 50 class Chantier { protected: // acces direct interdit static unsigned long Compteur; static unsigned long NbreEnVie; unsigned long idChantier; unsigned long idVille; unsigned long idEntreprise; char Denomination[cDENOMLEN]; char Adresse[cADRESSELEN]; char Localite[cLOCALITELEN]; unsigned int CodePostal; unsigned long DateDebutPrevue; unsigned long DateFinPrevue; double CoutPrevuEUR; public: x };

Comme un chantier est entrepris par un ville et réalisé par une entreprise, nous devons inclure les identifiants de ces deux entités.

Page 52: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 12 - La suite du programme

H. Schyns 12.2

12.3. Observation

Nous remarquons que de nombreux éléments se reprouvent dans les trois classes que nous avons créées Ville, Entreprise, Chantier.

Pour éviter de recopier les mêmes choses, nous allons introduire la notion d'héritage…

Page 53: Langage Orienté Objet - C++ - Notes de cours - …notesdecours.drivehq.com/courspdf/Cpp_Resolution_ABC_V1.pdf · Résolution du projet ABC - V1 Sommaire H. Schyns S.2 7.1. Rôle

Résolution du projet ABC - V1 13 - Références

H. Schyns 13.1

13. Références

13.1. Ouvrages

- Pont entre C et C++ P.-N. Lapointe Addison-Wesley ISBN 2-87908-094-0

- Professional C++ N. A. Solter, S. J. Kleper Wrox ISBN 0-7645-7484-1

13.2. Sites web

- C++ Documentation & didacticiels Œuvre collective Cplusplus http://www.cplusplus.com/

Une ressource de premier plan qui remplace avantageusement l'aide de Visual Studio

- C++ FAQ M. Cline Parashift http://www.parashift.com/c++-faq-lite/index.html