Class Inheritance


Object-Oriented Programming

Inheritance is one of the three pillars of Object Oriented Programming:
  1. Encapsulation (data abstraction/hiding; the class)
  2. Inheritance (Is-a relationship, extending a class)
  3. Polymorphism (dynamic binding, virtual methods)
You've already seen encapsulation (classes). Now we will look at extending a class via inheritance.

Other OO languages use different terminology than C++. Here are some equivalents:

OOP C++
Object Class object or instance
Instance variable Private data member
Method Public member function
Message passing Calling a public member function

Within a class, all of the data and functions are related. Within a program, classes can be related in various ways.

  1. Two classes are independent of each other and have nothing in common
  2. Two classes are related by inheritance
  3. Two classes are related by composition, also called aggregation or containment
Inheritance Composition

An inheritance relationship can be represented by a hierarchy.

A partial vehicle hierarchy:

A partial animal hierarchy:


A Simple Example

Structures to represent 2D and 3D points:

struct Point2D
{
  double x_;
  double y_;
};
struct Point3D
{
  double x_;
  double y_;
  double z_;
};

Another way to define the 3D struct so that we can reuse the Point2D struct:

struct Point3D_composite
{
  Point2D xy_; // Struct contains a Point2D object
  double z_;
};
The memory layout of Point3D and Point3D_composite is identical and is obviously compatible with C, as there is nothing "C++" about them yet.

Accessing the members:

void PrintXY(const Point2D &pt)
{
  std::cout << pt.x_ << ", " << pt.y_;
}

void PrintXYZ(const Point3D &pt)
{
  std::cout << pt.x_ << ", " << pt.y_ << ", " << pt.z_;
}

void PrintXYZ(const Point3D_composite &pt)
{
  std::cout << pt.xy_.x_ << ", " << pt.xy_.y_;
  std::cout << ", " << pt.z_;
}
Of course, the last function can be modified to reuse PrintXY:

void PrintXYZ(const Point3D_composite &pt)
{
  PrintXY(pt.xy_); // delegate for X,Y
  std::cout << ", " << pt.z_;
}
A "cleaner" way to do define the 3D point is to use inheritance.
// Struct inherits a Point2D object
struct Point3D_inherit : public Point2D
{
  double z_;
};
This new struct has the exact same physical structure as the previous two 3D point structs and is still compatible with C:

struct Point3D
{
  double x_;
  double y_;
  double z_;
};
struct Point3D_composite
{
  Point2D xy_; // Struct contains a Point2D object
  double z_;
};

Sample usage:

int main(void)
{
  Point3D pt3;
  Point3D_composite ptc;
  Point3D_inherit pti;

  char buffer[100];
  sprintf(buffer, "pt3: x=%p, y=%p, z=%p\n", &pt3.x_, &pt3.y_, &pt3.z_);
  std::cout << buffer;

  sprintf(buffer, "ptc: x=%p, y=%p, z=%p\n", &ptc.xy_.x_, &ptc.xy_.y_, &ptc.z_);
  std::cout << buffer;

  sprintf(buffer, "pti: x=%p, y=%p, z=%p\n", &pti.x_, &pti.y_, &pti.z_);
  std::cout << buffer;

    // Assign to Point3D members
  pt3.x_ = 1; 
  pt3.y_ = 2; 
  pt3.z_ = 3;
  PrintXYZ(pt3);
  std::cout << std::endl;

    // Assign to Point3D_composite members
  ptc.xy_.x_ = 4;
  ptc.xy_.y_ = 5;
  ptc.z_ = 6;
  PrintXYZ(ptc);
  std::cout << std::endl;

    // Assign to Point3D_inherit members
  pti.x_ = 7;
  pti.y_ = 8;
  pti.z_ = 9;
  PrintXYZ(pti);
  std::cout << std::endl;

  return 0;
}
Output:
pt3: x=0012FF68, y=0012FF70, z=0012FF78
ptc: x=0012FF50, y=0012FF58, z=0012FF60
pti: x=0012FF38, y=0012FF40, z=0012FF48
1, 2, 3
4, 5, 6
7, 8, 9
Notes about this syntax:

struct Point3D_inherit : public Point2D
Adding methods to the structs:

struct Point2D
{
  double x_;
  double y_;
  void print(void)
  {
    std::cout << x_ << ", " << y_;
  }

};
struct Point3D
{
  double x_;
  double y_;
  double z_;
  void print(void)
  {
    std::cout << x_ << ", " << y_ << ", " << z_;
  }
};

