Fundamentals of Programming (C/C++)

Chapter 9
Inheritance, Polymorphism, and Abstraction

Bachelors Program in Computer Applications
Fundamentals of Programming (C/C++)   •   Bachelor

Inheritance, Polymorphism, and Abstraction

Introduction

Object-oriented programming (OOP) provides mechanisms for representing software as cooperating objects. Inheritance, polymorphism, and abstraction are central to designing systems that can be extended without rewriting existing code. They are especially important in C++, where the language supports class hierarchies, virtual functions, abstract classes, multiple inheritance, templates, and runtime type identification.

These mechanisms are powerful, but they are not automatically good design. Inheritance can create excessive coupling, and polymorphism can obscure control flow if used carelessly. A professional programmer must therefore evaluate not only whether a feature works, but also whether the resulting design has appropriate cohesion, substitutability, maintainability, and clarity.

Consider a university system containing students, instructors, and administrative employees. These entities share properties such as an identity and contact information, but they also behave differently. A common interface can express shared capabilities, while specialized implementations preserve meaningful differences. The design challenge is to identify what should be shared, what should vary, and which relationship should be represented by inheritance or composition.

Learning Objectives

By the end of this chapter, you should be able to:

  • explain inheritance, composition, abstraction, and polymorphism;
  • distinguish “is-a” relationships from “has-a” relationships;
  • implement base and derived classes in C++;
  • use virtual functions and dynamic binding correctly;
  • design and use abstract classes and interface-like types;
  • explain substitutability and the Liskov Substitution Principle;
  • apply override, virtual destructors, and runtime type information safely;
  • evaluate coupling, cohesion, and the costs of inheritance;
  • select an appropriate object-oriented design for a practical problem.

1. Inheritance as a Type Relationship

Inheritance allows one class, called the derived class or subclass, to acquire accessible members of another class, called the base class or superclass. The derived class may add new state and behavior or redefine virtual behavior supplied by the base class.

class Person {
private:
    std::string name;

public:
    explicit Person(const std::string& n) : name(n) {}
    std::string getName() const { return name; }
};

class Student : public Person {
private:
    int studentId;

public:
    Student(const std::string& n, int id)
        : Person(n), studentId(id) {}
};

Student is publicly derived from Person. Public inheritance normally expresses an “is-a” relationship: a student is a person. A Student object contains a Person subobject, initialized by the base-class constructor before the derived-class members are initialized.

Access and visibility

A derived class does not automatically gain access to private members of its base class. Private data should generally be accessed through protected operations or public member functions. The three inheritance access modes have different effects:

Inheritance formPublic base members becomeProtected base members becomeTypical meaning
publicpublicprotectedPublic subtype relationship
protectedprotectedprotectedRestricted implementation relationship
privateprivateprivateImplementation reuse, rarely preferred

A base class’s private members remain inaccessible directly in all cases. Public inheritance is the form relevant to substitutability and runtime polymorphism.

Constructor and destructor order

When a derived object is created, the base portion is constructed first, followed by derived members. Destruction occurs in the reverse order. This ordering allows the derived class to rely on an initialized base subobject and ensures that resources acquired by the derived class are released before the base portion is destroyed.

2. Inheritance versus Composition

Composition represents a “has-a” relationship. An object is composed of other objects that provide services or represent internal parts.

class Battery {
public:
    void supplyPower() const {}
};

class Laptop {
private:
    Battery battery;

public:
    void start() {
        battery.supplyPower();
    }
};

A laptop has a battery; it is not a battery. Composition usually produces weaker coupling than inheritance because the containing class depends on an object’s service rather than on its entire type hierarchy.

A design comparison

QuestionInheritanceComposition
Relationship“is-a”“has-a” or “uses-a”
Reuse mechanismBase-class interface and implementationDelegation to contained objects
CouplingOften strongerUsually weaker
Runtime replacementMore restrictedOften easier
Appropriate whenSubstitutability is validBehavior or parts may vary independently

The “prefer composition over inheritance” guideline is not an absolute prohibition. Inheritance is appropriate when the subtype genuinely obeys the base abstraction’s contract. It is inappropriate merely because two classes share implementation details.

For example, making Square inherit from Rectangle may appear convenient because both have width and height. However, if a rectangle permits independent changes to width and height while a square must keep them equal, operations valid for Rectangle may violate the rules of Square. The apparent code reuse can therefore produce a poor subtype relationship.

3. Abstraction and Encapsulation

