C++ Object Oriented Programming
(Version 2)

1 . Introduction

    Cette fois on s'attaque a la vraie programmation orientée objet! Cette fois, tout ce qui va etre dit ne pourra
pas s'appliquer au C. Pour la bonne et simple raison que le C n'est pas orienté objet, justement. Ce tutorial a été rédigé en replique aux nombreuses critiques - constructives! - de l'ancienne version, ou j'ai commis l'erreur d'introduire le concept de liste doublement chainée en meme temps que l'OOP. Cette fois j'ai mis un exemple plus simple.

2 . Pourquoi l'OOP ? Exemples

    La ou le programmeur en C, lors de la conception de son programme, se préoccupe d'abord de la maniere dont il va découper son code en procédures, le programmeur C++ (orienté objet, je rappelle qu'il est possible de "programmer en C" en C++) va d'abord se préoccuper de découper la représentation de son probleme en
objets.
    Mais qu'est-ce qu'un objet, me direz vous? La question est - comme c'est souvent le cas en programmation - plus facile que la réponse. Un objet, en OOP, est la maniere en  de représenter une chose, une entité voire meme quelque chose d'abstrait. On peut comparer un objet a une instance de structure C/C++. Si nous avions une classe PLAYER et qu'on déclarait une instance du type PLAYER, qu'on nommerait p, eh bien on
pourrait dire que p est un objet de type PLAYER. Je dis bien "pourrait" car l'alternative au structures, qu'on nommera classe, présente de nombreuses particularités qui font d'elle un objet, titre que la structure ne mérite pas. Mais nous verrons cela plus tard.
    Cela dit, la premiere différence ne sera pas passée sous silence plus longtemps. A l'instar de la structure, la classe possede des membres; Mais ces membres ne sont plus limités aux variables. Ils peuvent etre des fonctions, des constantes, voire meme des nouveaux types.
    L'intéret de regrouper tout ca dans une classe est que notre objet, une fois implémenté, se comportera comme une
boite noire. Nos seules interactions avec cet objet se limiteront des-lors a des invocations de fonctions membres, rendues accessibles a l'extérieur, formant un groupe. Ce groupe de fonctions est nommé interface (terme qui ne s'applique pas qu'a l'OOP).
    Ainsi, si un developpeur A développait une classe pour modéliser un jeu d'échecs, par exemple, il définirait dans son interface des fonctions permettant de jouer un coup, de recommencer une partie, et une fonction pour vérifier si la partie est terminée, et si c'est le cas, qui a gagné. Bref, des fonctions permettant d'effectuer sur l'objet des opérations prédéfinies. Quant aux données qui serviraient a représenter l'état du jeu ( dans notre cas, ca se limiterait a un tableau et un compteur de tours ) seraient masquées. Ainsi lorsque le développeur B voudra utiliser cette classe dans son programme (pour, par exemple, en faire une version DirectX 3D en images de synthese), il ne lui sera pas possible d'effectuer des
opérations interdites. Un autre avantage est que le developpeur A pourra changer l'implémentation de sa classe sans que le developpeur B ait a s'en soucier; Car tant que l'interface restera la meme, le code source écrit par B n'aura pas besoin d'etre modifié.

3 . Approche en C de l'OOP

    Ce titre est une hérésie, mais contrairement a ce que j'ai dit dans l'introduction, ce qui va suivre pourra effectivement etre implémenté en C; du point de vue de l'approche - c'est a dire que cet exemple ne se servira pas des fonctionnalités OOP du C++. Par contre du point de vue de la syntaxe, je ne peux rien vous garantir!
    Nous allons implémenter la représentation d'un jeu d'échecs. Le but de ce tutorial n'est pas de vous apprendre les algorithmes d'un jeu d'échec, alors ne m'en voulez pas si je ne mentionne pas tout le code source. Le role de cette implémentation sera le meme que celui qu'un jeu d'echecs électronique en mode deux joueurs (donc pas d'AI, c'est pas le but ici).
    Voici a quoi ressemblerait l'interface :
   
   
void    NewGame()       
        - positionne les pions sur le plateau
   
   
