Part D - Inheritance

Derived Classes

Relate classes using inheritance hierarchies to minimize the duplication of object code
Introduce the terminology and syntax of derived classes
Specify access to the protected members of a base class

"Public inheritance must always model 'is-a' ('works-like-a')" (Liskov, 1988)
"In correct inheritance, a derived class models a special case of a more general ... concept"
(Sutter, Alexandrescu, 2005)

Hierarchies | Definition | Access | Summary | Exercises


Object-oriented languages implement reusability of coding structure through inheritance.  Inheritance is the second most prominent concept next to encapsulation.  It refers to the relationship between classes where one class inherits the entire structure of another class.  Inheritance is naturally hierarchical, a tighter relationship than composition and the most highly coupled relationship after friendship. 

This chapter introduces the terminology used to describe an inheritance relationship and the syntax for defining a class that inherits the structure of another class.  This chapter includes specification of accessibility privileges between classes within a hierarchy. 


Hierarchies

A comprehensive example of inheritance relationships is the Linnaean Hierarchy in Biology (a small portion is shown below).  The Linnaean hierarchy relates all biological species in existence to one another.  Proceeding from the bottom of the hierarchy, we identify a human as a Homo, which is a Hominidae, which is a Primate, which is a Mammal, which is a Chordata, which is an Animal.  Similarly a dog is a Canis, which is a Canidae, which is a Carnivora, which is a Mammal, which is a Chordata, which is an Animal.

linnean hierarchy

Carl Linnaeus earned himself the title of Father of Taxonomy after developing this hierarchy.  He grouped the genera of Biology into higher taxa based on shared similarities.  Using his taxa along with modern refinements, we say that the genus Homo, which includes the species Sapiens, belongs to the Family Hominidae, which belongs to the Order Primates, which belongs to the Class Mammalia, which belongs to the Phylum Chordata, which belongs to the Kingdom Animalia.  For more details see the University of Michigan Museum of Zoology's Animal Diversity Site.

Inheritance in Hierarchies

Inheritance is a transitive structural relationship.  A human inherits the structure of a Homo, which inherits the structure of a Hominoid, which inherits the structure of a Primate, which inherits the structure of a Mammal, which inherits the structure of a Chordata, which inherits the structure of an Animal. 

Inheritance is not commutative.  A Primate is an Animal, but an Animal is not necessarily a Primate: dogs and foxes are not Primates.  Primates have highly developed hands and feet, shorter snouts and larger brains than dogs and foxes. 

Terminology

is a kind of

The relative position of two classes in a hierarchy identifies their inheritance relationship.  A class lower in the hierarchy is a kind of the class that is higher in the hierarchy.  For example, a dog is a kind of canis, a fox is a kind of Vulpes and a human is a kind of Homo.  In our course example from the first chapter, a Hybrid Course is a kind of Course

We depict an inheritance relationship by an arrow pointed to the inherited class. 

is a kind of hierarchy

The Hybrid Course class inherits the entire structure of the Course class. 

Derived and Base Classes

We call the child in an is-a-kind-of relationship the derived class and we call the parent in the relationship the base class; that is, the Hybrid Course class is a derived class of the Course base class.  A derived class is lower in the hierarchy, while its base class is higher in the hierarchy.  The derived class inherits the entire structure of its base class. 

The inheritance arrow extends from the derived class to the base class:

inheritance arrow

We depict an object of a derived class by placing its instance variables after the instance variables of its base class in the direction of increasing addresses in memory:

object representation

A derived class object contains the instance variables of the base class and those of the derived class, while a base class object only contains the instance variables of the base class. 

The terms base class and derived class are C++ specific.  Equivalent terms for these object-oriented concepts include:

  • base class - super class, parent class
  • derived class - subclass, heir class, child class

Inherited Structure

A derived class contains all of the instance variables and all of the normal member functions of its base class in addition to its own instance variables and member functions.  A derived class does not inherit the base class' special functions: constructors, destructors or assignment operators.  The term normal member function excludes these special member functions. 


Definition of a Derived Class

The definition of a derived class takes the form

class Derived : access Base {

    // ...

};

where Derived is the name of the derived class and Base is the name of the base class.  access identifies the access that member functions of the derived class have to the non-private members of the base class.  The default access is private.  The most common access is public

Example

A Student is a kind of Person.  Every Person has a name.  Accordingly, let us derive our Student class from a Person class, where the Person class includes an instance variable that holds a name in the form of a character string. 

The header file for our Student class contains our definitions of the base and derived classes:

 // Student.h

 #include <iostream>
 const int NC = 30;
 const int NG  = 20;

 class Person {                   // start of Base Class Definition
     char name[NC+1];
   public:
     void set(const char* n);
     void displayName(std::ostream&) const;
 };                              // end of Base Class Definition

 class Student : public Person { // start of Derived Class Definition 
     int no;
     float grade[NG];
     int ng;
   public:
     Student();
     Student(int);
     Student(int, const float*, int);
     void display(std::ostream&) const;
 };                              // end of Derived Class definition

