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
). public
Inheritance: This is the most common type. It means “Derived is publicly a type of Base”. We’ll focus mainly on this. (protected
andprivate
inheritance 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
:
public
members ofBase
becomepublic
members ofDerived
. (Accessible from anywhere).protected
members ofBase
becomeprotected
members ofDerived
. (Accessible withinDerived
and its own future derived classes, but not outside).private
members ofBase
remainprivate
toBase
. They are part of theDerived
object’s memory, but they cannot be directly accessed by name withinDerived
‘s methods.Derived
can only interact with them viapublic
orprotected
methods 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
Dog
constructor must initialize itsAnimal
part. This is done in the initialization list by calling theAnimal
constructor (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:
Dog
methods can accessm_species
andm_age
(protected inAnimal
), but notm_internal_id
(private inAnimal
). Outside code (main
) cannot accessprotected
orprivate
members directly. - Code Reuse:
Dog
automatically getseat()
,getAge()
,getSpecies()
, etc., without redefining them. - Specialization:
Dog
adds its own data (m_breed
,m_owner_name
) and methods (bark()
,Workspace()
). - Overriding (Preview):
Dog
provided its ownsleep()
method. TheDog
version is called forDog
objects. (More on this with polymorphism next time). - Destructor Chaining: When
myDog
is destroyed, theDog
destructor runs first, followed automatically by theAnimal
destructor.
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| P3
Summary of Lesson 8:
- Inheritance allows a
Derived
class to inherit members from aBase
class usingclass Derived : public Base
. - It models “is-a” relationships and promotes code reuse.
public
Inheritance Rules:public
base members ->public
in derived.protected
base members ->protected
in derived.private
base members -> Inaccessible by name in derived.
protected
allows 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/