void    Play(short x1, short y1, short x2, short y2)
        - étant données les positions de départ et d'arrivée, joue un coup
   
   
bool    IsFinished()
        - vérifie si la partie est fini (échec et mat, pat, egalité)
   
   
short    GetWinner()
        - renvoie le numero du joueur gagnant (0=blanc, 1=noir)

    En C, on déclarerait ces fonctions dans un header que l'on mettrait a la disposition de l'extérieur. Et dans un fichier d'implémentation, on définirait deux variables globales, un tableau a deux dimensions pour représenter l'échiquier et un entier, le compteur de tours. Toute l'implémentation concernant les regles du jeu serait aussi contenue dans ce fichier.

   
// Chess.h
    void    NewGame();
   
void    Play(short x1, short y1, short x2, short y2);
   
bool    IsFinished();
   
short    GetWinner();
   
   
// Chess.c
    short    Board[8][8]; // l'échiquier
    int    Counter;
// compteur de tours

    void    NewGame()
    {
       
// ...............
    }

   
void    Play(short x1, short y1, short x2, short y2)
    {
       
// ...............
    }

   
bool    IsFinished()
    {
       
// ...............
    }

   
short    GetWinner()
    {
       
// ...............
    }

    Un probleme se pose cependant : comment représenter
plusieurs jeux d'échecs, puisque nous n'avons qu'une variable globale? Il faudrait pouvoir représenter plusieurs objets du meme type. La solution C est la structure. Il faut donc supprimer nos deux variables globales et définir dans Chess.h une structure chess. De plus, il faut modifier toute notre interface pour passer en tant que premier parametre un pointeur vers l'instance concernée de la structure, de maniere a ce que nos fonctions d'interface puissent voir de quelle instance on parle.

   
// Chess.h
    struct    chess
    {
       
short    board[8][8]; // l'échiquier
       
int    counter; // compteur de tours
    };

    void    NewGame();
   
void    Play(short x1, short y1, short x2, short y2);
   
bool    IsFinished();
   
short    GetWinner();
   
   
// Chess.c
    void    NewGame(chess* game)
    {
       
// ...............
    }

   
void    Play(chess* game, short x1, short y1, short x2, short y2)
    {
       
// ...............
    }

   
bool    IsFinished(chess* game)
    {
       
// ...............
    }

   
short    GetWinner(chess* game)
    {
       
// ...............
    }

    Ce n'est pas la meilleure solution. En effet nous avons pollué l'espace de nom global avec des noms de fonctions ainsi qu'une définition de structure dont l'extérieur n'a que faire... Cet exemple est un premier pas vers l'OOP mais il souffre de nombreux inconvénients. Les opérations interdites dont je parlais plus haut ne sont pas interdites, justement. Je m'explique : Supposez qu'un joueur veuillle déplacer son roi dans le champ d'action d'une tour adverse. Pour ceux qui ne savent pas jouer aux échecs, je précise que mettre son roi en danger est contraire au regles. Le travail consistant a vérifier ce genre de coups bas - qui risqueraient de faire crasher un programme AI car il ne correspond pas a une situation prévue - est normalement effectué par Play, qui s'assure que le coup est valable. Mais supposez maintenant que le developpeur B, l'utilisateur de la classe donc, ne se soit pas soucié de l'interface et qu'il ait programmé comme un porc, et qu'il ait fait sa propre version de Play; C'est tout a fait possible, vu que les membres de chess sont en acces complet. Rien ne va l'empecher de faire cette erreur et dans le pire des cas - mais aussi dans la majorité - il ne fera
pas la vérification de validité. Il modifira directement le membre board de chess. C'est pour remédier a ce probleme qu'on a inventé les classes.

4 . Classes

La syntaxe de déclaration d'une classe est la suivante, qui n'est pas sans rappeller celle d'une structure :

   
class    NomDeLaClasse
    {
       
/* membres */
    };

    La syntaxe de déclaration est la meme que celle des variables globales, a la différence qu'il n'est pas permis d'initialiser les variables membres - mais je vous rassure il existe une maniere de le faire. Les membres peuvent etre des fonctions, des variables, des constantes, des définitions de types (typedefs, structs), voire meme d'autres définitions de classes. Exemple de déclaration de classe :

   
