Inheritance
Overview
We have a fair amount to cover about Inheritance in C++
- Base and Derived Classes
- Access Control and Derivation
- Member Access from Derived Classes
- protected Class Members
- Access Declarations
- Inheritance and Constructors
- Inheritance and Destructors
- Inherited Member Functions
- Non-inherited Member Functions
- Upcasting and Downcasting
So, let’s get on to those topics…
Base and Derived Classes
A class can inherit the features or functionality of another class through class derivation.
- The class that inherits is called the derived class.
- The class that is inherited from is called the base class.
A class can be derived to :
- Restrict or reinterpret the base class.
- Factor out common functionality for reuse.
- Specialize or augment the base class to add a new layer of functionality
The syntax is:
class Base
{
// Base class members
};
class Derived : public Base // inherits from Base
{
// Derived class members
};
Example
This declares two classes: Student and GradStudent. GradStudent is a class derived from class Student
//
// Student.hpp
// Base and Derived Classes
//
// Created by Bryan Higgs on 8/24/24.
//
#ifndef Student_hpp
#define Student_hpp
#include <string>
// Student.h
class Student
{
public:
Student(const int id,
const std::string &name);
void Print() const;
double GetGPA() const;
void SetGPA(double gpa);
// ...
private:
int m_id; // Student ID
std::string m_name; // Student name
double m_gpa; // Grade Point Average
};
class GradStudent : public Student
{
public:
GradStudent(const int id,
const std::string &name,
const std::string &dept);
void Print() const;
const std::string &GetThesisTitle() const;
void SetThesisTitle(std::string &thesis_title);
// ...
private:
std::string m_dept; // Department
std::string m_thesis_title; // Thesis title
};
#endif /* Student_hpp */A class derived from class X is a kind of (or is a) X.
A class containing a member of class Y has a Y.
For example, classes Vehicle, Airplane, and Car. Vehicles have a Color, A Car has an Engine, and four Wheels.
//
// Vehicle.hpp
// Base and Derived Classes
//
// Created by Bryan Higgs on 8/25/24.
//
#ifndef Vehicle_h
#define Vehicle_h
enum class Color{White, Black, Green, Blue};
class Vehicle
{
// ...
private:
double max_speed;
int max_occupants;
Color color;
};
class Airplane : public Vehicle
{
// An Airplane is a Vehicle
// ...
private:
double m_wingspan;
};
enum class Engine(4Cylindar, 8Cylinder, Rotary)
class Car : public Vehicle
{
// A Car is a Vehicle
// ...
// A Car has an Engine, wheels
private:
Engine engine;
Wheel wheel[4];
};
#endif /* Vehicle_h */An instance of a derived class can be used anywhere an instance of its base class can be used:
- The derived class is a superset of the base class and so contains all the members of the base class.
The reverse is not true; a base class cannot be used in place of a class derived from it:
- It does not have all the members of the derived class.
Base & Derived Class Conversions
For example, conversion can take place in one direction only:
//
// Vehicle.cpp
// Base and Derived Classes
//
// Created by Bryan Higgs on 8/25/24.
//
#include <stdio.h>
#include "Vehicle.hpp"
void DoConversions()
{
Vehicle base;
Car derived;
base = derived;
derived = base;
}This code, given the previous class declarations for Vehicle and Car, produces the following:
base = derived compiles correctly.
Note that when a derived class is converted to a base class, the result has lost its derived class members — it has been ‘sliced’.
but:
derived = base produces a compile-time error:
Vehicle.cpp:18:11 No viable overloaded '='
Overriding Class Members
A derived class can define a member with the same name as a base class member.
This is called overriding (or refinement).
You can override a data member, or a member function:
//
// Base.hpp
// Base and Derived Classes
//
// Created by Bryan Higgs on 8/25/24.
//
#ifndef Base_h
#define Base_h
class Base
{
public:
Base(int i = 0) : m_i(i) {}
void Print() const;
private:
int m_i;
};
class Derived : public Base
{
public:
Derived(int x, int i) : Base(x), m_i(i) {}
void Print() const;
private:
int m_i;
};
#endif /* Base_h *///
// Base.cpp
// Base and Derived Classes
//
// Created by Bryan Higgs on 8/25/24.
//
#include <iostream>
#include "Base.hpp"
void Base::Print() const
{
std::cout << m_i << std::endl; // Base m_i
}
void Derived::Print() const
{
std::cout << m_i << std::endl; // Derived m_i
}//
// main.cpp
// Base and Derived Classes
//
// Created by Bryan Higgs on 8/24/24.
//
#include <iostream>
#include "Base.hpp"
int main(int argc, const char * argv[])
{
Base b(37);
Derived d(52, 101);
b.Print();
d.Print();
return 0;
}Produces the following output:
37
101
Program ended with exit code: 0
Can you explain this output?
Class Vector
Remember our Vector class?
//
// Vector.h
// Base & Derived Classes - Vector
//
// Created by Bryan Higgs on 8/26/24.
//
#ifndef Vector_h
#define Vector_h
class Vector // A vector of ints
{
public:
// Constructors & destructors
explicit Vector(const int elements = 10);
Vector(const Vector &source);
Vector(const int source[],
const int elements);
~Vector() { delete [] m_p; }
int getElementCount() { return m_elems; }
// Subscript operator overload
int &operator[](const int i);
private:
int *m_p; // Ptr to array
int m_elems; // Alloc. count
};
#endif /* Vector_h *///
// Vector.cpp
// Base & Derived Classes - Vector
//
// Created by Bryan Higgs on 8/26/24.
//
#include <iostream>
#include "Vector.h"
Vector::Vector(const int elements)
{
// Allocate space for elements
m_p = new int[m_elems = elements];
// Set all elements to 0
for (int i = 0; i < m_elems; i++)
m_p[i] = 0;
}
Vector::Vector(const Vector &source)
{
// Allocate space for elements
m_p = new int[m_elems = source.m_elems];
// Copy values from source elements
for (int i = 0; i < m_elems; i++)
m_p[i] = source.m_p[i];
}
Vector::Vector(const int source[],
const int elements)
{
// Allocate space for elements
m_p = new int[m_elems = elements];
// Copy values from source elements
// (assuming that source is properly sized)
for (int i = 0; i < m_elems; i++)
m_p[i] = source[i];
}
// Subscript operator overload
int &Vector::operator[](const int i)
{
// Do bounds checking...
if ( (i < 0) || (i >= m_elems) )
{
std::cerr << "Illegal Vector index: "
<< i << " (max: "
<< (m_elems - 1) << ')'
<< std::endl;
exit(1); // Abrupt exit!
}
// We're within bounds
return m_p[i];
}Class BoundedVector
Let’s create a class BoundedVector that is, in effect, a more generalized form of the Vector class: when an instance is created, the vector bounds are specified. While a Vector instance is created with a specified number of elements, and those elements may be accessed using the subscript operator, with subscripts from 0 to (<number-of-elements> – 1), a BoundedVector is created with a low bound and a high bound, and a size calculated from the low and high bounds.
The BoundedVector class is derived from the Vector class, and relies on its base class for some of its features.
//
// BoundedVector.h
// Base & Derived Classes - Vector
//
// Created by Bryan Higgs on 8/26/24.
//
#ifndef BoundedVector_h
#define BoundedVector_h
#include "Vector.h"
class BoundedVector : public Vector
{
public:
BoundedVector(int low, int high);
int LowBound() { return m_low; }
int HighBound() { return m_high; }
int &operator[](int i)
{ return Vector::operator[](i-m_low); }
private:
int m_low; // Low bound
int m_high; // High bound
};
#endif /* BoundedVector_h *///
// BoundedVector.cpp
// Base & Derived Classes - Vector
//
// Created by Bryan Higgs on 8/26/24.
//
#include <iostream>
#include "BoundedVector.h"
// Constructor
BoundedVector::BoundedVector(int low, int high)
: Vector((high - low) + 1), // NOTE!
m_low(low), m_high(high)
{
if (low > high) // Check the bounds
{
std::cerr << "\nLow bound " << low
<< " greater than high bound"
<< high << std::endl;
exit(0); // Abrupt exit!
}
}Here’s a simple test program for these classes:
//
// main.cpp
// Base & Derived Classes - Vector
//
// Created by Bryan Higgs on 8/26/24.
//
#include <iostream>
#include "BoundedVector.h"
int main(int argc, const char * argv[])
{
Vector values(6);
BoundedVector bvalues(-5, 3);
for (int i = 0; i < values.getElementCount(); i++)
{
values[i] = i;
}
std::cout << "Vector contents: " << std::endl;
for (int i = 0; i < values.getElementCount(); i++)
{
std::cout << "[" << i << "] = "
<< values[i] << std::endl;
}
for (int i = bvalues.LowBound(); i <= bvalues.HighBound(); i++)
{
bvalues[i] = i*i;
}
std::cout << "BoundedVector contents: " << std::endl;
for (int i = bvalues.LowBound(); i <= bvalues.HighBound(); i++)
{
std::cout << "[" << i << "] = "
<< bvalues[i] << std::endl;
}
return 0;
}Which outputs the following:
Vector contents:
[0] = 0
[1] = 1
[2] = 2
[3] = 3
[4] = 4
[5] = 5
BoundedVector contents:
[-5] = 25
[-4] = 16
[-3] = 9
[-2] = 4
[-1] = 1
[0] = 0
[1] = 1
[2] = 4
[3] = 9
Program ended with exit code: 0
Member Access Control from Derived Classes
Remember that a class can use member access control to specify access to members from outside the class:
class Bumble
{
int a; // private
public:
float b; // public
private:
long c; // private
// ...
};
An access specifier can be applied to a base class reference:
class Mumble : public Bumble
{
private:
int x;
public:
float y;
long z;
// ...
};
which controls what access the derived class will have to the base class members.
The default access specifier for a base class is:
- private when the derived class is a class
- public when the derived class is a struct
If we derive a class using a public base class (public inheritance):
class Derived : public Base { ... };
then the public members of the base class become publicly accessible through the derived class.
If we derive a class using a private base class (private inheritance):
class Derived : private Base { ... };
then the public members of the base class become privately accessible from within the derived class.
Regardless of the base class access specified, private members of a base class are not accessible from within the derived class or through it (unless access is explicitly granted through friend declarations — something to use very sparingly).
It’s analogous to a class being a member of another class:
class B { ... };
class C { ... };
class A
{
public:
B my_b; // public members of B
// accessible through class A
private:
C my_c; // public members of C not
// accessible through class A
};
When you inherit from a base class using public inheritance, you have an ‘is-a’ (or ‘is-a-kind-of’) relationship:
class Car : public Vehicle { ... };
A Car ‘is-a[-kind-of]’ Vehicle.
But beware!
When you inherit from a base class not using public inheritance, you do not have an ‘is-a’ relationship!
Unfortunately, the default:
class Car : Vehicle { ... };is equivalent to private inheritance:
class Car : private Vehicle { ... };which is not what you normally want!
For example:
private:
int a;
public:
int b; // Parent1's member functions have
// access to: a, b
}; // Normal code has access, through
// class Parent1, to: b
class Child1 : private Parent1 {
private:
int c;
public:
int d; // Child1's member functions have
// access to: b, c, d
}; // Normal code has access, through
// class Child1, to: d
class Child2 : public Parent1 {
private:
int e;
public:
int f; // Child2's member functions have
// access to: b, e, f
}; // Normal code has access, through
// class Child2, to: b, f
protected Class Members
It quickly became apparent that the simple private vs public access control was too ‘all or nothing’ to handle inheritance appropriately. The relationship between a derived class and its base class(es) is often closer than that of a class with the rest of the world.
To allow access from derived classes to base class members, it often became necessary to make the base class members public. However, this exposes those members to the rest of the world. Not good!
So, there came into being a third access specifier, protected, which provides a middle ground between private and public:
class Bumble
{
private: // accessible only within class
int a;
protected: // ...class or derived classes
float b;
public: // ...anyone
long c;
// ...
};
which can also be applied to a base class (protected inheritance):
class Mather : protected Blather { ... };
protected only affects access to base class members from derived classes.
Don’t bother using protected in a class if the class will never be used as a base class. A class needs to be designed to act as a base class.
Here’s a summary:
| Base Class Member Access Specifier | public Inheritance | protected Inheritance | private Inheritance |
|---|---|---|---|
| public | public in derived class | protected in derived class | private in derived class |
| protected | protected in derived class | protected in derived class | private in derived class |
| private | not accessible in derived class | not accessible in derived class | not accessible in derived class |
For example:
//
// Hierarchy.h
// Protected Class Members
//
// Created by Bryan Higgs on 8/26/24.
//
#ifndef Hierarchy_h
#define Hierarchy_h
class A
{
private:
int a;
protected:
int b;
public:
int c;
};
class B : public A
{
private:
int d;
public:
void Test();
};
class C : private A
{
private:
int e;
public:
void Test();
};
#endif /* Hierarchy_h *///
// Hierarchy.cpp
// Protected Class Members
//
// Created by Bryan Higgs on 8/26/24.
//
#include "Hierarchy.h"
void B::Test()
{
a = 1; // ERROR -- no access
b = 2;
c = 3;
d = 4;
}
void C::Test()
{
a = 1; // ERROR -- no access
b = 2;
c = 3;
e = 4;
}Compile-time errors:
Hierarchy.cpp:12:3 'a' is a private member of 'A'
Hierarchy.cpp:20:3 'a' is a private member of 'A'
//
// main.cpp
// Protected Class Members
//
// Created by Bryan Higgs on 8/26/24.
//
#include "Hierarchy.h"
int main(int argc, const char * argv[])
{
B myB;
C myC;
myB.a = 1; // ERROR -- no access
myB.b = 2; // ERROR -- no access
myB.c = 3;
myB.d = 4; // ERROR -- no access
myC.a = 1; // ERROR -- no access
myC.b = 2; // ERROR -- no access
myC.c = 3; // ERROR -- no access
myC.e = 4; // ERROR -- no access
return 0;
}Compile-time errors:
main.cpp:14:7 'a' is a private member of 'A'
main.cpp:15:7 'b' is a protected member of 'A'
main.cpp:17:7 'd' is a private member of 'B'
main.cpp:18:7 'a' is a private member of 'A'
main.cpp:19:7 'b' is a private member of 'A'
main.cpp:20:7 'c' is a private member of 'A'
main.cpp:21:7 'e' is a private member of 'C'
public, private, and protected Inheritance
public inheritance is used when the inheritance is part of the interface.
- The fact that X is-a Y is something you are willing to tell your clients about.
private inheritance is used when the inheritance is not part of the interface
- It is an implementation detail.
- The relationship is more like reuses-a.
- Rarely used — using composition (member class object) usually works as well, and often is simpler.
protected inheritance is used when the inheritance is part of the interface to the derived classes, but is not part of the interface to the clients.
- Even more rarely used — using composition is usually as good and simpler.
Inheritance with Constructors and Destructors
Constructors
Constructors are not inherited
A derived class constructor can (and by default will) call base class constructors
Constructors cannot be virtual (see later)
A public default constructor and a public copy constructor are generated by the compiler where they are needed.
When an instance of a derived class is created:
- If the constructor for the derived class does not explicitly call a base class constructor in its initializer list, then the default base class constructor is automatically called.
- If there is no default constructor for the base class, then a compile-time error occurs.
- If a base class constructor is explicitly called in the derived class initializer list, then that is the base class constructor executed; there is no default base class constructor initialization.
The order of constructor execution is:
- Base class constructor(s), in declaration order (independent of initializer list order)
- Derived class member constructor(s), in declaration order (independent of initializer list order)
- The body of the derived class constructor.
Virtual base classes are a special case (see later).
Destructors
A destructor cannot be inherited.
A derived class destructor can (and by default will) call its base class destructors.
A destructor can be virtual (see later).
- In fact, a base class destructor should be virtual.
A public default destructor is generated by the compiler when no destructor is explicitly declared. It calls the destructors for base classes and members of the derived class.
The order of destructor execution is:
- The body of the derived class destructor.
- Derived class member destructor(s), in reverse declaration order
- Base class destructor(s), in reverse declaration order
In other words, the exact opposite of constructor execution order.
Virtual base classes are a special case (see later).
Example:
//
// Hierarchy.h
// Constructors and Destructors
//
// Created by Bryan Higgs on 8/26/24.
//
#ifndef Hierarchy_h
#define Hierarchy_h
class A
{
public:
A();
~A();
};
class B
{
public:
B();
~B();
};
class C : public A // C is derived from A
{
public:
C();
~C();
private:
B b; // Member class object
};
class D : public C // D is derived from C
{
public:
D();
~D();
private:
A a; // Member class object
};
#endif /* Hierarchy_h *///
// Hierarchy.cpp
// Constructors and Destructors
//
// Created by Bryan Higgs on 8/26/24.
//
#include <iostream>
#include "Hierarchy.h"
// class A
A::A()
{
std::cout << "in A constructor" << std::endl;
}
A::~A()
{
std::cout << "in A destructor" << std::endl;
}
// class B
B::B()
{
std::cout << "in B constructor" << std::endl;
}
B::~B()
{
std::cout << "in B destructor" << std::endl;
}
// class C
C::C()
{
std::cout << "in C constructor" << std::endl;
}
C::~C()
{
std::cout << "in C destructor" << std::endl;
}
// class D
D::D()
{
std::cout << "in D constructor" << std::endl;
}
D::~D()
{
std::cout << "in D destructor" << std::endl;
}//
// main.cpp
// Constructors and Destructors
//
// Created by Bryan Higgs on 8/26/24.
//
#include "Hierarchy.h"
int main(int argc, const char * argv[])
{
D d;
return 0;
}Produces the following output:
in A constructor
in B constructor
in C constructor
in A constructor
in D constructor
in D destructor
in A destructor
in C destructor
in B destructor
in A destructor
Program ended with exit code: 0
Can you explain it?
Safe & Unsafe Downcasting
You can suppress compiler errors resulting from implicit downcasting, by explicitly downcasting, but it’s usually not a good idea:
//
// Header.h
// Safe Downcasting
//
// Created by Bryan Higgs on 8/26/24.
//
#ifndef Header_h
#define Header_h
class Base
{
public:
Base() { a = 10; }
private:
int a;
};
class Derived : public Base
{
public:
Derived() { myValue = 44; }
double GetMyValue() { return myValue; }
private:
double myValue;
};
#endif /* Header_h *///
// main.cpp
// Safe Downcasting
//
// Created by Bryan Higgs on 8/26/24.
//
#include <iostream>
#include "Header.h"
void process(Base &b)
{
Derived d;
d = (Derived &)b; // OK, no compile time error
std::cout << "Derived value = " << d.GetMyValue() << std::endl;
}
int main(int argc, const char * argv[])
{
Base b;
Derived d;
b = d; // OK, but sliced
process(d); // No problems
process(b); // Serious problems! Why?
return 0;
}Here’s what the above program produces:
Derived value = 44
Derived value = 6.95161e-310
Program ended with exit code: 0
See the problem?
Can you explain it?
What is the problem?
Explicit casts from a base class (pointer or reference) are inherently unsafe:
- You never know what the actual class instance really is
- If it’s not the type you’re casting it to, then bad things will happen!
- If it hurts, stop doing it!
But there are situations where you really need to do this. (Although I would think hard about whether you really want to do it!)
How can we do this, and retain safety?
We would like some kind of mechanism where, if the cast isn’t valid, a run-time error will occur.
dynamic_cast
The ISO C++ committee invented the dynamic_cast for such situations:
Shape *s = ...
Rectangle *r = dynamic_cast<Rectangle *>(s);
if (r != 0)
// The object pointed to by s is a
// Rectangle, and r points to it
else
// The object pointed to by s is not a
// Rectangle, and r is a null pointer
In general, the form of a dynamic_cast is:
dynamic_cast<T>(v)
where T is a type (subject to some restrictions), and v is an expression.
Note that dynamic_cast is not limited to downcasting.
The type T must be:
- A pointer to a complete class type, in which case:
- v must be an rvalue of a pointer to a complete class type
- the result is an rvalue of type T
- if the cast fails, the result is a null pointer; you have to test for it!
- This case is like a test in your program.
- or a reference to a complete class type, in which case:
- v must be an lvalue of a complete class type
- the result is an lvalue of the type referred to by T
- if the cast fails, a bad_cast exception occurs; you have to catch it!
- This case is like an assertion; if it fails, then you get an exception. (More about exceptions, later…)
- or a “pointer to cv void“. (“cv” here is a shorthand for “possibly cv-qualified”, which means “may have a
constor avolatileon it”) — Now, we’re really getting down into the weeds. Let’s ignore this for now…
Summary
Well, we’ve learned quite a bit about inheritance:
- Base and Derived classes
- Access Control and Derivation
- Member Access from Derived classes
- protected class members
- Access declarations
- Inheritance and Constructors
- Inheritance and Destructors
- Inherited Member Functions
- Non-inherited Member Functions
- Upcasting and Downcasting
All of the above discussion was about single inheritance. We’ll see later the considerable complexities introduced by multiple inheritance…
Onward!