Penser en C++

Volume 1


précédentsommairesuivant

5. Cacher l'implémentation

Une bibliothèque C typique contient un struct et quelques fonctions associées pour agir sur cette structure. Jusqu'ici vous avez vu comment le C++ prend les fonctions qui sont conceptuellement associées et les associe littéralement en

mettant les déclarations de fonctions à l'intérieur de la portée de la structure, en changeant la façon dont les fonctions sont appelées par la structure, en éliminant le passage de l'adresse de la structure en premier argument, et en ajoutant un nouveau nom de type au programme (donc vous n'avez pas à créer un typedef pour le label de la structure).

Tout ceci est très pratique - cela vous aide à organiser votre code et à le rendre plus facile à écrire et à lire. Cependant, il y a d'autres questions importantes quand on fait des bibliothèques simplifiées en C++, en particulier sur les problèmes de la sûreté et du contrôle. Ce chapitre s'intéresse au sujet des limites des structures.

5.1. Fixer des limites

Dans toute relation il est important d'avoir des limites respectées par toutes les parties concernées. Quand vous créez une bibliothèque, vous établissez une relation avec le programmeur client qui utilise la bibliothèque pour construire une application ou une autre bibliothèque.

Dans un struct C, comme avec la plupart des choses en C, il n'y a pas de règles. Les programmeurs clients peuvent faire ce qu'ils veulent avec la structure, et il n'y a aucune façon de forcer un comportement particulier. Par exemple, bien que vous ayez vu dans le dernier chapitre l'importance des fonctions appelées initialize( ) et cleanup( ), le programmeur client à la possibilité de ne pas appeler ces fonctions. (nous verrons une meilleure approche dans le prochain chapitre.) Et bien que vous préfèreriez vraiment que le programmeur client ne manipule pas directement certains membres de votre structure, en C il n'y aucun moyen de s'en prémunir. Tout est nu en ce monde.

Il y a deux raisons pour contrôler l'accès aux membres. La première est d'empêcher le programmeur client d'accéder à des outils auxquels il ne devrait pas toucher, des outils qui sont nécessaires pour les processus internes du type de données, mais pas de la partie de l'interface dont le programmeur client a besoin pour résoudre son problème particulier. C'est réellement un service rendu aux programmeurs clients parce qu'ils peuvent facilement voir ce qui est important pour eux et ce qu'ils peuvent ignorer.

La deuxième raison du contrôle d'accès est de permettre au concepteur de bibliothèque de changer les fonctionnements internes de la structure sans s'inquiéter de la façon dont cela affectera le programmeur client. Dans l'exemple de la Stack du dernier chapitre, vous pourriez vouloir assigner le stockage dans de grandes sections, pour la vitesse, plutôt que de créer un nouveau stockage chaque fois qu'un élément est ajouté. Si l'interface et l'exécution sont clairement séparées et protégées, vous pouvez accomplir ceci et exiger seulement un relink par le programmeur client.

5.2. Le contrôle d'accès en C++

Le C++ introduit trois nouveaux mots-clefs pour fixer les limites d'une structure : public, private et protected. Leur sens et leur usage sont remarquablement clairs. Ces spécificateurs d'accès sont utilisés seulement dans la déclaration d'une structure, et ils changent les limites pour toutes les déclarations qui viennent après eux. Quand vous utilisez un tel spécificateur, il doit être suivi par deux points.

public signifie que tous les membres qui suivent cette déclaration sont disponibles à tout le monde. Les membres public sont comme les membres d'un struct. Par exemple, les déclarations de structures suivantes sont équivalentes :

 
Sélectionnez
//: C05:Public.cpp
// Public est exactement comme une structure (struct) en C
 
struct A {
  int i;
  char j;
  float f;
  void func();
};
 
void A::func() {}
 
struct B {
public:
  int i;
  char j;
  float f;
  void func();
};
 
void B::func() {}  
 
int main() {
  A a; B b;
  a.i = b.i = 1;
  a.j = b.j = 'c';
  a.f = b.f = 3.14159;
  a.func();
  b.func();
} ///:~

Le mot-clef private, à l'inverse, signifie que personne ne peut accéder à ce membre sauf vous, le créateur de ce type, dans les fonctions membres de ce type. private est une brique dans le mur entre vous et le programmeur client ; si quelqu'un essaye d'accéder à un membre private, ils obtiennent une erreur de compilation (compile-time error). Dans struct B dans l'exemple ci-dessus, vous pourriez vouloir rendre des morceaux de la représentation (c'est-à-dire, des données membres) cachés, accessibles uniquement par vous :

 
Sélectionnez
//: C05:Private.cpp
// Fixer les limites
 
struct B {
private:
  char j;
  float f;
public:
  int i;
  void func();
};
 
void B::func() {
  i = 0;
  j = '0';
  f = 0.0;
};
 
int main() {
  B b;
  b.i = 1;    // OK, public
//!  b.j = '1';  // Illégal, private
//!  b.f = 1.0;  // Illégal, private
} ///:~

Bien que func( ) puisse accéder à n'importe quel membre de B(car func( ) est un membre de B, ce qui lui garantit automatiquement la permission), une fonction globale ordinaire comme main( ) ne le peut pas. Bien sûr, un membre d'une autre structure ne le peut pas non plus. Seules, les fonctions qui sont clairement écrites dans la déclaration de la structure (le "contrat") peuvent accéder aux membres private.

Il n'y a pas d'ordre requis pour les spécificateurs d'accès, et ils peuvent apparaître plus d'une fois. Ils affectent tous les membres déclarés après eux et avant le spécificateur d'accès suivant.

5.2.1. protected

Le dernier spécificateur est protected. protected agit exactement comme private, avec une exception dont nous ne pouvons pas vraiment parler maintenant : les structures "héritées" (qui ne peuvent accéder aux membres protected) peuvent accéder aux membres protected. Ceci deviendra plus clair au Chapitre 14 quand l'héritage sera introduit. Pour le moment, considérez que protected a le même effet que private.

5.3. L'amitié

Que faire si vous voulez donner accès à une fonction qui n'est pas membre de la structure courante ? Ceci est accompli en déclarant cette fonction friend(amie) dans la déclaration de la structure. Il est important que la déclaration friend ait lieu à l'intérieur de la déclaration de la structure parce que vous (ainsi que le compilateur) devez être capables de lire la déclaration de la structure et d'y voir toutes les règles concernant la taille et le comportement de ce type de données. Et une règle très importante dans toute relation est "qui peut accéder à mon implémentation privée ?"

La classe contrôle le code qui a accès à ses membres. Il n'y a pas de moyen magique de "forcer le passage" depuis l'extérieur si vous n'êtes pas friend; vous ne pouvez pas déclarer une nouvelle classe et dire "Salut, je suis friend(un ami, ndt) de Bob.

Vous pouvez déclarer une fonction globale friend, et vous pouvez également déclarer une fonction membre d'une autre structure, ou même une structure entière, en tant que friend. Voici un exemple :

 
Sélectionnez
//: C05:Friend.cpp
// Friend permet des accès spéciaux
 
// Déclaration (spécification du type incomplète ) :
struct X;
 
struct Y {
  void f(X*);
};
 
struct X { // Définition
private:
  int i;
public:
  void initialize();
  friend void g(X*, int); // friend global 
  friend void Y::f(X*);  // friend membre d'une structure 
  friend struct Z; // Structure entière comme friend
  friend void h();
};
 
void X::initialize() { 
  i = 0; 
}
 
void g(X* x, int i) { 
  x->i = i; 
}
 
void Y::f(X* x) { 
  x->i = 47; 
}
 
struct Z {
private:
  int j;
public:
  void initialize();
  void g(X* x);
};
 
void Z::initialize() { 
  j = 99;
}
 
void Z::g(X* x) { 
  x->i += j; 
}
 
void h() {
  X x;
  x.i = 100; // Manipulation directe des données
}
 
int main() {
  X x;
  Z z;
  z.g(&x);
} ///:~

struct Y a une fonction membre f( ) qui modifiera un objet de type X. Cela ressemble à un casse-tête car le compilateur C++ exige que vous déclariez tout avant de pouvoir y faire référence, donc struct Y doit être déclaré avant que son membre Y::f(X*) puisse être déclaré comme friend dans struct X. Mais pour déclarer Y::f(X*), struct X doit d'abord être déclaré !

Voici la solution. Remarquez que Y::f(X*) prend l' adresse d'un objet X. C'est critique parce que le compilateur sait toujours comment passer une adresse, qui est d'une taille fixe quelque soit le type d'objet passé, même s'il n'a pas toutes les informations à propos de la taille du type concerné. Toutefois, si vous essayez de passer l'objet complet le compilateur doit voir la déclaration de la structure X en intégralité, pour connaître sa taille et savoir comment le passer, avant qu'il ne vous permette de déclarer une fonction comme Y::g(X).

En passant l'adresse d'un X, le compilateur vous permet de faire une spécification de type incomplète de X avant de déclarer Y::f(X*). Ceci est accompli par la déclaration :

 
Sélectionnez
struct X;

Cette déclaration dit simplement au compilateur qu'il existe une structure portant ce nom, et donc que c'est OK pour y faire référence tant que vous n'avez pas besoin de plus de détails que le nom.

A présent, dans struct X, la fonction Y::f(X*) peut être déclarée comme friend sans problème. Si vous aviez essayé de la déclarer avant que le compilateur eût vu la définition complète de Y, cela aurait généré une erreur. C'est une sécurité pour assurer la cohérence et éliminer les bugs.

Notez les deux autres fonctions friend. La première déclaration concerne une fonction globale ordinaire g( ). Mais g( ) n'a pas été déclarée précédemment dans la portée générale ! Il s'avère que friend peut être utilisé de cette manière pour simultanément déclarer la fonction et lui donner le statut friend. Ce comportement s'applique aux structures en intégralité :

 
Sélectionnez
friend struct Z;

est une spécification de type incomplète pour Z, et donne à toute la structure le statut friend.

5.3.1. Amis emboîtés

Faire une structure emboîtée ne lui donne pas automatiquement accès aux membres private. Pour obtenir cela, vous devez suivre une procédure particulière : d'abord, déclarer (sans la définir) la structure emboîtée, puis la déclarer en tant que friend, et finalement définir la structure. La définition de la structure doit être séparée de la déclaration friend, autrement elle serait vu par le compilateur comme étant non membre. Voici un exemple :

 
Sélectionnez
//: C05:NestFriend.cpp
// friends emboîtés
#include <iostream>
#include <cstring> // memset()
using namespace std;
const int sz = 20;
 
struct Holder {
private:
  int a[sz];
public:
  void initialize();
  struct Pointer;
  friend struct Pointer;
  struct Pointer {
  private:
    Holder* h;
    int* p;
  public:
    void initialize(Holder* h);
    // Se déplace dans le tableau:
    void next();
    void previous();
    void top();
    void end();
    // Accession à des valeurs:
    int read();
    void set(int i);
  };
};
 
void Holder::initialize() {
  memset(a, 0, sz * sizeof(int));
}
 
void Holder::Pointer::initialize(Holder* rv) {
  h = rv;
  p = rv->a;
}
 
void Holder::Pointer::next() {
  if(p < &(h->a[sz - 1])) p++;
}
 
void Holder::Pointer::previous() {
  if(p > &(h->a[0])) p--;
}
 
void Holder::Pointer::top() {
  p = &(h->a[0]);
}
 
void Holder::Pointer::end() {
  p = &(h->a[sz - 1]);
}
 
int Holder::Pointer::read() {
  return *p;
}
 
void Holder::Pointer::set(int i) {
  *p = i;
}
 
int main() {
  Holder h;
  Holder::Pointer hp, hp2;
  int i;
 
  h.initialize();
  hp.initialize(&h);
  hp2.initialize(&h);
  for(i = 0; i < sz; i++) {
    hp.set(i);
    hp.next();
  }
  hp.top();
  hp2.end();
  for(i = 0; i < sz; i++) {
    cout << "hp = " << hp.read()
         << ", hp2 = " << hp2.read() << endl;
    hp.next();
    hp2.previous();
  }
} ///:~

Quand Pointer est déclaré, l'accès aux membres privés de Holder lui est accordé en disant :

 
Sélectionnez
friend struct Pointer;

struct Holder contient un tableau de int s et Pointer vous permet d'y accéder. Parce que Pointer est fortement lié avec Holder, il est judicieux d'en faire une structure membre de Holder. Mais comme Pointer est une classe différente de Holder, vous pouvez en créer plusieurs instances dans main( ) et les utiliser pour sélectionner différentes parties du tableau. Pointer est une structure au lieu d'un simple pointeur C, donc vous pouvez garantir qu'il pointera toujours sans risque dans Holder.

La fonction memset( ) de la bibliothèque C standard (dans <cstring>) est utilisée par commodité dans le programme ci-dessus. Elle initialise toute la mémoire démarrant à une certaine addresse (le premier argument) à une valeur particulière (le deuxième argument) sur n octets à partir de l'adresse de départ ( n est le troisième argument). Bien sûr, vous auriez pu simplement utiliser une boucle pour itérer sur toute la mémoire, mais memset( ) est disponible, abondamment testée (donc il est moins probable que vous introduisiez une erreur), et probablement plus efficace que si vous le codiez à la main.

5.3.2. Est-ce pur ?

La définition de classe vous donne une piste de vérification, afin que vous puissiez voir en regardant la classe quelles fonctions ont la permission de modifier les parties privées de la classe. Si une fonction est friend, cela signifie que ce n'est pas un membre, mais que vous voulez quand-même lui donner la permission de modifier des données privées, et elle doit être listée dans la définition de la classe afin que tout le monde puisse voir que c'est une des fonctions privilégiées.

Le C++ est un langage objet hybride, pas objet pur, et le mot-clé friend a été ajouté pour régler certains problèmes pratiques qui ont surgi. Il n'est pas choquant de souligner que cela rend le langage moins "pur" car C++ est conçu pour être pragmatique, et non pas par pour aspirer à un idéal abstrait.

5.4. Organisation physique d'un objet

Le chapitre 4 affirmait qu'un struct écrit pour un compilateur C puis compilé avec C++ resterait inchangé. Cette affirmation faisait principalement référence à l'organisation physique d'un struct, c'est-à-dire à l'emplacement mémoire individuel des variables au sein de la mémoire allouée pour l'objet. Si le compilateur C++ modifiait l'organisation des struct s conçus en C, alors tout code C que vous auriez écrit et qui serait basé sur la connaissance de l'emplacement précis des variables dans un struct cesserait de fonctionner.

Cependant, quand vous commencez à utiliser des spécificateurs d'accès, vous entrez de plein pied dans le royaume du C++, et les choses changent un peu. Dans un "bloc d'accès" particulier (un groupe de déclarations délimité par des spécificateurs d'accès), on a la garantie que les variables seront positionnées de manière contigües en mémoire, comme en C. Toutefois, les blocs d'accès peuvent ne pas apparaître au sein de l'objet dans l'ordre dans lequel vous les avez déclarés. Bien que le compilateur dispose en général les blocs exactement comme vous les voyez, il n'y a pas de règles à ce sujet, car l'architecture d'une machine particulière et/ou d'un système pourrait avoir un support explicite des mots-clefs private et protected qui imposerait à ces blocs de se trouver dans des emplacements mémoires particuliers. Les spécifications du langage ne veulent pas priver une implémentation de ce type d'avantage.

Les spécificateurs d'accès font partie de la structure et n'affectent pas les objets créés à partir de la structure. Toutes les informations relatives aux spécifications d'accès disparaissent avant que le programme ne soit exécuté ; en général, ceci se produit au moment de la compilation. Lors de l'exécution, les objets deviennent des "espaces de stockage" et rien de plus. Si vous le voulez vraiment, vous pouvez enfreindre toutes les règles et accéder directement à la mémoire, comme en C. C++ n'est pas conçu pour vous éviter de faire des choses imprudentes. Il vous fournit simplement une alternative bien plus simple, et autrement plus souhaitable.

En général, ce n'est pas une bonne idée de dépendre de quelque chose de spécifique à l'implémentation quand vous écrivez un programme. Quand vous devez avoir de telles dépendances, encapsulez-les dans une structure de façon à ce que les changements nécessaires au portage soient concentrés en un même endroit.

5.5. La classe

Le contrôle d'accès est souvent appelé le masquage de l'implémentation. Inclure les fonctions dans les structures (souvent désigné par le terme encapsulation (36))produisent un type de données avec des caractéristiques et des comportements, mais le contrôle d'accès impose des limites à ce type de données, pour deux motifs importants. La première est d'établir ce que le programmeur client peut et ne peut pas utiliser. Vous pouver construire vos mécanismes internes dans la structure sans vous soucier que des programmeurs clients pensent à ces mécanismes qui font partie de l'interface qu'ils devront employer.

Ceci amène directement la deuxième raison, qui est de séparer l'interface de l'implémentation. Si la structure est employée dans un ensemble de programmes, mais que les programmeurs clients ne peuvent faire rien d'autre qu'envoyer des messages à l'interface publique, alors vous pouvez changer tout ce qui est privé sans exiger des modifications à leur code.

L'encapsulation et le contrôle d'accès, pris ensemble, créent quelque chose de plus que la struct C. Nous sommes maintenant dans le monde de la programmation orientée-objet, où une structure décrit une classe d'objets comme vous pouvez décrire une classe de poissons ou une classe d'oiseaux : Tout objet appartenant à cette classe partagera ces caractéristiques et comportements. C'est ce qu'est devenue la déclaration de structure, une description de la façon dont tous les objets de ce type agiront et à quoi ils ressembleront.

Dans le langage POO d'origine, Simula-67, le mot-clé class était utilisé pour décrire un nouveau type de données. Ceci a apparamment inspiré à Stroustrup de choisir le même mot-clé pour le C++, pour souligner que c'était le point focal de tout le langage : la création de nouveaux types de données qui sont plus que des struct s C avec des fonctions. Cela semble être une justification adéquate pour un nouveau mot-clé.

Cependant, class est proche d'être un mot-clé inutile en C++. Il est identique au mot-clé struct à tous les points de vue sauf un : les membres d'une class sont private par défaut, tandis que ceux d'une struct sont public. Nous avons ici deux structures qui produisent le même résultat :

 
Sélectionnez
//: C05:Class.cpp
// Similarité entre une structure et une classe
 
struct A {
private:
  int i, j, k;
public:
  int f();
  void g();
};
 
int A::f() { 
  return i + j + k; 
}
 
void A::g() { 
  i = j = k = 0; 
}
 
// On obtient des résultats identiques avec :
 
class B {
  int i, j, k;
public:
  int f();
  void g();
};
 
int B::f() { 
  return i + j + k; 
}
 
void B::g() { 
  i = j = k = 0; 
} 
 
int main() {
  A a;
  B b;
  a.f(); a.g();
  b.f(); b.g();
} ///:~

La classe est le concept fondamental de la POO en C++. C'est un des mots-clés qui ne sera pas mis en gras dans ce livre - ça deviendrait fatigant avec un mot répété aussi souvent que “class.” Le changement avec les classes est si important que je suspecte que Stroustrup aurait préféré mettre définitivement aux clous la structure, mais le besoin de compatibilité ascendante avec le C ne permettait pas cela.

Beaucoup de personnes préfèrent un style de création de classe plutôt façon struct que façon classe parce que vous surchargez le comportement privé par défaut de la classe en commençant par les éléments public s :

 
Sélectionnez
class X {
public:
  void interface_function();
private:
  void private_function();
  int internal_representation;
}; 

La logique sous-jacente est qu'il est plus sensé pour le lecteur de voir les membres qui ont le plus d'intérêt pour lui, ainsi il peut ignorer tout ce qui est privé. En effet, la seule raison pour laquelle tous les autres membres doivent être déclarés dans la classe est qu'ainsi le compilateur sait de quelle taille est l'objet et peut les allouer correctement, et ainsi peut garantir l'uniformité.

Les exemples de ce livre, cependant, mettront les membres privés en premier, comme ceci :

 
Sélectionnez
class X {
  void private_function();
  int internal_representation;
public:
  void interface_function();
}; 

Certains se donnent même la peine de décorer leurs propres noms privés :

 
Sélectionnez
class Y {
public:
  void f();
private:
  int mX;  // nom "décoré"
}; 

Comme mX est déjà caché dans la portée de Y, le m(pour “membre”) n'est pas nécessaire. Cependant dans les projets avec beaucoup de variables globales (quelque chose que vous devriez essayer d'éviter, mais qui est parfois inévitable dans des projets existants), il est utile de pouvoir distinguer dans une définition de fonction membre une donnée globale d'une donnée membre.

5.5.1. Modifier Stash pour employer le contrôle d'accès

Il est intéressant de prendre les exemples du chapitre 4 et de les modifier pour employer les classes et le contrôle d'accès. Observez la façon dont la partie de l'interface accessible au client se distingue maintenant clairement, de telle sorte qu'il n'y a aucune possibilité pour les programmeurs clients de manipuler accidentellement une partie de la classe qu'ils ne devraient pas.

 
Sélectionnez
//: C05:Stash.h
// Converti pour utiliser le contrôle d'accès
#ifndef STASH_H
#define STASH_H
 
class Stash {
  int size;      // Taille de chaque espace
  int quantity;  // Nombre d'espaces de stockage
  int next;      // Prochain espace vide
  // Tableaux d'octets alloués dynamiquement :
  unsigned char* storage;
  void inflate(int increase);
public:
  void initialize(int size);
  void cleanup();
  int add(void* element);
  void* fetch(int index);
  int count();
};
#endif // STASH_H ///:~

La fonction inflate( ) a été déclaré privée parce qu'elle est utilisée seulement par la fonction add( ) et fait donc partie de l'implémentation interne, pas de l'interface. Cela signifie que, plus tard, vous pourrez changer l'implémentation interne pour utiliser un système différent pour la gestion de la mémoire.

En dehors du nom du fichier d'include, l'en-tête ci-dessus est la seule chose qui ait été changée pour cet exemple. Le fichier d'implémentation et le fichier de test sont identiques.

5.5.2. Modifier Stack pour employer le contrôle d'accès

Comme deuxième exemple, voici Stack changé en classe. Maintenant la structure imbriquée des données est privée, ce qui est est une bonne chose car cela assure que le programmeur client ne verra pas la représentation interne de la Stack et ne dépendra pas de cette dernière :

 
Sélectionnez
//: C05:Stack2.h
// Structure imbriquée via une liste chaînée
#ifndef STACK2_H
#define STACK2_H
 
class Stack {
  struct Link {
    void* data;
    Link* next;
    void initialize(void* dat, Link* nxt);
  }* head;
public:
  void initialize();
  void push(void* dat);
  void* peek();
  void* pop();
  void cleanup();
};
#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é) ou si 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 classes ou "chat du Cheshire" (37)- 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 :

 
Sélectionnez
//: 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

 
Sélectionnez
struct Cheshire;

est une spécification incomplète ou une déclaration de classe(une définition de classe inclut le corps de la classe). Ce code dit au compilateur que Cheshire est 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 struct n'a pas été fourni. Avec cette technique, le corps de cette structure est dissimulé dans le fichier d'implémentation :

 
Sélectionnez
//: 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;
} ///:~

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

 
Sélectionnez
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 private de 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.cpp car le fichier d'en-tête est intouché.

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

 
Sélectionnez
//: 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 peut voir, 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 ajouter plus 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 struct appelé Lib qui contienne trois objets stringa, b, et c. Dans main( ) créez un objet Lib appelé x et assignez une valeur à x.a, x.b, et x.c. Affichez les valeurs à l'écran. A présent remplacez a, b, et c par 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 stringprivatea, b, et c, et les fonctions membres seta( ), geta( ), setb( ), getb( ), setc( ), et getc( ) pour assigner ( set en angais, ndt) et lire ( get signifie obtenir en anglais, ndt) les valeurs. Ecrivez main( ) comme précédemment. A présent, changez les objets stringprivatea, b, et c en un tableau de string s[3]private. Vérifiez que le code dans main( ) n'est pas affecté par ce changement.
  3. Créez une classe et une fonction friend globale qui manipule les données private dans 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 Nest et Egg contiennent 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 Stash du Chapitre 4 afin que vous puissiez compiler et tester Stash.h dans ce chapitre.
  10. Placez des objets de la class Hen de 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 Stack du Chapitre 4 afin que vous puissiez compiler et tester Stack2.h dans ce chapitre.
  12. Placez des objets de la classe Hen de 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 Cheshire dans 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 int s) 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 int de 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.h n'a pas besoin d'être modifiée quand StackImp change.

précédentsommairesuivant
Comme nous l'avons dit précédemment, on appelle parfois le contrôle d'accès, l'encapsulation.
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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.