class    Vehicle
    {
       
struct vector
        {
           
double    velocity_x;
           
double    velocity_y;
           
double    velocity_z;
        };

        vector position, velocity;
       
       
int    mass;

       
bool    SelectGear(int gear);
       
double    Accelerate();
    };

    De meme que pour les structures, cette syntaxe ne correspond en aucun cas a une allocation de mémoire. Ainsi on ne crée pas un Vehicle, on ne fait que définir ce qu'est un Vehicle. L'allocation de mémoire proprement dite, elle peut se faire de la meme maniere qu'on ferait avec n'importe quel autre type de variable. Ca marche vraiment partout comme pour les structures en fait! Exemple :
   
    Vehicle    MyVehicle;
    Vehicle*    pVehicle;
    pVehicle =
new Vehicle;

   
void    Function(int a, int b, int c)
    {
        Vehicle     TmpVehicle;
        ....
    }

    On voit que le type Vehicle ne se distingue pas d'un type de base. La syntaxe est la meme que si vous utilisiez des types prédéfinis comme les int.

    Bon, ca a l'air assez simple, malheureusement ca ne l'est pas tant que ca. La définition d'au dessus n'est pas totalement opérationnelle, je l'ai volontairement allégée. La vraie définition serait  :

   
class    Vehicle
    {
   
private :
        struct vector
        {
           
double    velocity_x;
           
double    velocity_y;
           
double    velocity_z;
        };

        vector position, velocity;
       
       
int    mass;

   
public :
        Vehicle();
        ~Vehicle();

       
bool    SelectGear(int gear);
       
double    Accelerate();
    };

   
Vehicle() et ~Vehicle() sont ce qu'on appelle les constructeurs et les destructeurs de la classe.
    Les mots clef
private et public sont expliqués dans le chapitre suivant.
   
   
5 . Private / Public

    Reprenons la classe Vehicle du chapitre précédent; Les mots clef
private et public y sont utilisés dans cet exemple pour définir quelles parties de la classe sont accessibles a l'extérieur, et lesquelles ne le sont pas. Ainsi tout ce qui suit une directive public va etre délibérément exposé a l'extérieur, sans pour autant sortir de l'espace de nom. Par contre, ce qui suit une directive private va etre masqué, et uniquement les fonctions membres de CETTE classe (nous verrons qu'en fait ce ne sont pas tout a fait les seules) y ont acces.

    Par exemple, si on fait :

        Vehicle    MyVehicle;
   
        MyVehicle.SelectGear(2);
        MyVehicle.Accelerate();
        MyVehicle.SelectGear(3);

    Aucun probleme, puisque ces membres sont publics. Par contre, si on fait :

        MyVehicle.mass = 100;

    Le compilateur vous renvoie un message d'erreur : Nous n'avons pas le droit de modifier un membre private. Ainsi, la boite noire dont nous parlions auparavant commence a prendre forme, puisqu'on ne peut plus communiquer avec l'objet que par un début d'interface. En fait, il ne s'agit pas tout a fait d'une interface, puisqu'il reste possible de définir des variables public, et donc de modifier un membre sans passer par une fonction d'interface. Cependant, meme si c'est autorisé, je déconseille de faire ca. Ca n'est pas tres propre. Mais alors, comment donner une valeur a la masse du vehicule?
    Eh bien tout simplement en déclarant une fonction membre SetMass qui va modifier la variable elle meme.
       
6 . Fonctions membres
   
    Les fonctions membres sont les méthodes dont nous avons parlé précédemment. Leur emploi est assez intuitif, et vu que nous en avons déja parlé ce chapitre sera relativement court. Cependant nous verrons plus tard que les fonctions membres peuvent donner lieu a pas mal d'astuces assez sympathiques, mais compliquées.
    La déclaration d'une fonction membre se fait dans la définition de la classe, de la meme maniere qu'on ferait pour un prototype de fonction.

   
