Inheritance


Object-Oriented Programming Concepts

An object is a software bundle of related state (data members or properties) and behavior (member functions or methods). Software objects are often used to model the real-world objects that you find in everyday life.

A class is a blueprint or prototype from which objects are created. A class is an abstract definition that is made concrete at run-time when objects based upon the class are created.

Encapsulation, also known as data hiding, is the act of concealing the functionality of a class so that the internal operations are hidden, and irrelevant, to the programmer. With correct encapsulation, the developer does not need to understand how the class actually operates in order to communicate with it via its publicly available member functions and data members, known as its public interface. Encapsulation is essential to creating maintainable object-oriented programs. When the interaction with an object uses only the publicly available interface of member functions and properties, the class of the object becomes a correctly isolated unit. This unit can then be replaced independently to fix bugs, to change internal behavior or to improve functionality or performance. Encapsulation also promotes data integrity by allowing public "set" member functions to validate new values that are to be assigned to private data members.

Message passing, also known as interfacing, describes the communication between objects using their public interfaces. The primary way of passing a message to an object in C++ is to call a member function for that object.

Abstraction is the process of representing simplified versions of real-world objects in your classes and objects. A car class does not describe every possible detail of a car, only the relevant parts for the system being developed. Modeling software around real-world objects can vastly reduce the time required to understand a solution and be able to develop and maintain it.

Object Relationships

Objects can work together in many ways within a system. In some situations, classes and objects can be tightly coupled together to provide more complex functionality. This "has-a" relationship is known as composition. For example, modeling a car might involve creating individual classes such as wheel, engine, and transmission. The car class could then contain objects of these classes as data members, since a car "has" an engine, wheels, and a transmission. The internal workings of each class are not important due to encapsulation as the communication between the objects is still via passing messages to their public interfaces.

Other types of relationships may be modeled. A class may simply "use" an object of another class (perhaps creating the object as a local variable in one of its member functions). A class may also "know" about an object of another class without owning it (in C++, this association relationship might be modeled using a pointer or reference to the object).

Inheritance is an object-oriented programming concept used to model an "is-a" relationship between two classes. It allows one class (the derived class or subclass) to be based upon another (the base class or superclass) and inherit all of its functionality automatically. Additional code may then be added to create a more specialized version of the base class.

Ineritance

A derived class is more specific than its base class and represents a smaller group of objects.

A direct base class is the base class from which a derived class explicitly inherits. An indirect base class is inherited from two or more levels up the class hierarchy.

In the case of single inheritance, a class is derived from one base class. C++ also supports multiple inheritance, in which a derived class inherits from multiple (possibly unrelated) classes. Single inheritance is straightforward. Multiple inheritance can be complex and error prone.

Single-inheritance relationships form tree-like hierarchical structures - a base class exists in a hierarchical relationship with its derived classes.

C++ offers three kinds of inheritance - public, protected, and private. public inheritance in C++ is used to model "is a" relationships. Every object of a derived class is also an object of that derived class's base class. However, base-class objects are not objects of their derived classes. For example, all car objects are also vehicle objects, but not all vehicle objects are car objects.

With public inheritance, a derived class may

private and protected inheritance do not model "is-a" relationships and are not used as frequently.

Inheritance and Member Access

Base class access modifier public Inheritance protected Inheritance private Inheritance
public public in derived class protected in derived class Hidden in derived class
protected protected in derived class protected in derived class Hidden in derived class
private Hidden in derived class Hidden in derived class Hidden in derived class

A base class's public members are accessible anywhere that the program has a "handle" to an object (an object name or a pointer or reference to an object) of the base class or to an object of one of that base class's derived classes. Derived class member functions can access public base class data members directly.

A base class's private members are "hidden" - they are accessible only within the definition of that base class or from a friend of that class. A derived class cannot access the private members of its base class directly; allowing this would violate the encapsulation of the base class. A derived class can only access private base-class members through non-private member functions defined in the base class and inherited by the derived class.

