Chargement...

Notions d’héritage en C++

Apprenez les bases de l’héritage en C++, avec les classes de base, dérivées et les niveaux d’accès.

L’héritage (inheritance) permet à une classe d’hériter des propriétés et du comportement d’une autre classe. Cela réduit la duplication de code, permet une organisation hiérarchique des classes et rend le polymorphisme possible. Dans cet article, nous allons examiner une classe de base Book et ses classes dérivées EBook et PrintedBook, ainsi que les notions de chaîne de constructeurs, de fonctions virtuelles, de override, de destructeur virtuel, de upcasting/downcasting et de classe de base abstraite.


1. Classe de base : Book

Nous regroupons les champs et comportements communs dans la classe Book. Les classes dérivées les héritent et peuvent les personnaliser si nécessaire.


#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;

class Book {
protected: // accessible par les classes dérivées
    string title;
    string author;
    double basePrice;
    int pageCount;

public:
    Book(string t, string a, double price, int pages)
        : title(t), author(a), basePrice(price), pageCount(pages) {
        // Initialisation commune
    }

    // Destructeur virtuel : requis pour la suppression polymorphique
    virtual ~Book() = default;

    // Méthode virtuelle : les sous-classes peuvent personnaliser le calcul du prix
    virtual double GetFinalPrice() const {
        return basePrice;
    }

    // Méthode virtuelle : les sous-classes peuvent afficher plus d’informations
    virtual void PrintInfo() const {
        cout << "[Book] " << title
             << " | Auteur : " << author
             << " | Pages : " << pageCount
             << " | Prix : " << GetFinalPrice() << " €" << endl;
    }
};

Les membres protected sont inaccessibles depuis l’extérieur mais accessibles dans les sous-classes. Le destructeur virtuel est indispensable pour une suppression sûre via un pointeur de base.


2. Classe dérivée : EBook

Les livres électroniques n’ont pas de coût d’impression, mais peuvent inclure la TVA, des remises ou des frais de licence. Personnalisons le calcul du prix.


class EBook : public Book {
    double vatRate;      // TVA (ex. 0.10 = 10 %)
    bool drmProtected;   // Gestion des droits numériques (DRM)

public:
    EBook(string t, string a, double price, int pages,
          double vat, bool drm)
        : Book(t, a, price, pages), vatRate(vat), drmProtected(drm) {}

    double GetFinalPrice() const override {
        double priceWithVat = basePrice * (1.0 + vatRate);
        // Exemple : réduction de 5 % pour les livres de plus de 500 pages
        if (pageCount >= 500) priceWithVat *= 0.95;
        return priceWithVat;
    }

    void PrintInfo() const override {
        cout << "[EBook] " << title
             << " | DRM : " << (drmProtected ? "Oui" : "Non")
             << " | Prix final : " << GetFinalPrice() << " €" << endl;
    }
};

L’utilisation de override permet au compilateur de détecter toute incompatibilité de signature.


3. Classe dérivée : PrintedBook

Les livres imprimés ont des coûts supplémentaires (papier et expédition). Appliquons des multiplicateurs différents selon le type de couverture (souple ou rigide).


enum class Cover { Soft, Hard };

class PrintedBook : public Book {
    Cover coverType;
    double shipping;   // Frais de livraison
    double printCost;  // Coût d’impression par page

public:
    PrintedBook(string t, string a, double price, int pages,
                Cover cover, double shippingCost, double printPerPage)
        : Book(t, a, price, pages),
          coverType(cover), shipping(shippingCost), printCost(printPerPage) {}

    double GetFinalPrice() const override {
        double cost = basePrice + shipping + (printCost * pageCount);
        if (coverType == Cover::Hard) {
            cost *= 1.10; // +10 % pour la couverture rigide
        }
        return cost;
    }

    void PrintInfo() const override {
        cout << "[PrintedBook] " << title
             << " | Couverture : " << (coverType == Cover::Hard ? "Rigide" : "Souple")
             << " | Prix final : " << GetFinalPrice() << " €" << endl;
    }
};

La chaîne de constructeurs appelle d’abord le constructeur de la classe de base (Book), puis initialise les membres spécifiques à PrintedBook.


4. Polymorphisme : Upcasting et appels virtuels

Nous pouvons stocker des objets dérivés dans des pointeurs ou références de type base et appeler des fonctions virtuelles (polymorphisme dynamique).


int main() {
    vector<unique_ptr<Book>> library;
    library.push_back(make_unique<EBook>(
        "C++ 20 Guide", "J. Doe", 100.0, 520, 0.10, true));
    library.push_back(make_unique<PrintedBook>(
        "Clean Code", "R. Martin", 150.0, 464, Cover::Hard, 20.0, 0.05));

    for (const auto &bk : library) {
        // Appel virtuel via un pointeur Book*
        bk->PrintInfo();
    }
}

Grâce à unique_ptr<Book>, la gestion de la mémoire est automatique ; aucun delete n’est nécessaire.


5. Downcasting : utilisation de dynamic_cast

Parfois, il est nécessaire de revenir du type de base au type dérivé. Pour un transtypage sûr, on utilise dynamic_cast.


void TryPrintDRM(const Book* b) {
    if (auto eb = dynamic_cast<const EBook*>(b)) {
        cout << "Vérification de l’état DRM de l’EBook." << endl;
    } else {
        cout << "Ce n’est pas un EBook." << endl;
    }
}