class    Switch
    {
        ...
        int    GetState();
       
void    On();
       
void    Off();
        ...

       
int    m_state;

        enum
        {
            STATE_ON,
            STATE_OFF
        };

    };

    La définition, elle, est un peu plus compliquée, car il faut préciser au compilateur qu'on va définir une fonction membre de la classe, et non pas une fonction appartenant a l'espace de nom global.
    Il faut donc faire précéder le nom de la fonction par l'espace de nom, qui est dans ce cas la le nom de la classe.

   
Switch::GetState()
    {
       
return m_state;
    }

   
Switch::On()
    {
        m_state = STATE_ON;
    }

   
Switch::Off()
    {
        m_state = STATE_OFF;
    }

    Il convient de noter deux points importants :
        - puisque les fonctions membres sont, justement, membres, il n'y a pas lieu de préciser l'espace de nom lorsque l'on référence d'autres membres. Ainsi il est possible de se servir des variables membres comme si il s'agissait de variables globales - on modifie m_state sans préciser l'espace de nom. Il en va de meme pour les fonctions membres. Mais cela n'empeche pas non plus de se servir, de variables globales ou de fonctions appartenant a l'espace de noms global.
        - la définition de STATE_ON et de STATE_OFF se trouve dans la définition de la classe. Ainsi, ces deux constantes appartenant a l'espace de nom Switch, elles ne sont pas directement visibles de l'extérieur. Et c'est bien mieux ainsi, car on ne pollue pas l'espace de noms global.
        De ce fait l'utilisateur doit préciser de cette maniere dans quel espace de nom se trouve la constante qu'il utilise :

        Switch    a;

        a.On();
       
       
if(a.GetState() != Switch::STATE_ON)
        {
            .....
        }

    Ce qui est quand meme plus propre!

7 . Constructeurs / Destructeurs

    Le constructeur est une fonction membre automatiquement invoquée lorsqu'une instance de la classe est créée. Ainsi il est possible de faire subir a cet objet une initialisation. De meme un destructeur est invoqué lorsque l'objet est détruit. Destructeurs et constructeurs sont des fonctions spéciales qui portent le nom de la classe et qui ne renvoient aucune valeur, de plus on ne peut PAS les invoquer, car elles sont invoquées automatiquement :

    Exemple :

   
class    Database
    {
   
public :
        Database()
        {
            Init();
        };
        ~Database()
        {
            Destroy();
        };
    }

   
Remarque 1: Il est permis comme le montre l'exemple d'implémenter les fonctions directement dans la définition de la classe. Ce n'est cependant pas tres propre et ca n'est fait ici qu'a titre d'exemple.

   
Remarque 2: Le destructeur est précédé d'un ~ ce qui le différencie d'un constructeur.

    Ni les constructeurs ni les destructeurs ne sont obligatoires. Dans le cas ou vous n'en n'auriez pas besoin, ne les déclarez pas dans la classe, car si vous les déclarez quand meme mais que vous ne les définissez pas, le linker n'appréciera pas beaucoup!
    Ils peuvent etre tres pratiques mais attention : Pour des raisons que j'ignore encore, si la fonction de construction ou de destruction est trop lourde, trop importante, ou trop longue, le programme se plante. Donc attention! Si vous voulez allouer de la mémoire, charger un fichier, etc... utilisez plutot une fonction Init que vous invoquerez vous meme manuellement. Il en va de meme pour la destruction.

    Dans certains cas, les constructeurs peuvent etre déclarés en tant que private. C'est notamment le cas quand on ne veut pas permettre a la l'utilisateur de créer une instance de la classe. Nous verrons l'intéret de cette technique plus tard, dans les tutorials suivants. Pour l'instant, veillez a bien déclarer constructeurs et destructeurs public, sous peine de ne pas pouvoir instancier vos objets.

    Il est possible de surcharger un constructeur (jamais un destructeur!!) de la meme maniere qu'on ferait pour une fonction globale - ou membre, d'ailleurs - et les parametres doivent alors etre précisés lors de la déclaration de l'objet. Exemple :

   
