Part D - Inheritance

Functions in a Hierarchy

Describe the shadowing of base class functions by derived class functions
Show how to pass initialization data across the constructors in a hierarchy
Demonstrate the order of execution of constructors and destructors in a hierarchy

Shadowing | Constructors | Destructors | Helpers | Summary | Exercises


A derived class inherits from its base class the normal member functions along with the base class' instance variables.  A derived class does not however inherit the special member functions - constructor(s), destructor, and assignment operator - or any helper function of its base class, except possibly constructors.  A derived class' destructor automatically calls the base class' destructor and no additional coding is required in this regard.  Similarly, a derived class' default assignment operator automatically calls the base class' assignment operator and no additional coding is required in this regard. 

This chapter examines how member functions shadow one another in a hierarchy, describes the order in which constructors and destructors are called, shows how to define a derived class' constructor to access a specific base class constructor and finally, describes how to overload a helper operator for a derived class.


Shadowing

Any member function of a derived class shadows the member function(s) of its base class with the same name.  C++ binds a call on an instance of the derived class to the member function defined in the derived class, if one exists. 

To access a shadowed member function in a base class, we apply scope resolution.  A call to a shadowed function takes the form

 Base::identifier(arguments)

where Base identifies the class to which the shadowed function belongs.

For example, consider the following hierarchy.  The base and derived classes declare display() member functions.  The display() function of the Student class shadows the display() function of the Person class for any object of Student type:

 // Student.h

 #include <iostream>
 const int N = 30;
 const int M = 13;

 class Person {
     char person[N+1];
   public:
     void set(const char* n);
     void display(std::ostream&) const; 
 };

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(int, const char*);
     void display(std::ostream&) const;
 };

The implementation file accesses the base class version of display() through scope resolution:

 // Student.cpp

 #include <cstring>
 #include "Student.h"

 void Person::set(const char* n) {
     std::strncpy(person, n, N);
     person[N] = '\0';
 }

 void Person::display(std::ostream& os) const {
     os << person << ' ';
 }

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 Student::Student(int n, const char* g) {
     // see Current Object chapter for validation logic 
     no = n;
     std::strcpy(grade, g);
 }

 void Student::display(std::ostream& os) const {
     Person::display(os);
     os << no << ' ' << grade;
 }

The output from the following client program is shown on the right:

 // Shadowing
 // shadowing.cpp

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

 int main() {
     Person jane;
     Student harry(975, "ABBAD");

     harry.set("Harry");
     harry.display(std::cout);
     std::cout << std::endl;
     jane.set("Jane Doe");
     jane.display(std::cout);
     std::cout << std::endl;
 }













 Harry 975 ABBAD

 Jane Doe

The first call to display() (on harry) calls Student::display(), which in turn calls the shadowed Person::display().  The second call to display() (on jane) calls Person::display() directly.  The derived version of display() shadows the base version on the harry object.  The base version on the jane object is not shadowed.

By calling Person::display() within Student::display(), we have hidden the hierarchy from the client.  The main() function is hierarchy egnostic.

Exposing Overloaded Member Functions

C++ shadows on the name and not the signature.  To expose a member function in the base class other than the function with the same signature we insert a using declaration into the derived class' definition.  A using declaration takes the form

 using Base::identifier;

where Base identifies the base class and identifier is the name of the shadowed function.

For example, let us overload the display() member function is the Person class to take no arguments.  The header file contains its declaration in the base class.  The using declaration in the derived class exposes the member function for objects of the derived class. 

 // Student.h

 #include <iostream>
 const int N = 30;
 const int M = 13;

 class Person {
     char person[N+1];
   public:
     void set(const char* n);
     void display(std::ostream&) const; 
     void display() const; 
 };

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(int, const char*);
     void display(std::ostream&) const;
     using Person::display; 
 };