Abstraction identifies the essential operations of an entity while hiding irrelevant implementation details. Encapsulation groups data and operations and controls access to the representation. These ideas are related but distinct:

  • Encapsulation controls how implementation details are accessed.
  • Abstraction determines which concepts and operations are exposed.

An abstract type describes what clients can do without requiring them to know how it is done. For example, a payment processor may expose authorize() and refund() while hiding network protocols, encryption, and database transactions.

A useful abstraction should have:

  1. a clear responsibility;
  2. a small, coherent public interface;
  3. behavior that clients can understand from the contract;
  4. minimal dependence on implementation details;
  5. extension points that do not require unsafe modification of existing code.

Abstract classes

A class containing at least one pure virtual function is abstract. It cannot be instantiated directly.

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

The notation = 0 declares a pure virtual function. Shape specifies that every concrete shape must provide an area calculation. It may still contain implemented functions and data members.

class Circle : public Shape {
private:
    double radius;

public:
    explicit Circle(double r) : radius(r) {}

    double area() const override {
        return 3.141592653589793 * radius * radius;
    }
};

The area formula is A = πr2, where A is area in square units, r is radius in units, and π is the mathematical constant approximately equal to 3.14159. The formula assumes a Euclidean circle and a nonnegative radius.

Interface-like classes

C++ has no separate interface keyword. An interface-like class is commonly created with only public pure virtual functions and a virtual destructor:

class Printable {
public:
    virtual void print() const = 0;
    virtual ~Printable() = default;
};

A class can implement several independent interfaces through multiple inheritance:

class Report : public Printable {
public:
    void print() const override {
        // Print report contents.
    }
};

Interface-like classes should normally contain little or no state. Their purpose is to express capabilities rather than to provide a large shared implementation.

4. Polymorphism

Polymorphism means that one interface can represent objects with different concrete behavior. In C++, several forms occur:

  • Ad hoc polymorphism, such as function overloading;
  • Parametric polymorphism, commonly supported by templates;
  • Subtype polymorphism, in which derived objects are used through base references or pointers.

This chapter focuses mainly on subtype polymorphism.

#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual double area() const = 0;
    virtual void describe() const = 0;
    virtual ~Shape() = default;
};

class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }

    void describe() const override {
        std::cout << "Rectangle\n";
    }
};

A client can process shapes without knowing each concrete class:

void printArea(const Shape& shape) {
    std::cout << shape.area() << '\n';
}

A Circle, Rectangle, or another concrete shape can be passed to printArea. The function depends on the abstraction rather than on a particular implementation.

5. Virtual Functions and Dynamic Binding

A nonvirtual member function is selected primarily from the static type of an expression. A virtual function can be selected at runtime according to the object’s dynamic type. This is called dynamic binding or late binding.

class Document {
public:
    virtual void exportFile() const {
        std::cout << "Generic document\n";
    }
    virtual ~Document() = default;
};

class PdfDocument : public Document {
public:
    void exportFile() const override {
        std::cout << "Exporting PDF\n";
    }
};

void exportDocument(const Document& document) {
    document.exportFile();
}

When a PdfDocument is passed to exportDocument, the overridden PdfDocument::exportFile() is called because exportFile() is virtual.

The override specifier is strongly recommended. It asks the compiler to verify that the derived member actually overrides a virtual base member. It catches errors such as a misspelled function name, incorrect parameter type, or missing const qualifier.

Object slicing

Passing a derived object by value to a base parameter copies only the base subobject:

void process(Document document); // Risk of slicing

The derived portion is sliced away. To preserve polymorphism, use a reference or pointer:

void process(const Document& document);
void process(const Document* document);

References express a required existing object; pointers may represent optionality. Smart pointers are preferred for ownership.

Virtual destructors

If a class is intended to be used polymorphically, its destructor should usually be virtual:

class Base {
public:
    virtual ~Base() = default;
};

This ensures that deleting a derived object through a base pointer invokes the derived destructor first. Without a virtual destructor, destruction through the base pointer produces undefined behavior when the object has derived resources.

6. Ownership and Smart Pointers

Polymorphic collections often store pointers because objects in a collection may have different concrete types. Raw pointers do not express ownership clearly. Use std::unique_ptr for exclusive ownership and std::shared_ptr only when ownership is genuinely shared.

std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));

for (const auto& shape : shapes) {
    std::cout << shape->area() << '\n';
}

The container owns the shapes and automatically destroys them. This avoids manual new and delete, reducing leaks and exception-safety problems.

7. Substitutability and the Liskov Substitution Principle

The Liskov Substitution Principle (LSP) states that objects of a subtype should be usable wherever objects of the base type are expected without invalidating the program’s correctness. In practical terms, a subtype must honor the base type’s behavioral contract.