class    Animal
    {
   
public :
        Animal()
        {
            strcpy(name, "noname");
        };
        Animal(
char* new_name)
        {
            strcpy(name, new_name);
        };
        ~Animal()
        {
           
        };
   
private :
        char name[64];
    }


    Animal    unnamed_pet;
    Animal    named_pet("Médor");

    Remarque : Si on ne désire utiliser le constructeur par défaut - sans parametres - il ne faut surtout pas mettre des parentheses vides, en effet le compilateur croirait que c'est le prototype d'une fonction :

    Animal    unnamed_pet();

    On a crée la une fonction unnamed_pet qui ne prend aucun parametre et qui renvoie un objet de type Animal!

8 . Dérivation
   
    La dérivation est aussi appellée héritage dans le cadre de l'OOP. C'est l'un des points les plus important de l'OOP. Supposez qu'on décide de créer une classe Vehicle. Ou plutot CVehicle - oui, il vaut mieux faire précéder les identifieurs de classe d'une lettre, c'est une habitude a prendre. Cette classe contiendrait des informations sur la position du vehicule, sa masse, sa puissance, etc... - qui sont des caractéristiques communes a tous les véhicules. Maintenant supposez que nous ayons besoin de représenter une Formule1. Nous avons donc besoin d'une classe CFormule1 qui contienne des infos supplémentaires, du style vitesse de pointe, usure des pneus, jauge de fuel. Ok, mais me direz vous, une Formule1 EST un véhicule. Et dans notre approche, une Formule1 est un véhicule parce qu'elle a une position, une masse, et une puissance.
    Mais pourtant, on ne va pas créer une classe CFormule1 dont les membres seraient redondants par rapport a la définition de CVehicle. Nous aurions deux classes totalement différentes et indépendantes; pourtant on devrait pouvoir traiter une Formule1 en tant que véhicule!
    Ce dont nous avons besoin, c'est de dériver la classe CFormule1 de la classe CVehicle. Ainsi, la classe CFormule1 contiendra tous les membres de CVehicule. De meme, si on veut créer une classe CTruck pour représenter un camion, il faut la dériver de CVehicle. Par contre, si on veut représenter différent types de camions, il faudra les dériver de CTruck. De cette maniere, les différents types contiendront les membres de CTruck, et donc aussi les membres de CVehicle dont CTruck est dérivé. Vous commencez a saisir? Ce n'est pas tres compliqué.
    C'est l'un des éléments fondamentaux de l'OOP : quand deux types d'objets ont un des éléments en commun il faut regrouper ces éléments dans ce qu'on appelle une classe de base. On créé ainsi une compatiblité entre ces deux objets, de maniere a pouvoir les traiter sans se soucier de leur type.
    Supposons qu'on ait un pointeur du type CVehicle*. On sait que ce pointeur  (dans le cadre d'une programmation propre!) pointe vers un CVehicle. Faux! Il peut aussi pointer vers un dérivé de CVehicle. Donc, il peut pointer vers un CTruck, ou un CFormule1. Mais ca n'est pas notre probleme; Nous pouvons effectuer sur cet objet toutes les opérations que l'on peut effectuer sur un CVehicle. Par exemple, connaitre sa masse, sa position, sa puissance. Ou encore accélérer, qui est une caractéristique commune a tous les véhicules.
   
    La dérivation se fait lors de la définition de la classe :

       
class    CVehicle
        {
           
int position;
           
int mass;
           
int horsepower;

           
void Accelerate();
        };

       
class    CTruck        : public    CVehicle
        {
       
        };

       
class    CFormule1    : public    CVehicle
        {
       
        };

    Il y a un probleme avec cette l'implémentation, Car une formule 1 ne se comportera pas du tout de la meme maniere qu'un camion lorsqu'il s'agira d'accélérer. En fait, il faudrait que l'interface soit la meme, c'est a dire qu'il existe une fonction Accelerate() dont la déclaration soit la meme pour tous les types de dérivés de CVehicle, mais que l'implémentation (ou la définion, ici c'est la meme chose) de cette fonction soit spécifique a chaque type dérivé. Ainsi, si nous disposions d'un pointeur CVehicle* pVehicle, et qu'on faisait :

        pVehicle->Accelerate();