The implementation file defines the overloaded display() function:

 // Student.cpp

 #include <cstring>
 #include "Student.h"

 void Person::set(const char* n) {
     std::strncpy(person, n, N);
     person[N] = '\0';
 }

 void Person::display(std::ostream& os) const {
     os << person << ' ';
 }

 void Person::display() const {
     std::cout << person << ' ';
 }

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 Student::Student(int n, const char* g) {
     // see Current Object chapter for validation logic 
     no = n;
     std::strcpy(grade, g);
 }

 void Student::display(std::ostream& os) const {
     Person::display(os);
     os << no << ' ' << grade;
 }

The output from the following client program is shown on the right:

 // Overloading and Shadowing
 // overloading.cpp

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

 int main() {
     Person jane;
     Student harry(975, "ABBAD");

     harry.set("Harry");
     harry.display(std::cout);
     std::cout << std::endl;
     harry.display();
     std::cout << std::endl;

     jane.set("Jane Doe");
     jane.display(std::cout);
     std::cout << std::endl;
 }













 Harry 975 ABBAD

 Harry


 Jane Doe


Constructors

A base class constructor is not inherited by a derived class unless specified otherwise.  If we don't declare a constructor for the derived class, the compiler inserts an empty no-argument constructor.

An instance of a derived class constructs itself in four steps in two distinct stages:

  1. construct the base class portion of the complete object
    1. allocate memory for the instance variables in the order of their declaration
    2. execute the base class constructor
  2. construct the derived class portion of the object
    1. allocate memory for the instance variables in the order of their declaration
    2. execute the derived class constructor

inheritance constructors

For example, let us define a no-argument constructor for the base class.  The header file contains:

 // Student.h

 #include <iostream>
 const int N = 30;
 const int M = 13;

 class Person {
     char person[N+1];
   public:
     Person();
     void set(const char* n);
     void display(std::ostream&) const; 
 };

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(int, const char*);
     void display(std::ostream&) const;
 };

The implementation file defines the constructors:

 // Student.cpp

 #include <cstring>
 #include "Student.h"

 Person::Person() {
     person[0] = '\0';
 }

 void Person::set(const char* n) {
     std::strncpy(person, n, N);
     person[N] = '\0';
 }

 void Person::display(std::ostream& os) const {
     os << person << ' ';
 }

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 Student::Student(int n, const char* g) {
     // see Current Object chapter for validation logic 
     no = n;
     std::strcpy(grade, g);
 }

 void Student::display(std::ostream& os) const {
     Person::display(os);
     os << no << ' ' << grade;
 }

The following client program uses this implementation to produce the result shown on the right:

 // Derived Class Constructors
 // derivedCtors.cpp

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

 int main() {
     Person jane;
     Student harry(975, "ABBAD");

     harry.set("Harry");
     harry.display(std::cout);
     std::cout << std::endl;

     jane.set("Jane");
     jane.display(std::cout);
     std::cout << std::endl;
 }













 Harry 975 ABBAD


 Jane

In this example, the two objects were constructed as follows:

  1. memory for jane is allocated
    1. memory for person is allocated
    2. the base class constructor initializes person to an empty string
  2. memory for harray is allocated
    1. memory for person is allocated
    2. the base class constructor initializes person to an empty string
    3. memory for no and grade is allocated
    4. the derived class constructor initializes no and grade to 975 and "ABBAD" respectively

Passing Arguments to a Base Class Constructor

A base class constructor receives data in its parameters either directly from the client program or from the derived class constructor.  Each constructor of a derived class, other than the no-argument constructor, receives in its parameters all of the initial values including those to be passed to the base class constructor.  The base class constructor uses the values received from the derived class constructor after the object has built its base class part first and before the object builds its derived class part.  The call to the base class constructor that passes the values takes the form

 Derived( parameters ) : Base( arguments )

where Derived stands for the name of the derived class and Base stands for the name of the base class.  The single colon separates the header of the derived-class constructor from the call to the base class constructor.  Omitting the call defaults to a call to the no-argument base class constructor.  That is, the compiler inserts a call to the no-argument base class constructor.