A base class's protected members have an intermediate level of protection between public and private access. A base class's protected members can be directly accessed by member functions of that base class, by a friend of that base class, by member functions of a class derived from that base class, and by a friend of a class derived from that base class.

The use of protected data members allows for a slight increase in performance, because we avoid incurring the overhead of a call to a "set" or "get" member function. Unfortunately, protected data members often yield two major problems. First, the derived class object does not have to use a "set" member function to change the value of the base class's protected data. A derived class object can easily assign an illegal value to a protected data member. Second, derived class member functions are more likely to depend on base class implementation details. Changes to the base class may require changes to some or all of the derived classes of that base class.

Declaring data members private, while providing non-private member functions to manipulate and perform validation checking on this data, enforces good software engineering. The programmer should be able to change the base class implementation freely, while still providing the same services to the derived class. The performance increases gained by using protected data members are often negligible compared to the optimizations that compilers can perform. It is appropriate to use the protected access modifier when a base class should provide a service (i.e., a member function) only to its derived classes and should not provide the service to other clients.

When a base class member function is inappropriate for a derived class, that member function can be redefined in the derived class with an appropriate implementation. This is called overriding the base class member function.

When a derived class member function overrides a base class member function, the base class member function can still be accessed from the derived class by preceding the base class member function name with the base class name and the scope resolution operator (::).

When an object of a derived class is created, the base class's constructor is called immediately (either explicitly or implicitly) to initialize the base class data members in the derived class object (before the derived class data members are initialized). Explicitly calling a base class constructor requires using the same special "member initialization list syntax" used with composition and const data members.

When a derived class object is destroyed, the destructors are called in the reverse order of the constructors - first the derived class destructor is called, then the base class destructor is called.

Inheritance Syntax

To declare a derived class:


// car is a derived class of vehicle.
class car : public vehicle
{
    // Car data members and member functions
};

A constructor initialization list can be used to pass arguments from a derived class constructor to a base class constructor:


// Pass the string color to the base class vehicle constructor.
car::car(const string& color, int num_doors) : vehicle(color)
{
    this->num_doors = num_doors;
}

A derived class member function that overrides a base class member function can call the base class version of the function to do part of its work:


void car::print() const
{
    vehicle::print();   // Call the vehicle version of print() to print the car's color.
    cout << num_doors;
}

Upcasting and Downcasting

Upcasting is converting a derived class pointer (or reference) to a pointer (or reference) of the derived class's base class. In other words, upcasting allows us to treat a derived type as though it were its base type. It is always allowed for public inheritance, without an explicit type cast. This is a result of the "is-a" relationship between the base and derived classes. For example, if car is a class derived from vehicle, the following code is legal:


vehicle* vptr = new car();

The car object does not actually become a vehicle object as a result of this type cast (in a sense, it already is one). However, the vehicle pointer can only be used to access parts of the car object that are defined in the vehicle class. For example, you can only call member functions that are defined in the vehicle class. The car object is treated like any other vehicle, and its car-specific data members and member functions are unavailable.

Although there's no need to perform an explicit type cast on an upcast, you can do so if you want to:


vehicle* vptr = (vehicle*) new car();

The opposite process, converting a base class pointer (or reference) to a derived class pointer (or reference) is called downcasting. Downcasting is not allowed without an explicit type cast. The reason for this restriction is that the "is-a" relationship is not always symmetric. A car is a vehicle, but a vehicle may or may not be a car. For example:


car c1;
    
vehicle* vptr = &c1;          // Upcast - no type cast required.

car* car_ptr = (car*) vptr;   // Downcast - type cast required.

The code shown above works, because the object pointed to by vptr actually is a car object. If it wasn't, the results could lead to an unsafe operation.


bus b1;                       // Assume bus is also a derived class of vehicle.
    
vehicle* vptr = &b1;          // Upcast - no type cast required.
    