A subtype should generally:

  • accept every valid input accepted by the base abstraction;
  • preserve guarantees made by the base abstraction;
  • not unexpectedly strengthen preconditions;
  • not weaken postconditions;
  • preserve meaningful invariants.

Suppose a base class declares:

class Account {
public:
    virtual void withdraw(double amount) = 0;
    virtual ~Account() = default;
};

If clients reasonably expect every Account to permit withdrawal up to its available balance, a derived FixedDepositAccount that always throws for withdrawal may violate the expected contract. The problem is not the exception itself; it is that the abstraction promises a capability the subtype cannot support.

A better model may separate capabilities:

class Account {
public:
    virtual double balance() const = 0;
    virtual ~Account() = default;
};

class Withdrawable {
public:
    virtual void withdraw(double amount) = 0;
    virtual ~Withdrawable() = default;
};

Clients that require withdrawal can depend on Withdrawable, while all accounts can provide balance information.

8. Coupling, Cohesion, and Dependency Direction

Coupling measures the degree of dependence between modules. High coupling means that a change in one class is likely to force changes in others. Cohesion measures how closely the responsibilities within a module belong together. High cohesion is generally desirable.

Inheritance can increase coupling because a derived class depends on base-class details, initialization rules, protected members, and virtual behavior. A base-class change may affect many subclasses. Composition can improve flexibility when the contained component is represented by a stable interface.

A useful design principle is dependency inversion: high-level policy should depend on abstractions rather than on concrete implementation classes.

class PaymentGateway {
public:
    virtual bool charge(double amount) = 0;
    virtual ~PaymentGateway() = default;
};

class CheckoutService {
private:
    PaymentGateway& gateway;

public:
    explicit CheckoutService(PaymentGateway& g) : gateway(g) {}

    bool checkout(double amount) {
        return gateway.charge(amount);
    }
};

CheckoutService can work with a production gateway, a test double, or a different payment provider. This improves testability and reduces vendor-specific coupling.

9. Runtime Type Information

Runtime type information (RTTI) allows limited inspection of an object’s dynamic type. The principal tools are dynamic_cast and typeid.

Shape* shape = /* points to a polymorphic object */;
if (Circle* circle = dynamic_cast<Circle*>(shape)) {
    // The object is a Circle or a class derived from Circle.
}

For dynamic_cast to perform a checked downcast, the source type must be polymorphic, usually by having at least one virtual function. A failed pointer cast returns nullptr; a failed reference cast throws std::bad_cast.

typeid can obtain a std::type_info object:

if (typeid(*shape) == typeid(Circle)) {
    // Exact dynamic type is Circle.
}

RTTI is useful at system boundaries, plug-in mechanisms, serialization tools, and diagnostic code. Repeated downcasting in ordinary business logic often indicates a weak abstraction. Prefer virtual functions or separate interfaces when behavior can be expressed polymorphically.

10. Multiple Inheritance and the Diamond Problem

C++ permits a class to inherit from more than one base class:

class Scanner {
public:
    virtual void scan() = 0;
    virtual ~Scanner() = default;
};

class Printer {
public:
    virtual void print() = 0;
    virtual ~Printer() = default;
};

class MultifunctionDevice : public Scanner, public Printer {
public:
    void scan() override {}
    void print() override {}
};

Multiple inheritance from independent interfaces is often clear. Problems arise when two paths contain the same base class, producing duplicate base subobjects. Virtual inheritance can ensure that only one shared base subobject exists, but it adds complexity. Therefore, multiple inheritance should be used deliberately, especially when state and implementation are shared.

11. Worked Example: A Notification Framework

Assume an application must send notifications by email, SMS, and a mobile push service. A poor design may put every channel into one large Notification class:

class Notification {
public:
    void sendEmail();
    void sendSms();
    void sendPush();
};

This class has several unrelated responsibilities. It is difficult to test, and adding a new channel requires modifying the existing class.

A better design introduces an abstraction:

class Notifier {
public:
    virtual bool send(const std::string& recipient,
                      const std::string& message) = 0;
    virtual ~Notifier() = default;
};

class EmailNotifier : public Notifier {
public:
    bool send(const std::string& recipient,
              const std::string& message) override {
        // Email-specific implementation.
        return true;
    }
};

class SmsNotifier : public Notifier {
public:
    bool send(const std::string& recipient,
              const std::string& message) override {
        // SMS-specific implementation.
        return true;
    }
};

The application service depends on Notifier:

