Programmation C-C++/C++ : La couche objet/Déclaration de classes en C++

Cours de C/C++
^
C++ : La couche objet
Généralités
Extension de la notion de type du C
Déclaration de classes en C++
Encapsulation des données
Héritage
Classes virtuelles
Fonctions et classes amies
Constructeurs et destructeurs
Pointeur this
Données et fonctions membres statiques
Surcharge des opérateurs
Des entrées - sorties simplifiées
Méthodes virtuelles
Dérivation
Méthodes virtuelles pures - Classes abstraites
Pointeurs sur les membres d'une classe

Livre original de C. Casteyde

Afin de permettre la définition des méthodes qui peuvent être appliquées aux structures des classes C++, la syntaxe des structures C a été étendue (et simplifiée). Il est à présent possible de définir complètement des méthodes dans la définition de la structure. Cependant il est préférable de la reporter et de ne laisser que leur déclaration dans la structure. En effet, cela accroît la lisibilité et permet de masquer l'implémentation de la classe à ses utilisateurs en ne leur montrant que sa déclaration dans un fichier d'en-tête. Ils ne peuvent donc ni la voir, ni la modifier (en revanche, ils peuvent toujours voir la structure de données utilisée par son implémentation).

La syntaxe est la suivante :

struct Nom
{
    [type champs;
    [type champs;
    [...]]]

    [méthode;
    [méthode;
    [...]]]
};

où Nom est le nom de la classe. Elle peut contenir divers champs de divers types.

Les méthodes peuvent être des définitions de fonctions, ou seulement leurs déclarations. Si on ne donne que leurs déclarations, on devra les définir plus loin. Pour cela, il faudra spécifier la classe à laquelle elles appartiennent avec la syntaxe suivante :

type classe::nom(paramètres)
{
    /* Définition de la méthode. */
}

La syntaxe est donc identique à la définition d'une fonction normale, à la différence près que leur nom est précédé du nom de la classe à laquelle elles appartiennent et de deux deux-points (::). Cet opérateur :: est appelé l'opérateur de résolution de portée. Il permet, d'une manière générale, de spécifier le bloc auquel l'objet qui le suit appartient. Ainsi, le fait de précéder le nom de la méthode par le nom de la classe permet au compilateur de savoir de quelle classe cette méthode fait partie. Rien n'interdit, en effet, d'avoir des méthodes de même signature, pourvu qu'elles soient dans des classes différentes.

Exemple 8-1. Déclaration de méthodes de classe

modifier
struct Entier
{
    int i;                // Donnée membre de type entier.

    // Fonction définie à l'intérieur de la classe :
    int lit_i(void)
    {
        return i;
    }

    // Fonction définie à l'extérieur de la classe :
    void ecrit_i(int valeur);
};

void Entier::ecrit_i(int valeur)
{
    i=valeur;
    return ;
}
 Si la liste des paramètres de la définition de la fonction contient des initialisations supplémentaires à celles qui ont été spécifiées dans la déclaration de la fonction, les deux jeux d'initialisations sont fusionnées et utilisées dans le fichier où la définition de la fonction est placée. Si les initialisations sont redondantes ou contradictoires, le compilateur génère une erreur.
 L'opérateur de résolution de portée permet aussi de spécifier le bloc d'instructions d'un objet qui n'appartient à aucune classe. Pour cela, on ne mettra aucun nom avant l'opérateur de résolution de portée. Ainsi, pour accéder à une fonction globale à l'intérieur d'une classe contenant une fonction de même signature, on fera précéder le nom de la fonction globale de cet opérateur.

Exemple 8-2. Opérateur de résolution de portée

modifier
    int valeur(void)         // Fonction globale.
    {
        return 0;
    }

    struct A
    {
        int i;

        void fixe(int a)
        {
            i=a;
            return;
        }

        int valeur(void)       // Même signature que la fonction globale.
        {
            return i;
        }

        int global_valeur(void)
        {
            return ::valeur(); // Accède à la fonction globale.
        }
    };

De même, l'opérateur de résolution de portée permettra d'accéder à une variable globale lorsqu'une autre variable homonyme aura été définie dans le bloc en cours. Par exemple :

    int i=1;                 // Première variable de portée globale

    int main(void)
    {
        if (test())
        {
           int i=3;          // Variable homonyme de portée locale.
           int j=2*::i;      // j vaut à présent 2, et non pas 6.
           /* Suite ... */
        }

        /* Suite ... */

        return 0;
    }

Les champs d'une classe peuvent être accédés comme des variables normales dans les méthodes de cette classe.

Exemple 8-3. Utilisation des champs d'une classe dans une de ses méthodes

modifier
struct client
{
    char Nom[21], Prenom[21];    // Définit le client.
    unsigned int Date_Entree;    // Date d'entrée du client
                                 // dans la base de données.
    int Solde;

    bool dans_le_rouge(void)
    {
        return (Solde<0);
    }

    bool bon_client(void)        // Le bon client est
                                 // un ancien client.
    {
        return (Date_Entree<1993); // Date limite : 1993.
    }
};

Dans cet exemple, le client est défini par certaines données. Plusieurs méthodes sont définies dans la classe même.

L'instanciation d'un objet se fait comme celle d'une simple variable :

classe objet;

Par exemple, si on a une base de données devant contenir 100 clients, on peut faire :

client clientele[100];  /* Instancie 100 clients. */

On remarquera qu'il est à présent inutile d'utiliser le mot clé struct pour déclarer une variable, contrairement à ce que la syntaxe du C exigeait.

L'accès aux méthodes de la classe se fait comme pour accéder aux champs des structures. On donne le nom de l'objet et le nom du champ ou de la méthode, séparés par un point. Par exemple :

/* Relance de tous les mauvais payeurs. */
int i;
for (i=0; i<100; ++i)
    if (clientele[i].dans_le_rouge()) relance(clientele[i]);

Lorsque les fonctions membres d'une classe sont définies dans la déclaration de cette classe, le compilateur les implémente en inline (à moins qu'elles ne soient récursives ou qu'il existe un pointeur sur elles).

Si les méthodes ne sont pas définies dans la classe, la déclaration de la classe sera mise dans un fichier d'en-tête, et la définition des méthodes sera reportée dans un fichier C++, qui sera compilé et lié aux autres fichiers utilisant la classe client. Bien entendu, il est toujours possible de déclarer les fonctions membres comme étant des fonctions inline même lorsqu'elles sont définies en dehors de la déclaration de la classe. Pour cela, il faut utiliser le mot clé inline, et placer le code de ces fonctions dans le fichier d'en-tête ou dans un fichier .inl.

Sans fonctions inline, notre exemple devient :

Fichier client.h :

struct client
{
    char Nom[21], Prenom[21];
    unsigned int Date_Entree;
    int Solde;

    bool dans_le_rouge(void);
    bool bon_client(void);
};

/*
Attention à ne pas oublier le ; à la fin de la classe dans un
fichier .h ! L'erreur apparaîtrait dans tous les fichiers ayant
une ligne #include "client.h" , parce que la compilation a lieu
après l'appel au préprocesseur.
*/

Fichier client.cc :

/* Inclut la déclaration de la classe : */
#include "client.h"

/* Définit les méthodes de la classe : */

bool client::dans_le_rouge(void)
{
    return (Solde<0);
}

bool client::bon_client(void)
{
    return (Date_Entree<1993);
}