May 11, 2024

Clear and Concise Explanation of Inheritance in C++

In this C++ tutorial, we explain the concept of inheritance in C++. Inheritance is an important object-oriented programming concept that is used to create new classes that use and combine attributes (members and functions) of the existing classes and that add new functionalities to the existing classes. The inheritance process enables us to reuse and add new functionality to the existing code. It also enables us to quickly implement new ideas and concepts. The YouTube tutorial accompanying this post is given below.

Explanation of Inheritance

Loosely speaking, inheritance is the process of creating a new class from the existing class and adding new functionalities to the existing class. The new class is called the derived class and the existing class from which the new class is derived is called the base class. In its essence, inheritance implements “is a relationship“. For example, “is a” relationship is

  • A cat is a mammal. Here, a cat can be seen as the derived class, and a mammal can be seen as a base class.
  • Honda Civic is a car. Here, “Honda Civic” can be seen as the derived class, and a car can be seen as the base class.
  • A circle is a shape. A rectangle is a shape. Here, circle and rectangle can be seen as separate derived classes from the base class shape.

Every base class inherits almost all the member functions (methods) and all the member variables from its base class. Depending on how the member variables and member functions of the base class are declared (by using private, public, or protected access specifiers), the member functions of the derived class CAN or CANNOT directly access the member variables of the base class. This will be explained later in the text.

We explain the concept of inheritance by using the following example. Let us imagine that we work in an administration office of a university. Our goal is to keep track of student records. We can have several types of students. For example, we can have undergraduate, masters, or PhD students. For simplicity of explanation, we only consider two classes: undergraduate and graduate students.

Both undergraduate and graduate students can be seen as derived classes of the base class student. The base class student has two member variables:

  1. Student name. This is a C++ string.
  2. Student GPA. This is a float in the interval from 0 to 4.

Of course, in practice, the base class student will have many more member variables. However, for presentation clarity, we simply ignore them. The derived classes undergraduate student and graduate student should also have these member variables. However, in addition, they should also have their own member variables that are specific to these classes. The class graduate student can have a member variable tracking how many journal papers a student has published, and another member variable tracking if a student is eligible to defend a thesis. On the other hand, the class undergraduate student can have member variable stracking its current year, number of courses left to graduate, did (s)he complete a required internship, etc.

Let us first declare and define the base class called “Student”. The header file “Student.h” is given below.

#ifndef STUDENT_H
#define STUDENT_H
// This is a header file for the class Student

using namespace std;
#include<string>

class Student
{
	public:
		
		// constructors
		// default constructor
		// sets name=No name, gpa=0.0
		Student(); 
		// overloaded constructor, sets name=nameStudent , gpa=0.0
		Student(string nameStudent, float gpaStudent);
		
		// accessor functions
		string getName() const;
		float getGpa() const;
		// printing data
		void printData() const;
		
		//mutator functions
		void setName(string nameStudent);
		void setGpa(float gpaStudent);
				
		
	private:
		
		string name;
		float gpa;
};

#endif

The implementation file called “Student.cpp” is given below.

#include<string>
#include<iostream>
// This is the implementation file of the class "Student.h"

using namespace std;

//default constructor
Student::Student(): name("No name"), gpa(0.0)
{
	cout<<"Default student constructor called!"<<endl;
}

//overloaded constructor
Student::Student(string nameStudent, float gpaStudent):   name(nameStudent), gpa(gpaStudent)
{
	cout<<"Student constructor called with parameters!"<<endl;
}

// accessor functions
string Student::getName() const 
{
	return name;
}
float Student::getGpa() const 
{
	return gpa;
}

// function that prints the private members
void Student::printData() const 
{
	cout<<"Print function from the base Student class is called!"<<endl;
	cout<<"Name of the student:"<<name<<endl;
	cout<<"GPA of the student:"<<gpa<<endl;
}

// mutator functions 
void Student::setName(string nameStudent)
{
	name=nameStudent;
}
void Student::setGpa(float gpaStudent)
{
	gpa=gpaStudent;
}

The member variables “name” and “gpa” are used to store the student name and student’s GPA. They are deliberately declared by using the private access specifier. We will see what is the direct consequence of this type of declaration later in the text. Here we have to briefly mention that we can also use public and protected access specifiers. The protected access specifier will be explained later in the text. We have two constructors.

