• <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    9.2. Remontée des exceptions

    Les fonctions intéressées par les exceptions doivent les capter avec le mot clé catch comme on l'a vu ci-dessus. Elles peuvent alors effectuer tous les traitements d'erreurs que le C++ ne fera pas automatiquement. Ces traitements comprennent généralement le rétablissement de l'état des données manipulées par la fonction (dont, pour les fonctions membres d'une classe, les données membres de l'objet courant), ainsi que la libération des ressources non encapsulées dans des objets de classe de stockage automatique (par exemple, les fichiers ouverts, les connexions réseau, etc.).

    Une fois ce travail effectué, elles peuvent, si elles le désirent, relancer l'exception, afin de permettre un traitement complémentaire par leur fonction appelante. Le parcours de l'exception s'arrêtera donc dès que l'erreur aura été complètement traitée. Bien entendu, il est également possible de lancer une autre exception que celle que l'on a reçue, comme ce peut être par exemple le cas si le traitement de l'erreur provoque lui-même une erreur.

    Pour relancer l'exception en cours de traitement dans un gestionnaire d'exception, il faut utiliser le mot clé throw. La syntaxe est la suivante :

    throw ;
    L'exception est alors relancée, avec comme valeur l'objet que le compilateur a construit en interne pour propager l'exception. Les gestionnaires d'exception peuvent donc modifier les paramètres des exceptions, s'ils les attrapent avec une référence.

    Si, lorsqu'une exception se produit dans un bloc try, il est impossible de trouver le bloc catch correspondant à la classe de cette exception, il se produit une erreur d'exécution. La fonction prédéfinie std::terminate est alors appelée. Elle se contente d'appeler une fonction de traitement de l'erreur, qui elle-même appelle la fonction abort de la bibliothèque C. Cette fonction termine en catastrophe l'exécution du programme fautif en générant une faute (les ressources allouées par le programme ne sont donc pas libérées, et des données peuvent être perdues). Ce n'est généralement pas le comportement désiré, aussi est-il possible de le modifier en changeant la fonction appelée par std::terminate.

    Pour cela, il faut utiliser la fonction std::set_terminate, qui attend en paramètre un pointeur sur la fonction de traitement d'erreur, qui ne prend aucun paramètre et renvoie void. La valeur renvoyée par std::set_terminate est le pointeur sur la fonction de traitement d'erreur précédente. std::terminate et std::set_terminate sont déclaréee dans le fichier d'en-tête exception.

    Note : Comme leurs noms l'indiquent, std::terminate et std::set_terminate sont déclarées dans l'espace de nommage std::, qui est réservé pour tous les objets de la bibliothèque standard C++. Si vous ne voulez pas à avoir à utiliser systématiquement le préfixe std:: devant ces noms, vous devrez ajouter la ligne « using namespace std; » après avoir inclus l'en-tête exception. Vous obtiendrez de plus amples renseignements sur les espaces de nommage dans le Chapitre 11.

    Exemple 9-2. Installation d'un gestionnaire d'exception avec set_terminate

    #include <iostream>
    #include <exception>
    
    using namespace std;
    
    void mon_gestionnaire(void)
    {
        cout << "Exception non gérée reçue !" << endl;
        cout << "Je termine le programme proprement..."
             << endl;
        exit(-1);
    }
    
    int lance_exception(void)
    {
        throw 2;
    }
    
    int main(void)
    {
        set_terminate(&mon_gestionnaire);
        try
        {
            lance_exception();
        }
        catch (double d)
        {
            cout << "Exception de type double reçue : " <<
               d << endl;
        }
        return 0;
    }

    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    Chapitre 9. Les exceptions en C++

     

    Une exception est l'interruption de l'exécution du programme à la suite d'un événement particulier. Le but des exceptions est de réaliser des traitements spécifiques aux événements qui en sont la cause. Ces traitements peuvent rétablir le programme dans son mode de fonctionnement normal, auquel cas son exécution reprend. Il se peut aussi que le programme se termine, si aucun traitement n'est approprié.

    Le C++ supporte les exceptions logicielles, dont le but est de gérer les erreurs qui surviennent lors de l'exécution des programmes. Lorsqu'une telle erreur survient, le programme doit lancer une exception. L'exécution normale du programme s'arrête dès que l'exception est lancée, et le contrôle est passé à un gestionnaire d'exception. Lorsqu'un gestionnaire d'exception s'exécute, on dit qu'il a attrapé l'exception.

    Les exceptions permettent une gestion simplifiée des erreurs, parce qu'elles en reportent le traitement. Le code peut alors être écrit sans se soucier des cas particuliers, ce qui le simplifie grandement. Les cas particuliers sont traités dans les gestionnaires d'exception.

    En général, une fonction qui détecte une erreur d'exécution ne peut pas se terminer normalement. Comme son traitement n'a pas pu se dérouler normalement, il est probable que la fonction qui l'a appelée considère elle aussi qu'une erreur a eu lieu et termine son exécution. L'erreur remonte ainsi la liste des appelants de la fonction qui a généré l'erreur. Ce processus continue, de fonction en fonction, jusqu'à ce que l'erreur soit complètement gérée ou jusqu'à ce que le programme se termine (ce cas survient lorsque la fonction principale ne peut pas gérer l'erreur).

    Traditionnellement, ce mécanisme est implémenté à l'aide de codes de retour des fonctions. Chaque fonction doit renvoyer une valeur spécifique à l'issue de son exécution, permettant d'indiquer si elle s'est correctement déroulée ou non. La valeur renvoyée est donc utilisée par l'appelant pour déterminer la nature de l'erreur, et, si erreur il y a, prendre les mesures nécessaires. Cette méthode permet à chaque fonction de libérer les ressources qu'elle a allouées lors de la remontée des erreurs, et d'effectuer ainsi sa part du traitement d'erreur.

    Malheureusement, cette technique nécessite de tester les codes de retour de chaque fonction appelée, et la logique d'erreur développée finit par devenir très lourde, puisque ces tests s'imbriquent les uns à la suite des autres et que le code du traitement des erreurs se trouve mélangé avec le code du fonctionnement normal de l'algorithme. Cette complication peut devenir ingérable lorsque plusieurs valeurs de codes de retour peuvent être renvoyées afin de distinguer les différents cas d'erreur possible, car il peut en découler un grand nombre de tests et beaucoup de cas particuliers à gérer dans les fonctions appelantes.

    Certains programmes utilisent donc une solution astucieuse, qui consiste à déporter le traitement des erreurs à effectuer en dehors de l'algorithme par des sauts vers la fin de la fonction. Le code de nettoyage, qui se trouve alors après l'algorithme, est exécuté complètement si tout se passe correctement. En revanche, si la moindre erreur est détectée en cours d'exécution, un saut est réalisé vers la partie du code de nettoyage correspondante au traitement qui a déjà été effectué. Ainsi, ce code n'est écrit qu'une seule fois, et le traitement des erreurs est situé en dehors du traitement normal.

    La solution précédente est tout à fait valable (en fait, c'est même la solution la plus simple), mais elle souffre d'un inconvénient. Elle rend le programme moins structuré, car toutes les ressources utilisées par l'algorithme doivent être accessibles depuis le code de traitement des erreurs. Ces ressources doivent donc être placées dans une portée relativement globale, voire déclarées en tête de fonction. De plus, le traitement des codes d'erreurs multiples pose toujours les mêmes problèmes de complication des tests.

    La solution qui met en oeuvre les exceptions est beaucoup plus simple, puisque la fonction qui détecte une erreur peut se contenter de lancer une exception. Cette exception interrompt l'exécution de la fonction, et un gestionnaire d'exception approprié est recherché. La recherche du gestionnaire suit le même chemin que celui utilisé lors de la remontée des erreurs : à savoir la liste des appelants. La première fonction appelante qui contient un gestionnaire d'exception approprié prend donc le contrôle, et effectue le traitement de l'erreur. Si le traitement est complet, le programme reprend son exécution normale. Dans le cas contraire, le gestionnaire d'exception peut relancer l'exception (auquel cas le gestionnaire d'exception suivant est recherché) ou terminer le programme.

    Le mécanisme des exceptions du C++ garantit que tous les objets de classe de stockage automatique sont détruits lorsque l'exception qui remonte sort de leur portée. Ainsi, si toutes les ressources sont encapsulées dans des classes disposant d'un destructeur capable de les détruire ou de les ramener dans un état cohérent, la remontée des exceptions effectue automatiquement le ménage. De plus, les exceptions peuvent être typées, et caractériser ainsi la nature de l'erreur qui s'est produite. Ce mécanisme est donc strictement équivalent en termes de fonctionnalités aux codes d'erreurs utilisés précédemment.

    Comme on le voit, les exceptions permettent de simplifier le code, en reportant en dehors de l'algorithme normal le traitement des erreurs. Par ailleurs, la logique d'erreur est complètement prise en charge par le langage, et le programmeur n'a plus à faire les tests qui permettent de déterminer le traitement approprié pour chaque type d'erreur. Les mécanismes de gestion des exceptions du C++ sont décrits dans les paragraphes suivants.

    9.1. Lancement et récupération d'une exception

    En C++, lorsqu'il faut lancer une exception, on doit créer un objet dont la classe caractérise cette exception, et utiliser le mot clé throw. Sa syntaxe est la suivante :

    throw objet;
    objet est l'objet correspondant à l'exception. Cet objet peut être de n'importe quel type, et pourra ainsi caractériser pleinement l'exception.

    L'exception doit alors être traitée par le gestionnaire d'exception correspondant. On ne peut attraper que les exceptions qui sont apparues dans une zone de code limitée (cette zone est dite protégée contre les erreurs d'exécution), pas sur tout un programme. On doit donc placer le code susceptible de lancer une exception d'un bloc d'instructions particulier. Ce bloc est introduit avec le mot clé try :

    try
    {
        // Code susceptible de générer des exceptions...
    }

    Les gestionnaires d'exceptions doivent suivre le bloc try. Ils sont introduits avec le mot clé catch :

    catch (classe [&][temp])
    {
        // Traitement de l'exception associée à la classe
    }

    Notez que les objets de classe de stockage automatique définis dans le bloc try sont automatiquement détruits lorsqu'une exception fait sortir le contrôle du programme de leur portée. C'est également le cas de l'objet construit pour lancer l'exception. Le compilateur effectue donc une copie de cet objet pour le transférer au premier bloc catch capable de le recevoir. Cela implique qu'il y ait un constructeur de copie pour les classes d'exceptions non triviales.

    De même, les blocs catch peuvent recevoir leurs paramètres par valeur ou par référence, comme le montre la syntaxe indiquée ci-dessus. En général, il est préférable d'utiliser une référence, afin d'éviter une nouvelle copie de l'objet de l'exception pour le bloc catch. Toutefois, on prendra garde au fait que dans ce cas, les modifications effectuées sur le paramètre seront effectuées dans la copie de travail du compilateur et seront donc également visibles dans les blocs catch des fonctions appelantes ou de portée supérieure, si l'exception est relancée après traitement.

    Il peut y avoir plusieurs gestionnaires d'exceptions. Chacun traitera les exceptions qui ont été générées dans le bloc try et dont l'objet est de la classe indiquée par son paramètre. Il n'est pas nécessaire de donner un nom à l'objet (temp) dans l'expression catch. Cependant, cela permet de le récupérer, ce qui peut être nécessaire si l'on doit récupérer des informations sur la nature de l'erreur.

    Enfin, il est possible de définir un gestionnaire d'exception universel, qui récupérera toutes les exceptions possibles, quels que soient leurs types. Ce gestionnaire d'exception doit prendre comme paramètre trois points de suspension entre parenthèses dans sa clause catch. Bien entendu, dans ce cas, il est impossible de spécifier une variable qui contient l'exception, puisque son type est indéfini.

    Exemple 9-1. Utilisation des exceptions

    #include <iostream>
    
    using namespace std;
    
    class erreur   // Première exception possible, associée
                   // à l'objet erreur.
    {
    public:
        int cause;  // Entier spécifiant la cause de l'exception.
        // Le constructeur. Il appelle le constructeur de cause.
        erreur(int c) : cause(c) {}
        // Le constructeur de copie. Il est utilisé par le mécanisme
        // des exceptions :
        erreur(const erreur &source) : cause(source.cause) {}
    };
    
    class other {};   // Objet correspondant à toutes
                      // les autres exceptions.
    
    int main(void)
    {
        int i;            // Type de l'exception à générer.
        cout << "Tapez 0 pour générer une exception Erreur, "
            "1 pour une Entière :";
        cin >> i;         // On va générer une des trois exceptions
                          // possibles.
        cout << endl;
        try               // Bloc où les exceptions sont prises en charge.
        {
            switch (i)    // Selon le type d'exception désirée,
            {
            case 0:
                {
                    erreur a(0);
                    throw (a);   // on lance l'objet correspondant
                                 // (ici, de classe erreur).
                                 // Cela interrompt le code. break est
                                 // donc inutile ici.
                }
            case 1:
                {
                    int a=1;
                    throw (a);   // Exception de type entier.
                }
            default:             // Si l'utilisateur n'a pas tapé 0 ou 1,
                {
                    other c;     // on crée l'objet c (type d'exception
                    throw (c);   // other) et on le lance.
                }
            }
        }                 // fin du bloc try. Les blocs catch suivent :
        catch (erreur &tmp) // Traitement de l'exception erreur ...
        {                 // (avec récupération de la cause).
            cout << "Erreur erreur ! (cause " << tmp.cause << ")" << endl;
        }
        catch (int tmp)   // Traitement de l'exception int...
        {
            cout << "Erreur int ! (cause " << tmp << ")" << endl;
        }
        catch (...)       // Traitement de toutes les autres
        {                 // exceptions (...).
                          // On ne peut pas récupérer l'objet ici.
            cout << "Exception inattendue !" << endl;
        }
        return 0;
    }

    Selon ce qu'entre l'utilisateur, une exception du type erreur, int ou other est générée.

     


    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    8.16. Pointeurs sur les membres d'une classe

    Nous avons déjà vu les pointeurs sur les objets. Il nous reste à voir les pointeurs sur les membres des classes.

    Les classes regroupent les caractéristiques des données et des fonctions des objets. Les membres des classes ne peuvent donc pas être manipulés sans passer par la classe à laquelle ils appartiennent. Par conséquent, il faut, lorsqu'on veut faire un pointeur sur un membre, indiquer le nom de sa classe. Pour cela, la syntaxe suivante est utilisée :

    définition classe::* pointeur

    Par exemple, si une classe test contient des entiers, le type de pointeurs à utiliser pour stocker leur adresse est :

    int test::*

    Si on veut déclarer un pointeur p de ce type, on écrira donc :

    int test::*p1;   // Construit le pointeur sur entier
                     // de la classe test.

    Une fois le pointeur déclaré, on pourra l'initialiser en prenant l'adresse du membre de la classe du type correspondant. Pour cela, il faudra encore spécifier le nom de la classe avec l'opérateur de résolution de portée :

    p1 = &test::i;   // Récupère l'adresse de i.

    La même syntaxe est utilisable pour les fonctions. L'emploi d'un typedef est dans ce cas fortement recommandé. Par exemple, si la classe test dispose d'une fonction membre appelée lit, qui n'attend aucun paramètre et qui renvoie un entier, on pourra récupérer son adresse ainsi :

    typedef int (test::* pf)(void);  // Définit le type de pointeur.
    pf p2=&test::lit;                // Construit le pointeur et
                                     // lit l'adresse de la fonction.

    Cependant, ces pointeurs ne sont pas utilisables directement. En effet, les données d'une classe sont instanciées pour chaque objet, et les fonctions membres reçoivent systématiquement le pointeur this sur l'objet de manière implicite. On ne peut donc pas faire un déréférencement direct de ces pointeurs. Il faut spécifier l'objet pour lequel le pointeur va être utilisé. Cela se fait avec la syntaxe suivante :

    objet.*pointeur

    Pour les pointeurs d'objet, on pourra utiliser l'opérateur ->* à la place de l'opérateur .* (appelé pointeur sur opérateur de sélection de membre).

    Ainsi, si a est un objet de classe test, on pourra accéder à la donnée i de cet objet à travers le pointeur p1 avec la syntaxe suivante :

    a.*p1 = 3;  // Initialise la donnée membre i de a avec la valeur 3.

    Pour les fonctions membres, on mettra des parenthèses à cause des priorités des opérateurs :

    int i = (a.*p2)();   // Appelle la fonction lit() pour l'objet a.

    Pour les données et les fonctions membres statiques, cependant, la syntaxe est différente. En effet, les données n'appartiennent plus aux objets de la classe, mais à la classe elle-même, et il n'est plus nécessaire de connaître l'objet auquel le pointeur s'applique pour les utiliser. De même, les fonctions membres statiques ne reçoivent pas le pointeur sur l'objet, et on peut donc les appeler sans référencer ce dernier.

    La syntaxe s'en trouve donc modifiée. Les pointeurs sur les membres statiques des classes sont compatibles avec les pointeurs sur les objets et les fonctions non-membres. Par conséquent, si une classe contient une donnée statique entière, on pourra récupérer son adresse directement et la mettre dans un pointeur d'entier :

    int *p3 = &test::entier_statique;   // Récupère l'adresse
                                        // de la donnée membre
                                        // statique.

    La même syntaxe s'appliquera pour les fonctions :

    typedef int (*pg)(void);
    pg p4 = &test::fonction_statique;   // Récupère l'adresse
                                        // d'une fonction membre
                                        // statique.

    Enfin, l'utilisation des ces pointeurs est identique à celle des pointeurs classiques, puisqu'il n'est pas nécessaire de fournir le pointeur this. Il est donc impossible de spécifier le pointeur sur l'objet sur lequel la fonction doit travailler aux fonctions membres statiques. Cela est naturel, puisque les fonctions membres statiques ne peuvent pas accéder aux données non statiques d'une classe.

    Exemple 8-27. Pointeurs sur membres statiques

    #include <iostream>
    
    using namespace std;
    
    class test
    {
        int i;
        static int j;
    
    public:
        test(int j)
        {
            i=j;
            return ;
        }
    
        static int get(void)
        {
            /* return i ;  INTERDIT : i est non statique
                           et get l'est ! */
            return j;	// Autorisé.
        }
    };
    
    int test::j=5;             // Initialise la variable statique.
    
    typedef int (*pf)(void);   // Pointeur de fonction renvoyant
                               // un entier.
    pf p=&test::get;           // Initialisation licite, car get
                               // est statique.
    
    int main(void)
    {
        cout << (*p)() << endl;// Affiche 5. On ne spécifie pas l'objet.
        return 0;
    }

    votre commentaire
  • <script type="text/javascript" src="http://ads.allotraffic.com/bandeau?id=26856"></script> <script type="text/javascript" src="http://nodes.reactivpub.fr/scripts/slidein.php?idsite=|8764|&theme="></script>

    8.15. Méthodes virtuelles pures - Classes abstraites

    Une méthode virtuelle pure est une méthode qui est déclarée mais non définie dans une classe. Elle est définie dans une des classes dérivées de cette classe.

    Une classe abstraite est une classe comportant au moins une méthode virtuelle pure.

    Étant donné que les classes abstraites ont des méthodes non définies, il est impossible d'instancier des objets pour ces classes. En revanche, on pourra les référencer avec des pointeurs.

    Le mécanisme des méthodes virtuelles pures et des classes abstraites permet de créer des classes de base contenant toutes les caractéristiques d'un ensemble de classes dérivées, pour pouvoir les manipuler avec un unique type de pointeur. En effet, les pointeurs des classes dérivées sont compatibles avec les pointeurs des classes de base, on pourra donc référencer les classes dérivées avec des pointeurs sur les classes de base, donc avec un unique type sous-jacent : celui de la classe de base. Cependant, les méthodes des classes dérivées doivent exister dans la classe de base pour pouvoir être accessibles à travers le pointeur sur la classe de base. C'est ici que les méthodes virtuelles pures apparaissent. Elles forment un moule pour les méthodes des classes dérivées, qui les définissent. Bien entendu, il faut que ces méthodes soient déclarées virtuelles, puisque l'accès se fait avec un pointeur de classe de base et qu'il faut que ce soit la méthode de la classe réelle de l'objet (c'est-à-dire la classe dérivée) qui soit appelée.

    Pour déclarer une méthode virtuelle pure dans une classe, il suffit de faire suivre sa déclaration de « =0 ». La fonction doit également être déclarée virtuelle :

    virtual type nom(paramètres) =0;
    =0 signifie ici simplement qu'il n'y a pas d'implémentation de cette méthode dans cette classe.

    Note : =0 doit être placé complètement en fin de déclaration, c'est-à-dire après le mot clé const pour les méthodes const et après la déclaration de la liste des exceptions autorisées (voir le Chapitre 9 pour plus de détails à ce sujet).

    Un exemple vaut mieux qu'un long discours. Soit donc, par exemple, à construire une structure de données pouvant contenir d'autres structures de données, quels que soient leurs types. Cette structure de données est appelée un conteneur, parce qu'elle contient d'autres structures de données. Il est possible de définir différents types de conteneurs. Dans cet exemple, on ne s'intéressera qu'au conteneur de type sac.

    Un sac est un conteneur pouvant contenir zéro ou plusieurs objets, chaque objet n'étant pas forcément unique. Un objet peut donc être placé plusieurs fois dans le sac. Un sac dispose de deux fonctions permettant d'y mettre et d'en retirer un objet. Il a aussi une fonction permettant de dire si un objet se trouve dans le sac.

    Nous allons déclarer une classe abstraite qui servira de classe de base pour tous les objets utilisables. Le sac ne manipulera que des pointeurs sur la classe abstraite, ce qui permettra son utilisation pour toute classe dérivant de cette classe. Afin de différencier deux objets égaux, un numéro unique devra être attribué à chaque objet manipulé. Le choix de ce numéro est à la charge des objets, la classe abstraite dont ils dérivent devra donc avoir une méthode renvoyant ce numéro. Les objets devront tous pouvoir être affichés dans un format qui leur est propre. La fonction à utiliser pour cela sera print. Cette fonction sera une méthode virtuelle pure de la classe abstraite, puisqu'elle devra être définie pour chaque objet.

    Passons maintenant au programme...

    Exemple 8-26. Conteneur d'objets polymorphiques

    #include <iostream>
    
    using namespace std;
    
    /*************  LA CLASSE ABSTRAITE DE BASE   *****************/
    
    class Object
    {
        unsigned long int new_handle(void);
    
    protected:
        unsigned long int h;         // Identifiant de l'objet.
    
    public:
        Object(void);                // Le constructeur.
        virtual ~Object(void);       // Le destructeur virtuel.
        virtual void print(void) =0; // Fonction virtuelle pure.
        unsigned long int handle(void) const;  // Fonction renvoyant
                                     // le numéro d'identification
                                     // de l'objet.
    };
    
    // Cette fonction n'est appelable que par la classe Object :
    
    unsigned long int Object::new_handle(void)
    {
        static unsigned long int hc = 0;
        return hc = hc + 1;          // hc est l'identifiant courant.
                                     // Il est incrémenté
    }                                // à chaque appel de new_handle.
    
    // Le constructeur de Object doit être appelé par les classes dérivées :
    
    Object::Object(void)
    {
        h = new_handle();            // Trouve un nouvel identifiant.
        return;
    }
    
    Object::~Object(void)
    {
        return ;
    }
    
    unsigned long int Object::handle(void) const
    {
        return h;                    // Renvoie le numéro de l'objet.
    }
    
    /******************** LA CLASSE SAC   ******************/
    
    class Bag : public Object       // La classe sac. Elle hérite
                                    // de Object, car un sac peut
                                    // en contenir un autre. Le sac
                                    // est implémenté sous la forme
                                    // d'une liste chaînée.
    {
        struct BagList
        {
            BagList *next;
            Object  *ptr;
        };
    
        BagList *head;               // La tête de liste.
    
    public:
        Bag(void);        // Le constructeur : appel celui de Object.
        ~Bag(void);       // Le destructeur.
        void print(void); // Fonction d'affichage du sac.
        bool has(unsigned long int) const;
                          // true si le sac contient l'objet.
        bool is_empty(void) const;   // true si le sac est vide.
        void add(Object &);          // Ajoute un objet.
        void remove(Object &);       // Retire un objet.
    };
    
    Bag::Bag(void) : Object()
    {
        return;  // Ne fait rien d'autre qu'appeler Object::Object().
    }
    
    Bag::~Bag(void)
    {
        BagList *tmp = head;   // Détruit la liste d'objet.
        while (tmp != NULL)
        {
            tmp = tmp->next;
            delete head;
            head = tmp;
        }
        return;
    }
    
    void Bag::print(void)
    {
        BagList *tmp = head;
        cout << "Sac n° " << handle() << "." << endl;
        cout << "    Contenu :" << endl;
    
        while (tmp != NULL)
        {
            cout << "t";        // Indente la sortie des objets.
            tmp->ptr->print();   // Affiche la liste objets.
            tmp = tmp->next;
        }
        return;
    }
    
    bool Bag::has(unsigned long int h) const
    {
        BagList *tmp = head;
        while (tmp != NULL && tmp->ptr->handle() != h)
            tmp = tmp->next;     // Cherche l'objet.
        return (tmp != NULL);
    }
    
    bool Bag::is_empty(void) const
    {
        return (head==NULL);
    }
    
    void Bag::add(Object &o)
    {
        BagList *tmp = new BagList;   // Ajoute un objet à la liste.
        tmp->ptr = &o;
        tmp->next = head;
        head = tmp;
        return;
    }
    
    void Bag::remove(Object &o)
    {
        BagList *tmp1 = head, *tmp2 = NULL;
        while (tmp1 != NULL && tmp1->ptr->handle() != o.handle())
        {
            tmp2 = tmp1;        // Cherche l'objet...
            tmp1 = tmp1->next;
        }
        if (tmp1!=NULL)         // et le supprime de la liste.
        {
            if (tmp2!=NULL) tmp2->next = tmp1->next;
            else head = tmp1->next;
            delete tmp1;
        }
        return;
    }

    Avec la classe Bag définie telle quelle, il est à présent possible de stocker des objets dérivant de la classe Object avec les fonctions add et remove :

    class MonObjet : public Object
    {
        /*  Définir la méthode print() pour l'objet...  */
    };
    
    Bag MonSac;
    
    int main(void)
    {
        MonObjet a, b, c;    // Effectue quelques opérations
                             // avec le sac :
        MonSac.add(a);
        MonSac.add(b);
        MonSac.add(c);
        MonSac.print();
        MonSac.remove(b);
        MonSac.add(MonSac);  // Un sac peut contenir un sac !
        MonSac.print();      // Attention ! Cet appel est récursif !
                             // (plantage assuré).
        return 0;
    }

    Nous avons vu que la classe de base servait de moule aux classes dérivées. Le droit d'empêcher une fonction membre virtuelle pure définie dans une classe dérivée d'accéder en écriture non seulement aux données de la classe de base, mais aussi aux données de la classe dérivée, peut donc faire partie de ses prérogatives. Cela est faisable en déclarant le pointeur this comme étant un pointeur constant sur objet constant. Nous avons vu que cela pouvait se faire en rajoutant le mot clé const après la déclaration de la fonction membre. Par exemple, comme l'identifiant de l'objet de base est placé en protected au lieu d'être en private, la classe Object autorise ses classes dérivées à le modifier. Cependant, elle peut empêcher la fonction print de le modifier en la déclarant const :

    class Object
    {
        unsigned long int new_handle(void);
    
    protected:
        unsigned long int h;
    
    public:
        Object(void);                      // Le constructeur.
        virtual void print(void) const=0;  // Fonction virtuelle pure.
        unsigned long int handle(void) const; // Fonction renvoyant
                                           // le numéro d'identification
                                           // de l'objet.
    };

    Dans l'exemple donné ci-dessus, la fonction print peut accéder en lecture à h, mais plus en écriture. En revanche, les autres fonctions membres des classes dérivées peuvent y avoir accès, puisque c'est une donnée membre protected. Cette méthode d'encapsulation est donc coopérative (elle requiert la bonne volonté des autres fonctions membres des classes dérivées), tout comme la méthode qui consistait en C à déclarer une variable constante. Cependant, elle permettra de détecter des anomalies à la compilation, car si une fonction print cherche à modifier l'objet sur lequel elle travaille, il y a manifestement une erreur de conception.

    Bien entendu, cela fonctionne également avec les fonctions membres virtuelles non pures, et même avec les fonctions non virtuelles.


    votre commentaire
  • Chapitre 11. Les espaces de nommage


    Les espaces de nommage sont des zones de déclaration qui permettent de délimiter la recherche des noms des identificateurs par le compilateur. Leur but est essentiellement de regrouper les identificateurs logiquement et d'éviter les conflits de noms entre plusieurs parties d'un même projet. Par exemple, si deux programmeurs définissent différemment une même structure dans deux fichiers différents, un conflit entre ces deux structures aura lieu au mieux à l'édition de liens, et au pire lors de l'utilisation commune des sources de ces deux programmeurs. Ce type de conflit provient du fait que le C++ ne fournit qu'un seul espace de nommage de portée globale, dans lequel il ne doit y avoir aucun conflit de nom. Grâce aux espaces de nommage non globaux, ce type de problème peut être plus facilement évité, parce que l'on peut éviter de définir les objets globaux dans la portée globale.

    11.1. Définition des espaces de nommage

    11.1.1. Espaces de nommage nommées

    Lorsque le programmeur donne un nom à un espace de nommage, celui-ci est appelé un espace de nommage nommé. La syntaxe de ce type d'espace de nommage est la suivante :

    namespace nom
    {
    déclarations | définitions
    }
    nom est le nom de l'espace de nommage, et déclarations et définitions sont les déclarations et les définitions des identificateurs qui lui appartiennent.

    Contrairement aux régions déclaratives classiques du langage (comme par exemple les classes), un namespace peut être découpé en plusieurs morceaux. Le premier morceaux sert de déclaration, et les suivants d'extensions. La syntaxe pour une extension d'espace de nommage est exactement la même que celle de la partie de déclaration.

    Exemple 11-1. Extension de namespace

    namespace A   // Déclaration de l'espace de nommage A.
    {
    int i;
    }

    namespace B // Déclaration de l'espace de nommage B.
    {
    int i;
    }

    namespace A // Extension de l'espace de nommage A.
    {
    int j;
    }

    Les identificateurs déclarés ou définis à l'intérieur d'un même espace de nommage ne doivent pas entrer en conflit. Ils peuvent avoir les mêmes noms, mais seulement dans le cadre de la surcharge. Un espace de nommage se comporte donc exactement comme les zones de déclaration des classes et de la portée globale.

    L'accès aux identificateurs des espaces de nommage se fait par défaut grâce à l'opérateur de résolution de portée (::), et en qualifiant le nom de l'identificateur à utiliser du nom de son espace de nommage. Cependant, cette qualification est inutile à l'intérieur de l'espace de nommage lui-même, exactement comme pour les membres des classes à l'intérieur de leur classe.

    Exemple 11-2. Accès aux membres d'un namespace

    int i=1;    // i est global.

    namespace A
    {
    int i=2; // i de l'espace de nommage A.
    int j=i; // Utilise A::i.
    }

    int main(void)
    {
    i=1; // Utilise ::i.
    A::i=3; // Utilise A::i.
    return 0;
    }

    Les fonctions membres d'un espace de nommage peuvent être définies à l'intérieur de cet espace, exactement comme les fonctions membres de classes. Elles peuvent également être définies en dehors de cet espace, si l'on utilise l'opérateur de résolution de portée. Les fonctions ainsi définies doivent apparaître après leur déclaration dans l'espace de nommage.

    Exemple 11-3. Définition externe d'une fonction de namespace

    namespace A
    {
    int f(void); // Déclaration de A::f.
    }

    int A::f(void) // Définition de A::f.
    {
    return 0;
    }

    Il est possible de définir un espace de nommage à l'intérieur d'un autre espace de nommage. Cependant, cette déclaration doit obligatoirement avoir lieu au niveau déclaratif le plus externe de l'espace de nommage qui contient le sous-espace de nommage. On ne peut donc pas déclarer d'espaces de nommage à l'intérieur d'une fonction ou à l'intérieur d'une classe.

    Exemple 11-4. Définition de namespace dans un namespace

    namespace Conteneur
    {
    int i; // Conteneur::i.
    namespace Contenu
    {
    int j; // Conteneur::Contenu::j.
    }
    }

    11.1.2. Espaces de nommage anonymes

    Lorsque, lors de la déclaration d'un espace de nommage, aucun nom n'est donné, un espace de nommage anonyme est créé. Ce type d'espace de nommage permet d'assurer l'unicité du nom de l'espace de nommage ainsi déclaré. Les espaces de nommage anonymes peuvent donc remplacer efficacement le mot clé static pour rendre unique des identificateurs dans un fichier. Cependant, elles sont plus puissantes, parce que l'on peut également déclarer des espaces de nommage anonymes à l'intérieur d'autres espaces de nommage.

    Exemple 11-5. Définition de namespace anonyme

    namespace
    {
    int i; // Équivalent à unique::i;
    }

    Dans l'exemple précédent, la déclaration de i se fait dans un espace de nommage dont le nom est choisi par le compilateur de manière unique. Cependant, comme on ne connaît pas ce nom, le compilateur utilise une directive using (voir plus loin) afin de pouvoir utiliser les identificateurs de cet espace de nommage anonyme sans préciser leur nom complet avec l'opérateur de résolution de portée.

    Si, dans un espace de nommage, un identificateur est déclaré avec le même nom qu'un autre identificateur déclaré dans un espace de nommage plus global, l'identificateur global est masqué. De plus, l'identificateur ainsi défini ne peut être accédé en dehors de son espace de nommage que par un nom complètement qualifié à l'aide de l'opérateur de résolution de portée. Toutefois, si l'espace de nommage dans lequel il est défini est un espace de nommage anonyme, cet identificateur ne pourra pas être référencé, puisqu'on ne peut pas préciser le nom des espaces de nommage anonymes.

    Exemple 11-6. Ambiguïtés entre namespaces

    namespace
    {
    int i; // Déclare unique::i.
    }

    void f(void)
    {
    ++i; // Utilise unique::i.
    }

    namespace A
    {
    namespace
    {
    int i; // Définit A::unique::i.
    int j; // Définit A::unique::j.
    }

    void g(void)
    {
    ++i; // Erreur : ambiguïté entre unique::i
    // et A::unique::i.
    ++A::i; // Erreur : A::i n'est pas défini
    // (seul A::unique::i l'est).
    ++j; // Correct : ++A::unique::j.
    }
    }

    11.1.3. Alias d'espaces de nommage

    Lorsqu'un espace de nommage porte un nom très compliqué, il peut être avantageux de définir un alias pour ce nom. L'alias aura alors un nom plus simple.

    Cette opération peut être réalisée à l'aide de la syntaxe suivante :

    namespace nom_alias = nom;
    nom_alias est ici le nom de l'alias de l'espace de nommage, et nom est le nom de l'espace de nommage lui-même.

    Les noms donnés aux alias d'espaces de nommage ne doivent pas entrer en conflit avec les noms des autres identificateurs du même espace de nommage, que celui-ci soit l'espace de nommage de portée globale ou non.



    votre commentaire