The implementation file defines the member functions:

 // Student.cpp

 #include <cstring>
 #include "Student.h"
 using namespace std;

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

 void Person::displayName(std::ostream& os) const { 
     os << name << ' ';
 }

 Student::Student() {
     no = 0;
     ng = 0;
 }

 Student::Student(int n) {
     float g[] = {0.0f};
     *this = Student(n, g, 0);
 }

 Student::Student(int sn, const float* g, int ng_) { 
     bool valid = sn > 0 && g != nullptr && ng_ >= 0;
     if (valid)
         for (int i = 0; i < ng_ && valid; i++)
             valid = g[i] >= 0.0f && g[i] <= 100.0f;

     if (valid) {
         // accept the client's data
         no = sn;
         ng = ng_ < NG ? ng_ : NG;
         for (int i = 0; i < ng; i++)
             grade[i] = g[i];
     } else {
         *this = Student();
     }
 }

 void Student::display(std::ostream& os) const {
     if (no > 0) {
         os << no << ":\n";
         os.setf(ios::fixed);
         os.precision(2);
         for (int i = 0; i < ng; i++) { 
             os.width(6);
             os << grade[i] << endl;
         }
         os.unsetf(ios::fixed);
         os.precision(6);
     } else {
         os << "no data available" << endl; 
     }
 }

The following client code uses this implementation to produce the results on the right:

 // Derived Classes
 // derived.cpp

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

 int main() {
     float gh[] = {89.4f, 67.8f, 45.5f};
     Student harry(1234, gh, 3);
     harry.set("Harry");           // inherited
     harry.displayName(std::cout); // inherited 
     harry.display(std::cout);     // not inherited
 }









 Harry 1234: 
  89.40
  67.80
  45.50

Note that the main() function refers to the Student type, without referring to the Person type.  Here, the hierarchy itself is invisible to the client code.  We can upgrade the hierarchy without having to alter the client code in any way. 


Access

The C++ language supports three modifiers for granting access to the members of class:

  • private - bars all access
  • protected - limits access to derived classes only
  • public - unlimited access

Since the data member of the Person class is private, the member functions of our Student class and the client code cannot access that data member.  Since the member functions of the Person and Student classes are public, the main() function can access all of them. 

Limiting Access to Derived Classes

The keyword protected limits access to members of a derived class. 

For example, let us limit access to displayName() to classes derived for the Person class.  Then, the main() function cannot call this member function and we must call it directly from Student::display().  The header file limits the access:

 // Student.h

 #include <iostream>
 const int NC = 30;
 const int NG = 20;

 class Person {
     char name[NC+1];
   public:
     void set(const char* n);
   protected:
     void displayName(std::ostream&) const; 
 };

 class Student : public Person {
     int no;
     float grade[NG];
     int ng;
   public:
     Student();
     Student(int);
     Student(int, const float*, int);
     void display(std::ostream&) const;
 };

Our implementation of Student::display() calls displayName() directly:

 // Student.cpp

 #include <cstring>
 #include "Student.h"
 using namespace std;

 void Person::set(const char* n) {
     strncpy(name, n, NC); // validates length 
     name[NC] = '\0';
 }

 void Person::displayName(std::ostream& os) const {
     os << name << ' ';
 }

 Student::Student() {
     no = 0;
     ng = 0;
 }

 Student::Student(int n) {
     float g[] = {0.0f};
     *this = Student(n, g, 0);
 }

 Student::Student(int sn, const float* g, int ng_) { 
     bool valid = sn > 0 && g != nullptr && ng_ >= 0;
     if (valid)
         for (int i = 0; i < ng_ && valid; i++)
             valid = g[i] >= 0.0f && g[i] <= 100.0f;

     if (valid) {
         // accept the client's data
         no = sn;
         ng = ng_ < NG ? ng_ : NG;
         for (int i = 0; i < ng; i++)
             grade[i] = g[i];
     } else {
         *this = Student();
     }
 }

 void Student::display(std::ostream& os) const {
     if (no > 0) {
         displayName(os);
         os << no << ":\n";
         os.setf(ios::fixed);
         os.precision(2);
         for (int i = 0; i < ng; i++) { 
             os.width(6);
             os << grade[i] << endl;
         }
         os.unsetf(ios::fixed);
         os.precision(6);
     } else {
         os << "no data available" << endl; 
     }
 }

We refer to displayName() directly without any scope resolution as if this function is a member of our Student class. 

The following client code produces the output shown on the right:

 // Protected Access
 // protected.cpp

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

 int main() {
     float gh[] = {89.4f, 67.8f, 45.5f};
     Student harry(1234, gh, 3);
     harry.set("Harry");           // inherited
     harry.display(std::cout);     // not inherited
 }








 Harry 1234: 
  89.40
  67.80
  45.50

Avoid Granting Protected Access to Data Members

Granting data members protected access introduces a security hole.  If a derived class has protected access to any data member of its base class, any member function of the derived class can circumvent any validation procedure in the base class.  If the base class in the above example granted client code access to the person data member, we could change its contents from our Student class to a string of more than NC characters, which would probably break our Student object. 

Good Design Tip

Granting protected access to any data member exposes that member to potential corruption and is considered poor design.  A protected read-only query is a preferable alternative to protected access to a data member.  The query does not allow any modification of the value in the data member.


Summary

  • inheritance is a hierarchical relationship between classes. 
  • a derived class inherits the entire structure of its base class
  • the access modifier protected grants access to member functions of the derived class
  • any member function of a derived class may access any protected or public member of its base class
  • keeping a data member private and accessing it through a protected query is good design

Exercises




Previous Reading  Previous: Input and Output Operators Next: Functions in a Hierarchy   Next Reading


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