I. Encapsulation

Nous allons ici nous attarder sur le traitement associé à l'encapsulation en C++. Sans surprise, on retrouve ici la notion de Classe.

Plan du chapitre :

  1. Représentation des classes
  2. Organisation du code source
  3. Les modificateurs d'accès
  4. Déclaration et définition des membres de classe
  5. L'opérateur de résolution de portée
  6. Les méthodes inline

Liste des figures :

  1. Transposition en C++ d'une classe décrite avec UML

Liste des programmes :

  1. Structure du fichier de déclaration d'une classe
  2. Structure du fichier de définition d'une classe
  3. Utilisation de l'opérateur de résolution de portée
  4. Point.hxx : fichier d'entête de la classe Point
  5. Point.cc: fichier d'implémentation de la classe Point

1.1 Représentation des classes

En C++, une classe est représentée sous la forme d'une forme particulière de structure (struct) qui rassemble à la fois les attributs et les méthodes. En C++, la notion de message n'existe pas, aussi l'interface d'une classe est constituée de la liste des méthodes déclarées visibles. La figure ci-dessous illustre la transposition d'un diagramme de classe en sa représentation C++.

Représentation d'une classe en C++

Figure 1.1 Transposition en C++ d'une classe décrite avec UML

Quelques explications s'imposent :

1.2 Organisation du code source

A l'exception de classes fortement reliées, il est conseillé de placer une seule classe par fichier source. En fait, une classe a même besoin de 2 fichiers sources : un fichier header (.h, .H, .hxx, .hh ou .hpp selon les environnements) où l'on place la déclaration de la classe et un fichier de définition des méthodes et des variables de classe (.C, .cpp, .cc ou .cxx selon les environnements — je déconseille fortement l'utilisation de .C car certains environnements ne font pas la distinction entre les majuscules et les minuscules et .C risquerait d'être interprété comme du C et non du C++).