The default constructor is used to initialize the member variables with generic values, and the overloaded constructor is used to assign specific variables chosen by the user. Then, we introduced standard accessor and mutator functions used to access and modify the member variables. Finally, we introduced the function for printing the member variable.

Next, we declare the class graduate student. The inheritance type is specified to be public. Public inheritance means that public members of the base class become public members of the derived class. Protected members of the base class become protected members of the derived class. Here it should be emphasized that private members of the base class are not directly accessible by the member functions of the derived class. However, these private members can still be accessed indirectly from the derived class by using public and protected member functions of the base class. The inherited class is specified below (file “GraduateStudent.h”):

#ifndef GRADUATESTUDENT_H
#define GRADUATESTUDENT_H
// This is the header file for the GraduateStudent class that is inherited from the Student class 

#include "Student.h"

class GraduateStudent: public Student
{
	public:
		// default constructor, calls the default constructor from the base class and sets numberPapersPublished=0
		GraduateStudent();
		// overloaded constructor, calls the overloaded constructor from the base class with the parameters nameStudent and gpaStudent
		// and sets numberPapersPublished=numberPapers
		GraduateStudent(string nameStudent, float gpaStudent,int numberPapers);
		// accessor function
		unsigned int getNumberPapersPublished();
		// printing function - this function is originally defined in the base class, here it is re-declared and in the implementation file it is re-implemented 
		// it calls the original printData() function from the base class 
		void printData() const;	
		
		//mutator function 
		void setNumberPapersPublished(unsigned int numberPapers);
	private:
		unsigned int numberPapersPublished;
			
};
#endif

Before we implement this class, we have to explain a few important things. We specify that the class is derived from another class by using

class GraduateStudent: public Student

Here, we are saying “GraduateStudent” is derived from “Student” class through public inheritance. The class “GraduateStudent” has its own private member variable “numberPapersPublished”. In addition to this variable, it also inherits the member variables “name” and “gpa” from the base Student class. However, the variables “name” and “gpa” are not directly accessible by using the member functions of the GraduateStudent class. This is a direct consequence of the fact that these variables are declared by using the private access specifier in the base class. On the other hand, if we declared these variables by using the protected access specifier then the member functions of the derived class GraduateStudent would be able to directly access these variables. Beside the member variables, the derived class also inherits almost all member functions of the base class. That is, we can directly access almost all member functions of the base class from the derived class. By using the accessor functions of the base class we can indirectly access the base-class private variables “name” and “gpa” from the derived class. The implementation of the derived class is given below.

// This is the implementation file of the class GraduateStudent
#include "GraduateStudent.h"

// default constructor
// note that this constructor calls the default constructor of the base class
// even if you didn't explicitly call the default base class constructor, it will be automatically called
GraduateStudent::GraduateStudent(): Student(), numberPapersPublished(0)
{
	cout<<"Default graduate student constructor called!"<<endl;
}

// overloaded constructor,
// note that this constructor explicitly calls the overloaded constructor of the base class
GraduateStudent::GraduateStudent(string nameStudent, float gpaStudent,int numberPapers):
	Student(nameStudent,gpaStudent), numberPapersPublished(numberPapers)
{ 
	cout<<"Graduate student constructor called with parameters!"<<endl;
}

// accessor function
unsigned int GraduateStudent::getNumberPapersPublished()
{
	return numberPapersPublished;
}


//redefine the print function in the inherited class
// note here that we still call the print function from the base class
void GraduateStudent::printData() const
{
	cout<<"Print function from the inherited GraduateStudent class is called!"<<endl;
	// call the print function from the base class to print the member variables originally declared in the base class 
	// this is how we call the base class
	Student::printData();
	
	// then print the variable declared in the inherited class
	cout<<"Number of published papers: "<<numberPapersPublished<<endl;
	
 } 

//mutator function 
void GraduateStudent::setNumberPapersPublished(unsigned int numberPapers)
{
	numberPapersPublished=numberPapers;
}

It is very important to keep in mind the following:

Default constructor. The default constructor is defined as follows:

GraduateStudent::GraduateStudent(): Student(), numberPapersPublished(0)
{
	cout<<"Default graduate student constructor called!"<<endl;
}

