• Aucun résultat trouvé

#endif // STACK2_H ///:~

Comme précédemment, l'implémentation ne change pas et donc on ne la répète pas ici. Le test, lui aussi, est identique. La seule chose qui ait changé c'est la robustesse de l'interface de la classe. L'intérêt réel du contrôle d'accès est de vous empêcher de franchir les limites pendant le développement. En fait, le compilateur est la seule chose qui connaît le niveau de protection des membres de la classe. Il n'y a aucune information de contrôle d'accès dans le nom du membre au moment de l'édition de liens. Toute la vérification de protection est faite par le compilateur ; elle a disparu au moment de l'exécution.

Notez que l'interface présentée au programmeur client est maintenant vraiment celle d'une pile push-down. Elle est implémentée comme une liste chaînée, mais vous pouvez changer cela sans affecter ce avec quoi le programmeur client interagit, ou (ce qui est plus important) une seule ligne du code client.

5.6 - Manipuler les classes

Les contrôles d'accès du C++ vous permettent de séparer l'interface de l'implémentation, mais la dissimulation de l'implémentation n'est que partielle. Le compilateur doit toujours voir les déclarations de toutes les parties d'un objet afin de le créer et de le manipuler correctement. Vous pourriez imaginer un langage de programmation qui requiert seulement l'interface publique d'un objet et autorise l'implémentation privée à être cachée, mais C++

effectue autant que possible la vérification des types de façon statique (au moment de la compilation). Ceci signifie que vous apprendrez aussi tôt que possible s'il y a une erreur, et que votre programme est plus efficace. Toutefois, inclure l'implémentation privée a deux effets : l'implémentation est visible même si vous ne pouvez réellement y accéder, et elle peut causer des recompilations inutiles.

5.6.1 - Dissimuler l'implémentation

Certains projets ne peuvent pas se permettre de rendre leur implémentation visible au programmeur client. Cela peut révéler des informations stratégiques dans les fichiers d'en-tête d'une librairie que la compagnie ne veut pas rendre disponible aux concurrents. Vous pouvez travailler sur un système où la sécurité est un problème (un algorithme d'encryptage, par exemple) et vous ne voulez pas laisser le moindre indice dans un fichier d'en-tête qui puisse aider les gens à craquer votre code. Ou vous pouvez mettre votre librairie dans un environnement "hostile", où les programmeurs auront de toute façon directement accès aux composants privés, en utilisant des pointeurs et du forçage de type. Dans toutes ces situations, il est intéressant d'avoir la structure réelle compilée dans un fichier d'implémentation plutôt que dans un fichier d'en-tête exposé.

5.6.2 - Réduire la recompilation

Le gestionnaire de projet dans votre environnement de programmation causera la recompilation d'un fichier si ce

fichier est touché (c'est-à-dire modifié) ousi un autre fichier dont il dépend (un fichier d'en-tête inclu) est touché.

Cela veut dire qu'à chaque fois que vous modifiez une classe, que ce soit l'interface publique ou les déclarations de membres privés, vous forcerez une recompilation de tout ce qui inclut ce fichier d'en-tête. On appelle souvent cela le problème de la classe de base fragile. Pour un grand projet dans ses étapes initiales cela peut être peu pratique car l'implémentation sous-jacente peut changer souvent ; si le projet est très gros, le temps de compilation peut prévenir tout changement de direction.

La technique pour résoudre ce problème est souvent appelée manipulation de classesou "chat du Cheshire" Ce nom est attribué à John Carolan, un des premiers pionniers du C++, et, bien sûr, Lewis Carrol. Cette technique peut aussi être vue comme une forme du “bridge” design pattern, décrit dans le Volume 2.– tout ce qui concerne l'implémentation disparaît sauf un unique pointeur, le "sourire". Le pointeur fait référence à une structure dont la définition est dans le fichier d'implémentation avec toutes les définitions des fonctions membres. Ainsi, tant que l'interface n'est pas modifiée, le fichier d'en-tête reste intouché. L'implémentation peut changer à volonté, et seuls les fichiers d'implémentation ont besoin d'être recompilés et relinkés avec le projet.

Voici un exemple simple démontrant la technique. Le fichier d'en-tête contient uniquement l'interface publique et un unique pointeur de classe incomplètement spécifiée :

//: C05:Handle.h // Handle classes

#ifndef HANDLE_H

#define HANDLE_H class Handle {

struct Cheshire; // Déclaration de classe uniquement Cheshire* smile;

public:

void initialize();

void cleanup();

int read();

void change(int);

};

