Programming Pandit

c/c++/c#/Javav/Python


Latest Update

Friday, October 23, 2026

Virtual Function Hierarchy, Pure Virtual Functions and Abstract Classes

 

MODULE 6

Virtual Function Hierarchy, Pure Virtual Functions and Abstract Classes

 

Learning Objectives

After completing this module, students should be able to:

•      Predict which virtual function runs in a class hierarchy.

•      Declare pure virtual functions and abstract classes, and explain what a derived class must do.

•      Define a pure virtual function and a pure virtual destructor with a body, and explain why.

•      Use abstract classes as interfaces, including multiple interfaces.

•      Explain what happens when virtual functions are called in constructors and destructors.

•      Use polymorphic containers of smart pointers.

 

1. Virtual Functions and the Class Hierarchy

In a hierarchy, a virtual function can be redefined at any level. When it is called through a base-class pointer or reference, C++ uses the most derived override that is available for the object’s real class. If the real class (or the nearest class above it) has no override, the next version up the chain is used.

Situation

Version that runs

The object’s class overrides the function

The version in the object’s own class

The object’s class does not override it, but a parent class does

The nearest override above it

No class below the base overrides it

The base-class version

The function is pure virtual in the base class

Every concrete class must provide an override (Section 2)

 

Program 6.1: A hierarchy Person → Student and Teacher.

#include <iostream>

using namespace std;

 

class Person {

public:

    virtual void displayRole() { cout << "General person" << '\n'; }

    virtual ~Person() = default;

};

 

class Student : public Person {

public:

    void displayRole() override { cout << "University student" << '\n'; }

};

 

class Teacher : public Person {

public:

    void displayRole() override { cout << "University teacher" << '\n'; }

};

 

int main()

{

    Student s;

    Teacher t;

    Person *p = &s;

    p->displayRole();

    p = &t;

    p->displayRole();

    return 0;

}

 

Output:

University student

University teacher

 

The same pointer variable p first refers to a Student and then to a Teacher; each time the call p->displayRole() reaches the correct override. The earlier example with three levels (Program 5.4, Module 5) shows the “nearest override” rule.

Sometimes the base class has no sensible default behaviour. A generic Shape cannot compute an area, and a general Employee cannot calculate a salary. For such cases C++ provides pure virtual functions.

2. Pure Virtual Functions and Abstract Classes

Definition: A pure virtual function is a virtual function declared with = 0 and normally without a body: virtual double area() = 0;. A class that has at least one pure virtual function is an abstract class.

 

Title: Shape is abstract; Circle and Rectangle are concrete classes. - Description: Shape is abstract; Circle and Rectangle are concrete classes.

Figure 6.1: Shape is abstract; Circle and Rectangle are concrete classes.

A pure virtual function says: “every concrete class derived from me must provide this function”. It defines an interface and leaves the implementation to the derived classes.

Program 6.2: An abstract class Shape and two concrete classes.

#include <iostream>

using namespace std;

 

class Shape {

public:

    virtual double area() = 0;                   // pure virtual function

    virtual ~Shape() = default;

};

 

class Rectangle : public Shape {

private:

    double length, breadth;

 

public:

    Rectangle(double l, double b) : length(l), breadth(b) {}

    double area() override { return length * breadth; }

};

 

class Circle : public Shape {

private:

    double radius;

 

public:

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

    double area() override { return 3.14159 * radius * radius; }

};

 

int main()

{

    Rectangle r(5, 4);

    Circle c(3);

 

    Shape *ptr = &r;

    cout << "Rectangle area: " << ptr->area() << '\n';

    ptr = &c;

    cout << "Circle area: " << ptr->area() << '\n';

    return 0;

}

 

Output:

Rectangle area: 20

Circle area: 28.2743

 

•      Shape is abstract: area() is pure virtual. Rectangle and Circle override it and are concrete.

•      The pointer ptr has the type Shape *, yet ptr->area() computes the area of the real object: 5 × 4 = 20 and 3.14159 × 3 × 3 = 28.2743.