We explicitly call the default constructor of the base class Student. That is, first the base class if constructed with all the member functions belonging to the base class, and then we construct the derived class. Note here that even if we did not explicitly call the default constructor, C++ will automatically call the default constructor of the base class.

Overloaded constructor. The overloaded constructor is defined as follows

GraduateStudent::GraduateStudent(string nameStudent, float gpaStudent,int numberPapers):
	Student(nameStudent,gpaStudent), numberPapersPublished(numberPapers)
{ 
	cout<<"Graduate student constructor called with parameters!"<<endl;
}

Here, we explicitly call the overloaded constructor of the base class to construct the base class with the specified parameters. Then, we initialize the private member functions of the derived class.

Redefined member functions and explicit call of member functions of the base class. In the derived class implementation, we redefined the function “void GraduateStudent::printData() const”. The redefinition is given below.

void GraduateStudent::printData() const
{
	cout<<"Print function from the inherited GraduateStudent class is called!"<<endl;
	// call the print function from the base class to print the member variables originally declared in the base class 
	// this is how we call the base class
	Student::printData();
	
	// then print the variable declared in the inherited class
	cout<<"Number of published papers: "<<numberPapersPublished<<endl;
	
 } 

When we call this function from an object of the derived class GraduateStudent, this redefined function will be called and not the original function in the base class. That is, we can redefine functions from the base class in the derived class. In the redefined class, we explicitly call the original print function from the base class. This is done by using the following code line

	Student::printData();

That is, we can explicitly call the base class functions from the derived class functions.

We have also defined accessor and mutator functions that are used to access and change the value of the private member variable “numberPapersPublished”. Here, we need to emphasize that we do not need to redefine the functions of the base class in the derived class, if we want to keep the functions from the base class as they are. This for example means that we can directly access mutator and accessor functions of the base class in the derived class.

Here are some additional important comments:

  • Let us assume that we defined a function outside of our classes that accepts an argument of the type “Student”. For example, consider the following declaration:

    void functionCheck(Student studentObject);

    Now, since GraduateStudent is also Student, we can call this function with an argument of type GraduateStudent. That is, we can use an object of a derived class if a function/variable is declared to accept an object of the base class.
  • Although the default constructor of the base class will automatically be called from the constructor of the derived class, we prefer to explicitly call the proper (overloaded) constructor of the base class from the constructor of the derived class. This is especially important when the overloaded constructor of the base class allocates the memory.
  • A derived class does not inherit the constructors of the base class. However, we can still call the base-class constructors from the constructor of the derived class.
  • Assignment operator, destructors, and copy constructors are also not inherited.
  • Instead of using the private access specifier, we can use the protected access specifier to define the member variables in the base class. In this way, we can access member variables of the base class from the derived class. However, we cannot access the protected variables outside of the base and derived classes.

Next, we present two driver code files. The first driver code file is given below.

# include "Student.h"
# include "Student.cpp"
# include "GraduateStudent.h"
# include "GraduateStudent.cpp"
using namespace std;

int main()
{
	
	GraduateStudent student2;
	student2.printData();
	
	return 0;
	
}

The output is given below.

Default student constructor called!
Default graduate student constructor called!
Print function from the inherited GraduateStudent class is called!
Print function from the base Student class is called!
Name of the student:No name
GPA of the student:0
Number of published papers: 0

Let us analyze the output. By creating an object of the class GraduateStudent without input parameters, we call the default constructor (that does not have parameters). In practice, first, default constructor of the base class is called, and then the default constructor of the derived class is called. Then, we call the print function. Since this function is redefined in the derived class, we actually call the print function from the derived class. The print function first calls the print function from the base class to print its private member functions. Finally, we print the private member variable of the derived class.

The second driver code is given below

# include "Student.h"
# include "Student.cpp"
# include "GraduateStudent.h"
# include "GraduateStudent.cpp"
using namespace std;

int main()
{
	
	GraduateStudent student3("John Doe",3.45,5);
	student3.printData();
	
	return 0;
	
}

This drive code produces the following output

Student constructor called with parameters!
Graduate student constructor called with parameters!
Print function from the inherited GraduateStudent class is called!
Print function from the base Student class is called!
Name of the student:John Doe
GPA of the student:3.45
Number of published papers: 5

The analysis of this output is the same as the analysis of the previous output.