• <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.4. Encapsulation des données

    Les divers champs d'une structure sont accessibles en n'importe quel endroit du programme. Une opération telle que celle-ci est donc faisable :

    clientele[0].Solde = 25000;

    Le solde d'un client peut donc être modifié sans passer par une méthode dont ce serait le but. Elle pourrait par exemple vérifier que l'on n'affecte pas un solde supérieur au solde maximal autorisé par le programme (la borne supérieure des valeurs des entiers signés). Par exemple, si les entiers sont codés sur 16 bits, cette borne maximum est 32767. Un programme qui ferait :

    clientele[0].Solde = 32800;
    obtiendrait donc un solde de -12 (valeur en nombre signé du nombre non signé 32800), alors qu'il espérerait obtenir un solde positif !

    Il est possible d'empêcher l'accès des champs ou de certaines méthodes à toute fonction autre que celles de la classe. Cette opération s'appelle l'encapsulation. Pour la réaliser, il faut utiliser les mots clés suivants :

    • public : les accès sont libres ;

    • private : les accès sont autorisés dans les fonctions de la classe seulement ;

    • protected : les accès sont autorisés dans les fonctions de la classe et de ses descendantes (voir la section suivante) seulement. Le mot clé protected n'est utilisé que dans le cadre de l'héritage des classes. La section suivante détaillera ce point.

    Pour changer les droits d'accès des champs et des méthodes d'une classe, il faut faire précéder ceux-ci du mot clé indiquant les droits d'accès suivi de deux points (':'). Par exemple, pour protéger les données relatives au client, on changera simplement la déclaration de la classe en :

    struct client
    {
    private:   // Données privées :
    
        char Nom[21], Prenom[21];
        unsigned int Date_Entree;
        int Solde;
        // Il n'y a pas de méthode privée.
    
    public:    // Les données et les méthodes publiques :
    
        // Il n'y a pas de donnée publique.
        bool dans_le_rouge(void);
        bool bon_client(void)
    };

    Outre la vérification de la validité des opérations, l'encapsulation a comme intérêt fondamental de définir une interface stable pour la classe au niveau des méthodes et données membres publiques et protégées. L'implémentation de cette interface, réalisée en privé, peut être modifiée à loisir sans pour autant perturber les utilisateurs de cette classe, tant que cette interface n'est pas elle-même modifiée.

    Par défaut, les classes construites avec struct ont tous leurs membres publics. Il est possible de déclarer une classe dont tous les éléments sont par défaut privés. Pour cela, il suffit d'utiliser le mot clé class à la place du mot clé struct.

    Exemple 8-4. Utilisation du mot clé class

    class client
    {
        // private est à présent inutile.
    
        char Nom[21], Prenom[21];
        unsigned int Date_Entree;
        int Solde;
    
    public:    // Les données et les méthodes publiques.
    
        bool dans_le_rouge(void);
        bool bon_client(void);
    };

    Enfin, il existe un dernier type de classe, que je me contenterai de mentionner : les classes union. Elles se déclarent comme les classes struct et class, mais avec le mot clé union. Les données sont, comme pour les unions du C, situées toutes au même emplacement, ce qui fait qu'écrire dans l'une d'entre elle provoque la destruction des autres. Les unions sont très souvent utilisées en programmation système, lorsqu'un polymorphisme physique des données est nécessaire (c'est-à-dire lorsqu'elles doivent être interprétées de différentes façons selon le contexte).

    Note : Les classes de type union ne peuvent pas avoir de méthodes virtuelles et de membres statiques. Elles ne peuvent pas avoir de classes de base, ni servir de classe de base. Enfin, les unions ne peuvent pas contenir des références, ni des objets dont la classe a un constructeur non trivial, un constructeur de copie non trivial ou un destructeur non trivial. Pour toutes ces notions, voir la suite du chapitre.

    Les classes définies au sein d'une autre classe sont considérées comme faisant partie de leur classe hôte, et ont donc le droit d'accéder aux données membres private et protected de celle-ci. Remarquez que cette règle est assez récente dans la norme du langage, et que la plupart des compilateurs refuseront ces accès. Il faudra donc déclarer amies de la classe hôte les classes qui sont définies au sein de celle-ci. La manière de procéder sera décrite dans la Section 8.7.2.


    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.3. Déclaration de classes en C++

    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;
        [...]]]
    };

    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

    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 ;
    }

    Note : 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.

    Note : 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

    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

    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);
    }

    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.2. Extension de la notion de type du C

    Il faut avant tout savoir que la couche objet n'est pas un simple ajout au langage C, c'est une véritable extension. En effet, les notions qu'elle a apportées ont été intégrées au C à tel point que le typage des données de C a fusionné avec la notion de classe. Ainsi, les types prédéfinis char, int, double, etc. représentent à présent l'ensemble des propriétés des variables ayant ce type. Ces propriétés constituent la classe de ces variables, et elles sont accessibles par les opérateurs. Par exemple, l'addition est une opération pouvant porter sur des entiers (entre autres) qui renvoie un objet de la classe entier. Par conséquent, les types de base se manipuleront exactement comme des objets. Du point de vue du C++, les utiliser revient déjà à faire de la programmation orientée objet.

    De même, le programmeur peut, à l'aide de la notion de classe d'objets, définir de nouveaux types. Ces types comprennent la structure des données représentées par ces types et les opérations qui peuvent leur être appliquées. En fait, le C++ assimile complètement les classes avec les types, et la définition d'un nouveau type se fait donc en définissant la classe des variables de ce type.


    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 8. C++ : la couche objet

     

    La couche objet constitue sans doute la plus grande innovation du C++ par rapport au C. Le but de la programmation objet est de permettre une abstraction entre l'implémentation des modules et leur utilisation, apportant ainsi un plus grand confort dans la programmation. Elle s'intègre donc parfaitement dans le cadre de la modularité. Enfin, l'encapsulation des données permet une meilleure protection et donc une plus grande fiabilité des programmes.

    8.1. Généralités

    Théoriquement, il y a une nette distinction entre les données et les opérations qui leur sont appliquées. En tout cas, les données et le code ne se mélangent pas dans la mémoire de l'ordinateur, sauf cas très particuliers (autoprogrammation, alias pour le chargement des programmes ou des overlays, débogueurs, virus).

    Cependant, l'analyse des problèmes à traiter se présente d'une manière plus naturelle si l'on considère les données avec leurs propriétés. Les données constituent les variables, et les propriétés les opérations qu'on peut leur appliquer. De ce point de vue, les données et le code sont logiquement inséparables, même s'ils sont placés en différents endroits de la mémoire de l'ordinateur.

    Ces considérations conduisent à la notion d'objet. Un objet est un ensemble de données sur lesquelles des procédures peuvent être appliquées. Ces procédures ou fonctions applicables aux données sont appelées méthodes. La programmation d'un objet se fait donc en indiquant les données de l'objet et en définissant les procédures qui peuvent lui être appliquées.

    Il se peut qu'il y ait plusieurs objets identiques, dont les données ont bien entendu des valeurs différentes, mais qui utilisent le même jeu de méthodes. On dit que ces différents objets appartiennent à la même classe d'objets. Une classe constitue donc une sorte de type, et les objets de cette classe en sont des instances. La classe définit donc la structure des données, alors appelées champs ou variables d'instances, que les objets correspondants auront, ainsi que les méthodes de l'objet. À chaque instanciation, une allocation de mémoire est faite pour les données du nouvel objet créé. L'initialisation de l'objet nouvellement créé est faite par une méthode spéciale, le constructeur. Lorsque l'objet est détruit, une autre méthode est appelée : le destructeur. L'utilisateur peut définir ses propres constructeurs et destructeurs d'objets si nécessaire.

    Comme seules les valeurs des données des différents objets d'une classe diffèrent, les méthodes sont mises en commun pour tous les objets d'une même classe (c'est-à-dire que les méthodes ne sont pas recopiées). Pour que les méthodes appelées pour un objet sachent sur quelles données elles doivent travailler, un pointeur sur l'objet contenant ces données leur est passé en paramètre. Ce mécanisme est complètement transparent pour le programmeur en C++.

    Nous voyons donc que non seulement la programmation orientée objet est plus logique, mais elle est également plus efficace (les méthodes sont mises en commun, les données sont séparées).

    Enfin, les données des objets peuvent être protégées : c'est-à-dire que seules les méthodes de l'objet peuvent y accéder. Ce n'est pas une obligation, mais cela accroît la fiabilité des programmes. Si une erreur se produit, seules les méthodes de l'objet doivent être vérifiées. De plus, les méthodes constituent ainsi une interface entre les données de l'objet et l'utilisateur de l'objet (un autre programmeur). Cet utilisateur n'a donc pas à savoir comment les données sont gérées dans l'objet, il ne doit utiliser que les méthodes. Les avantages sont immédiats : il ne risque pas de faire des erreurs de programmation en modifiant les données lui-même, l'objet est réutilisable dans un autre programme parce qu'il a une interface standardisée, et on peut modifier l'implémentation interne de l'objet sans avoir à refaire tout le programme, pourvu que les méthodes gardent le même nom, les mêmes paramètres et la même sémantique. Cette notion de protection des données et de masquage de l'implémentation interne aux utilisateurs de l'objet constitue ce que l'on appelle l'encapsulation. Les avantages de l'encapsulation seront souvent mis en valeur dans la suite au travers d'exemples.

    Nous allons entrer maintenant dans le vif du sujet. Cela permettra de comprendre ces généralités.

     


    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 7. Comment faire du code illisible ?

    Il est facile, très facile, de faire des programmes illisibles en C ou en C++. Il existe même un concours du code le plus obscur ! Cela dit, deux choses peuvent être dites à ce propos :

    1. Ça n'accroît pas la vitesse du programme. Si l'on veut aller plus vite, il faut revoir l'algorithme ou changer de compilateur (inutile de faire de l'assembleur : les bons compilateurs se débrouillent mieux que les être humains sur ce terrain. L'avantage de l'assembleur est que là, au moins, on est sûr d'avoir un programme illisible.).

    2. Ça augmente les chances d'avoir des bogues.

    Si vous voulez malgré tout vous amuser, voici quelques conseils utiles :

    • écrivez des macros complexes qui font des effets de bords insoupçonnés et qui modifient des variables globales ;

    • abusez de l'opérateur ternaire ?: et surtout de l'opérateur virgule ,. Utilisez les opérateurs d'incrémentation et de décrémentation à outrance, en version préfixée et suffixée, tout spécialement dans des expressions utilisant des pointeurs ;

    • placez ces opérateurs dans les structures de contrôles. Notamment, utilisez l'opérateur virgule pour faire des instructions composées dans les tests du while et dans tous les membres du for. Il est souvent possible de mettre le corps du for dans les parenthèses ;

    • si nécessaire, utiliser les expressions composées ({ et }) dans les structures de contrôle ;

    • choisissez des noms de variable et de fonction aléatoires (pensez à une phrase, et prenez les premières ou les deuxièmes lettres des mots au hasard) ;

    • regroupez toutes les fonctions dans un même fichier, par ordre de non-appariement ;

    • inversement, dispersez les définitions des variables globales dans tout le programme, si possible dans des fichiers où elles ne sont pas utilisées ;

    • faites des fonctions à rallonge ;

    • ne soignez pas l'apparence de votre programme (pas d'indentation ou, au contraire, trop d'indentations), regroupez plusieurs instructions sur une même ligne ;

    • rajoutez des parenthèses là où elles ne sont pas nécessaires ;

    • rajoutez des transtypages là où ils ne sont pas nécessaires ;

    • ne commentez rien, ou mieux, donnez des commentaires sans rapport avec le code.

    Exemple 7-1. Programme parfaitement illisible

    /* Que fait ce programme ? */
    #include <stdio.h>
    int main(void)
       {
    int zkmlpf,geikgh,wdxaj;
        scanf("%u",&zkmlpf);for (wdxaj=0,
       geikgh=0;
          ((wdxaj+=++geikgh),geikgh)<zkmlpf;);
       printf("%u",wdxaj); return 0;
    }

    Vous l'aurez compris : il est plus simple de dire ici ce qu'il ne faut pas faire que de dire comment il faut faire. Je ne prétends pas imposer à quiconque une méthodologie quelconque, car chacun est libre de programmer comme il l'entend. En effet, certaines conventions de codages sont aussi absurdes qu'inutiles et elles ont l'inconvénient de ne plaire qu'à celui qui les a écrites (et encore...). C'est pour cette raison que je me suis contenté de lister les sources potentielles d'illisibilité des programmes. Sachez donc simplement que si vous utilisez une des techniques données dans ce paragraphe, vous devriez vous assurer que c'est réellement justifié et revoir votre code. Pour obtenir des programmes lisibles, il faut simplement que chacun y mettre un peu du sien, c'est aussi une marque de politesse envers les autres programmeurs.


    votre commentaire