Composite and inheritance (everything is public):

struct Point3D_composite
{
  Point2D xy_;
  double z_;
  void print(void)
  {
      // 2D members are public
    std::cout << xy_.x_ << ", " << xy_.y_;  
    std::cout << ", " << z_;
  }
};
struct Point3D_inherit : public Point2D
{
  double z_;
  void print(void)
  {
      // 2D members are public
    std::cout << x_ << ", " << y_;  
    std::cout << ", " << z_;        
  }
};

And in main we would have something that looks like this:

Point3D pt3;
Point3D_composite ptc;
Point3D_inherit pti;

// setup points

pt3.print();
ptc.print();
pti.print();  // Is this legal? Ambiguous? Which method is called?


Let's make it more C++-like with private members and public methods:

// Class is a stand-alone 2D point
class Point2D
{
  public:
    Point2D(double x, double y) : x_(x), y_(y) {};
    void print(void)
    {
      std::cout << x_ << ", " << y_;
    }
  private:
    double x_;
    double y_;
};
// Class is a stand-alone 3D point
class Point3D
{
  public:
    Point3D(double x, double y, double z) : x_(x), y_(y), z_(z) {};
    void print(void)
    {
      std::cout << x_ << ", " << y_ << ", " << z_;
    }
  private:
    double x_;
    double y_;
    double z_;
};
With composition, we must initialize the contained Point2D object in the initializer list:

// Class contains a Point2D object
struct Point3D_composite
{
  public:
    Point3D_composite(double x, double y, double z) : xy_(x, y), z_(z) {};
    void print(void)
    {
      xy_.print(); // 2D members are private
      std::cout << ", " << z_;
    }
  private:
    Point2D xy_;
    double z_;
};
With inheritance, we must initialize the Point2D subobject in the initializer list:
// Class inherits a Point2D object
struct Point3D_inherit : public Point2D
{
  public:
    Point3D_inherit(double x, double y, double z) : Point2D(x ,y), z_(z) {};
    void print(void)
    {
      Point2D::print(); // 2D members are private
      std::cout << ", " << z_;        
    }
  private:
    double z_;
};

Sample usage:

int main(void)
{
    // Create Point3D 
  Point3D pt3(1, 2, 3);
  pt3.print();
  std::cout << std::endl;

    // Create Point3D_composite
  Point3D_composite ptc(4, 5, 6);
  ptc.print();
  std::cout << std::endl;

    // Create Point3D_inherit
  Point3D_inherit pti(7, 8, 9);
  pti.print();
  std::cout << std::endl;

  return 0;
}


A Larger Example

The Base Class

class Time
{
  public:
    Time(int h, int m, int s);
    Time();
    void Set(int h, int m, int s);
    void Write(void) const;
    void Increment(void);

  private:
    int hrs_;
    int mins_;
    int secs_;
};
A diagram of the Time class:

Note that sizeof(Time) is 12 bytes.

Partial implementation from Time.cpp: (Notice the code reuse even in this simple example.)

Time::Time()
{
  Set(0, 0, 0);
}

Time::Time(int h, int m, int s)
{
  Set(h, m, s);
}

void Time::Set(int h, int m, int s)
{
  hrs_ = h;
  mins_ = m;
  secs_ = s;
}

Class Design Tip: When a class has more than one constructor, move the common initialization code into a separate private method whenever possible.


Extending the Time class

Now we decide that we'd like the Time class to include a Time Zone:
enum TimeZone {EST, CST, MST, PST, EDT, CDT, MDT, PDT};
We have several choices at this point:
  1. Modify the Time class to include a TimeZone.
  2. Create a new class by copying and pasting the code for the existing Time class and adding the TimeZone.
  3. Create a new class by inheriting from the Time class.
What are the pros and cons of each method above?

Deriving ExtTime from Time:

class ExtTime : public Time
{
  public:
    ExtTime();
    ExtTime(int h, int m, int s, TimeZone z);
    void Set(int h, int m, int s, TimeZone z);
    void Write(void) const;

  private:
    TimeZone zone_;
};

What is sizeof(ExtTime)? How might it be laid out in memory?

