C++ for C Developers | Lesson 8: Inheritance: Building on What You Have (Code Reuse)
Goal: Understand how inheritance allows one class (derived) to acquire the properties and methods of another class (base), promoting code reuse and establishing hierarchical relationships (“is-a”). Learn how access specifiers (public, protected, private) work in inheritance.
1. Motivation: Code Reuse and Relationships
Imagine you have different types of vehicles: Car, Truck, Motorcycle. They likely share common attributes (speed, weight, license plate) and behaviors (start engine, stop engine, accelerate). Instead of copying and pasting this common code into each vehicle class, inheritance lets us define a general Vehicle class and have Car, Truck, etc., inherit from it.
flowchart TD
Animal["Animal (Base Class)"]
Dog["Dog (Derived Class)"]
Cat["Cat (Derived Class)"]
Animal -->|public inheritance| Dog
Animal -->|public inheritance| Cat
Inheritance models an “is-a” relationship: a Car is a type of Vehicle, a Dog is a type of Animal.
2. Basic Syntax
You define a derived class using a colon : followed by an access specifier and the base class name.
class Base {
// ... base class members ...
};
class Derived : public Base { // Derived inherits publicly from Base
// ... members specific to Derived ...
};
- Base Class: The class being inherited from (
Base). - Derived Class: The class doing the inheriting (
Derived). publicInheritance: This is the most common type. It means “Derived is publicly a type of Base”. We’ll focus mainly on this. (protectedandprivateinheritance exist but are less common and used for different relationship types).
3. How Access Specifiers Work with Inheritance (public inheritance)
When Derived inherits publicly from Base:
publicmembers ofBasebecomepublicmembers ofDerived. (Accessible from anywhere).protectedmembers ofBasebecomeprotectedmembers ofDerived. (Accessible withinDerivedand its own future derived classes, but not outside).privatemembers ofBaseremainprivatetoBase. They are part of theDerivedobject’s memory, but they cannot be directly accessed by name withinDerived‘s methods.Derivedcan only interact with them viapublicorprotectedmethods inherited fromBase.
Access Summary Table (public Inheritance):
| Base Member Access | Accessible in Base Methods? | Accessible in Derived Methods? | Accessible Outside? |
|---|---|---|---|
public | Yes | Yes | Yes |
protected | Yes | Yes | No |
private | Yes | No | No |
4. Example: Animals
Let’s model some animals.
#include <iostream>
#include <string>
// --- Base Class ---
class Animal {
private: // Private to Animal
std::string m_internal_id; // Only accessible via Animal's methods
protected: // Accessible by Animal and derived classes (like Dog)
std::string m_species;
int m_age;
public: // Accessible by anyone
// Constructor for Animal
Animal(const std::string& species, int age)
: m_species(species), m_age(age)
{
// Generate some internal ID (example)
m_internal_id = species + std::to_string(age); // std::to_string needs <string>
std::cout << "Animal Constructor for " << m_species << std::endl;
}
// Destructor for Animal
~Animal() {
std::cout << "Animal Destructor for " << m_species << std::endl;
}
// Public methods
void eat() const {
std::cout << "The " << m_species << " is eating." << std::endl;
}
void sleep() const {
std::cout << "The " << m_species << " is sleeping." << std::endl;
}
int getAge() const {
return m_age; // Provide public access to protected data if needed
}
std::string getSpecies() const {
return m_species;
}
std::string getInternalIdInfo() const {
// Derived classes cannot access m_internal_id directly,
// but they can call this public method.
return "Internal ID starts with: " + m_internal_id.substr(0, 4) + "...";
}
};
// --- Derived Class ---
class Dog : public Animal { // Dog "is-a" Animal (publicly inherits)
private: // Specific to Dog
std::string m_breed;
std::string m_owner_name;
public:
// Dog's Constructor - MUST call the Base class (Animal) constructor
Dog(const std::string& breed, int age, const std::string& owner)
: Animal("Dog", age), // *** Call Animal constructor via initialization list ***
m_breed(breed),
m_owner_name(owner)
{
std::cout << "Dog Constructor for breed " << m_breed << std::endl;
}
~Dog() {
std::cout << "Dog Destructor for breed " << m_breed << std::endl;
}
// Dog-specific method
void bark() const {
// Can access protected members from Animal directly
std::cout << "The " << m_age << "-year-old " << m_species << " (breed: " << m_breed << ") says: Woof! Woof!" << std::endl;
// std::cout << m_internal_id; // ERROR! Cannot access private base member
}
// Can call public methods inherited from Animal
void fetch() const {
std::cout << m_owner_name << "'s dog (" << m_species << ") is fetching." << std::endl;
eat(); // Call inherited public method
}
// We can provide a Dog-specific version of an Animal method (Overriding preview)
void sleep() const {
std::cout << "The dog (" << m_breed << ") curls up to sleep." << std::endl;
// Can still call the base version if needed:
// Animal::sleep();
}
void showInternalIdHint() const {
// Can access Base's private data *indirectly* via Base's public method
std::cout << getInternalIdInfo() << std::endl;
}
};
int main() {
std::cout << "--- Creating Dog object ---" << std::endl;
Dog myDog("Golden Retriever", 5, "Alice");
std::cout << "-------------------------\n" << std::endl;
// Call methods:
myDog.eat(); // Inherited from Animal
myDog.bark(); // Defined in Dog
myDog.fetch(); // Defined in Dog (calls inherited eat())
myDog.sleep(); // Uses the Dog version (overrides Animal's)
myDog.showInternalIdHint(); // Calls inherited method to access private base data hint
std::cout << "\nMy dog's species: " << myDog.getSpecies() << std::endl; // Inherited public method
std::cout << "My dog's age: " << myDog.getAge() << std::endl; // Inherited public method
// Cannot access protected members from outside:
// std::cout << myDog.m_species; // ERROR! m_species is protected
// Cannot access private members from outside:
// std::cout << myDog.m_breed; // ERROR! m_breed is private to Dog
// std::cout << myDog.m_internal_id; // ERROR! m_internal_id is private to Animal
std::cout << "\n--- Dog object going out of scope ---" << std::endl;
// Destructors called in reverse order of construction:
// 1. Dog destructor
// 2. Animal destructor
return 0;
}
Key Observations from Example:
- Constructor Chaining: The
Dogconstructor must initialize itsAnimalpart. This is done in the initialization list by calling theAnimalconstructor (Animal("Dog", age)). If the base class had a default constructor, it would be called automatically if not specified, but it’s good practice to be explicit. - Access Control:
Dogmethods can accessm_speciesandm_age(protected inAnimal), but notm_internal_id(private inAnimal). Outside code (main) cannot accessprotectedorprivatemembers directly. - Code Reuse:
Dogautomatically getseat(),getAge(),getSpecies(), etc., without redefining them. - Specialization:
Dogadds its own data (m_breed,m_owner_name) and methods (bark(),Workspace()). - Overriding (Preview):
Dogprovided its ownsleep()method. TheDogversion is called forDogobjects. (More on this with polymorphism next time). - Destructor Chaining: When
myDogis destroyed, theDogdestructor runs first, followed automatically by theAnimaldestructor.
sequenceDiagram
participant Main
participant Dog
participant Animal
Main->>Dog: Create Dog object
Dog->>Animal: Call Animal constructor
Animal-->>Dog: Animal initialized
Dog-->>Main: Dog initialized
Main->>Main: Use Dog object
Main->>Dog: Dog object goes out of scope
Dog->>Dog: Dog destructor runs
Dog->>Animal: Animal destructor runs
Why protected?
Use protected for members that are implementation details of the base class but which derived classes might reasonably need direct access to for their own implementation, without exposing them publicly. Use private for things truly internal to the base class only. Start with private and change to protected only if necessary for derived classes.
flowchart LR
Base("Base Class")
Derived("Derived Class")
Outside("Outside Code")
subgraph Members in Base
P1("public m1()")
P2("protected m2()")
P3("private m3()")
end
Base --> P1
Base --> P2
Base --> P3
Derived -->|Can Access| P1
Derived -->|Can Access| P2
Derived -.->|Cannot Access| P3
Outside -->|Can Access| P1
Outside -.->|Cannot Access| P2
Outside -.->|Cannot Access| P3Summary of Lesson 8:
- Inheritance allows a
Derivedclass to inherit members from aBaseclass usingclass Derived : public Base. - It models “is-a” relationships and promotes code reuse.
publicInheritance Rules:publicbase members ->publicin derived.protectedbase members ->protectedin derived.privatebase members -> Inaccessible by name in derived.
protectedallows access within the class and derived classes, but not outside.- Derived class constructors must call the appropriate base class constructor in their initialization list.
- Destructors are called automatically in reverse order of construction (Derived then Base).
Next Lesson: We’ll unlock the true power of inheritance with Polymorphism and Virtual Functions, allowing you to treat objects of derived classes through base class pointers/references and have the correct, specialized methods called automatically at runtime.
More Sources:
cppreference – Derived Classes: https://en.cppreference.com/w/cpp/language/derived_class
Learn C++ – Inheritance Tutorial: https://www.learncpp.com/cpp-tutorial/basic-inheritance-in-c/