Afin d'éviter les inclusions redondantes, on place des balises de compilation avec des #ifdef comme dans l'exemple suivant (je suppose que l'on utilise .hxx et .cc comme extensions de fichiers)

#ifndef __NOM_DE_LA_CLASSE_HXX__
#define __NOM_DE_LA_CLASSE_HXX__

// Placer ici les inclusions et les déclarations externes nécessaires

// Placer ici la déclaration de la classe

class NomDeLaClasse
{
}; // Ne pas oublier ce #@##@@! de ";"

// Sauver sous le nom : nom_de_la_classe.hxx

#endif

Programme 1.1 Structure du fichier de déclaration d'une classe

Le fichier de définition sera :

#include "nom_de_la_classe.hxx"
// autres inclusions nécessaires

// Définitions des variables de classe

// Définitions des méthodes

Programme 1.2 Structure du fichier de définition d'une classe

et le tour est joué !

1.3 Les modificateurs d'accès

Les mots clefs public et private sont des modificateurs d'accès. Leur portée s'étend jusqu'au prochain modificateur. Le modificateur par défaut est private. Les membres déclarés private ne sont visibles que les méthodes de la classe elle même. En revanche, tout membre déclaré public aura une visibilité universelle. Le respect du principe d'encapsulation impose donc que :

Pour résumer, seules les méthodes de l'interface doivent être déclarées public, tout le reste doit être private. Nous verrons qu'un troisième modificateur de visibilité, nommé protected sera utilisé lorsque nous traiterons de l'héritage.

L'accès aux membres, c'est à dire en fait, l'appel de méthodes, est traité dans le chapitre concernant le cycle de vie des objets.

1.4 Déclaration et définition des membres de classes

Les membres de classe sont déclarés avec le mot clef static. Contrairement aux modificateurs d'accès, static n'a d'effet que sur une seule ligne de déclaration.

Particularité du C++ : la déclaration d'une donnée membre static ne lui affecte pas d'adresse, il faudra définir cette donnée membre par ailleurs dans le fichier de définitions. C'est précisément le rôle de la définition int Point::NbPoints_=0 qui définit la donnée membre de classe NbPoints et lui affecte la valeur initiale 0.

Rappelons que si les méthodes d'instance peuvent très bien utiliser les attributs de classe en plus des attributs d'instance, les méthodes de classe elles ne peuvent qu'utiliser les attributs de classe. En effet, sur les attributs de quelle instance devrait on appliquer les actions ?

1.5 La résolution de portée

Vous aurez remarqué un opérateur particulier "::" appelé « opérateur de résolution de portée ». Il sert à désigner à quelle classe appartient une méthode ou un attribut.

Cet opérateur est nécessaire à la définition des méthodes car le langage C++ ne vous oblige pas à respecter la règle de séparation de l'implémentation dans différents fichiers. Aussi, la définition de chaque méthode doit être précédée du nom de la classe à laquelle elle se rattache et de l'opérateur de résolution de portée. Par exemple, supposons que dans le même fichier, vous vouliez implémenter la méthode met1 de la classe A ainsi que la méthode met2 de la classe B, alors, vous auriez à spécifier :

typeRetour A::met1(paramètres)
{
  // code d'implémentation
}

typeRetour B::met2(paramètres)
{
  // code d'implémentation
}

Programme 1.3 utilisation de l'opérateur de résolution de portée

L'accès à une fonction ou une variable globale synonyme d'un membre de classe peut se faire en utilisant ::identificateur

1.6 Les méthodes inline

Il existe 2 manières de spécifier le code des méthodes. La première consiste à séparer la déclaration de la méthode de son implémentation. C'est celle que nous avons illustrée dans l'exemple précédent. Ce système présente néanmoins un inconvénient : j'entends déjà quelques râleurs vociférer :

« Utiliser un appel de méthode pour récupérer la valeur d'un attribut c'est une perte de temps lamentable »

Qu'ils se rassurent, les méthodes inline ont été inventées pour cela ! La principale caractéristique des méthodes inline est leur faculté à développeur leur code en lieu et place d'un appel à la manière d'une macro. Vous saisissez tout de suite l'avantage : vous gagnez le temps nécessaire à l'appel d'une fonction, ainsi l'accès à un attribut ne coûte plus rien !

Il y a néanmoins un inconvénient, comme tout appel est remplacé par le développement du code de la méthode, il en résulte un accroissement de la taille du code cible. Aussi, ces méthodes doivent être limitées à quelques instructions sous peine d'accroître quasi indéfininiment la taille de l'exécutable. En outre, certains compilateurs refusent de mettre en ligne les méthodes qui contiennent des boucles.

Comment une méthode devient elle inline ? il existe 2 manières de le faire :

  1. Décrire l'implémentation de la méthode au niveau de sa déclaration. C'est la manière la plus simple mais elle présente un sérieux défaut : ne pas séparer l'implémentation de la déclaration ce qui est contraire au principe de dissimulation.
  2. Par opposition aux méthodes décrites dans la déclaration d'une classe, on appelle méthode déportée une méthode dont le code n'est pas transcrit dans la déclaration de sa classe mais en dehors. Notons qu'il est néanmoins possible de faire développer inline une méthode déportée. Il faut alors préfixer sa déclaration ainsi que sa définition du mot clef inline. En outre, il faut donner son implémentation dans le fichier de déclaration à la suite de la déclaration de la classe. En effet, si vous souhaitez que le compilateur puisse développer le code de la méthode sur le lieu de l'appel, il faut qu'il connaisse sa taille !

Pour terminer, signalons qu'il est possible de rendre inline non seulement des méthodes mais également des fonctions. Les règles de codage de ces dernières doivent suivrent les principes indiqués pour la déclaration inline des méthodes déportées.

Par exemple, nous allons réécrire la classe Point en utilisant des méthodes inline. Nous allons mettre inline les deux méthodes d'accès aux attributs ainsi que le constructeur. Afin d'exploiter toutes les possibilités, les méthodes d'accès aux attributs seront placées inline dans la déclaration alors que le constructeur sera mis inline externe. En respectant la structure en 2 fichiers, nous obtenons finalement :

#ifndef __POINT_HXX__
#define __POINT_HXX__

class Point
{
  public:

    inline Point(int absc, int ordo); // Constructeur déclaré inline
                                      // le compilateur va rechercher le code plus loin

    int x(void) const                 // Déclaration et définition inline
    {
      return abscisse_;
    }

    int y(void) const                 // Déclaration et définition inline
    {
      return ordonnee_;
    }

    void deplacerDe(int incX, int incY); // Juste la déclaration, méthode NON inline
    void deplacerVers(int dX, int dY);   // Juste la déclaration, méthode NON inline

    static int NbPoints(void);
  private:
    int abscisse_;
    int ordonnee_;
    
    static int NbPoints_;   
};

// Definition inline deportee du constructeur

inline Point::Point(int absc, int ordo)
{
  abscisse_=absc;
  ordonnee_=ordo;
}

#endif

Programme 1.4 Point.hxx : fichier d'entête de la classe Point

#include "Point.hxx"

// Definition de l'attribut statique NbPoints_
// notez que l'on ne repete pas le mot clef static
// la valeur d'initialisation est OBLIGATOIRE

int Point::NbPoints_=0;

void Point::deplacerDe(int incX, int incY)
{
  abscisse_+=incX;
  ordonnee_+=incY;
}

void Point::deplacerVers(int dX, int dY)
{
  abscisse_=dX;
  ordonnee_=dY;
}

Programme 1.5 Point.cc fichier d'implémentation de la classe Point