< Langage C++
fin de la boite de navigation du chapitre

La classe

En programmation orientée objet, tout est basé sur le concept de la classe. La classe est une entité autonome capable de maintenir, une fois instanciée (définie), la cohérence des données qu'elle est chargée d'entretenir. (nous mettrons en évidence la syntaxe dans la partie implémentation)


Les visibilités : niveaux de visibilité dans une classe

Il existe trois niveaux de visibilité dans une classe :

  • Privé (Niveau par défaut) : Permet de masquer complètement les données et méthodes à des classes tierces et même aux classes dérivées. On parle d'encapsulation (peut être utilisé par les méthodes mais est surtout destiné aux variables d'une classe).
  • Protégé : permet de partager des données uniquement aux classes dérivées. (Normalement uniquement utilisé pour les méthodes)
  • Public : permet de partager les données avec toutes les classes tierces. (Normalement uniquement utilisé pour les méthodes)

Les attributs : variable de classes

La classe est une entité qui peut être composée de variables internes que l’on nomme attributs membres.

La théorie veut que les attributs d'une classe ne soient accessibles directement qu’à cette classe. Bien qu'en C++ il soit possible de définir des attributs membres de visibilité publique ou protégée, il est normalement indispensable de rendre les attributs membres privés (je n'ai jamais eu à rendre publiques ou même protégés des attributs).

En effet le paradigme-objet étant de rendre la classe responsable de la gestion de ses attributs pour assurer la gestion correcte et la cohésion des données, il serait mal vu qu'une autre classe accède malencontreusement à ces attributs et vienne bouleverser l'organisation que la classe permet de maintenir entre eux. Quand une classe privatise un attribut on dit qu'elle l'encapsule.

Les méthodes : méthodes de classes

Nous avons déjà vu les méthodes auparavant.

Les méthodes déterminent le comportement qu'une classe est capable de réaliser. La classe possède au moins trois méthodes spéciales :

  • Un constructeur sans paramètres appelé aussi constructeur par défaut ou, selon le cas, un constructeur paramétré qui oblige à fournir des paramètres pour créer la classe.
  • Un constructeur par copie qui prend en paramètre une référence sur la même classe (dans la majorité des cas elle est fournie par défaut par le compilateur mais il peut être fréquemment nécessaire de la définir manuellement).
  • Un destructeur qui est chargé d'appliquer un traitement pour éventuellement nettoyer la mémoire. Bien qu’il soit souvent vide, l'implémenter explicitement et systématiquement permet de faire fonctionner le mécanisme d'héritage polymorphe.

Les méthodes donnant accès aux attributs sont appelés suivant le sens d'accès :

  • Des accesseurs pour les méthodes accédant en lecture seule.
  • Des mutateurs pour les méthodes accédant en écriture seule.
  • Des propriétés pour les méthodes accédant en écriture et en lecture.

La théorie veux que tout attribut qui aurait besoin d’être transmis ou modifié, le soit par l'une de ces méthodes d'accès. (Je n'ai jamais eu à faire autrement.) Les méthodes définissent aussi l'interface d'une classe.


Généralisation de classes

En programmation orientée objet, on peut structurer une hiérarchie de classes en arborescence. Les classes du sommet de l'arbre sont les classes les plus abstraites et générales. Les classes les plus profondes sont les plus concrètes et les plus spécialisés.

Pour illustrer ces propos supposons les figures suivantes :

  • Carré
  • Rectangle
  • Parallélogramme
  • Losange
  • Quadrilatère
  • Cercle
  • Ovale

Imaginons maintenant que nous devions réaliser des classes basées sur ces figures. Supposons que chaque classe doit être capable de dessiner. Supposons aussi que nous devions gérer chacune de ces classes avec le même (et unique) pointeur et appeler la même méthode pour le dessin de chaque classe.

Dans les figures imposées on peut voir que "Carré" est un cas particulier de (ou "une sorte de") "Rectangle" qui est lui-même un cas particulier du "Parallélogramme" qui est lui-même un "Quadrilatère". Quant à "Losange" c’est un "Quadrilatère".

On peut voir aussi que "Cercle" est un cas particulier de "Ovale".

Cependant rien ne lie Ovale et Quadrilatère.

Nous allons donc devoir généraliser Ovale et Quadrilatère en "Figure"

Ainsi Quadrilatère et Ovale sont des Figures, ainsi un pointeur sur Figure est capable de manipuler n’importe quelle classe sous-jacente, puisque, après tout, un carré est une figure tout comme un cercle.

Traitons maintenant le problème de la méthode unique. En fait depuis que l’on a créé la classe Figure ce n'est plus un problème.

En effet il suffit de définir la méthode "Dessine()" dans la classe Figure pour que toutes les autres classes en héritent.

Abstraction de classes

Lors de généralisations successives il est courant de se rendre compte qu'une classe est trop "abstraite" pour avoir suffisamment de données exploitables pour pouvoir en générer une instance. C'est le cas pour notre classe "Figure". En effet elle est trop générique et rien d'intéressant ne peut en sortir, cependant elle fournit une interface que toutes les autres classes devront reproduire fidèlement. La méthode "Dessiner" ne représente pas grand chose pour une Figure. Nous ne pouvons pas décrire de comportement pour cette méthode dans cette classe. Nous ne pouvons donc pas instancier la classe car cela n'aurait pas de sens. Nous allons donc devoir rendre cette classe abstraite. Cela signifie que la classe n’est pas instanciable en l'état mais qu'une classe héritée non abstraite peut être interprétée comme cette classe (via l'utilisations de pointeurs ou de références).

Héritage de classes

L'héritage est la faculté qu’à une classe de pouvoir transmettre ses attributs et méthodes à ses classes dérivées et, sous certaines conditions, permettre à ses classes dérivées de redéfinir ses méthodes pour pouvoir les améliorer et les spécialiser.

Tout d’abord il faut savoir qu'en C++ une classe dérivée reçoit toujours une copie de l'intégralité des attributs et des méthodes de sa classe ancêtre. En C++, bien que l’on puisse choisir la façon dont sont copiés les membres de la classe ancêtre, la théorie-objet veux que l’on hérite toujours des classes ancêtres de manière publique afin que tous les membres publics de la classe ancêtre soient aussi disponibles de manière publique dans la classe dérivée. L'héritage permet de ne pas réécrire éternellement les mêmes codes, de réutiliser les objets, de pouvoir les spécialiser et mettre en œuvre le polymorphisme de classe.

Polymorphisme de classes

Le polymorphisme en informatique se traduit par la capacité qu'a un pointeur de classe ancêtre présentant une interface donnée, à appeler la méthode de l'instance de la classe dérivée correspondant à la méthode de la classe ancêtre. En C++ la mise en œuvre du polymorphisme se fait à l'aide du mot clé "virtual". Dans la pratique et pour reprendre l'exemple vu précédemment :

Si l’on créé un pointeur sur Figure que l’on lui assigne l'adresse de l'instance d'un carré et que l’on demande au pointeur-figure de se dessiner alors le pointeur va appeler la méthode virtuelle et par le biais de l'héritage virtuel appeler la méthode implémentée dans la classe Carré.

Implémentation

Les classes sont donc la représentation logique du concept d'objet. Voici en C++ comment implémenter ces classes conformément à la théorie de l'objet.

Définition
Syntaxe:
class <NomNouvelleClasse> : [public: <ClasseAncêtre>[, <AutreClasseAncêtre>][, ...]]
{
     [ [<Visibilitée>:] <TypeAttribut1> <NomAttribut1>;]
       [<Visibilitée>:] <TypeAttributN> <NomAttributN>;] ]
     [<Visibilitée>:] <TypeMethode1> <NomMethode1>(<TypeParamettre> <NomParamettre>[ = <ValeurParDefaut>][, ...]);     
     [ [<Visibilitée>:] <TypeMethodeN> <NomMethodeN>(<TypeParamettre> <NomParamettre>[ = <ValeurParDefaut>][, ...]); ]
};

Où <NomNouvelleClasse> est le nom de la classe, <ClasseAncêtre> est une classe ancêtre tout comme <AutreClasseAncêtre> et sont facultatives si la classe n'a pas à avoir d'ancêtre, <TypeAttribut1> et <TypeAttributN> sont les types des attributs, <NomAttribut1> et <NomAttributN> sont les noms des attributs, les attributs sont facultatifs, <TypeParamettre> et <NomParamettre> sont les paramètres des méthodes de la classe, <TypeMethode1> et <TypeMethodeN> sont les types de retours des méthodes de la classe, <NomMethode1> et <NomMethodeN> sont les noms des méthodes de la classe, , <Visibilitée> peut prendre trois valeurs: public, protected ou private (par défaut). Un membre déclaré public peut être manipulé par n’importe quelle classe, un membre protected ne peut être manipulé que par la classe et ses dérivées tandis qu'un membre private ne peut être manipulé que par les méthodes de la classe.

Les méthodes sont soit définies directement dans la déclaration de classe (au quel cas ce sont des Macros), soit définies en dehors de la déclaration dans un fichier source séparé de la manière suivante :

Définition
Syntaxe:
<TypeRetour> [<NomClasse>::]<NomMethode>([<TypeParametre> <NomParametre>[,<...>]])
{
     [<Instructions>;]
}

Voici un exemple de la classe la plus simple à réaliser. Il faut dire aussi qu'elle ne fait strictement rien.

Exemple

dans le ".h"

#ifndef FAINEANTE_H
#define FAINEANTE_H

class Faineante;
{
     public:
         // Constructeur par défaut
         Faineante();

         // Constructeur par copie
         Faineante(Faineante& pCopie);

         // Destructeur
         virtual ~Faineante(); // "virtual" active le polymorphisme
};

#endif //FAINEANTE_H

Dans le ".cpp"

Faineante::Faineante()
{
}

Faineante::Faineante(Faineante& pCopie)
{
}

Faineante::~Faineante()
{
}
Fin de l'exemple

Exemples:

Maintenant reprenons nos exemples de figures de tout à l’heure.

Exemple

dans "Carre.h"

#ifndef Carre_H
#define Carre_H

#include "Rectangle.h"
// Un carré est un rectangle dont toutes les arrêtes sont égales.
class Carre : public Rectangle
{
    public:
        // Constructeur paramétré
        Carre(double pArrete);
        // Destructeur
        virtual ~Carre();
        virtual void mDessine();
};

dans "Carre.cpp"

#include "Carre.h"

// Constructeur paramétré
// Appelle le constructeur de la classe ancêtre en le paramétrant correctement 
Carre::Carre(double pArrete):Rectangle(pArrete, pArrete) 
{
}

// Destructeur
Carre::~Carre()
{
}

void Carre::mDessine()
{
    //Dessine ici le Carre (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
    cout << "Dessine Carre :\n" << endl;
    Rectangle::mDessine();
}

dans "Rectangle.h"

#ifndef Rectangle_H
#define Rectangle_H

#include "Parallelogramme.h"

//Un Rectangle est un Prarallelogramme avec tous ses angles droits.
class Rectangle : public Parallelogramme
{
    public:
        Rectangle(double pArreteAB, double pArreteBC);
        virtual ~Rectangle();
        virtual void mDessine();
};

dans "Rectangle.cpp"

#include "Rectangle.h"

// Appelle le constructeur de la classe ancêtre en le paramétrant correctement 
Rectangle::Rectangle(double pArreteAB, double pArreteBC):Parallelogramme(pArreteAB, pArreteBC, 90.0)
{
}

// Destructeur
Rectangle::~Rectangle()
{
}

void Rectangle::mDessine()
{
    //Dessine ici le Rectangle (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
    cout << "Dessine Rectangle :\n" << endl;
    Parallelogramme::mDessine();
}

dans "Parallelogramme.h"

#ifndef Parallelogramme_H
#define Parallelogramme_H

#include "Quadrilatere.h"

// Un Parallélogramme est un Quadrilatère dont les cotés parallèles sont égaux.
class Parallelogramme: public Quadrilatere
{
    private: 
        double& mCorrectionAngle(double& pAngle);

    protected:
        virtual void mAngleA(double& pAngle);
        virtual void mAngleB(double& pAngle);
        virtual void mAngleC(double& pAngle);
        virtual void mAngleD(double& pAngle);

    public:
        // Constructeur parametré
        Parallelogramme(double pArreteAB, double pArreteBC, double pAngleA);
        // Destructeur
        virtual ~Parallelogramme();
        // Méthode de dessin des parallélogrammes.
        virtual void mDessine();
};

dans "Parallelogramme.cpp"

#include <iostream>
#include "Parallelogramme.h"

using namespace std;

double& mCorrectionAngle(double& pAngle)
{
    // Corriger les dépassements
    double vAngleMaximal = 180.0;
    pAngle %= vAngleMaximal;
    // Si l'angle est 
    if(pAngle < 0)
    {
        // Corriger le signe.
        pAngle *= -1;
    }
    return pAngle;
}

virtual void mAngleA(double& pAngle)
{
    Quadrilatere::mAngleA(pAngle);
}

virtual void mAngleB(double& pAngle)
{
    Quadrilatere::mAngleB(pAngle);
}

virtual void mAngleC(double& pAngle)
{
    Quadrilatere::mAngleC(pAngle);
}

virtual void mAngleD(double& pAngle)
{
    Quadrilatere::mAngleD(pAngle);
}

// Constructeur paramétré
Parallelogramme::Parallelogramme(double pArreteAB, double pArreteBC, double pAngleA):Quadrilatère(pArreteAB, pArreteBC, pArreteAB, pArreteBC, 0.0, 0.0, 0.0, 0.0)
{
    // Dans un Parallélogramme les arrêtes :
    // AB = CD et BC = AD.
    // Les angles : 
    // A = C, B = D et A + B + C + D = 360°.
    // Ici nous passons par les méthodes de Parallélogramme pour garantir que les angles seront inférieur ou égal à 180.

    double vAngleMax = 180.0;
    this->mAngleA(pAngleA);
    this->mAngleB(vAngleMax - this->mCorrectionAngle(pAngleA));
    this->mAngleC(pAngleA); 
    this->mAngleD(vAngleMax - this->mCorrectionAngle(pAngleA));
}

// Destructeur
virtual Parallelogramme::~Parallelogramme()
{
}
        
// Méthode de dessin des parallélogrammes.
virtual void Parallelogramme::mDessine()
{
    //Dessine ici le Parallelogramme (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
    cout << "Dessine Parallelogramme :\n" << endl;
    Quadrilatere::mDessine();
}

dans "Losange.h"

#ifndef Losange_H
#define Losange_H

#include "Quadrilatere.h"

class Losange : public Quadrilatere
{
    private: 
        double& mCorrectionAngle(double& pAngle);

    protected:
        virtual void mAngleA(double& pAngle);
        virtual void mAngleB(double& pAngle);
        virtual void mAngleC(double& pAngle);
        virtual void mAngleD(double& pAngle);

    public:
        Losange(double pArreteAB, double pArreteBC, double pAngleA);
        virtual ~Losange();
        virtual void mDessine();
};

dans "Losange.cpp"

#include "Losange.h"

// L'angle maximal d'un angle de losange est : 180°
double& Losange::mCorrectionAngle(double& pAngle)
{
    // Corriger les dépassements
    double vAngleMaximal = 180.0;
    pAngle %= vAngleMaximal;
    // Si l'angle est 
    if(pAngle < 0)
    {
        // Corriger le signe.
        pAngle *= -1;
    }
    return pAngle;
}

void Losange::mAngleA(double& pAngle)
{
    // Mettre à jour l'angle.
    Quadrilatere::mAngleA(mCorrectionAngle(pAngle));  
}

void Losange::mAngleB(double& pAngle)
{
    // Mettre à jour l'angle.
    Quadrilatere::mAngleB(mCorrectionAngle(pAngle));  
}

void Losange::mAngleC(double& pAngle)
{
    // Mettre à jour l'angle.
    Quadrilatere::mAngleC(mCorrectionAngle(pAngle)); 
}

void Losange::mAngleD(double& pAngle)
{
    // Mettre à jour l'angle.
    Quadrilatere::mAngleD(mCorrectionAngle(pAngle)); 
}

Losange::Losange(double pArreteAB, double pArreteBC, double pAngleA):Quadrilatere(pArreteAB, pArreteBC, pArreteBC, pArreteAB, 0.0, 0.0, 0.0, 0.0)
{
    // Dans un losange les arrêtes :
    // AB = DA et BC = CD.
    // Les angles : 
    // A = C, B = D et A + B + C + D = 360°.
    // Ici nous passons par les méthodes de Losange pour garantir que les angles seront inférieur ou égal à 180.

    double vAngleMax = 180.0;
    this->mAngleA(pAngleA);
    this->mAngleB(vAngleMax - this->mCorrectionAngle(pAngleA));
    this->mAngleC(pAngleA); 
    this->mAngleD(vAngleMax - this->mCorrectionAngle(pAngleA));
}

~Losange::Losange()
{
}

void Losange::mDessine()
{
    //Dessine ici le Losange (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
    cout << "Dessine Losange :\n" << endl;
    Quadrilatere::mDessine();
}

dans "Quadrilatere.h"

#ifndef Quadrilatere_H
#define Quadrilatere_H

#include "Figure.h"

class Quadrilatere : public Figure
{
    private: 
        double aArreteAB;
        double aArreteBC;
        double aArreteCD; 
        double aArreteDA;
        double aAngleA;
        double aAngleB;
        double aAngleC;
        double aAngleD;
        double& mCorrectionAngle(double& pAngle);
        double& mCorrectionArrete(double& pArrete);
    
    protected:
        void mArreteAB(double& pArrete);
        void mArreteBC(double& pArrete);
        void mArreteCD(double& pArrete); 
        void mArreteDA(double& pArrete);

        virtual void mAngleA(double& pAngle);
        virtual void mAngleB(double& pAngle);
        virtual void mAngleC(double& pAngle);
        virtual void mAngleD(double& pAngle);

    public:
        Quadrilatere(double& pArreteAB, double& pArreteBC, double& pArreteCD, double& pArreteDA, double& pAngleA, double& pAngleB, double& pAngleC, double& pAngleD);
        virtual ~Quadrilatere();
        virtual void mDessine();
        double mArreteAB();
        double mArreteBC();
        double mArreteCD(); 
        double mArreteDA();
        double mAngleA();
        double mAngleB();
        double mAngleC();
        double mAngleD();
};

dans "Quadrilatere.cpp"

#include <math.h>
#include "Quadrilatere.h"

// L'angle maximal d'un angle de quadrilatère est : 360°
double& Quadrilatere::mCorrectionAngle(double& pAngle)
{
    // Corriger les dépassements
    double vAngleMaximal = 360.0;
    pAngle %= vAngleMaximal;
    // Si l'angle est 
    if(pAngle < 0)
    {
        // Corriger le signe.
        pAngle *= -1;
    }
    return pAngle;
}

// Propriété AngleA de Quadrilatere. Remarquez comment la propriété contraint l'intégrité des données de la classe via la méthode mCorrectionAngle.
void Quadrilatere::mAngleA(double& pAngle): Figure()
{
    // Mettre à jour l'angle.
    this->aAngleA = mCorrectionAngle(pAngle);
}

void Quadrilatere::mAngleB(double& pAngle)
{
    // Mettre à jour l'angle.
    this->aAngleB = mCorrectionAngle(pAngle);
}

void Quadrilatere::mAngleC(double& pAngle)
{
    // Mettre à jour l'angle.
    this->aAngleC = mCorrectionAngle(pAngle);
}

void Quadrilatere::mAngleD(double& pAngle)
{
    // Mettre à jour l'angle.
    this->aAngleD = mCorrectionAngle(pAngle);
}

// Une arrête n’est pas négative
double& Quadrilatere::mCorrectionArrete(double& pArrete)
{
    if(pArrete < 0)
    {
        pArrete *= -1;
    }
    return pArrete;
}

void Quadrilatere::mArreteAB(double& pArrete)
{
    this->aArreteAB = mCorrigeArrette(pArrete);
}

void Quadrilatere::mArreteBC(double& pArrete)
{
    this->aArreteBC = mCorrigeArrette(pArrete);
}

void Quadrilatere::mArreteCD(double& pArrete)
{
    this->aArreteCD = mCorrigeArrette(pArrete);
}

void Quadrilatere::mArreteDA(double& pArrete)
{
    this->aArreteCD = mCorrigeArrette(pArrete);
}

Quadrilatere::Quadrilatere(double& pArreteAB, double& pArreteBC, double& pArreteCD, double& pArreteDA, double& pAngleA, double& pAngleB, double& pAngleC, double& pAngleD)
{
    // Assure l'intégrité de la classe;
    this->mArreteAB(pArreteAB);
    this->mArreteBC(pArreteBC);
    this->mArreteCD(pArreteCD);
    this->mArreteDA(pArreteAD);
    this->mAngleA(pAngleA);
    this->mAngleB(pAngleB);
    this->mAngleC(pAngleC);
    this->mAngleD(pAngleD);
}

Quadrilatere::~Quadrilatere()
{
    // rien à détruire.
}

void Quadrilatere::mDessine()
{
    //Dessine ici le parallélogramme (vrais code de dessin non pertinent car trop volumineux pour le gain obtenu sur l’intérêt de la leçon).
    cout << "Dessine Quadrilatere :\n" <<
            "\tAB = " << this->mArreteAB() << ",\n" <<
            "\tBC = " << this->mArreteBC() << ",\n" <<
            "\tCD = " << this->mArreteCD() << ",\n" <<
            "\tDA = " << this->mArreteDA() << ",\n" <<
            "\tAngle A = " << this->mAngleA() << ",\n" <<
            "\tAngle B = " << this->mAngleB() << ",\n" <<
            "\tAngle C = " << this->mAngleC() << ",\n" <<
            "\tAngle D = " << this->mAngleD() << endl;
}

double Quadrilatere::mArreteAB()
{
    return this->aArreteAB;
}

double Quadrilatere::mArreteBC()
{
    return this->aArreteBC;
}

double Quadrilatere::mArreteCD()
{
    return this->aArreteCD;
}

double Quadrilatere::mArreteDA()
{
    return this->aArreteDAB;
}

double Quadrilatere::mAngleA()
{
    return this->aAngleA;
}

double Quadrilatere::mAngleB()
{
    return this->aAngleB;
}

double Quadrilatere::mAngleC()
{
    return this->aAngleC;
}

double Quadrilatere::mAngleD()
{
    return this->aAngleD;
}

dans "Figure.h"

#ifndef Figure_H
#define Figure_H

class Figure
{
    public:
        Figure();
        virtual ~Figure();
        virtual void mDessine() = 0; // "virtual void mDessine() = 0;" est une méthode dite "virtuelle pure" (à cause du "= 0"). 
                                     // Elle n'a pas de corps et rend donc de fait la classe "Figure" abstraite. Cela signifie 
                                     // que l’on ne peux pas instancier cette classe directement. Pour pouvoir instancier cette
                                     // classe il faut la dériver et implémenter la méthode (lui donner un corps).
};

dans "Figure.cpp"

#include "Figure.h"

Figure::Figure()
{
}

Figure::~Figure()
{
}

// Le corps de mDessine n'apparait pas car c’est une méthode virtuelle pure
Fin de l'exemple
Cet article est issu de Wikiversity. Le texte est sous licence Creative Commons - Attribution - Partage dans les Mêmes. Des conditions supplémentaires peuvent s'appliquer aux fichiers multimédias.