•      The abstract class gives all shapes the same interface, so code written for Shape works with every shape added later.

 

2.1 Rules for abstract classes

Rule

Explanation

No objects

An object of an abstract class cannot be created, nor can one be passed or returned by value

Pointers and references are allowed

Shape * and Shape & are the way abstract classes are used

Must override to be concrete

A derived class that overrides all pure virtual functions is concrete; otherwise it is abstract too

May contain anything else

Data members, constructors, ordinary and virtual functions, and a destructor

Constructors

Allowed; they run when a derived object is created, to initialise the abstract part

Destructor

Should be virtual, because objects are deleted through base pointers

 

Trying to create an object of an abstract class is a compile-time error:

error: cannot declare variable ‘s’ to be of abstract type ‘Shape’

 

A derived class that overrides only some of the pure virtual functions is still abstract:

error: cannot declare variable ‘h’ to be of abstract type ‘Half’

 

Program 6.3: A payroll hierarchy: one pure virtual and one ordinary virtual function.

#include <iostream>

using namespace std;

 

class Employee {

public:

    virtual double calculateSalary() = 0;

    virtual void display() { cout << "Employee information" << '\n'; }

    virtual ~Employee() = default;

};

 

class PermanentEmployee : public Employee {

private:

    double monthlySalary;

 

public:

    PermanentEmployee(double salary) : monthlySalary(salary) {}

    double calculateSalary() override { return monthlySalary; }

    void display() override { cout << "Permanent employee" << '\n'; }

};

 

class ContractEmployee : public Employee {

private:

    double hourlyRate;

    int hoursWorked;

 

public:

    ContractEmployee(double rate, int hours)

        : hourlyRate(rate), hoursWorked(hours) {}

    double calculateSalary() override { return hourlyRate * hoursWorked; }

    void display() override { cout << "Contract employee" << '\n'; }

};

 

int main()

{

    PermanentEmployee p(50000);

    ContractEmployee c(500, 80);

    Employee *employees[] = {&p, &c};

 

    for (Employee *e : employees) {

        e->display();

        cout << "Salary: " << e->calculateSalary() << '\n';

    }

    return 0;

}

 

Output:

Permanent employee

Salary: 50000

Contract employee

Salary: 40000

 

calculateSalary() is pure virtual because the salary rule differs in every kind of employee. display() is an ordinary virtual function with a default body that derived classes may keep or replace. Both are called through Employee *, in a loop, without any if on the type of the employee.

3. Pure Virtual Functions with a Body; Pure Virtual Destructors

Although a pure virtual function normally has no body, C++ allows a definition outside the class. The class stays abstract, but derived classes can call the shared code with the qualified name, for example Logger::log().

Program 6.4: A pure virtual function that has a body.

#include <iostream>

using namespace std;

 

class Logger {

public:

    virtual void log() = 0;                      // pure virtual, but defined below

    virtual ~Logger() = default;

};

 

// a pure virtual function may have a body

void Logger::log()

{

    cout << "[common log header] ";

}

 

class FileLogger : public Logger {

public:

    void log() override

    {

        Logger::log();                           // reuse the shared code explicitly

        cout << "writing to file" << '\n';

    }

};

 

int main()

{

    // Logger l;                                 // error: Logger is abstract

    FileLogger f;

    f.log();

    return 0;

}

 

Output:

[common log header] writing to file

 

3.1 Pure virtual destructor

Sometimes a class must be abstract but has no other function that is naturally pure. A pure virtual destructor makes the class abstract. Unlike other pure virtual functions it must be defined, because every derived destructor calls it at the end of its execution.

Program 6.5: A pure virtual destructor with a definition.

#include <iostream>

using namespace std;

 

class Interface {

public:

    virtual ~Interface() = 0;                    // pure virtual destructor

};

 

Interface::~Interface()                          // it MUST be defined

{

    cout << "Interface destructor" << '\n';

}

 

class Impl : public Interface {

public:

    ~Impl() override { cout << "Impl destructor" << '\n'; }

};

 

int main()

