Aller au contenu

Programmation C++/Les classes

Un livre de Wikilivres.

La notion de classe en programmation orientée objet

[modifier | modifier le wikicode]

Une classe permet de regrouper dans une même entité des données et des fonctions membres (appelées aussi méthodes) permettant de manipuler ces données. La classe est la notion de base de la programmation orientée objet. Il s'agit en fait d'une évolution de la notion de structure, qui apporte de nouvelles notions orientées objet absolument fondamentales. Un objet est un élément d'une certaine classe : on parle de l'instance d'une classe.

L'encapsulation en C++

[modifier | modifier le wikicode]

L'encapsulation est un mécanisme qui interdit d'accéder à certaines données depuis l'extérieur de la classe. Ainsi, un utilisateur de la classe ne pourra pas accéder à tous les éléments de celles-ci. Il sera obligé d'utiliser certaines fonctions membres de la classe (celles qui sont publiques). L'avantage de cette restriction est qu'il empêche par exemple un utilisateur de la classe de mettre les données dans un état incohérent. Vue de l'extérieur, la classe apparaît comme une boîte noire, qui a un certain comportement à laquelle on ne peut accéder que par les méthodes publiques. Cette notion est extrêmement puissante et permet d'éviter de nombreux effets de bord.

L'encapsulation permet de distinguer très nettement ce que fait la classe et sa sémantique précise, de la manière dont on l'implémente. Cette réflexion permet de répondre dans un premier temps à la question Comment utilise-t-on la classe ? Ce n'est que dans un second temps que l'aspect technique entre en jeu et que le programmeur doit répondre à la question Comment vais-je programmer les fonctionnalités de la classe qui ont été spécifiées ? L'encapsulation permet de faire abstraction du fonctionnement interne (c'est-à-dire, l'implémentation) d'une classe et ainsi de ne se préoccuper que des services rendus par celle-ci.