Some implementations of the ExtTime constructors:

  1. The derived class default constructor: (the base class default constructor is implicitly called)
    ExtTime::ExtTime()
    {
      zone_ = EST; // arbitrary default
    }
    
  2. The derived class non-default constructor: (the base class default constructor is implicitly called)
    ExtTime::ExtTime(int h, int m, int s, TimeZone z)
    {
      zone_ = z;
      // what do we do with h, m, and s?
    }
    
  3. Calling a non-default base class constructor explicitly:
    ExtTime::ExtTime(int h, int m, int s, TimeZone z) : Time(h, m, s)
    {
      zone_ = z;
    }
    
  4. Same as above using initializer list for derived member initialization:
ExtTime::ExtTime(int h, int m, int s, TimeZone z) : Time(h, m, s), zone_(z)
{
}
Notes:
The relationship between the Time and ExtTime classes:

In the ExtTime class:

Given our classes:

class Time
{
  public:
    Time(int h, int m, int s);
    Time();
    void Set(int h, int m, int s);
    void Write(void) const;
    void Increment(void);

  private:
    int hrs_;
    int mins_;
    int secs_;
};
class ExtTime : public Time
{
  public:
    ExtTime();
    ExtTime(int h, int m, int s, TimeZone z);
    void Set(int h, int m, int s, TimeZone z);
    void Write(void) const;

  private:
    TimeZone zone_;
};

What is the result of this code:

ExtTime time();
time.Set(9, 30, 0);
time.Write();

Another Example of Inheritance

The specification (Employee.h) for an Employee class:
#ifndef EMPLOYEE_H
#define EMPLOYEE_H
const int MAX_LENGTH = 50;

class Employee           
{
  private:               
    char firstName_[MAX_LENGTH];  
    char lastName_[MAX_LENGTH];   
    float salary_;    
    int years_;       

  public:                
    Employee(const char* first, const char* last, float salary, int years);

    void setName(const char* first, const char* last);
    void setSalary(float newSalary);
    void setYears(int numYears);
    void Display(void) const;
};
#endif
A diagram of the Employee class:

What is sizeof(Employee)?

An implementation (Employee.cpp) for the Employee class:

#include "Employee.h"
#include <string>
#include <iomanip>
#include <iostream>
using namespace std;

Employee::Employee(const char* first, const char* last, float sal, int yrs)
{
  strcpy(firstName_, first);
  strcpy(lastName_, last);
  salary_ = sal;
  years_ = yrs;
}

void Employee::setName(const char* first, const char* last)
{
  strcpy(firstName_, first);
  strcpy(lastName_, last);
}

void Employee::setSalary(float newSalary)
{
  salary_ = newSalary;
}

void Employee::setYears(int numYears)
{
  years_ = numYears;
}

void Employee::Display(void) const
{
  cout << "  Name: " << lastName_;
  cout << ", " << firstName_ << endl;
  cout << setprecision(2);
  cout.setf(ios::fixed);
  cout << "Salary: $" << salary_ << endl;
  cout << " Years: " << years_ << endl;
}
The specification (manager.h) for the Manager class:
#ifndef MANAGER_H
#define MANAGER_H
#include "employee.h"

class Manager : public Employee
{
  private:
    int deptNumber_;    // department managed
    int numEmployees_;  // employees in department

  public:
    Manager(const char* first, const char* last, float salary, int years, int dept, int emps);

    void setDeptNumber(int dept);
    void setNumEmployees(int emps);
    void Display(void) const;
};
#endif

A diagram of the Manager class:

What is sizeof(Manager)?

An implementation (Manager.cpp) for the Manager class:

#include "manager.h"
#include <iostream>
using namespace std;

Manager::Manager(const char* first, const char* last, float salary, 
                 int years, int dept, int emps) : Employee(first, last, salary, years)
{
  deptNumber_ = dept;
  numEmployees_ = emps;
}

void Manager::Display(void) const
{
  Employee::Display();
  cout << "  Dept: " << deptNumber_ << endl;
  cout << "  Emps: " << numEmployees_ << endl;
}

void Manager::setDeptNumber(int dept)
{
  deptNumber_ = dept;
}

void Manager::setNumEmployees(int emps)
{
  numEmployees_ = emps;
}
Trace the execution of the following program through the class hierarchy. What is the output?

#include "employee.h"
#include "manager.h"
#include <iostream>
using namespace std;

