Advanced C++ Inheritance Techniques for Effective Object-Oriented Programming

Advanced C++ Inheritance Techniques

In Object-Oriented Programming, Inheritance empowers one class’s objects to inherit both properties and functionalities from another class. It forms the foundation for hierarchical classification. Consider a bird like a robin, belonging to the broader category of flying birds, which in turn is a subset of the class bird.

In Object Oriented Programming Inheritance is the process by which objects of one class acquire the properties and functionality of objects of another class. It supports the concept of hierarchical classification. For example, the bird robin is a part of the class flying bird which is again a part of the class bird.

In the last tutorial on inheritance in C++ we developed a model using modes of transportation to illustrate the concept of inheritance. In this article we will use that model to illustrate some of the finer points of inheritance and what it can be used for.

Table of Contents

Why should you embrace inheritance?

It enables the efficient reuse of code from previous projects, offering the flexibility to tweak it as needed for new endeavors. Rather than starting each project from scratch, leverage existing code and build upon it. Modifying the original class might introduce errors, whereas extending it minimizes the likelihood of mistakes. Additionally, inheritance proves invaluable when dealing with projects that demand multiple classes sharing similarities with subtle distinctions.

In our recent tutorial, we crafted a model utilizing transportation modes to exemplify inheritance. In this chapter, we will delve into the nuances of inheritance using this model, shedding light on its practical applications and benefits for C++ programmers eager to deepen their understanding.

One reason to use inheritance is that it allows you to reuse code from a previous project but gives you the flexibility to slightly modify it if the old code doesn’t do exactly what you need for the new project. It doesn’t make sense to start every new project from scratch since some code will certainly be repeated in several programs and you should strive to build on what you did previously. Moreover, it is easy to make an error if we try to modify the original class, but we are less likely to make an error if we leave the original alone and only add to it. Another reason for using inheritance is if the project requires the use of several classes which are very similar but slightly different.

Organize File Structure

Take a close examination of the file named inherit1.cpp below, it is identical to the program we developed in previous tutorial named allvehicle.cpp except that the program text is rearranged.

#include <iostream>

class Vehicle {
protected:
    int wheels;
    float weight;

public:
    void initialize(int in_wheels, float in_weight);
    int getWheels() const { return wheels; }
    float getWeight() const { return weight; }
    float wheelLoading() const { return weight / wheels; }
};

class Car : public Vehicle {
    int passengerLoad;

public:
    void initialize(int in_wheels, float in_weight, int people = 4);
    int passengers() const { return passengerLoad; }
};

class Truck : public Vehicle {
    int passengerLoad;
    float payload;

public:
    void initTruck(int howMany = 2, float maxLoad = 24000.0);
    float efficiency() const;
    int passengers() const { return passengerLoad; }
};

int main() {
    Vehicle unicycle;
    unicycle.initialize(1, 12.5);
    std::cout << "The unicycle has " << unicycle.getWheels() << " wheel.\n";
    std::cout << "The unicycle's wheel loading is " << unicycle.wheelLoading()
              << " pounds on the single tire.\n";
    std::cout << "The unicycle weighs " << unicycle.getWeight() << " pounds.\n\n";

    Car sedan;
    sedan.initialize(4, 3500.0, 5);
    std::cout << "The sedan carries " << sedan.passengers() << " passengers.\n";
    std::cout << "The sedan weighs " << sedan.getWeight() << " pounds.\n";
    std::cout << "The sedan's wheel loading is " << sedan.wheelLoading()
              << " pounds per tire.\n\n";

    Truck semi;
    semi.initialize(18, 12500.0);
    semi.initTruck(1, 33675.0);
    std::cout << "The semi weighs " << semi.getWeight() << " pounds.\n";
    std::cout << "The semi's efficiency is " << 100.0 * semi.efficiency() << " percent.\n";

    return 0;
}

// Initialize to any data desired
void Vehicle::initialize(int in_wheels, float in_weight) {
    wheels = in_wheels;
    weight = in_weight;
}

void Car::initialize(int in_wheels, float in_weight, int people) {
    passengerLoad = people;
    wheels = in_wheels;
    weight = in_weight;
}

void Truck::initTruck(int howMany, float maxLoad) {
    passengerLoad = howMany;
    payload = maxLoad;
}

float Truck::efficiency() const {
    return payload / (payload + weight);
}

We have simplified the program by converting some of the shorter methods within the classes to inline code. This alteration results in a more concise file, as inline methods eliminate the overhead of calling non-inline methods for short computations. This also enhances code readability and execution efficiency.