#endif // HANDLE_H ///:~

Voici tout ce que le programmeur client est capable de voir. La ligne

struct Cheshire;

est une spécification incomplèteou une déclaration de classe(une définition de classeinclut le corps de la classe).

Ce code dit au compilateur que Cheshireest un nom de structure, mais ne donne aucun détail à propos du struct.

Cela représente assez d'information pour créer un pointeur pour le struct; vous ne pouvez créer d'objet tant que le corps du structn'a pas été fourni. Avec cette technique, le corps de cette structure est dissimulé dans le fichier d'implémentation :

//: C05:Handle.cpp {O}

// Handle implementation

#include "Handle.h"

#include "../require.h"

// Define Handle's implementation:

struct Handle::Cheshire { int i;

};

void Handle::initialize() { smile = new Cheshire;

smile->i = 0;

}

void Handle::cleanup() {

delete smile;

}

int Handle::read() { return smile->i;

}

void Handle::change(int x) { smile->i = x;

} ///:~

Cheshireest une structure imbriquée, donc elle doit être définie avec la définition de portée :

struct Handle::Cheshire {

Dans Handle::initialize( ), le stockage est alloué pour une structure Cheshire, et dans Handle::cleanup( )ce stockage est libéré. Ce stockage est utilisé en lieu et place de tous les éléments que vous mettriez normalement dans la section privatede la classe. Quand vous compilez Handle.cpp, cette définition de structure est dissimulée dans le fichier objet où personne ne peut la voir. Si vous modifiez les éléments de Cheshire, le seul fichier qui doit être recompilé est Handle.cppcar le fichier d'en-tête est intouché.

L'usage de Handleest similaire à celui de toutes les classes: inclure l'en-tête, créer les objets, et envoyer des messages.

//: C05:UseHandle.cpp //{L} Handle

// Use the Handle class

#include "Handle.h"

int main() { Handle u;

u.initialize();

u.read();

u.change(1);

u.cleanup();

} ///:~

La seule chose à laquelle le programmeur client peut accéder est l'interface publique, donc tant que l'implémentation est la seule chose qui change, le fichier ci-dessus ne nécessite jamais une recompilation. Ainsi, bien que ce ne soit pas une dissimulation parfaite de l'implémentation, c'est une grande amélioration.

5.7 - Résumé

Le contrôle d'accès en C++ donne un bon contrôle au créateur de la classe. Les utilisateurs de la classe peuvent [clairement] voir exactement ce qu'ils peuvent utiliser et ce qui est à ignorer. Plus important, ceci-dit, c'est la possibilité de s'assurer qu'aucun programmeur client ne devienne dépendant d'une partie quelconque [partie] de l'implémentation interne d'une classe. Si vous faites cela en tant que créateur de la classe, vous pouvez changer l'implémentation interne tout en sachant qu'aucun programmeur client ne sera affecté par les changements parce qu'ils ne peuvent accéder à cette partie de la classe.

Quand vous avez la capacité de changer l'implémentation interne, vous pouvez non seulement améliorer votre conception ultérieurement, mais vous avez également la liberté de faire des erreurs. Peu importe le soin avec lequel vous planifiez et concevez, vous ferez des erreurs. Savoir que vous pouvez faire des erreurs avec une sécurité relative signifie que vous expérimenterez plus, vous apprendrez plus vite, et vous finirez votre projet plus tôt.

L'interface publique dans une classe est ce que le programmeur client peutvoir, donc c'est la partie la plus importante à rendre “correcte” pendant l'analyse et la conception. Mais même cela vous permet une certaine marge de sécurité pour le changement. Si vous n'obtenez pas la bonne d'interface la première fois, vous pouvez ajouterplus de fonctions, tant que vous n'en enlevez pas que les programmeurs clients ont déjà employés dans leur code.

5.8 - Exercices

Les solutions des exercices suivants peuvent être trouvées dans le document électronique The Thinking in C++

Annotated Solution Guide, disponible à petit prix sur www.BruceEckel.com.

1 Créez une classe avec des données et des fonctions membres public, private, et protected. Créez un objet de cette classe et regardez quel genre de message le compilateur vous donne quand vous essayez

d'accéder à chacun des membres.

2 Ecrivez un structappelé Libqui contienne trois objets string a, b,et c. Dans main( )créez un objet Libappelé xet assignez une valeur à x.a, x.b, et x.c. Affichez les valeurs à l'écran. A présent remplacez a, b,et cpar un tableau de string s[3]. Vérifiez que le code dans main( )ne compile plus suite à ce changement. Créez maintenant une classe appelée Libc, avec des objets string private a, b,et c, et les fonctions membres seta( ), geta( ), setb( ), getb( ), setc( ), et getc( )pour assigner ( seten angais, ndt) et lire ( getsignifie obtenir en anglais, ndt) les valeurs. Ecrivez main( )comme précédemment. A présent, changez les objets string private a, b,et cen un tableau de string s[3] private. Vérifiez que le code dans main( )n'est pasaffecté par ce changement.

3 Créez une classe et une fonction friendglobale qui manipule les données privatedans la classe.

4 Ecrivez deux classes, chacune ayant une fonction membre qui reçoit un pointeur vers un objet de l'autre classe. Créez une instance des deux objets dans main( )et appelez la fonction membre mentionnée ci-dessus dans chaque classe.

5 Créez trois classes. La première classe contient des données private, et accorde le status de friendà l'ensemble de la deuxième classe ainsi qu'à une fonction membre de la troisième. Dans main( ), vérifiez que tout marche correctement.

6 Créez une classe Hen. Dans celle-ci, imbriquez une classe Nest. Dans Nest, placez une classe Egg.

Chaque classe devrait avoir une fonction membre display( ). Dans main( ), créez une instance de chaque classe et appelez la fonction display( )de chacune d'entre elles.

7 Modifiez l'exercice 6 afin que Nestet Eggcontiennent chacune des données private. Donnez accès à ces données privées aux classes englobantes au moyen du mot-clé friend.

8 Créez une classe avec des données membres réparties au sein de nombreuses sections public, private,et protected. Ajoutez une fonction membre showMap( )qui affiche à l'écran les noms de chacune des données membre ainsi que leurs addresses. Si possible, compilez et exécutez ce programme sur plusieurs

compilateurs et/ou ordinateur et/ou système d'exploitation pour voir s'il y a des différences d'organisation dans l'objet.

9 Copiez l'implémentation et les fichiers de test pour Stashdu Chapitre 4 afin que vous puissiez compiler et tester Stash.hdans ce chapitre.

10 Placez des objets de la class Hende l'Exercice 6 dans un Stash. Développez les ensembles et affichez-les à l'écran (si vous ne l'avez pas déjà fait, vous aurez besoin d'ajouter Hen::print( )).

11 Copiez l'implémentation et les fichiers de test pour Stackdu Chapitre 4 afin que vous puissiez compiler et tester Stack2.hdans ce chapitre.

12 Placez des objets de la classe Hende l'Exercice 6 dans un Stack. Développez les ensemble et affichez-les à l'écran (si vous ne l'avez pas déjà fait, vous aurez besoin d'ajouter Hen::print( )).

13 Modifiez Cheshiredans Handle.cpp, et vérifiez que votre gestionnaire de projets recompile et refasse l'édition de liens uniquement pour ce fichier, mais ne recompile pas UseHandle.cpp.

14 Créez une classe StackOfInt(une pile qui contient des ints) en utilisant la technique du "chat du Cheshire"

qui disimule les structures de données de bas niveau utilisées pour stocker les éléments dans une classe appelée StackImp. Implémentez deux versions de StackImp: une qui utilise un tableau de intde taille fixe, et une qui utilise un vector<int>. Assignez une taille maximum dans le premier cas pour la pile afin que vous n'ayiez pas à vous soucier de l'agrandissement du tableau dans la première version. Remarquez que la

classe StackOfInt.hn'a pas besoin d'être modifiée quand StackImpchange.