Il faudrait que cette syntaxe puisse invoquer des fonctions différentes en fonction du type de l'objet pointé. La solution, ce sont les fonctions virtuelles.

9 . Fonctions virtuelles

    A la différence des fonctions surchargées, ou le choix de la fonction a invoquer est fait au moment de la compilation, le choix de la fonction a invoquer, dans le cas des fonctions virtuelles, est fait lors de l'éxécution. Exemple :

       
class    CVehicle
        {
           
int position;
           
int mass;
           
int horsepower;

            virtual    void    Accelerate();
        };

       
class    CTruck        : public    CVehicle
        {
    // Cette directive indique au compilateur qu'on va redéfinir accélérate pour CTruck
            virtual    void    Accelerate();
        };

       
class    CFormule1    : public    CVehicle
        {
    // Cette directive indique au compilateur qu'on va redéfinir accélérate pour CFormule1
            virtual    void    Accelerate();       
        };

        Il faut apres redéfinir les fonctions :

       
void    CFormule::Accelerate()
        {
            ......
        }

       
void    CTruck::Accelerate()
        {
            .......
        }

        Remarque : Le mot clef virtual n'apparait pas dans les définitions.

        Il est évident que maintenant, nous avons la possibilité d'accélérer différemment en fonction du type de véhicule; sans pour autant en connaitre le type! Mais, si pour une raison ou pour une autre, nous ayions besoin d'invoquer la fonction Accelerate contenue dans la classe de base, on pourrait le faire en précisant explicitement :

        pVehicle->CVehicle::Accelerate();

10. Création de la classe Chess

    Maintenant que nous en savons suffisament, nous allons refaire une approche orientée objet de notre jeu d'échecs.

   
// Chess.h
    class    GChess
    {
   
public :
       
void    NewGame();
       
void    Play(short x1, short y1, short x2, short y2);
       
bool    IsFinished();
       
short    GetWinner();

   
private :
        short    m_board[8][8]; // l'échiquier
       
int    m_counter; // compteur de tours
    };


    Dorénavant les fonctions de notre interface sont masquées par l'espace de nom GChess. Pour accéder aux membres il faudra employer les  
:: pour les constantes, et . pour les variables. Ainsi lorsque on va définir nos fonctions on va le faire de la maniere suivante.
   
   
// Chess.c
    void    GChess::NewGame()
    {
       
// ...............
    }

   
void    GChess::Play(short x1, short y1, short x2, short y2)
    {
       
// ...............
    }

   
bool    GChess::IsFinished()
    {
       
// ...............
    }

   
short    GChess::GetWinner()
    {
       
// ...............
    }


    Ainsi notre jeu d'échec forme maintenant un tout et non plus un jeu de fonctions du meme niveau d'imbrication que la fonction main.

11. Le pointeur this

    Vous avez remarqué que le pointeur chess* qui était le premier parametre de chaque fonction a disparu? On appelle ce pointeur le pointeur
this. En fait c'est le compilateur qui va le simuler. Ainsi, quand le compilateur tombe sur :

   
class GChess
    {
   
// ...............
        short IsFinished();
   
// ...............
    }

    Il va créer une fonction qui prendra un parametre, le pointeur this; Et quand le compilateur tombe sur :

    GChess game;
    game.IsFinished();

    Il va passer en premier paramatre a IsFinished l'addresse de game. Ce pointeur reste accessible au code source, si pour une raison ou pour une autre vous avez besoin de connaitre l'addresse de l'objet englobant vous pouvez toujours faire :
   
    MyClass::DoSomething()
    {
        MyClass* pThisInstance =
this;
    }

12. Credits

    Je remercie NeoSeb et Fab pour leur soutien.
Vous etes autorisés a repomper ces tutorials, en faire ce que vous voulez, les diffuser, les modifier, enfin, tout quoi! Simplement pensez que meme si ce fut un plaisir de les écrire c'est pour vous que je l'ai fait; et c'était quand meme du boulot! Alors ça serait sympa que mon nom apparaisse.
   
Ces tutorials ont été écrits par Ace, pour Ace Corporation (http://acecorp.free.fr/)