Loading...

Basics of Inheritance in C++

Learn the basics of inheritance in C++, including base and derived classes, access specifiers, and code reuse.

Inheritance allows a class to acquire the properties and behaviors of another class. This reduces code duplication, enables hierarchical organization of classes, and makes polymorphism possible. In this article, we’ll examine a Book base class with derived classes EBook and PrintedBook, along with constructor chaining, virtual functions, override, virtual destructors, upcasting/downcasting, and abstract base classes.


1. Base Class: Book

We gather common fields and behaviors in the Book class. Subclasses inherit and customize them when needed.


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

class Book {
protected: // use protected so derived classes can access these
    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) {
        // Common initialization
    }

    // Virtual destructor: required when deleting derived objects via base pointers
    virtual ~Book() = default;

    // Virtual method: derived classes can customize price calculation
    virtual double GetFinalPrice() const {
        return basePrice;
    }

    // Virtual method: derived classes can print extra information
    virtual void PrintInfo() const {
        cout << "[Book] " << title
             << " | Author: " << author
             << " | Pages: " << pageCount
             << " | Price: " << GetFinalPrice() << " TL" << endl;
    }
};

protected members are hidden from the outside but accessible by subclasses. A virtual destructor is mandatory for safe polymorphic deletion.


2. Derived Class: EBook

E-books have no printing cost; but VAT, discounts, or licensing fees may apply. Let’s customize the pricing.


class EBook : public Book {
    double vatRate;      // VAT (e.g., 0.10 = 10%)
    bool drmProtected;   // Digital Rights Management

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);
        // Example promo for e-books: if pages >= 500, apply 5% discount
        if (pageCount >= 500) priceWithVat *= 0.95;
        return priceWithVat;
    }

    void PrintInfo() const override {
        cout << "[EBook] " << title
             << " | DRM: " << (drmProtected ? "Yes" : "No")
             << " | Final Price: " << GetFinalPrice() << " TL" << endl;
    }
};

Using override catches signature mismatches at compile time for safety.


3. Derived Class: PrintedBook

Printed books may have paper and shipping costs. Apply different multipliers based on cover type (soft/hard).


enum class Cover { Soft, Hard };

class PrintedBook : public Book {
    Cover coverType;
    double shipping;   // shipping fee
    double printCost;  // per-page printing cost

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; // hard cover adds 10%
        }
        return cost;
    }

    void PrintInfo() const override {
        cout << "[PrintedBook] " << title
             << " | Cover: " << (coverType == Cover::Hard ? "Hard" : "Soft")
             << " | Final Price: " << GetFinalPrice() << " TL" << endl;
    }
};

The constructor chain first calls the base class constructor (Book), then initializes PrintedBook-specific fields.


4. Polymorphism: Upcasting and Virtual Calls

We can hold derived objects via base-type pointers or references and call virtual functions (dynamic polymorphism).


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) {
        // Virtual call through Book* → executes according to the actual derived type
        bk->PrintInfo();
    }
}

With unique_ptr<Book>, memory management is automatic; no delete needed.


5. Downcasting: Using dynamic_cast

Sometimes we need to get back to the actual derived type from a base reference. Use dynamic_cast for safe casts.


void TryPrintDRM(const Book* b) {
    if (auto eb = dynamic_cast<const EBook*>(b)) {
        cout << "Checked EBook DRM status." << endl;
    } else {
        cout << "Not an EBook." << endl;
    }
}

If dynamic_cast fails (with pointers), it returns nullptr. This preserves type safety. It requires RTTI and a proper virtual table.


6. Abstract Base Class

Classes with at least one pure virtual function are abstract and cannot be instantiated directly. By abstracting Book, we can enforce a contract across the 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; // pure virtual → makes the class abstract
    virtual void PrintInfo() const = 0;
};

// Derived class must override all pure virtuals
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
             << " | Price: " << GetFinalPrice() << endl;
    }
};

An abstract base class provides contract-driven design (interface-like) in the project.


7. Access Specifiers and Inheritance Types

Inheritance can be public, protected, or private.

In practice, public inheritance is typically preferred (an “is-a” relationship).


8. final and override Keywords

override tells the compiler a virtual method is actually being overridden. final prevents further overriding of a method or class.


class FinalEBook : public EBook {
public:
    using EBook::EBook;
    double GetFinalPrice() const override final {
        return EBook::GetFinalPrice(); // cannot be overridden further
    }
};

9. Complete Example


#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() << " TL" << 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? "Yes":"No")
             << " | " << GetFinalPrice() << " TL" << 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
             << " | Cover: " << (coverType==Cover::Hard?"Hard":"Soft")
             << " | " << GetFinalPrice() << " TL" << 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(); // polymorphic call

    // Safe downcast attempt
    if (auto pb = dynamic_cast<PrintedBook*>(books[1].get())) {
        cout << "PrintedBook price: " << pb->GetFinalPrice() << endl;
    }
}

The code compiles and runs on Visual Studio 2022 and GCC 11+.


10. TL;DR

  • With inheritance, we centralize common behavior in the base class and customize in derived classes.
  • virtual methods and the override keyword enable dynamic polymorphism.
  • Without a virtual destructor, deleting via a base pointer is unsafe.
  • Constructor chain: base constructor runs first, then the derived constructor.
  • Use dynamic_cast for safe downcasting (returns nullptr on failure).
  • Abstract base classes (pure virtual) cannot be instantiated and enforce contracts.
  • Examples are compatible with Visual Studio 2022 and GCC 11+.

Related Articles