int main(void)
{
    // Create an Employee and a Manager
  Employee emp1("John", "Doe", 30000, 2);
  Manager mgr1("Mary", "Smith", 50000, 10, 5, 8); 

    // Display them
  emp1.Display();
  cout << endl;
  mgr1.Display();
  cout << endl;

    // Change the manager's last name
  mgr1.setName("Mary", "Jones");
  mgr1.Display();
  cout << endl;

    // add two employees and give a raise
  mgr1.setNumEmployees(10);
  mgr1.setSalary(80000);
  mgr1.Display();
  cout << endl;
  return 0;
}
Output:
  Name: Doe, John
Salary: $30000.00
 Years: 2

  Name: Smith, Mary
Salary: $50000.00
 Years: 10
  Dept: 5
  Emps: 8

  Name: Jones, Mary
Salary: $50000.00
 Years: 10
  Dept: 5
  Emps: 8

  Name: Jones, Mary
Salary: $80000.00
 Years: 10
  Dept: 5
  Emps: 10


Protected vs. Private Data

In a nutshell: Let's extend the Employee class slightly by including accessor methods for the private data. (These methods would most likely be necessary in any real-world class but were intentionally left out to keep the example and diagrams very simple.) These methods simply retrieve the private data for clients.

Interface:

const char *getFirstName(void) const;
const char *getLastName(void) const;
float getSalary(void) const;
int getYears(void) const;
Implementation:

const char *Employee::getFirstName(void) const
{
  return firstName_;
}

const char *Employee::getLastName(void) const
{
  return lastName_;
}
float Employee::getSalary(void) const
{
  return salary_;
}

int Employee::getYears(void) const
{
  return years_;
}
We'll also add a new method to the Manager class that simply reports its internal state. (This type of operation was not required when the Employee class was designed and implemented.)

void Manager::LogActivity(void) const
{
  char buffer[105];
  const char *fn = getFirstName();  // use public accessor method
  const char *ln = getLastName();   // use public accessor method

  sprintf(buffer, "%s, %s", ln, fn); // Format lastname, firstname

  cout << "============================" << endl;
  cout << "Manager data:" << endl;
  cout << "  Dept: " << deptNumber_ << endl;
  cout << "  Emps: " << numEmployees_ << endl;
  cout << "Employee data:" << endl;
  cout << "  Name: " << buffer << endl;
  cout << "  Salary: " << getSalary() << endl;
  cout << "  Years: " << getYears() << endl;
  cout << "============================" << endl;
}
Some sample client code:

#include "Manager.h"

int main(void)
{
  Manager m1("Ian", "Faith", 5, 80000, 7, 25);
  m1.LogActivity();
  
  return 0;
}
Output:
============================
Manager data:
  Dept: 7
  Emps: 25
Employee data:
  Name: Faith, Ian
  Salary: 5
  Years: 80000
============================


Modification #1

Modifying the Employee class so that the private data is now protected:

class Employee           
{
  protected:
    char firstName_[MAX_LENGTH];
    char lastName_[MAX_LENGTH];
    float salary_;    
    int years_;       

  public:                

    // Public methods...  
  
};
How will this affect existing code, both derived classes and "global" code?

This will allow us to change the implementation of Manager to access the fields directly:

void Manager::LogActivity(void) const
{
  char buffer[105];
  const char *fn = firstName_;  // direct access
  const char *ln = lastName_;   // direct access

  sprintf(buffer, "%s, %s", ln, fn);

  cout << "============================" << endl;
  cout << "Manager data:" << endl;
  cout << "  Dept: " << deptNumber_ << endl;
  cout << "  Emps: " << numEmployees_ << endl;
  cout << "Employee data:" << endl;
  cout << "  Name: " << buffer << endl;
  cout << "  Salary: " << salary_ << endl;
  cout << "  Years: " << years_ << endl;
  cout << "============================" << endl;
}
Note that "regular" clients still must go through the public member functions:

#include <iostream>
#include "Manager.h"

int main(void)
{
  Manager m1("Ian", "Faith", 5, 80000, 7, 25);
  m1.LogActivity();  // now using protected data directly; main is unaware

    // Error: lastName_ is a protected member (still inaccessible here)
  const char *last1 = m1.lastName_;

    // OK: getLastName() is a public member
  const char *last2 = m1.getLastName();

  return 0;
}
Notes:


Modification #2

Modifying the implementation of the Employee class.

#include "String.h"

class Employee           
{
  protected:
    String firstName_;  // String is a user-defined class
    String lastName_;   // String is a user-defined class
    float salary_;    
    int years_;       

  public:                

    // Same public methods...
};
How will this affect existing code, both derived classes and "global" code?
Now what is sizeof(Employee)?

Notes: