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.
- public inheritance: base public members remain public in the derived class (most common).
- protected inheritance: base public members become protected in the derived class (hidden from outside).
- private inheritance: base public/protected members become private in the derived class.
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.
virtualmethods and theoverridekeyword 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_castfor safe downcasting (returnsnullptron failure). - Abstract base classes (pure virtual) cannot be instantiated and enforce contracts.
- Examples are compatible with Visual Studio 2022 and GCC 11+.