Abstract Classes in C++: Pure Virtual Functions, Examples and Errors

Learn why an abstract base cannot be instantiated but remains useful through pointers and references. Trace two shapes, fix override errors and practise safe polymorphism.

KnowledgeGate Team

Exam prep & CS education

Updated 3 Aug 20267 min read

virtual, = 0 and override often appear together, but copying the syntax does not explain why Shape shape; is forbidden while Shape* shape is useful. The reason is that = 0 leaves area() with no body to call, so a bare Shape object would carry a hole where its area calculation belongs, while a Shape* always points at a finished Rectangle or Triangle. A rectangle of area 40.0 and a triangle of area 30.0 reach a total of 70.0 through base pointers alone, with no type test anywhere. The same pattern runs through the Coding and DSA courses, wherever one interface has to serve several implementations.

What makes a C++ class abstract

The key declaration is virtual double area() const = 0;. virtual enables runtime polymorphism through a base pointer or reference, the trailing const belongs to the signature, and = 0 makes the function pure virtual. It does not mean that the function returns zero.

A class is abstract as soon as one of its virtual functions is still pure, and that includes a pure function it inherited without overriding. So a derived class stays abstract until it overrides every inherited pure virtual function. Shape* and const Shape& remain valid because neither creates a Shape object.

An abstract class can still have data, constructors, concrete functions and protected helpers. Its constructor runs when the base part of a derived object is built. The C++ Programming Course provides wider language practice.

Complete worked example: two shapes through one abstract base

Save this complete program as abstract_classes.cpp. Compile it with c++ -std=c++17 -Wall -Wextra -pedantic abstract_classes.cpp -o abstract_classes.

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

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

class Rectangle final : public Shape {
    double width_;
    double height_;
public:
    Rectangle(double width, double height)
        : width_(width), height_(height) {}

    double area() const override { return width_ * height_; }
    const char* name() const override { return "Rectangle"; }
};

class Triangle final : public Shape {
    double base_;
    double height_;
public:
    Triangle(double base, double height)
        : base_(base), height_(height) {}

    double area() const override { return 0.5 * base_ * height_; }
    const char* name() const override { return "Triangle"; }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Rectangle>(8.0, 5.0));
    shapes.push_back(std::make_unique<Triangle>(10.0, 6.0));

    double total = 0.0;
    std::cout << std::fixed << std::setprecision(1);
    for (const auto& shape : shapes) {
        double current = shape->area();
        total += current;
        std::cout << shape->name() << ": " << current << '\n';
    }
    std::cout << "Total area: " << total << '\n';
}

The rectangle calculation is 8.0 * 5.0 = 40.0. The triangle calculation is 0.5 * 10.0 * 6.0 = 30.0. The loop begins with 0.0, adds 40.0, then adds 30.0, so its running total is 0.0 -> 40.0 -> 70.0.

Compiled with those flags, the program prints:

Rectangle: 40.0
Triangle: 30.0
Total area: 70.0

The vector owns different derived objects through std::unique_ptr<Shape> and destroys them automatically. Because destruction occurs through a base pointer, Shape needs a virtual destructor. Otherwise, deleting a derived object through Shape* is unsafe.

Class hierarchy: abstract Shape with Rectangle (area 40.0) and Triangle (area 30.0) overrides, totalling 70.0 through Shape pointers.

How runtime dispatch selects the derived override

In the loop, shape has static type const std::unique_ptr<Shape>&, and shape-> reaches the owned Shape*. Its dynamic object is first a Rectangle, then a Triangle. Virtual calls select the override belonging to that object.

At slot 0, the calls reach the Rectangle overrides and produce Rectangle: 40.0. At slot 1, they reach the Triangle overrides and produce Triangle: 30.0. C++ guarantees this dispatch, although it does not require a particular virtual-table layout. What the compiler actually stores per object to make that choice is traced in Virtual Functions and VTable in C++.

Runtime dispatch: a base pointer resolves to Rectangle 40.0 at slot 0 and Triangle 30.0 at slot 1, running total 70.0.

Abstract bases can own state and provide working methods

An abstract class can store shared state and implement behaviour that applies to every derived type:

class Meter {
protected:
    int units_;
public:
    explicit Meter(int units) : units_(units) {}
    void add(int units) { units_ += units; }
    virtual int bill() const = 0;
    virtual ~Meter() = default;
};

class DomesticMeter final : public Meter {
public:
    using Meter::Meter;
    int bill() const override { return units_ * 6; }
};

Dry-run DomesticMeter meter{120}; meter.add(30); std::cout << meter.bill();. The base constructor sets units_ to 120, add changes it to 120 + 30 = 150, and the override returns 150 * 6 = 900.

Meter owns common state and behaviour, while each concrete meter decides how bill() works. Meter meter{120}; is invalid because bill() remains pure virtual there.

override exposes signature mistakes early

Suppose a derived class declares double area() override { return 40.0; }. It lacks the trailing const, so override makes the compiler reject it immediately. The correction is double area() const override.

Without override, that declaration is a different function, leaving area() const unimplemented and the derived class abstract. Use override so the diagnostic identifies the faulty declaration.

Changing the return type from double to int also fails. In the program, final prevents further derivation from Rectangle and Triangle; it is optional for abstract-class correctness.

Common abstract-class errors and their fixes

Code or symptom

Why it fails

Correction

Shape shape;

It tries to instantiate an abstract class.

Create a concrete Rectangle or Triangle.

A derived class omits name() const

One inherited pure virtual function has no override.

Implement every pure virtual function.

double area() lacks const

Its signature does not override area() const.

Match the qualifier and retain override.

void print(Shape value)

Passing by value requires an abstract Shape object.

Accept const Shape&.

A base destructor is not virtual

Ownership may delete a derived object through Shape*.

Declare virtual ~Shape() = default.

Do not call a pure virtual operation from the base constructor or destructor. The complete derived part is unavailable then, so base construction should establish state without depending on a derived override.

A pure virtual function may have an out-of-class definition, and a pure virtual destructor must have one. The pure declaration still keeps the class abstract.

Abstract class, interface-style base or composition

Use an abstract class with state and concrete methods when derived types share an is-a relationship and implementation. A DomesticMeter is a Meter. When only the contract is shared, use an interface-style base made from pure virtual functions and a virtual destructor.

Prefer composition when an object merely uses another object. Rectangle and Triangle are substitutable Shape objects, but a Canvas containing shapes is not a Shape. It should own a collection instead of inheriting from Shape.

How assessments and interviews test abstract classes, then practise

Common checks ask whether a class remains abstract, whether a signature overrides, which implementation a base pointer calls, and why a polymorphic destructor is virtual. Written papers usually hand you a short class listing and ask which line is ill-formed; the two that most often carry the mark are Shape shape; and an area() that has lost its trailing const. Interviews push one step further. Deleting a derived object through a Shape* whose destructor is not virtual is undefined behaviour, and in practice the derived destructor never runs, so whatever that class owned is never released. That is the whole reason Shape declares virtual ~Shape() = default.

Try these three exercises:

  1. Given Rectangle r{8.0, 5.0}; const Shape& s = r; std::cout << s.area();, the output is 40 without fixed formatting. The reference denotes the Rectangle, so the virtual call reaches Rectangle::area().

  2. Derive BrokenShape from Shape, implement only double area() const override { return 12.0; }, and omit name() const. BrokenShape remains abstract, so BrokenShape b; is ill-formed.

  3. Add Square final : public Shape with side_ = 6.0, an area() that returns side_ * side_, and name() returning "Square". Append it after the triangle. The new line is Square: 36.0, and the total becomes 70.0 + 36.0 = 106.0.

For broader preparation, continue with Technical Interview: OS, DBMS, CN & OOP Prep.

The short version

  • Declare as pure virtual every operation whose behaviour differs by derived type.

  • Keep shared state and concrete methods only when they belong to every derived type.

  • Implement every pure virtual function with the exact signature.

  • Write override on every intended override.

  • Use base references or owning smart pointers for polymorphism.

  • Give every polymorphic base a virtual destructor.

Two base-owned pointers called two different implementations and accumulated 40.0 + 30.0 = 70.0 without type tests. Your next step is to solve coding problems that apply C++ and other languages under assessment conditions in Coding for Placements.