Additionally, we have reordered classes and their associated methods for improved clarity. All class definitions are now positioned at the beginning of the file, creating a cohesive interface for easier understanding.

We suggest compile and execute it with your C++ compiler. This arrangement simplifies the compilation and execution process, eliminating the need for a “make” file or a “project” capability. This user-friendly setup facilitates the seamless compilation and execution of example programs within this article.

The Scope Operator

The method initialize() in the derived class car may hides a method with the same name in the base class. In certain situations, you might find it necessary to invoke the method from the base class within the derived class object. To achieve this, you can employ the scope operator, denoted by ::. In the main program, the syntax is as follows:

sedan.vehicle::initialize(4, 3500.0);

This usage explicitly specifies the base class (vehicle) to which the method initialize() belongs, allowing you to call the method from the base class even when it’s hidden in the derived class.

Protected Data in C++ Inheritance

Let’s delve into the INHERIT4.CPP program to understand the concept of protected data in C++ inheritance. The inclusion of the keyword protected in line 5 of the vehicle class facilitates the direct inheritance of all data from the vehicle class into any derived classes. However, this data remains inaccessible outside the class or its derived classes, preserving information hiding.

#include <iostream.h>

class vehicle {
protected:
    int wheels;
    float weight;

public:
    void initialize(int in_wheels, float in_weight);
    int get_wheels(void) { return wheels; }
    float get_weight(void) { return weight; }
    float wheel_loading(void) { return weight / wheels; }
};

class car : public vehicle {
private:
    int passenger_load;

public:
    void initialize(int in_wheels, float in_weight, int people = 4);
    int passengers(void) { return passenger_load; }
};

class truck : public vehicle {
private:
    int passenger_load;
    float payload;

public:
    void init_truck(int how_many = 2, float max_load = 24000.0);
    float efficiency(void);
    int passengers(void) { return passenger_load; }
};

main() {
    // Main program implementation...
}

// initialize to any data desired
void vehicle::initialize(int in_wheels, float in_weight) {
    wheels = in_wheels;
    weight = in_weight;
}

void car::initialize(int in_wheels, float in_weight, int people) {
    passenger_load = people;
    wheels = in_wheels;
    weight = in_weight;
}

void truck::init_truck(int how_many, float max_load) {
    passenger_load = how_many;
    payload = max_load;
}

float truck::efficiency(void) {
    return payload / (payload + weight);
}

Output of the Program:

The unicycle has 1 wheel.
The unicycle’s wheel loading is 12.5 pounds on the single tire.
The unicycle weighs 12.5 pounds.

The sedan carries 5 passengers.
The sedan weighs 3500 pounds.
The sedan’s wheel loading is 875 pounds per tire.

The semi weighs 12500 pounds.
The semi’s efficiency is 72.929072 percent.

In this program, the vehicle class declares the variables wheels and weight as protected, making them directly accessible to any derived class. The car and truck classes then inherit these protected variables and include additional private variables. The program demonstrates the use of protected, private, and public access specifiers in class definitions.

To reiterate, protected data in a base class is inheritable by derived classes which allows controlled access within the inheritance hierarchy. Compile and execute this program to grasp the practical implications of protected data in C++ inheritance.

You will notice that the variables named wheels and weight are available to use in the method named initialize(). We can now state the rules for the three means of defining variables and methods.

  • private – The variables and methods are not available to any outside calling routines, and they are not available to any derived classes inheriting this class.
  • protected – The variables and methods are not available to any outside calling routines, but they are directly available to any derived class inheriting this class.
  • public – All variables and methods are freely available to all outside calling routines and to all derived classes.

You will note that these three means of definition can also be used in a struct data type. The only difference with a struct is that everything defaults to public until one of the other keywords is used.

Be sure to compile and execute this program before continuing on to the next example program.

Understanding Private Data in C++ Inheritance

Examine the program inherit5.cpp to comprehend the use of private data within C++ inheritance. In this example, the default access specifier for class members is private, which means that data in the base class (vehicle) is not directly accessible in the derived classes (car and truck). Instead, accessing this data requires invoking methods from the base class.

#include <iostream.h>

class vehicle {
    int wheels;
    float weight;

public:
    void initialize(int in_wheels, float in_weight);
    int get_wheels(void) { return wheels; }
    float get_weight(void) { return weight; }
    float wheel_loading(void) { return weight / wheels; }
};

class car : public vehicle {
private:
    int passenger_load;

public:
    void initialize(int in_wheels, float in_weight, int people = 4);
    int passengers(void) { return passenger_load; }
};

class truck : public vehicle {
private:
    int passenger_load;
    float payload;

public:
    void init_truck(int how_many = 2, float max_load = 24000.0);
    float efficiency(void);
    int passengers(void) { return passenger_load; }
};