Le C++ implémente l'encapsulation en permettant de déclarer les membres d'une classe avec l'un des mots réservés public, private et protected. Ainsi, lorsqu'un membre est déclaré:

  • public, il sera accessible depuis n'importe quelle fonction.
  • private, il sera uniquement accessible d'une part, depuis les fonctions qui sont membres de la classe et, d'autre part, depuis les fonctions et classes autorisées explicitement par la classe (par l'intermédiaire du mot réservé friend). Ces dernières fonctions/classes sont appelées fonctions/classes amies de la classe.
  • protected, il aura les mêmes restrictions que s'il était déclaré private, mais il sera en revanche accessible par les classes filles.

Le C++ n'impose pas l'encapsulation des membres dans leurs classes. On pourrait donc déclarer tous les membres publiques, mais en perdant une partie des bénéfices apportés par la programmation orientée objet. Il est de bon usage de déclarer toutes les données privées, ou au moins protégées, et de rendre publiques les méthodes agissant sur ces données. Ceci permet de cacher les détails de l'implémentation de la classe.

Les fonctions membres

[modifier | modifier le wikicode]

Parmi les fonctions membres, on distingue :

  • les Accesseurs: ce sont des fonctions membres qui ne modifient pas l'état de l'objet. Le C++ permet de déclarer une fonction membre const, indiquant au compilateur qu'elle ne modifie pas l'état de l'objet. Si l'implémentation de la fonction const tente de modifier une variable membre de la classe, le compilateur signalera une erreur. Seules les fonctions membres const peuvent être invoquées depuis un objet déclaré const. Une tentative d'appel à une fonction membre non const depuis un objet const échouerait à la compilation.
  • les Modificateurs : ce sont des fonctions membres qui peuvent modifier l'état de l'objet. Dans ce cas, on omet le const dans la déclaration. Si une fonction membre déclarée non const ne modifie pas l'état de l'objet, il y a lieu de se demander s'il ne faudrait pas la déclarer const.

La déclaration d'une fonction membre se fait de la même manière qu'une fonction régulière mais au sein de la portée de la classe. En outre, on pourra la suffixer du mot clé const pour signifier qu'il s'agit d'un accesseur.

  • Syntaxe
class A
{
public:
    int getValue() const;
    void setValue(int value);
private:
    int value;
};

Dans le cas d'une définition au sein de la classe même, elle est faite comme une fonction régulière. Dans ce cas, la fonction membre est automatiquement considérée comme inline.
Dans le cas d'une définition en dehors de la classe, il faudra préfixer le nom de la fonction membre par le nom de la classe suivi de l'opérateur de portée (::). Dans ce cas, elle est automatiquement considérée comme non inline.

  • Syntaxe
class A
{
public:
    int getValue() const { return this->value; } // définition au sein de la classe
    void setValue(int value);
    void print() const;
private:
    int value;
};

inline void A::setValue(int value) // définition au sein de la classe (inline)
{
    this->value = value;
}

void A::print() const // définition en dehors de la classe (non inline)
{
    std::cout << "Value=" << this->value << std::endl;
}

En règle générale, les fonctions membres non inline devront être déclarées dans le fichier source .cpp de la classe, pour éviter d'avoir plusieurs fois la même définition au moment de l'édition des liens, et les fonctions membres inline devront être déclarées dans le fichier d'entête .hpp de la classe, pour permettre au compilateur de générer le code à chaque appel de la fonction.

Constructeurs et destructeur

[modifier | modifier le wikicode]

Les constructeurs et destructeur d'une classe sont des fonctions membres particulières de cette classe.

  • Lorsqu'on crée une nouvelle instance d'une classe, les données membres de cette instance ne sont pas initialisées par défaut. Un constructeur est une fonction membre qui sera appelée au moment de la création d'une nouvelle instance d'une classe. Il peut y avoir plusieurs constructeurs d'une classe avec différents paramètres qui serviront à initialiser notre classe. Le constructeur d'une classe A est appelé automatiquement lorsqu'on crée une instance en écrivant : A toto; ou encore A toto(6,9);. Ils sont également appelés lorsqu'une instance est créée grâce à l'opérateur new.
  • Parmi les constructeurs, on retrouve une série de constructeurs particuliers:
    • Le constructeur par défaut : c'est le constructeur sans argument. Si aucun constructeur n'est présent explicitement dans la classe, il est généré par le compilateur.
    • Le constructeur de copie : c'est un constructeur à un argument dont le type est celui de la classe. En général, l'argument est passé par référence constante. Plus rarement, il peut l'être par valeur ou référence non constante. Si il n'est pas explicitement présent dans la classe, il est généré par le compilateur. Notons encore que:
      • Si un constructeur de copie est fourni dans une classe, il sera très probable de devoir fournir l'opérateur d'assignation avec le même type d'argument.
      • Il est possible de rendre une classe non copiable en privatisant la déclaration du constructeur de copie et de l'opérateur d'assignation. Dans ce cas, il n'est pas nécessaire (recommandé) de fournir leur définition.
    • Les constructeurs de conversion implicite : il s'agit de constructeurs à un seul argument dont le type est différent de celui de la classe et qui ne sont pas déclarés explicit. Ces constructeurs rendent possible la conversion implicte du type d'argument vers le type de la classe.
  • Syntaxe : (Pour une classe A)
    • A(); // constructeur par défaut
    • A(const A &); // constructeur de copie
    • A(const T &); // constructeur de conversion implicite
    • explicit A(const T &); // constructeur à un argument

Le destructeur d'une classe est appelé lorsqu'une de ses instances doit être détruite. Cela arrive à la fin de l'exécution d'une fonction/méthode lors d'une allocation dynamique : si vous avez déclaré, localement à une fonction/méthode, un élément d'une classe A par A toto; alors à la fin de l'exécution de la fonction/méthode, le destructeur de la classe est appelé automatiquement. Il est également appelé lorsqu'on détruit une instance avec delete.

Il ne peut y avoir qu'un seul destructeur pour une même classe. S'il n'est pas explicitement présent dans la classe, il sera généré par le compilateur. La syntaxe est similaire à celle du constructeur, à ceci près que le destructeur est introduit par un tilde et qu'il ne reçoit aucun argument :

~A();

Un appel explicite avec delete est nécessaire lorsqu'une instance a été allouée avec new. Dans le cas contraire, si le pointeur est détruit sans appeler le destructeur, l'instance ne sera pas réellement supprimée et persistera en mémoire mais sans être accessible, ce qui occasionnera ce qu'on appelle une fuite de mémoire.

Exemples de classes

[modifier | modifier le wikicode]

Dans cet exemple de classe, les fichiers Point.h, Point.cpp et main.cpp vont vous être exposés. Tout d'abord, Point.h:

#ifndef POINT_H
#define POINT_H

#include <iostream>
using namespace std;

class Point
{
public:
  // Constructeurs
  Point();
  Point(double x, double y);

  //Accesseurs et mutateurs
  void setX(double x);
  void setY(double y);
  double getX() const;
  double getY() const;

  // Autres méthodes
  double distance(const Point &P) const;
  Point milieu(const Point &P) const;

  void saisir();
  void afficher() const;

private: 
  double x,y;
};

#endif

Voici le fichier Point.cpp:

#include "Point.h"
#include <cmath>
#include <iostream>
using namespace std;

Point::Point() : x(0), y(0)
{}

Point::Point(double x, double y) : x(x), y(y)
{}

void Point::setX(double x)
{
    this->x = x;
}

void Point::setY(double y)
{
    this->y = y;
}

double Point::getX() const
{
    return this->x;
} 

double Point::getY() const
{
    return this->y;
} 

double Point::distance(const Point &P) const
{
    double dx = this->x - P.x;
    double dy = this->y - P.y;
    return sqrt(dx*dx + dy*dy); 
}

Point Point::milieu(const Point &P) const
{
    Point result;
    result.x = (P.x + this->x) / 2;
    result.y = (P.y + this->y) / 2;
    return result;
}

void Point::saisir()
{
    cout << "Tapez l'abscisse : ";  cin >> this->x;
    cout << "Tapez l'ordonnée : ";  cin >> this->y;
} 

void Point::afficher() const
{
    cout << "L'abscisse vaut " << this->x << endl;
    cout << "L'ordonnée vaut " << this->y << endl;
}

Et le fichier principal main.cpp:

#include <iostream> 
using namespace std;

#include "Point.h"

int main() 
{ 
    Point A, B, C;
    double d;

    cout << "SAISIE DU POINT A" << endl;
    A.saisir();
    cout << endl;

    cout << "SAISIE DU POINT B" << endl;
    B.saisir();
    cout << endl;

    C = A.milieu(B);
    d = A.distance(B);

    cout << "MILIEU DE AB" << endl;
    C.afficher();
    cout << endl;

    cout << "La distance AB vaut : " << d << endl;
    return 0;
}

Les opérateurs new et delete

[modifier | modifier le wikicode]

Il est parfois intéressant de créer dynamiquement de nouvelles instances d'une classe. Cela s'avère indispensable lorsque vous manipulez certaines structures de données complexes. L'opérateur new permet de créer une nouvelle instance d'une classe A en écrivant :

A* pointeur=new A; // Ou bien:
A* pointeur=new A();

Si le constructeur de la classe A prend des arguments, la syntaxe devient:

A* pointeur=new A(arguments);

La variable pointeur doit alors être du type A*.

La libération se fait en utilisant l'opérateur delete et le destructeur de la classe est appelé:

delete pointeur;

Surcharge d'opérateurs

[modifier | modifier le wikicode]

Pour certaines classes, la notion d'addition ou de multiplication est totalement naturelle. La surcharge d'opérateurs permet d'écrire directement U+V lorsqu'on veut additionner deux instances U et V d'une même classe A.

Opérateurs surchargeables

[modifier | modifier le wikicode]
  • Les opérateurs unaires : ce sont des opérateurs qui s'appliquent sans argument supplémentaire
opérateur définition
+ opérateur signe +
- opérateur négation
* opérateur déréférencement
& opérateur adresse
-> opérateur indirection
~ opérateur négation binaire
++ opérateur post/pré-incrémentation
-- opérateur post/pré-décrémentation
  • Les opérateurs binaires: ce sont des opérateurs qui s'appliquent moyennant un argument supplémentaire
opérateur définition
= opérateur assignation
+ opérateur addition
+= opérateur addition/assignation
- opérateur soustraction
-= opérateur soustraction/assignation
* opérateur multiplication
*= opérateur multiplication/assignation
/ opérateur division
/= opérateur division/assignation
% opérateur modulo
%= opérateur modulo/assignation
^ opérateur ou exclusif
^= opérateur ou exclusif/assignation
& opérateur et binaire
&= opérateur et binaire/assignation
| opérateur ou binaire
|= opérateur ou binaire/assignation
< opérateur plus petit que
<= opérateur plus petit ou égal
> opérateur plus grand que
>= opérateur plus grand ou égal
<< opérateur de décalage à gauche
<<= opérateur de décalage à gauche/assignation
>> opérateur de décalage à droite
>>= opérateur de décalage à droite/assignation
== opérateur d'égalité
!= opérateur d'inégalité
&& opérateur et logique
|| opérateur ou logique
[] opérateur indexation
, opérateur enchaînement
() opérateur fonction

Les opérateurs unaires doivent être surchargés en tant que méthode de la classe.

La signature générique au sein de la classe est

  • type_retour operator @(); // pour les méthodes non const
  • type_retour operator @() const; // pour les méthodes const

@ est l'opérateur à surcharger: + - * / ...

Excepté pour certains d'entre eux, les opérateurs binaires peuvent être surchargés en tant que méthode de la classe ou comme une fonction à 2 arguments.

La signature générique est la suivante

  • type_retour operator @(Arg); // pour les méthodes non const
  • type_retour operator @(Arg) const; // pour les méthodes const
  • type_retour operator @(Arg1, Arg2) const; // pour les fonctions

@ est l'opérateur à surcharger: + - * / ...

Pour surcharger l'opérateur+ de la classe A, il suffit de créer une méthode:

  • A operator+(const A& a) const;

ou encore une fonction externe à la classe:

  • A operator+(const A&, const A &)
  • Fichier Fraction.h
#define Fraction_h

#include <iostream>
using namespace std;

class Fraction
{
    friend ostream & operator<<(ostream & out, const Fraction &f);
    friend istream & operator>>(istream &in, Fraction &f); 

 public:
    Fraction();
    Fraction(int i);
    Fraction(int num, int den);
    
    Fraction operator+(const Fraction & f) const;
    Fraction operator-(const Fraction & f) const;
    Fraction operator*(const Fraction & f) const;
    Fraction operator/(const Fraction & f) const;

 private:
    int num,den;    
    static int pgcd(int x, int y);
    void normalise();
};

#endif
  • Fichier Fraction.cpp
#include "Fraction.h"
#include <sstream>

Fraction::Fraction() : num(0), den(1)
{
}

Fraction::Fraction(int i) : num(i), den(1)
{
}

Fraction::Fraction(int num, int den) : num(num), den(den)
{
    normalise();
} 

ostream & operator<<(ostream &out, const Fraction &f)
{
    if (f.den != 1)
        out << f.num << "/" << f.den;
    else
        out << f.num;

    return out;
} 

istream & operator>>(istream &in, Fraction &f)
{
    string s;
    bool ok = true;

    do
    {
        cout << "Tapez le numerateur : ";  getline(in, s);
        istringstream is1(s);
        ok = (is1 >> f.num) && is1.eof();
    } while(!ok);

    do
    {
        cout << "Tapez le denominateur : ";  getline(in, s);
        istringstream is2(s);
        ok = (is2 >> f.den) && is2.eof();
    } while(!ok);

    f.normalise();
    return in;
}
    
int Fraction::pgcd(int x, int y)
{
    int r;

    if (x <= 0 || y <= 0)
      r = -1;
    else
    {
      while (x != 0 && y != 0 && x != y)
      {
        if (y > x)
          y = y % x;
        else
          x = x % y;
      }

      if (x == 0)
        r = y;
      else
        r = x;
    }

    return r;
}

void Fraction::normalise()
{
    int s, n, d, p;
    if (den < 0)
    {
        s = -1;
        d = -den;
    }
    else
    {
        s = 1;
        d = den;
    }

    if(num < 0)
    {
        s = -s;
        n = -num;
    }
    else
        n = num;

    if(n != 0)
    {
        if(d != 0)
        {
            p = pgcd(n, d);
            n = n/p;
            d = d/p;
            num = n*s;
            den = d;
        }
    }   
    else
    {
        num = 0;
        den = 1;
    }
} 

Fraction Fraction::operator+(const Fraction & f) const
{
    Fraction r;
    r.num = f.den*num + den*f.num;
    r.den = f.den*den;
    r.normalise();
    return r;
} 

Fraction Fraction::operator-(const Fraction & f) const
{
    Fraction r;
    r.num = f.den*num - den*f.num;
    r.den = f.den*den;
    r.normalise();
    return r;
} 

Fraction Fraction::operator*(const Fraction & f) const
{
    Fraction r;
    r.num = f.num*num;
    r.den = f.den*den;
    r.normalise();
    return r;
} 

Fraction Fraction::operator/(const Fraction & f) const
{
    Fraction r;
    r.num = f.den*num;
    r.den = f.num*den;
    r.normalise();
    return r;
}
  • Fichier main.cpp
#include <iostream>
using namespace std;

#include "Fraction.h"

int main()
{
    Fraction f1, f2, f3, f4, E;
    cout << "SAISIE de f1 " << endl;  cin >> f1;
    cout << "SAISIE de f2 " << endl;  cin >> f2;

    f3 = Fraction(3, 4);
    f4 = Fraction(5, 8);

    E = (f1 + f3 - f2) / (f1*f2 - f4) + 4;

    cout << "E = " << E << endl;

    return 0;
}

Dans la conception orientée objet, la généralisation consiste à modéliser des concepts communs à un ensemble d'autres concepts. Les autres concepts deviennent dès lors des spécialisations de la généralisation. Cette manière de modéliser s'appelle l'héritage, car les spécialisations héritent de la généralisation. En C++, les classes peuvent hériter d'autres classes et la relation d'héritage est exprimée à l'aide de l'opérateur de dérivation ":".

À partir d'une classe A, on peut créer une classe B qui possède toutes les caractéristiques de la classe A, à laquelle on ajoute un certain nombre de méthodes qui sont spécifiques à B. Cette notion orientée objet fondamentale s'appelle l'héritage.

On dit que :

  • la classe B hérite de la classe A ;
  • la classe B est une sous-classe de la classe A ;
  • la classe B spécialise la classe A ;
  • la classe B étend la classe A ;
  • la classe B dérive de la classe A (cette notation n'est toutefois pas appréciée de tous, mais a l'avantage d'éviter les ambiguïtés à propos de super classe et sous classe) ;
  • la classe A est une super-classe de la classe B ;
  • la classe A généralise la classe B.

On dit aussi que:

  • la classe A est une classe de base
  • la classe B est une classe dérivée


Il existe en fait trois types d'héritage public, private ou protected qui permet de spécifier si oui ou non une méthode de la classe B peut modifier une donnée membre de la classe A, selon qu'elle soit public, private ou protected.

class B : ''type_héritage'' A
{
    ''.......''
};

type_héritage est l'un des mots clés d'accès public, protected ou private. Les membres de A sont alors à la fois accessibles à la classe dérivée B et en dehors de ces classes selon la valeur utilisée pour type_héritage :

  • Quand type_héritage est public, les membres de la classe A conservent leur accès (privé, protégé et public).
  • Quand type_héritage est protected, les membres publics de la classe A deviennent protégés.
  • Quand type_héritage est private, les membres publics et protégés de la classe A deviennent privés.

Pour résumer, type_héritage ne peut que restreindre l'accès des membres de la classe A, ou le conserver.

  • Fichier VehiculeRoulant.h
class VehiculeRoulant
{
public:
    VehiculeRoulant();
    void avancer();
    void accelerer();
protected:
    int position, vitesse;
};
  • Fichier VehiculeRoulant.cpp
// Initialement à l'arrêt
VehiculeRoulant::VehiculeRoulant()
    : position(0)
    , vitesse(0)
{}

void VehiculeRoulant::avancer()
{
    position += vitesse;
}

void VehiculeRoulant::accelerer()
{
    vitesse++;
}
  • Fichier Automobile.h
class Automobile : public VehiculeRoulant
{
public:
    Automobile(int places);
protected:
    int m_places;
};
  • Fichier Automobile.cpp
Automobile::Automobile(int places)
    : VehiculeRoulant()   // <--- pour initialiser les variables héritées
                          //      position et vitesse
    , m_places(places)    // initialiser le nombre de places
{}

L'appel explicite au constructeur de la classe de base dans cet exemple n'est pas nécessaire car par défaut, le constructeur (sans paramètres) est appelé. Dans le cas où il est nécessaire d'appeler un autre constructeur, il faut le faire dans la liste d'initialisation du constructeur de la classe dérivée.

Méthodes virtuelles

[modifier | modifier le wikicode]

Dans l'exemple précédent, si l'on ajoute une autre méthode à la classe VehiculeRoulant :

  • Fichier VehiculeRoulant.h
class VehiculeRoulant
{
public:
    ...
    void accelererEtAvancer();
    ...
};
  • Fichier VehiculeRoulant.cpp
void VehiculeRoulant::accelererEtAvancer()
{
    accelerer();
    avancer();
}

et que l'on modifie la façon d'avancer d'une automobile :

  • Fichier Automobile.h
class Automobile : public VehiculeRoulant
{
public:
    ...
    void avancer();
    ...
};
  • Fichier Automobile.cpp
void Automobile::avancer()
{
    position += vitesse - frottementRoues;
}

Alors si on utilise la méthode accelererEtAvancer() sur un objet de classe Automobile, le résultat sera incorrect, car la méthode avancer() appelée sera celle définie par la classe VehiculeRoulant.

Pour que ce soit celle de la classe Automobile qui soit appelée, il faut que la méthode avancer() soit définie comme virtuelle dans la classe de base VehiculeRoulant :

  • Fichier VehiculeRoulant.h
class VehiculeRoulant
{
public:
    ...
    virtual void avancer();
    ...
};
  • Fichier VehiculeRoulant.cpp
...

virtual void VehiculeRoulant::avancer()
{
    position += vitesse;
}

...

Le mot clé virtual indique une méthode virtuelle, c'est-à-dire que son adresse n'est pas fixe, mais dépend de la classe de l'objet (le compilateur construit une table interne des méthodes virtuelles pour chaque classe). Il est possible d'accéder à une méthode précise de la table des méthodes virtuelles, en indiquant la classe de la méthode à appeler. Par exemple, pour appeler la méthode avancer() de VehiculeRoulant à partir d'une méthode de automobile:

void Automobile::avancer()
{
    VehiculeRoulant::avancer();

    position -= frottementRoues;
}

Destructeur virtuel

[modifier | modifier le wikicode]

Il est important d'utiliser un destructeur virtuel pour toute classe qui sera dérivée. Dans le cas contraire seul celui de la classe de base est appelé, sans libérer les membres des sous-classes.

Ne pas déclarer un destructeur comme virtuel interdit donc de dériver cette classe.

Héritage multiple

[modifier | modifier le wikicode]

L'héritage multiple permet à une classe d'hériter de plusieurs super-classes. Ceci permet d'intégrer dans la sous-classe plusieurs concept d'abstractions qui caractérisent cette sous-classe. Pour cela, il suffit de déclarer les super-classes les unes après les autres. Si une classe C hérite de A et de B, la syntaxe sera la suivante:

class C : ''type_héritage'' A, ''type_héritage'' B ''...''
{
    ......
};

Par exemple, si l'on possède une classe décrivant un véhicule roulant, et une classe décrivant un navire, on pourrait créer une classe décrivant un véhicule amphibie qui hériterait des propriétés et méthodes de véhicule roulant et de navire.

L'héritage multiple peut poser des problèmes de définitions multiples de méthodes virtuelles. Supposons que l'on ait une classe Base ayant les sous-classes H1 et H2 qui en dérivent. Si l'on crée une classe Finale héritant à la fois de H1 et de H2, alors, les instanciations des objets de classe provoqueront des appels successifs des constructeurs. D'abord Finale::Finale(), puis pour ses parents H1::H1() et H2::H2(), puis, le parent de H1 Base::Base(), et enfin Base:Base() pour le parent de H2. On remarque alors que le constructeur de la classe Base est appelé à toute instanciation d'un objet de classe Finale. En C++, il existe un moyen d'éviter cela et de ne faire appel au constructeur de Base qu'une seule fois. Pour cela, il suffit d'indiquer lors des héritages le mot-clé virtual pour indiquer au compilateur que les constructeurs des classes multi-héritées ne doivent être appelées qu'une seule fois.

class Base
{
    Base::Base()
    {
        cout << "+++ Construction de Base" << endl;
    }

    virtual Base::~Base()
    {
        cout << "--- Destruction de Base" << endl;
    }
};

class H1 : virtual public Base
{
    H1::H1() : Base()
    {
        cout << "+++ Construction de H1" << endl;
    }

    virtual H1::~H1()
    {
        cout << "--- Destruction de H1" << endl;
    }
};

class H2 : virtual public Base
{
    H2::H2() : Base()
    {
        cout << "+++ Construction de H2" << endl;
    }

    virtual H2::~H2()
    {
        cout << "--- Destruction de H2" << endl;
    }
};

class Finale : virtual public H1, virtual public H2
{
    Finale::Finale() : H1(), H2()
    {
        cout << "+++ Construction de Finale" << endl;
    }

    Finale::~Finale()
    {
        cout << "--- Destruction de Finale" << endl;
    }
};

Classes abstraites

[modifier | modifier le wikicode]

Une classe abstraite est une classe pour laquelle on a défini une méthode mais on a explicitement indiqué qu'on ne fournira aucune implémentation de cette méthode. Il est interdit de créer une instance d'une classe abstraite. Ce mécanisme est extrêmement puissant pour manipuler des concepts abstraits. On peut même avoir une classe pour laquelle toutes les méthodes sont abstraites, on parle alors de classe abstraite pure.

Une classe héritant d'une classe abstraite doit fournir une implémentation des méthodes abstraites ou bien elle sera à son tour une classe abstraite.

virtual type nom_méthode(paramètres)=0;

Une telle méthode définie uniquement dans une sous-classe est nécessairement virtuelle. Le mot-clé virtual est donc optionnel.

type nom_méthode(paramètres)=0; // Méthode d'une sous classe encore abstraite

type nom_méthode(paramètres); // Méthode d'une sous classe plus abstraite
class Animal
{
public:
    // méthode à implémenter par les sous-classes
    virtual cstring cri() = 0;
}

Pointeur de membre

[modifier | modifier le wikicode]

Un pointeur de membre pointe un membre d'un objet (variable ou méthode).

Ce genre de pointeur, rarement utilisé, possède une syntaxe spéciale. L'étoile * est remplacée par class::* signifiant un pointeur sur un membre de cette classe, ou d'une classe dérivée. Ce genre de pointeur occupe plus de place qu'un pointeur classique, et occupe un nombre d'octets variable selon la classe.

int CBox::* pBoxInt;
// pBoxInt pointe un entier dans les objets de la classe CBox

L'utilisation d'un tel pointeur nécessite un objet de la classe concernée (ou classe dérivée). L'opérateur de déréférencement * est alors remplacé par objet.* ou bien par pointeur_objet->*.

pBoxInt = &CBox::length;  // désigne l'adresse du membre length de la classe

CBox mybox(10, 12, 15);
cout << "length = " << mybox.*pBoxInt;

CBox* pbox = new CBox(20, 24, 30);
cout << "length = " << pbox->*pBoxInt;