class AlertService {
private:
    Notifier& notifier;

public:
    explicit AlertService(Notifier& n) : notifier(n) {}

    bool raiseAlert(const std::string& recipient,
                    const std::string& message) {
        return notifier.send(recipient, message);
    }
};

Analysis

Background: An application needs multiple notification channels.

Problem: A monolithic class creates low cohesion and high coupling.

Approach: Define one stable capability, Notifier, and implement each channel separately. Inject the required notifier into AlertService.

Outcome: New channels can be added without changing the alert service. Test code can provide a fake notifier that records messages instead of contacting an external provider.

Learning points: The design uses abstraction, subtype polymorphism, dependency inversion, composition, and dynamic binding. It does not use inheritance merely for code reuse; each concrete notifier is substitutable because it fulfills the same sending contract.

The example remains illustrative. A production implementation would also address authentication, retries, rate limits, delivery status, personal-data protection, and failure reporting.

12. Challenges and Limitations

Fragile base-class problem

A change to a base class can unexpectedly break derived classes, especially when subclasses depend on protected data or undocumented behavior. Keep base interfaces small, document contracts, and avoid exposing implementation details.

Deep hierarchies

Deep inheritance trees make behavior difficult to trace and increase the number of interactions that must be understood. Prefer shallow hierarchies organized around stable abstractions.

Overriding versus hiding

A derived function with the same name but a different signature may hide base overloads rather than override them. Use override, and use using Base::function; when intentionally importing base overloads.

Resource management

Polymorphic code must handle ownership, copying, moving, and destruction correctly. The Rule of Zero—designing classes so that standard library members manage resources—usually reduces errors.

Performance considerations

Virtual calls may involve an indirect dispatch and can inhibit some compiler optimizations. In most application code this cost is small compared with I/O or database operations. Performance-sensitive code should be measured rather than optimized by assumption. Templates and static polymorphism may be appropriate when compile-time selection is required.

13. Professional, Ethical, and Contemporary Considerations

Object-oriented design affects reliability, security, and maintainability. A notification, payment, or identity hierarchy may process sensitive personal information. Classes should expose only necessary data, validate inputs, protect credentials, and produce auditable error information without leaking secrets.

Software teams should document public contracts and test substitutability. Automated tests should cover base-interface behavior, derived implementations, ownership, failure paths, and boundary conditions. Code review should ask whether inheritance represents a genuine subtype relationship or merely avoids duplicating a few lines.

Modern C++ development commonly combines runtime polymorphism with templates, concepts, standard containers, smart pointers, and dependency injection. std::variant can be preferable when a closed set of alternatives is known and exhaustive handling is desirable. Virtual interfaces are often preferable when third parties may add new implementations independently. The design choice should follow the variability required by the system.

Best-Practice Checklist

  • Use public inheritance only for a valid substitutable “is-a” relationship.
  • Prefer composition when components may vary independently.
  • Give polymorphic base classes virtual destructors.
  • Mark overrides with override.
  • Keep abstract interfaces small and cohesive.
  • Pass polymorphic objects by reference or pointer, not by value.
  • Express ownership with smart pointers.
  • Avoid unnecessary downcasts and RTTI-based branching.
  • Minimize protected state in base classes.
  • Test contracts, not only concrete implementations.
  • Consider std::variant, templates, or composition where they provide a clearer design.

Chapter Summary

Inheritance defines a relationship in which a derived class extends a base abstraction, but it should be used only when the derived object is genuinely substitutable for the base object. Composition represents “has-a” or “uses-a” relationships and often reduces coupling. Abstraction identifies essential operations, while encapsulation protects implementation details.

Virtual functions enable dynamic binding: a call through a base reference or pointer can execute behavior belonging to the object’s dynamic type. Abstract classes and pure virtual functions define contracts without permitting incomplete objects to be instantiated. Virtual destructors are essential when polymorphic objects may be destroyed through base pointers. Smart pointers clarify ownership and make polymorphic collections safer.

Good object-oriented design balances cohesion, coupling, flexibility, and clarity. The Liskov Substitution Principle provides a behavioral test for inheritance. RTTI has legitimate uses but should not replace well-designed polymorphic interfaces. Multiple inheritance can be useful for independent interfaces but requires care when shared state is involved. In professional C++ programming, the central question is not “Can inheritance be used?” but “Does this abstraction accurately express the required behavior and future variation?”

Key Terms

Inheritance: A mechanism by which a derived class acquires or extends a base class.

Base class: A class that provides an interface or implementation for derived classes.

Derived class: A class that inherits from and specializes a base class.

Composition: Building a class from contained objects or components.