For example, let us replace the set() member function in the base class with a one-argument constructor and expand the Student's two-argument constructor to receive the student's name.  The header file contains:

 // Student.h

 #include <iostream>
 const int N = 30;
 const int M = 13;

 class Person {
     char person[N+1];
   public:
     Person();
     Person(const char*);
     void display(std::ostream&) const; 
 };

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(const char*, int, const char*); 
     void display(std::ostream&) const;
 };

The implementation file defines the constructors:

 // Student.cpp

 #include <cstring>
 #include "Student.h"

 Person::Person() {
     person[0] = '\0';
 }

 Person::Person(const char* nm) {
     std::strncpy(person, nm, N);
     person[N] = '\0';
 }

 void Person::display(std::ostream& os) const {
     os << person << ' ';
 }

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 Student::Student(const char* nm, int n, const char* g) : Person(nm) { 
     // see Current Object chapter for validation logic
     no = n;
     std::strcpy(grade, g);
 }

 void Student::display(std::ostream& os) const {
     Person::display(os);
     os << no << ' ' << grade;
 }

The following client program uses this implementation to produce the result shown on the right:

 // Derived Class Constructors with Arguments
 // drvdCtorsArgs.cpp

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

 int main() {
     Person jane("Jane");
     Student harry("Harry", 975, "ABBAD");

     harry.display(std::cout);
     std::cout << std::endl;

     jane.display(std::cout);
     std::cout << std::endl;
 }












 Harry 975 ABBAD

 Jane

Inheriting Base Class Constructors

By default, a derived class does not inherit the constructors of its base class.  To avoid coding derived class constructors that call their respective base class counterparts but contain no additional logic or their own, C++ lets us override the default and inherit the base class constructors directly.  The declaration for inheriting base class constructors takes the form:

 using Base::Base;

where Base stands for the name of the base class. 

For example, let us derive an Instructor class from the Person base class and inherit all of the constructors of the base class.  The header file contains:

 // Student.h

 // compiles with GCC 4.8 or greater or equivalent

 #include <iostream>
 const int N = 30;
 const int M = 13;

 class Person {
     char person[N+1];
   public:
     Person();
     Person(const char*);
     void set(const char* n);
     void display(std::ostream&) const; 
 };

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(const char*, int, const char*); 
     void display(std::ostream&) const;
 };

 class Instructor : public Person {
   public:
     using Person::Person;
 };

The implementation file reamins unchanged.  The following client program uses this new definition to produce the result shown on the right:

 // Inherited Constructors
 // inheritCtors.cpp

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

 int main() {
     Instructor john("John");
     Person jane("Jane");
     Student harry("Harry", 975, "ABBAD");

     john.display(std::cout);
     std::cout << std::endl;

     harry.display(std::cout);
     std::cout << std::endl;

     jane.display(std::cout);
     std::cout << std::endl;
 }











 John


 Harry 975 ABBAD


 Jane



Destructors

Destructors, without exception, are not inherited.  They execute in opposite order to the order of their object's construction.  The derived class destructor executes before the base class destructor. 

inheritance destructors

For example, let us introduce destructors for our base and derived classes that insert a message to standard output.  The header file contains:

 // Student.h

 #include <iostream>
 const int N = 30;
 const int M = 13;

 class Person {
     char person[N+1];
   public:
     Person();
     Person(const char*);
     ~Person();
     void set(const char* n);
     void display(std::ostream&) const; 
 };

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(const char*, int, const char*); 
     ~Student();
     void display(std::ostream&) const;
 };

The implementation file defines the destructors, which include messages:

 // Student.cpp

 #include <cstring>
 #include "Student.h"

 Person::Person() {
     person[0] = '\0';
 }

 Person::Person(const char* nm) {
     std::strncpy(person, nm, N);
     person[N] = '\0';
 }

 void Person::set(const char* n) {
     std::strncpy(person, n, N);
     person[N] = '\0';
 }

 Person::~Person() {
     std::cout << "Leaving " << person << std::endl;
 }

 void Person::display(std::ostream& os) const {
     os << person << ' ';
 }

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 Student::Student(const char* nm, int n, const char* g) : Person(nm) { 
     // see Current Object chapter for validation logic
     no = n;
     std::strcpy(grade, g);
 }

 Student::~Student() {
     std::cout << "\nLeaving " << no << std::endl;
 }

 void Student::display(std::ostream& os) const {
     Person::display(os);
     os << no << ' ' << grade;
 }