main() {
    vehicle unicycle;
unicycle.initialize(1, 12.5);
cout << "The unicycle has " <<
unicycle.get_wheels() << " wheel.n";
cout << "The unicycle's wheel loading is " <<
unicycle.wheel_loading() << " pounds on the single tire.n";
cout << "The unicycle weighs " <<
unicycle.get_weight() << " pounds.nn";

car sedan;

sedan.initialize(4, 3500.0, 5);
cout << "The sedan carries " << sedan.passengers() <<
" passengers.n";
cout << "The sedan weighs " << sedan.get_weight() << " pounds.n";
cout << "The sedan's wheel loading is " <<
sedan.wheel_loading() << " pounds per tire.nn";

truck semi;

semi.initialize(18, 12500.0);
semi.init_truck(1, 33675.0);
cout << "The semi weighs " << semi.get_weight() << " pounds.n";
cout << "The semi's efficiency is " <<
100.0 * semi.efficiency() << " percent.n";
}

// initialize to any data desired
void vehicle::initialize(int in_wheels, float in_weight) {
    wheels = in_wheels;
    weight = in_weight;
}

void car::initialize(int in_wheels, float in_weight, int people) {
    passenger_load = people;
    vehicle::initialize(in_wheels, in_weight);
}

void truck::init_truck(int how_many, float max_load) {
    passenger_load = how_many;
    payload = max_load;
}

float truck::efficiency(void) {
    return payload / (payload + get_weight());
}

Output of the Program:

The unicycle has 1 wheel.
The unicycle’s wheel loading is 12.5 pounds on the single tire.
The unicycle weighs 12.5 pounds.

The sedan carries 5 passengers.
The sedan weighs 3500 pounds.
The sedan’s wheel loading is 875 pounds per tire.

The semi weighs 12500 pounds.
The semi’s efficiency is 72.929072 percent.

Although it might seem somewhat cumbersome to call methods in the base class to access data that logically belongs to the derived class, this is a fundamental aspect of how C++ operates. This design choice encourages careful consideration of how a class is intended to be used. If future users might inherit the class and extend it, it’s advisable to make data members protected for easy utilization in the new class.

Compile and execute this program to observe how private data influences the interaction between base and derived classes in C++.

Inheriting Constructors in C++

Explore the program inherit6.cpp to understand the concept of inheriting constructors in C++, introducing constructors in both base and derived classes.

#include <iostream.h>

class vehicle {
protected:
    int wheels;
    float weight;

public:
    vehicle(void) { wheels = 7; weight = 11111.0; }
    void initialize(int in_wheels, float in_weight);
    int get_wheels(void) { return wheels; }
    float get_weight(void) { return weight; }
    float wheel_loading(void) { return weight / wheels; }
};

class car : public vehicle {
    int passenger_load;

public:
    car(void) { passenger_load = 4; }
    void initialize(int in_wheels, float in_weight, int people = 4);
    int passengers(void) { return passenger_load; }
};

class truck : public vehicle {
    int passenger_load;
    float payload;

public:
    truck(void) { passenger_load = 3; payload = 22222.0; }
    void init_truck(int how_many = 2, float max_load = 24000.0);
    float efficiency(void);
    int passengers(void) { return passenger_load; }
};

main() {
    vehicle unicycle;
    // unicycle.initialize(1, 12.5);
    cout << "The unicycle has " << unicycle.get_wheels() << " wheel.n";
    cout << "The unicycle's wheel loading is " << unicycle.wheel_loading() << " pounds on the single tire.n";
    cout << "The unicycle weighs " << unicycle.get_weight() << " pounds.nn";

    car sedan;
    // sedan.initialize(4, 3500.0, 5);
    cout << "The sedan carries " << sedan.passengers() << " passengers.n";
    cout << "The sedan weighs " << sedan.get_weight() << " pounds.n";
    cout << "The sedan's wheel loading is " << sedan.wheel_loading() << " pounds per tire.nn";

    truck semi;
    // semi.initialize(18, 12500.0);
    // semi.init_truck(1, 33675.0);
    cout << "The semi weighs " << semi.get_weight() << " pounds.n";
    cout << "The semi's efficiency is " << 100.0 * semi.efficiency() << " percent.n";
}

// initialize to any data desired
void vehicle::initialize(int in_wheels, float in_weight) {
    wheels = in_wheels;
    weight = in_weight;
}

void car::initialize(int in_wheels, float in_weight, int people) {
    passenger_load = people;
    wheels = in_wheels;
    weight = in_weight;
}