car* car_ptr = (car*) vptr;   // Downcast - type cast required. Fails because vptr
                              // points to a bus, not a car.

C++ provides a special explicit cast called dynamic_cast that allows for safe downcasting. If the type cast fails, it will return nullptr rather than crashing your program:


car* carptr = dynamic_cast<car*>(vptr);
if (carptr != nullptr)
{
    // Type cast succeeded, vptr was pointing to a car object
    // Can now safely call car-specific member functions using carptr
}

Use of the dynamic_cast operator requires enabling the C++ compiler's run-time type information (RTTI) feature. For the g++ compiler, this feature is enabled by default.

Subtype Polymorphism

The term binding means matching a function or member function call to a function or member function definition.

In C++, binding normally takes place when the program is compiled and linked. This is referred to as early binding or static binding.

In object-oriented programming, subtype polymorphism refers to the ability of objects belonging to different types to respond to member function calls of the same name, each one according to an appropriate type-specific behavior. The calling code does not have to know the exact type of the called object; which member function definition is called is determined at run-time (this is called late binding or dynamic binding).

In order for dynamic binding to take place in C++, several conditions must be met:

  1. The call must be to a member function, not a standalone function. Function calls in C++ always use static binding.
  2. The member function must have been declared using the keyword virtual. Calls to non-virtual member functions always use static binding.
  3. The member function must be called through a pointer or reference to an object, not an object name. All calls to member functions (including those to virtual member functions) through object names use static binding.

With dynamic binding, C++ distinguishes between a static type and a dynamic type of a variable. The static type is determined at compile time. It's the type specified in the pointer declaration. For example, the static type of vptr is vehicle*. However, the dynamic type of the pointer is determined by the type of object to which it actually points: car* in this case. When a virtual member function is called using vptr, C++ resolves the dynamic type of vptr and ensures that the appropriate version of the member function is invoked, a process referred to as virtual dispatch.

Dynamic binding exacts a toll. Resolving the dynamic type of an object takes place at runtime and therefore incurs performance overhead. However, this penalty is negligible in most cases.

One of the most common runtime techniques for implementing virtual dispatch is a virtual member function table, or v-table. A v-table is simply an array of pointers to member functions. Each class that contains virtual member functions has a v-table. Each object that is an instance of a class with virtual member functions contains, as a hidden field, a pointer to the class's v-table. The compiler encodes a member function call as an offset into a v-table, and the appropriate v-table is used with that offset at runtime to access the correct member function.

Declaring Virtual Member Functions

To make a member function virtual, put the keyword virtual at the beginning of its prototype:


virtual void print() const;

A member function in a derived class that overrides a virtual member function in a base class is automatically virtual as well.

Destructors may also be virtual. You should make the destructor for your class virtual if it contains any virtual member functions.

Abstract or Pure virtual Member Functions

An abstract member function is a member function that has a special prototype, but no definition. C++ refers to abstract member functions as pure virtual member functions. The prototype for a pure virtual member function ends with = 0, like this:


virtual void earnings() const = 0;

Since a pure virtual member function has no definition, you can't really call it. However, if a base class contains a pure virtual member function, a derived class is allowed to override the member function and provide a definition.

Abstract Classes

A class that contains one or more pure virtual member functions is called an abstract class (as opposed to a concrete class that provides definitions for all of its member functions).

You cannot create an object of an abstract class. However, an abstract class can be used as a base class for inheritance purposes. A class derived from an abstract class must provide definitions for any pure virtual member functions that it inherits, or it is also an abstract class.

You can also declare a pointer (or a reference) of an abstract class type. Such a pointer (or reference) would typically be used to point to a derived class object.

Interface Inheritance

Interface inheritance allows a derived class to inherit a base class's data type (which can be useful for subtype polymorphism) without actually inheriting any of the base class's implementation (member function definitions, etc.).

An interface can be defined in C++ as an abstract class that contains only pure virtual member functions and symbolic constants (public data members that are static and const).