In this guide, we’ll explore inheritance in C++, one of the fundamental pillars of object-oriented programming. We’ll cover everything from basic concepts to advanced techniques, with practical examples and best practices that you can apply in your own code.
Table of Contents
Introduction
Inheritance is a core concept in object-oriented programming that allows us to create new classes based on existing ones. This mechanism promotes code reuse by enabling us to define shared behaviour in a base class and extend or customize it in derived classes. It establishes relationships between classes that share common characteristics, forming a hierarchy that reflects real-world relationships.
Think of inheritance like a family tree: a child inherits traits from their parents, such as eye color or height. Similarly, in programming, a derived class inherits properties and behaviours from its base class, but it can also introduce unique characteristics. For example, a “Vehicle” class might define general attributes like speed or capacity, and a “Car” class can inherit these while adding features specific to cars, like the number of doors.
Inheritance promotes efficiency by enabling shared characteristics to be defined once and reused across related classes, much like documenting common family traits without repeating them for every individual. However, it’s essential to apply inheritance thoughtfully to maintain simplicity and avoid unnecessary complexity in your designs.
💡 Key Insight: While inheritance is powerful, remember that composition is often a better choice for many design scenarios. Use inheritance when you truly have an “is-a” relationship between classes, such as “Car is a Vehicle,” but consider composition when the relationship is “has-a,” such as “Car has an Engine.”
Basic Inheritance Concepts
Inheritance allows us to create a new class (a derived class) that is based on an existing class (a base class), reusing its attributes and behaviours while enabling specific customizations. This example demonstrates how inheritance works in C++ through a base class Animal
and a derived class Dog
.
The base class Animal
encapsulates common properties such as name
and age
, and provides a generic implementation of the makeSound()
method. The derived class Dog
inherits these properties and behaviours but overrides the makeSound()
method to provide a more specific behaviour for a dog.
#include <iostream>
#include <string>
// Base class representing a generic animal
class Animal {
protected:
std::string name; // Name of the animal
int age; // Age of the animal
public:
// Constructor to initialize the name and age
Animal(const std::string& name, int age)
: name(name), age(age) {}
// Virtual function to make a sound
// Can be overridden by derived classes
virtual void makeSound() const {
std::cout << "Some generic animal sound" << std::endl;
}
// Function to display the animal's information
void displayInfo() const {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
// Derived class representing a Dog, inheriting from Animal
class Dog : public Animal {
public:
// Constructor to initialize the Dog using the Animal constructor
Dog(const std::string& name, int age)
: Animal(name, age) {}
// Override the makeSound function to provide a specific behaviour for dogs
void makeSound() const override {
std::cout << "Woof! Woof!" << std::endl;
}
};
int main() {
// Create an instance of Animal
Animal genericAnimal("Generic", 5);
// Create an instance of Dog
Dog rover("Rover", 3);
// Display information and sound for the generic animal
std::cout << "Generic Animal:" << std::endl;
genericAnimal.displayInfo();
genericAnimal.makeSound();
// Display information and sound for the dog
std::cout << "\nDog:" << std::endl;
rover.displayInfo();
rover.makeSound();
return 0;
}
Name: Generic, Age: 5
Some generic animal sound
Dog:
Name: Rover, Age: 3
Woof! Woof!
Explanation:
- The
Animal
Class: This class defines the shared propertiesname
andage
, and a generic behaviourmakeSound()
. ThedisplayInfo()
method is also defined to print the animal’s details. - The
Dog
Class: This class inherits fromAnimal
, reusing itsname
andage
properties, as well as thedisplayInfo()
method. However, it overrides themakeSound()
method to provide a dog-specific implementation (“Woof! Woof!”). - In the
main()
Function:- An instance of
Animal
is created with generic details and calls its methods, producing a generic output. - An instance of
Dog
is created, which inherits thedisplayInfo()
method but uses its own implementation ofmakeSound()
, demonstrating polymorphism.
- An instance of
Why It Matters: This example showcases how inheritance promotes code reuse and allows for method overriding to enable specific behaviours in derived classes. It demonstrates the “is-a” relationship, where a Dog
is an Animal
, making the system extensible for new animal types in the future.
Types of Inheritance
C++ supports five types of inheritance, each serving different design purposes:
- Single Inheritance: A derived class inherits from a single base class.
- Multiple Inheritance: A derived class inherits from two or more base classes.
- Hierarchical Inheritance: Multiple derived classes inherit from a single base class.
- Multilevel Inheritance: A class is derived from another derived class.
- Hybrid Inheritance: A combination of multiple inheritance and other types.
Let’s explore single and multiple inheritance with practical examples:
#include <iostream>
// Base class providing a basic value and functionality
class Base {
protected:
int value; // A protected integer that derived classes can access
public:
// Constructor to initialize the value
Base(int v) : value(v) {}
// Method to display the current value
void display() const {
std::cout << "Value: " << value << std::endl;
}
};
// Single Inheritance: Derived inherits from Base
class Derived : public Base {
public:
// Constructor to initialize the base class
Derived(int v) : Base(v) {}
// Method to multiply the value by a given factor
void multiply(int factor) {
value *= factor;
}
};
// Multiple Inheritance example
// Engine class represents a car's engine type
class Engine {
protected:
std::string type; // Type of the engine (e.g., V8, Electric)
public:
// Constructor to initialize the engine type
Engine(const std::string& t) : type(t) {}
// Method to simulate starting the engine
void start() {
std::cout << type << " engine starting..." << std::endl;
}
};
// Vehicle class represents a general vehicle
class Vehicle {
protected:
std::string brand; // Brand of the vehicle (e.g., Toyota)
public:
// Constructor to initialize the vehicle's brand
Vehicle(const std::string& b) : brand(b) {}
// Method to display the vehicle's brand
void displayBrand() {
std::cout << "Brand: " << brand << std::endl;
}
};
// Car class inherits from both Engine and Vehicle
class Car : public Engine, public Vehicle {
public:
// Constructor to initialize both the engine and vehicle attributes
Car(const std::string& type, const std::string& brand)
: Engine(type), Vehicle(brand) {}
// Method to display car info by combining Engine and Vehicle behaviour
void info() {
displayBrand();
start();
}
};
int main() {
// Single inheritance example
Derived d(5); // Create an object of Derived with value 5
d.display(); // Output the initial value
d.multiply(3); // Multiply value by 3
d.display(); // Output the updated value
std::cout << "\n";
// Multiple inheritance example
Car car("V8", "Toyota"); // Create a Car object with a V8 engine and Toyota brand
car.info(); // Display car information (brand and engine type)
return 0;
}
Value: 15
Brand: Toyota
V8 engine starting…
Explanation:
- Single Inheritance:
- The
Derived
class inherits from theBase
class. - The
Base
class provides a propertyvalue
and a methoddisplay()
. - The
Derived
class extends the behaviour ofBase
by adding themultiply()
method, which modifiesvalue
.
- The
- Multiple Inheritance:
- The
Car
class inherits from both theEngine
andVehicle
classes. - The
Engine
class provides a methodstart()
, while theVehicle
class providesdisplayBrand()
. - The
Car
class combines these behaviours in theinfo()
method, which displays the brand and starts the engine.
- The
Why It Matters: These examples illustrate how inheritance enables code reuse and modular design. Single inheritance is simpler and avoids potential conflicts, while multiple inheritance is powerful but requires careful management to resolve ambiguities when inheriting from multiple sources.
Other types, such as hierarchical, multilevel, and hybrid inheritance, are less commonly used. They often increase code complexity and are best applied in specific scenarios where they are absolutely necessary.
Virtual Functions and Polymorphism
Virtual functions are essential for enabling runtime polymorphism in C++, allowing derived classes to override base class methods. This means that the behaviour of a method is determined dynamically at runtime, rather than statically at compile time. Here’s a practical example to illustrate this concept:
#include <iostream>
#include <vector>
#include <memory>
// Abstract base class representing a general shape
class Shape {
public:
virtual ~Shape() = default; // Virtual destructor for proper cleanup
virtual double area() const = 0; // Pure virtual function to compute area
virtual void draw() const = 0; // Pure virtual function to draw the shape
};
// Derived class representing a Circle
class Circle : public Shape {
private:
double radius; // Radius of the circle
public:
// Constructor to initialize the radius
Circle(double r) : radius(r) {}
// Override the area function to compute the circle's area
double area() const override {
return 3.14159 * radius * radius;
}
// Override the draw function to display a circle-specific message
void draw() const override {
std::cout << "Drawing a circle" << std::endl;
}
};
// Derived class representing a Rectangle
class Rectangle : public Shape {
private:
double width; // Width of the rectangle
double height; // Height of the rectangle
public:
// Constructor to initialize width and height
Rectangle(double w, double h) : width(w), height(h) {}
// Override the area function to compute the rectangle's area
double area() const override {
return width * height;
}
// Override the draw function to display a rectangle-specific message
void draw() const override {
std::cout << "Drawing a rectangle" << std::endl;
}
};
int main() {
// Vector to store unique pointers to Shape objects
std::vector<std::unique_ptr<Shape>> shapes;
// Add a Circle object to the vector
shapes.push_back(std::make_unique<Circle>(5));
// Add a Rectangle object to the vector
shapes.push_back(std::make_unique<Rectangle>(4, 6));
// Iterate through each shape in the vector
for (const auto& shape : shapes) {
shape->draw(); // Calls the appropriate draw() method based on the object type
std::cout << "Area: " << shape->area() << std::endl; // Outputs the area
}
return 0;
}
Area: 78.5397
Drawing a rectangle
Area: 24
Explanation:
- Base Class (
Shape
):- The
Shape
class declares two pure virtual functions:area()
anddraw()
. This makesShape
an abstract class, meaning you cannot instantiate it directly. - The virtual destructor ensures proper cleanup of derived class objects, preventing resource leaks.
- The
- Derived Classes (
Circle
andRectangle
):- Both classes inherit from
Shape
and provide specific implementations for thearea()
anddraw()
methods. - The
Circle
class calculates the area using the formula for the area of a circle, while theRectangle
class uses the formula for a rectangle. - The
draw()
method outputs a message indicating the shape being drawn.
- Both classes inherit from
- Polymorphism in Action:
- The
shapes
vector holdsstd::unique_ptr
objects pointing toShape
instances. Even though the vector is of typeShape
, the overridden methods of the derived classes are called dynamically. - This demonstrates runtime polymorphism, where the correct
draw()
andarea()
methods are called based on the actual type of the object stored in the vector.
- The
Why It Matters: Virtual functions allow for flexible and extensible designs by enabling runtime behaviour changes. This is particularly useful in systems with heterogeneous objects, such as graphical applications, where the behaviour of an object depends on its type but must be handled uniformly.
💡 Best Practice: Always make destructors virtual in base classes that are meant to be inherited from. This ensures proper cleanup of derived class objects through base class pointers, preventing memory leaks or incomplete destruction.
Best Practices and Common Pitfalls
When working with inheritance in C++, keep these best practices in mind:
- Use virtual destructors in base classes when inheritance is intended
- Prefer composition to inheritance when there isn’t a clear “is-a” relationship
- Follow the Liskov Substitution Principle – derived classes should be substitutable for their base classes
- Avoid deep inheritance hierarchies – they can make code harder to understand and maintain
- Use
override
keyword for virtual function implementations in derived classes
#include <iostream>
// Pitfall 1: Missing virtual destructor
class Base {
public:
Base() { std::cout << "Base constructor\n"; }
~Base() { std::cout << "Base destructor\n"; } // Should be virtual!
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[100]) {
std::cout << "Derived constructor\n";
}
~Derived() {
delete[] data; // Proper cleanup of allocated memory
std::cout << "Derived destructor\n";
}
};
// Pitfall 2: Hiding base class methods
class Parent {
public:
// A method that takes one argument
void display(int x) {
std::cout << "Parent: " << x << std::endl;
}
};
class Child : public Parent {
public:
// A method with the same name but different arguments
// This hides the Parent::display(int) method
void display(int x, int y) {
std::cout << "Child: " << x << ", " << y << std::endl;
}
};
int main() {
// Pitfall 1: Memory leak due to missing virtual destructor
Base* ptr = new Derived(); // Polymorphic behaviour
delete ptr; // Only calls the Base destructor, causing a memory leak
std::cout << "\n";
// Pitfall 2: Method hiding
Child child;
child.display(5, 10); // Calls Child::display(int, int)
// child.display(5); // Error: Parent::display(int) is hidden
return 0;
}
Derived constructor
Base destructor
Child: 5, 10
Explanation:
- Pitfall 1: Missing virtual destructor
- The destructor of the base class
Base
is not virtual, which causes undefined behaviour when a derived class object is deleted through a base class pointer. - In this example, deleting
ptr
(of typeBase*
) only calls the destructor ofBase
, leaving the dynamically allocateddata
inDerived
undeleted, causing a memory leak. - To fix this, declare the destructor of
Base
asvirtual
.
- The destructor of the base class
- Pitfall 2: Hiding base class methods
- The
Child
class defines a methoddisplay(int, int)
, which hides thedisplay(int)
method inParent
. - As a result, calling
child.display(5)
results in a compilation error because thedisplay(int)
method is no longer visible. - To fix this, explicitly bring the base class method into scope in the derived class using
using Parent::display;
.
- The
#include <iostream>
// Fixing Pitfall 2: Using `using` to avoid method hiding
class Parent {
public:
void display(int x) {
std::cout << "Parent: " << x << std::endl;
}
};
class Child : public Parent {
public:
using Parent::display; // Bring Parent::display(int) into Child's scope
void display(int x, int y) {
std::cout << "Child: " << x << ", " << y << std::endl;
}
};
int main() {
// Fix for Pitfall 2: Access both methods in Child
Child child;
child.display(5, 10); // Calls Child::display(int, int)
child.display(5); // Now correctly calls Parent::display(int)
return 0;
}
Derived constructor
Base destructor
Child: 5, 10
Fixed Pitfall 2:
- Used
using Parent::display;
to bring theParent::display(int)
method into theChild
class’s scope. - This ensures that both
Parent::display(int)
andChild::display(int, int)
are accessible without conflicts.
Practical Applications
Let’s look at a real-world example of using inheritance to build a flexible logging system:
#include <iostream>
#include <fstream>
#include <string>
#include <memory>
#include <vector>
// Base Logger class
// Provides a common interface for different types of loggers
class Logger {
public:
virtual ~Logger() = default; // Virtual destructor for proper cleanup
virtual void log(const std::string& message) = 0; // Pure virtual function for logging
};
// Console Logger: Logs messages to the console
class ConsoleLogger : public Logger {
public:
// Overrides the log method to print messages to the console
void log(const std::string& message) override {
std::cout << "[Console] " << message << std::endl;
}
};
// File Logger: Logs messages to a file
class FileLogger : public Logger {
private:
std::ofstream outFile; // File stream for writing logs
public:
// Constructor to initialize and open the log file
FileLogger(const std::string& fileName) {
outFile.open(fileName, std::ios::app);
if (!outFile.is_open()) {
throw std::ios_base::failure("Failed to open log file");
}
}
// Destructor ensures the file stream is properly closed
~FileLogger() {
if (outFile.is_open()) {
outFile.close();
}
}
// Overrides the log method to write messages to the file
void log(const std::string& message) override {
outFile << "[File] " << message << std::endl;
}
};
// Aggregated Logger: Combines multiple loggers
// Allows logging messages to multiple destinations simultaneously
class AggregatedLogger : public Logger {
private:
std::vector<std::unique_ptr<Logger>> loggers; // Collection of loggers
public:
// Adds a new logger to the collection
void addLogger(std::unique_ptr<Logger> logger) {
loggers.push_back(std::move(logger));
}
// Overrides the log method to forward messages to all loggers
void log(const std::string& message) override {
for (const auto& logger : loggers) {
logger->log(message);
}
}
};
int main() {
try {
// Create a console logger
auto consoleLogger = std::make_unique<ConsoleLogger>();
// Create a file logger with "log.txt" as the log file
auto fileLogger = std::make_unique<FileLogger>("log.txt");
// Create an aggregated logger to combine console and file logging
AggregatedLogger aggregatedLogger;
// Add the console and file loggers to the aggregated logger
aggregatedLogger.addLogger(std::move(consoleLogger));
aggregatedLogger.addLogger(std::move(fileLogger));
// Log messages using the aggregated logger
aggregatedLogger.log("Application started");
aggregatedLogger.log("Performing some tasks...");
aggregatedLogger.log("Application ended");
std::cout << "Logs written to console and file." << std::endl;
} catch (const std::exception& e) {
// Handle any exceptions during logging
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
[Console] Performing some tasks…
[Console] Application ended
Logs written to console and file:
File “log.txt” will also contain:
[File] Application started
[File] Performing some tasks…
[File] Application ended
Explanation:
- The
Logger
Base Class:- Defines the common interface for all types of loggers with the pure virtual method
log(const std::string& message)
. - Allows different logging mechanisms (e.g., console, file) to be implemented without changing the client code.
- Defines the common interface for all types of loggers with the pure virtual method
- The
ConsoleLogger
Class:- Implements the
log
method to print messages to the console usingstd::cout
. - Logs messages prefixed with
[Console]
.
- Implements the
- The
FileLogger
Class:- Implements the
log
method to write messages to a file. - Handles file management with an
std::ofstream
object. - Throws an exception if the file cannot be opened, ensuring robust error handling.
- Implements the
- The
AggregatedLogger
Class:- Combines multiple loggers into one by maintaining a collection of
Logger
objects usingstd::vector
. - Calls the
log
method on each contained logger, allowing messages to be logged to multiple destinations (e.g., console and file).
- Combines multiple loggers into one by maintaining a collection of
Why It Matters: This logging system demonstrates the power of inheritance and polymorphism for creating modular, scalable designs. It allows the addition of new loggers (e.g., a network logger) without modifying existing code. Such a design is invaluable in real-world applications requiring flexibility, such as server-side logging or distributed systems.
💡 Real-world Application: This pattern is commonly used in software systems that require flexible and scalable logging mechanisms, such as server-side applications, CLI tools, and large-scale distributed systems.
Conclusion
Inheritance is a key feature of C++ that enables code reuse, reduces redundancy, and supports polymorphism. When used thoughtfully, it leads to maintainable and extensible designs. However, misuse can result in unnecessary complexity, so consider alternatives like composition when an “is-a” relationship isn’t clear.
By following best practices, such as using virtual destructors and avoiding deep hierarchies, inheritance can be a powerful tool for building flexible and adaptable systems.
Further Reading
-
Online C++ Compiler
Dive deeper into the concepts from this blog post with our free online C++ compiler. Experiment with the examples discussed here, such as inheritance, virtual functions, and polymorphism. Modify and test the code snippets directly in your browser, exploring single and multiple inheritance, or creating and extending logging systems. This interactive practice will reinforce your understanding of C++ inheritance and its practical applications.
-
C++ Virtual Functions FAQ
Comprehensive guide to virtual functions and polymorphism in C++.
-
Bjarne Stroustrup’s FAQ on Multiple Inheritance
The creator of C++ explains multiple inheritance and its use cases.
-
Liskov Substitution Principle
Deep dive into one of the fundamental principles of object-oriented design.
Attribution and Citation
If you found this guide and tools helpful, feel free to link back to this page or cite it in your work!
Suf is a senior advisor in data science with deep expertise in Natural Language Processing, Complex Networks, and Anomaly Detection. Formerly a postdoctoral research fellow, he applied advanced physics techniques to tackle real-world, data-heavy industry challenges. Before that, he was a particle physicist at the ATLAS Experiment of the Large Hadron Collider. Now, he’s focused on bringing more fun and curiosity to the world of science and research online.