Si dynamic_cast échoue, il retourne nullptr (dans le cas d’un pointeur). Cela garantit la sécurité des types. Il nécessite que le RTTI soit activé et qu’une table virtuelle soit présente.


6. Classe de base abstraite

Une classe contenant au moins une fonction virtuelle pure est abstraite et ne peut pas être instanciée directement. En rendant Book abstraite, nous pouvons imposer un contrat clair dans l’architecture.


class AbstractBook {
protected:
    string title, author;
    int pageCount;

public:
    AbstractBook(string t, string a, int p)
        : title(t), author(a), pageCount(p) {}
    virtual ~AbstractBook() = default;

    virtual double GetFinalPrice() const = 0; // virtuelle pure
    virtual void PrintInfo() const = 0;
};

// La classe dérivée doit obligatoirement redéfinir les fonctions virtuelles pures
class SimpleEBook : public AbstractBook {
    double basePrice, vat;
public:
    SimpleEBook(string t, string a, int p, double price, double v)
        : AbstractBook(t,a,p), basePrice(price), vat(v) {}

    double GetFinalPrice() const override { return basePrice*(1+vat); }
    void PrintInfo() const override {
        cout << "[SimpleEBook] " << title
             << " | Prix : " << GetFinalPrice() << endl;
    }
};

Une classe de base abstraite favorise un design orienté contrat (similaire à une interface).


7. Modificateurs d’accès et types d’héritage

En C++, il existe trois types d’héritage : public, protected et private.

En pratique, on utilise le plus souvent l’héritage public (relation « est-un » / is-a).


8. Les mots-clés final et override

Le mot-clé override indique au compilateur qu’une méthode de la classe dérivée redéfinit réellement une méthode virtuelle. Le mot-clé final empêche toute redéfinition ultérieure d’une méthode ou d’une classe.


class FinalEBook : public EBook {
public:
    using EBook::EBook;
    double GetFinalPrice() const override final {
        return EBook::GetFinalPrice(); // ne peut plus être redéfini
    }
};

9. Exemple complet d’application


#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;

enum class Cover { Soft, Hard };

class Book {
protected:
    string title, author;
    double basePrice;
    int pageCount;

public:
    Book(string t, string a, double price, int pages)
        : title(t), author(a), basePrice(price), pageCount(pages) {}
    virtual ~Book() = default;

    virtual double GetFinalPrice() const { return basePrice; }
    virtual void PrintInfo() const {
        cout << "[Book] " << title
             << " | " << author
             << " | " << pageCount << " pages"
             << " | " << GetFinalPrice() << " €" << endl;
    }
};

class EBook : public Book {
    double vatRate; bool drmProtected;
public:
    EBook(string t, string a, double price, int pages, double vat, bool drm)
        : Book(t,a,price,pages), vatRate(vat), drmProtected(drm) {}
    double GetFinalPrice() const override {
        double x = basePrice*(1+vatRate);
        if (pageCount >= 500) x *= 0.95;
        return x;
    }
    void PrintInfo() const override {
        cout << "[EBook] " << title
             << " | DRM : " << (drmProtected? "Oui":"Non")
             << " | " << GetFinalPrice() << " €" << endl;
    }
};

class PrintedBook : public Book {
    Cover coverType; double shipping; double printCost;
public:
    PrintedBook(string t, string a, double price, int pages,
                Cover cover, double ship, double perPage)
        : Book(t,a,price,pages), coverType(cover),
          shipping(ship), printCost(perPage) {}
    double GetFinalPrice() const override {
        double c = basePrice + shipping + printCost*pageCount;
        if (coverType == Cover::Hard) c *= 1.10;
        return c;
    }
    void PrintInfo() const override {
        cout << "[PrintedBook] " << title
             << " | Couverture : " << (coverType==Cover::Hard?"Rigide":"Souple")
             << " | " << GetFinalPrice() << " €" << endl;
    }
};

int main() {
    vector<unique_ptr<Book>> books;
    books.push_back(make_unique<EBook>("Modern C++", "A. Dev", 120.0, 520, 0.10, true));
    books.push_back(make_unique<PrintedBook>("Clean Code", "R. Martin", 150.0, 464, Cover::Hard, 25.0, 0.04));

    for (const auto& b : books) b->PrintInfo(); // appel polymorphique

    // Tentative de downcast sécurisé
    if (auto pb = dynamic_cast<PrintedBook*>(books[1].get())) {
        cout << "Prix du PrintedBook : " << pb->GetFinalPrice() << endl;
    }
}

Ce code peut être compilé et exécuté avec Visual Studio 2022 ou GCC 11+.


10. TL;DR

  • L’héritage permet de regrouper le comportement commun dans la classe de base et de le spécialiser dans les classes dérivées.
  • Les méthodes virtual et le mot-clé override assurent le polymorphisme dynamique.
  • Sans destructeur virtuel, la suppression via un pointeur de base est dangereuse.
  • Chaîne de constructeurs : le constructeur de base s’exécute avant celui de la classe dérivée.
  • Le dynamic_cast permet un downcasting sûr (retourne nullptr en cas d’échec).
  • Une classe de base abstraite (pure virtual) ne peut pas être instanciée et impose un contrat.
  • Les exemples sont compatibles avec Visual Studio 2022 et GCC 11+.

Articles connexes