void truck::init_truck(int how_many, float max_load) {
    passenger_load = how_many;
    payload = max_load;
}

float truck::efficiency(void) {
    return payload / (payload + weight);
}

Output of the Program:

The unicycle has 7 wheel.
The unicycle’s wheel loading is 1587.285767 pounds on the single tire.
The unicycle weighs 11111 pounds.

The sedan carries 4 passengers.
The sedan weighs 11111 pounds.
The sedan’s wheel loading is 1587.285767 pounds per tire.

The semi weighs 11111 pounds.
The semi’s efficiency is 66.666667 percent.

In this example, the vehicle class includes a constructor to initialize the number of wheels and weight, and the car and truck classes each have their own constructors to set unique variables. When a constructor is called for a derived class, the constructor for the parent class is also called, ensuring the initialization of all data, including inherited data. This behavior is important to recognize in the context of inheritance and constructors. Compile and execute this example program to observe the results.

Pointers to an Object and an Array of Objects

Examine the example program named inherit9.cpp for examples of the use of an array of objects and a pointer to an object.

#include <iostream.h>

class vehicle {
protected:
    int wheels;
    float weight;

public:
    void initialize(int in_wheels, float in_weight);
    int get_wheels(void) { return wheels; }
    float get_weight(void) { return weight; }
    float wheel_loading(void) { return weight / wheels; }
};

class car : public vehicle {
    int passenger_load;

public:
    void initialize(int in_wheels, float in_weight, int people = 4);
    int passengers(void) { return passenger_load; }
};

class truck : public vehicle {
    int passenger_load;
    float payload;

public:
    void init_truck(int how_many = 2, float max_load = 24000.0);
    float efficiency(void);
    int passengers(void) { return passenger_load; }
};

main() {
    vehicle unicycle;

    unicycle.initialize(1, 12.5);
    cout << "The unicycle has " << unicycle.get_wheels() << " wheel.n";
    cout << "The unicycle's wheel loading is " << unicycle.wheel_loading() << " pounds on the single tire.n";
    cout << "The unicycle weighs " << unicycle.get_weight() << " pounds.nn";

    car sedan[3];
    int index;

    for (index = 0; index < 3; index++) {
        sedan[index].initialize(4, 3500.0, 5);
        cout << "The sedan carries " << sedan[index].passengers() << " passengers.n";
        cout << "The sedan weighs " << sedan[index].get_weight() << " pounds.n";
        cout << "The sedan's wheel loading is " << sedan[index].wheel_loading() << " pounds per tire.nn";
    }

    truck *semi;

    semi = new truck;
    semi->initialize(18, 12500.0);
    semi->init_truck(1, 33675.0);
    cout << "The semi weighs " << semi->get_weight() << " pounds.n";
    cout << "The semi's efficiency is " << 100.0 * semi->efficiency() << " percent.n";
    delete semi;
}

// initialize to any data desired
void vehicle::initialize(int in_wheels, float in_weight) {
    wheels = in_wheels;
    weight = in_weight;
}

void car::initialize(int in_wheels, float in_weight, int people) {
    passenger_load = people;
    wheels = in_wheels;
    weight = in_weight;
}

void truck::init_truck(int how_many, float max_load) {
    passenger_load = how_many;
    payload = max_load;
}

float truck::efficiency(void) {
    return payload / (payload + weight);
}

Output of the Program:

The unicycle has 1 wheel.
The unicycle’s wheel loading is 12.5 pounds on the single tire.
The unicycle weighs 12.5 pounds.

The sedan carries 5 passengers.
The sedan weighs 3500 pounds.
The sedan’s wheel loading is 875 pounds per tire.

The sedan carries 5 passengers.
The sedan weighs 3500 pounds.
The sedan’s wheel loading is 875 pounds per tire.

The sedan carries 5 passengers.
The sedan weighs 3500 pounds.
The sedan’s wheel loading is 875 pounds per tire.

The semi weighs 12500 pounds.
The semi’s efficiency is 72.929072 percent.

In this program, objects are instantiated from an inherited class, showcasing the use of an array of objects and a pointer to an object. Understanding these concepts will enhance your proficiency with pointers and arrays of objects in C++. Compile and execute this program after studying the code.

M. Saqib: Saqib is Master-level Senior Software Engineer with over 14 years of experience in designing and developing large-scale software and web applications. He has more than eight years experience of leading software development teams. Saqib provides consultancy to develop software systems and web services for Fortune 500 companies. He has hands-on experience in C/C++ Java, JavaScript, PHP and .NET Technologies. Saqib owns and write contents on mycplus.com since 2004.
Related Post