Abstraction: Representing essential behavior while hiding unnecessary implementation details.

Encapsulation: Restricting direct access to an object’s representation and operations.

Polymorphism: The ability to use one interface with objects of different forms.

Virtual function: A member function eligible for dynamic dispatch.

Pure virtual function: A virtual function declared with = 0.

Abstract class: A class that cannot be instantiated, usually because it has a pure virtual function.

Dynamic binding: Runtime selection of an overridden virtual function.

Static type: The type known by the compiler for an expression.

Dynamic type: The actual runtime type of an object.

Object slicing: Loss of the derived portion when a derived object is copied into a base object by value.

Substitutability: The ability to use a subtype wherever its base abstraction is expected.

Coupling: The degree of dependence between software components.

Cohesion: The degree to which responsibilities within a component belong together.

RTTI: Runtime type information, including dynamic_cast and typeid.

Interface: A set of operations describing a capability without prescribing its implementation.

Dependency inversion: Designing high-level code to depend on abstractions rather than concrete details.

Review Questions

Short Answer Questions

  1. What is the difference between inheritance and composition?
  2. Why should a polymorphic base class usually have a virtual destructor?
  3. What does the override keyword accomplish?
  4. Define dynamic binding.
  5. What is object slicing, and how can it be avoided?

Long Answer Questions

  1. Explain abstraction, encapsulation, and their relationship in C++ design.
  2. Describe the Liskov Substitution Principle and illustrate it with a class hierarchy.
  3. Compare interface-like abstract classes with concrete base classes.
  4. Explain how smart pointers support safe polymorphic object ownership.
  5. Discuss the advantages and risks of multiple inheritance.

Analytical / Application Questions

  1. A Report base class has print(), saveAsPdf(), and sendByEmail(). Some derived reports cannot email themselves. Analyze whether the hierarchy is well designed and propose a better abstraction.
  2. Design a C++ interface for interchangeable storage providers such as local disk and cloud storage. Explain how composition, dependency inversion, and testing apply.
  3. A program repeatedly uses dynamic_cast to determine whether an object is a Circle, Rectangle, or Triangle. Evaluate this design and propose an alternative using polymorphic behavior or another suitable C++ mechanism.

Multiple Choice Questions with Answers

Question 1

Which relationship is most naturally represented by public inheritance?

A. A laptop has a battery

B. A student is a person

C. A company uses a database

D. A car contains an engine

Correct Answer: B. A student is a person

Question 2

What is the primary purpose of a pure virtual function?

A. To prevent all derived classes from adding members

B. To require derived classes to provide behavior

C. To make a function execute faster

D. To allocate an object dynamically

Correct Answer: B. To require derived classes to provide behavior

Question 3

Which keyword tells the compiler that a derived member is intended to override a virtual function?

A. virtual

B. inherits

C. override

D. extends

Correct Answer: C. override

Question 4

What happens when a derived object is passed by value to a base-class parameter?

A. Dynamic binding is guaranteed

B. The derived portion may be sliced away

C. The base destructor is skipped

D. The object becomes abstract

Correct Answer: B. The derived portion may be sliced away

Question 5

Why should a polymorphic base class usually have a virtual destructor?

A. To allow overloaded constructors

B. To prevent all inheritance

C. To ensure correct destruction through a base pointer

D. To make private members accessible

Correct Answer: C. To ensure correct destruction through a base pointer

Question 6

Which design generally reduces coupling when behavior must be replaceable?

A. A large inheritance hierarchy with protected data

B. Composition using a stable interface

C. Global variables shared by all classes

D. Repeated downcasting to concrete classes

Correct Answer: B. Composition using a stable interface

Question 7

What does a failed pointer dynamic_cast normally return?

A. -1

B. An empty string

C. nullptr

D. The base object

Correct Answer: C. nullptr

Question 8

Which principle requires a subtype to remain usable wherever its base type is expected?

A. Single Responsibility Principle

B. Liskov Substitution Principle

C. Open Addressing Principle

D. Parsing Principle

Correct Answer: B. Liskov Substitution Principle

Question 9

Which C++ feature is most appropriate for exclusive ownership of a polymorphic object?

A. std::unique_ptr

B. A raw pointer without documentation

C. A global reference

D. A preprocessor macro

Correct Answer: A. std::unique_ptr

Question 10

A class with at least one pure virtual function is generally:

A. Immutable

B. Abstract and not directly instantiable

C. Automatically a template

D. Required to use multiple inheritance

Correct Answer: B. Abstract and not directly instantiable

Fundamentals of Programming (C/C++)
Chapter 9 of 15