The following client program uses this implementation to produce the result shown on the right:

 // Derived Class Destructors
 // drvdDtors.cpp

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

 int main() {
     Person jane("Jane");
     Student harry("Harry", 975, "ABBAD");

     harry.display(std::cout);
     std::cout << std::endl;

     jane.display(std::cout);
     std::cout << std::endl;
 }










 Harry 975 ABBAD 

 Jane
 Leaving 975
 Leaving Harry
 Leaving Jane

Helper Operators

Helper functions, like the special member functions, are not inherited.  A helper function is dedicated to the class that it supports.  To distinguish calls to helper functions that support base classes from those that support derived classes, we need to identify the proper type in each function call. 

Consider insertion and extraction operators overloaded for both base and derived classes.  C++ determines which definition to call by the type of the right operand. 

For example, let us upgrade our Student class to include overloads of these operators for both base and derived classes.  The header file contains:

 // Student.h

 #include <iostream>
 const int N = 30;
 const int M = 13;

 class Person {
     char person[N+1];
   public:
     Person();
     Person(const char*);
     void display(std::ostream&) const; 
 };
 std::istream& operator>>(std::istream&, Person&); 
 std::ostream& operator<<(std::ostream&, const Person&);

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(const char*, int, const char*); 
     void display(std::ostream&) const;
 };
 std::istream& operator>>(std::istream&, Student&); 
 std::ostream& operator<<(std::ostream&, const Student&); 

The implementation file defines the helper operators:

 // Student.cpp

 #include <cstring>
 #include "Student.h"

 Person::Person() {
     person[0] = '\0';
 }

 Person::Person(const char* nm) {
     std::strncpy(person, nm, N);
     person[N] = '\0';
 }

 std::istream& operator>>(std::istream& is, Person& p) {
     char name[N+1];
     std::cout << "Name: ";
     is.getline(name, N+1);
     p = Person(name);
     return is;
 }

 std::ostream& operator<<(std::ostream& os, const Person& p) {
     p.display(os);
     return os;
 }

 void Person::display(std::ostream& os) const {
     os << person << ' ';
 }

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 Student::Student(const char* nm, int n, const char* g) : Person(nm) { 
     // see Current Object chapter for validation logic
     no = n;
     std::strcpy(grade, g);
 }

 void Student::display(std::ostream& os) const {
     os << no << ' ' << grade;
 }

 std::istream& operator>>(std::istream& is, Student& s) { 
     int no;
     char name[N + 1];
     char grade[M + 1];
     std::cout << "Name: ";
     is.getline(name, N+1);
     std::cout << "Number: ";
     is >> no;
     is.ignore(); // remove newline
     std::cout << "Grades: ";
     is.getline(grade, M+1);
     s = Student(name, no, grade);
     return is;
 }

 std::ostream& operator<<(std::ostream& os, const Student& s) { 
     const Person& p = s; // copies base class address
     os << p;
     s.display(os);
     return os;
 }

The following client program uses this implementation to produce the result shown on the right:

 // Helpers to Derived Classes
 // drvdHelpers.cpp

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

 int main() {
     Person jane;
     Student harry;

     std::cin >> jane;
     std::cin >> harry;
     std::cout << jane << std::endl;
     std::cout << harry << std::endl;
 }








 Name: Jane Doe
 Name: Harry
 Number: 975
 Grades: ABBD

 Jane Doe
 Harry 975 ABBD 

Summary

  • member functions of a derived class shadow the identically named members of a base class
  • a derived class does not inherit the constructors, destructor, assignment operators or helper functions of a base class
  • constuctors in a hierarchy execute in order from the base class to the derived class
  • destructors in a hierarchy execute in order from the derived class to the base class

Exercises




Previous Reading  Previous: Derived Classes Next: Derived Classes with Resources   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License