{

    Interface *p = new Impl;

    delete p;

    return 0;

}

 

Output:

Impl destructor

Interface destructor

 

Deleting through Interface * runs ~Impl() first and then the body of ~Interface(). If the body of ~Interface() were missing, the program would fail to link.

4. Abstract Classes as Interfaces

An interface is an abstract class whose members are all pure virtual (and usually a virtual destructor). It describes what an object can do and not how. A class can implement several interfaces by inheriting them all; since interfaces have no data, multiple inheritance of interfaces avoids most problems of Module 2.

Title: Report implements two interfaces, Drawable and Printable. - Description: Report implements two interfaces, Drawable and Printable.

Figure 6.2: Report implements two interfaces, Drawable and Printable.

Program 6.6: A class that implements two interfaces.

#include <iostream>

using namespace std;

 

/* Interfaces: classes with only pure virtual functions */

class Drawable {

public:

    virtual void draw() const = 0;

    virtual ~Drawable() = default;

};

 

class Printable {

public:

    virtual void print() const = 0;

    virtual ~Printable() = default;

};

 

class Report : public Drawable, public Printable {

public:

    void draw() const override { cout << "Drawing the report" << '\n'; }

    void print() const override { cout << "Printing the report" << '\n'; }

};

 

void render(const Drawable &d) { d.draw(); }

void output(const Printable &p) { p.print(); }

 

int main()

{

    Report r;

    render(r);                   // used through the Drawable interface

    output(r);                   // used through the Printable interface

    return 0;

}

 

Output:

Drawing the report

Printing the report

 

render() knows only about Drawable and output() only about Printable; both work with a Report because it is-a Drawable and is-a Printable.

Note: C++ has no interface keyword. By convention an “interface” is a class with only pure virtual functions. Languages such as Java and C# have a separate keyword for the same idea.

 

5. Virtual Functions in Constructors and Destructors

While the constructor of a base class runs, the derived part of the object does not exist yet, so the object is treated as an object of the base class. A virtual call made inside a constructor therefore calls the version of the class whose constructor is running, not the derived override. The same holds in destructors: the derived part has already been destroyed.

Title: During construction and destruction a virtual call follows the part of the object that is currently active. - Description: During construction and destruction a virtual call follows the part of the object that is currently active.

Figure 6.3: During construction and destruction a virtual call follows the part of the object that is currently active.

Program 6.7: Virtual calls during construction and destruction.

#include <iostream>

using namespace std;

 

class Base {

public:

    Base() { cout << "Base constructor: "; whoAmI(); }

    virtual void whoAmI() const { cout << "I am Base" << '\n'; }

    virtual ~Base() { cout << "Base destructor: "; whoAmI(); }

};

 

class Derived : public Base {

public:

    Derived() { cout << "Derived constructor: "; whoAmI(); }

    void whoAmI() const override { cout << "I am Derived" << '\n'; }

    ~Derived() override { cout << "Derived destructor: "; whoAmI(); }

};

 

int main()

{

    Derived d;

    cout << "In main: ";

    d.whoAmI();

    return 0;

}

 

Output:

Base constructor: I am Base

Derived constructor: I am Derived

In main: I am Derived

Derived destructor: I am Derived

Base destructor: I am Base

 

•      In the Base constructor whoAmI() prints “I am Base”, although the object being built is a Derived.

•      In main(), after construction, the call reaches Derived::whoAmI().

•      In the destructors the order is reversed: first the Derived destructor (Derived version), then the Base destructor (Base version).

 

Important: Never call a pure virtual function (directly or indirectly) from a constructor or destructor of the abstract class: no override exists at that time, and the behaviour is undefined (a typical program crashes with a “pure virtual function called” message).

 

6. Polymorphic Containers

Because polymorphism needs pointers or references, a collection of objects of different classes is stored as a collection of base-class pointers. Modern C++ uses smart pointers, so that the objects are deleted automatically.

Program 6.8: A vector of unique_ptr<Shape>.

#include <iostream>

#include <memory>

#include <vector>

using namespace std;

 

class Shape {

public:

    virtual double area() const = 0;

    virtual const char *name() const = 0;

    virtual ~Shape() = default;

};

 

class Rectangle : public Shape {

    double l, b;

 

public:

    Rectangle(double l, double b) : l(l), b(b) {}

    double area() const override { return l * b; }

    const char *name() const override { return "Rectangle"; }

};

 

class Circle : public Shape {

    double r;

 

public:

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

    double area() const override { return 3.14159 * r * r; }

    const char *name() const override { return "Circle"; }

};

 

int main()

{

    vector<unique_ptr<Shape>> shapes;

    shapes.push_back(make_unique<Rectangle>(5, 4));

    shapes.push_back(make_unique<Circle>(3));

    shapes.push_back(make_unique<Rectangle>(2, 2.5));

 

    double total = 0;

    for (const auto &s : shapes) {

        cout << s->name() << " area = " << s->area() << '\n';

        total += s->area();

    }

    cout << "Total area = " << total << '\n';

    return 0;                    // unique_ptr deletes the objects through ~Shape()

}

 

Output:

Rectangle area = 20

Circle area = 28.2743

Rectangle area = 5

Total area = 53.2743

 

•      The vector owns the shapes through unique_ptr; there is no delete in the program.

•      The loop calls area() and name() without knowing the real class of each element. Total: 20 + 28.2743 + 5 = 53.2743.

•      Deleting a Shape through ~Shape() is safe because the destructor is virtual.

 

Version Note: unique_ptr and shared_ptr (header <memory>) are available from C++11; make_unique from C++14. In C++11 write unique_ptr<Shape>(new Circle(3)); in C++98 use raw pointers and delete them in a loop (the old auto_ptr was removed in C++17).

Range-based for and auto, used in the loop, are C++11.

 

7. Comparison Tables

Feature

Ordinary function

Virtual function

Pure virtual function

Declaration

void f();

virtual void f();

virtual void f() = 0;

Body in the base class

Required

Required

Not required (optional)

Overriding in derived class

Hides

Overrides

Must be overridden to be concrete

Binding

Early

Late (through pointer/reference)

Late

Makes the class abstract

No

No

Yes

 

Feature

Concrete class

Abstract class

Objects can be created

Yes

No

Pointers and references

Yes

Yes

Can be a base class

Yes

Yes (that is its purpose)

Contains pure virtual functions

None (or all overridden)

At least one

 

8. Quick Revision

•      Virtual call through a base pointer ⇒ the most derived override for the object’s class runs; if none, the nearest one above it.

•      Pure virtual function: virtual R f() = 0;. A class with one is abstract: no objects, but pointers and references allowed.

•      A derived class must override all pure virtual functions to become concrete.

•      A pure virtual function may have a body (call it with Class::f()); a pure virtual destructor must have one.

•      An interface is a class with only pure virtual functions; a class may implement several.

•      In constructors and destructors virtual calls resolve to the class being constructed or destroyed; never call a pure virtual function there.

•      Store polymorphic objects as unique_ptr or shared_ptr to the base class.

 

9. Important Examination Questions

Short-answer questions

1.      What is a pure virtual function? Write its syntax.

2.      What is an abstract class? Can we create its object? Can we create a pointer to it?

3.      What happens if a derived class does not override a pure virtual function?

4.      Can a pure virtual function have a body? Can a pure virtual destructor?

5.      What is an interface in C++?

6.      What happens when a virtual function is called inside a constructor?

7.      Differentiate between a virtual function and a pure virtual function.

 

Long-answer and programming questions

1.      Explain abstract classes and pure virtual functions with a program for geometric shapes.

2.      Write a program for an Employee hierarchy with calculateSalary() pure virtual.

3.      Explain how a class can implement two interfaces, with a program.

4.      Explain virtual function calls inside constructors and destructors with a program and output.

5.      Write a program that stores objects of different derived classes in a vector of base-class smart pointers and calls a virtual function for each.

6.      Compare ordinary, virtual and pure virtual functions in a table.

 

 

No comments:

Post a Comment