Skip to content

Abstract Classes — Pure Virtual Functions, Interfaces, Virtual Destructors

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Abstract Classes. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ abstract classes are classes with at least one pure virtual function that define interfaces without implementation, forcing derived classes to provide concrete behavior.

What You'll Learn

You will declare pure virtual functions using = 0 syntax, create interface classes that define contracts for derived classes, understand why abstract classes need virtual destructors, work with pointers and references to abstract types, and compare C++ interface design with Java interfaces.

Why It Matters

Abstract classes establish contracts. When you define a pure virtual function, you are saying: "If you want to be a type X, you must implement this operation." This is the foundation of interface-based design, which enables loose coupling and testability. Many design patterns (Strategy, Observer, Command) rely on abstract base classes.

Learning Path

graph LR
    A["16: Polymorphism"] --> B["17: Abstract Classes"]
    B --> C["18: Multiple Inheritance"]
    C --> D["19: Operator Overloading"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Pure Virtual Functions

#include <iostream>
#include <cmath>

class Shape {
public:
    virtual double area() const = 0;        // pure virtual
    virtual double perimeter() const = 0;   // pure virtual
    virtual void print() const = 0;         // pure virtual
    
    virtual ~Shape() = default;  // virtual destructor
};

class Circle : public Shape {
private:
    double radius_;
    
public:
    Circle(double r) : radius_(r) {}
    
    double area() const override {
        return M_PI * radius_ * radius_;
    }
    
    double perimeter() const override {
        return 2.0 * M_PI * radius_;
    }
    
    void print() const override {
        std::cout << "Circle(r=" << radius_ << ")\n";
    }
};

class Rectangle : public Shape {
private:
    double width_, height_;
    
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    
    double area() const override {
        return width_ * height_;
    }
    
    double perimeter() const override {
        return 2.0 * (width_ + height_);
    }
    
    void print() const override {
        std::cout << "Rectangle(" << width_ << "x" << height_ << ")\n";
    }
};

int main() {
    // Shape s;  // Error: cannot instantiate abstract class
    
    Circle c(5);
    Rectangle r(3, 4);
    
    Shape* shapes[] = {&c, &r};
    for (Shape* s : shapes) {
        s->print();
        std::cout << "  Area: " << s->area() << "\n";
        std::cout << "  Perimeter: " << s->perimeter() << "\n";
    }
}

A class is abstract if it has at least one pure virtual function. You cannot create instances of an abstract class. Derived classes must implement all pure virtual functions to become concrete.

Interface Classes

In C++, an "interface" is a class with all pure virtual functions and no data members.

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

// Interface
class Serializable {
public:
    virtual ~Serializable() = default;
    virtual std::string serialize() const = 0;
    virtual bool deserialize(const std::string& data) = 0;
};

// Interface
class Drawable {
public:
    virtual ~Drawable() = default;
    virtual void draw() const = 0;
    virtual void resize(double factor) = 0;
};

// Concrete class implementing multiple interfaces
class Button : public Serializable, public Drawable {
private:
    std::string label_;
    double width_, height_;
    
public:
    Button(const std::string& label, double w, double h)
        : label_(label), width_(w), height_(h) {}
    
    std::string serialize() const override {
        return "Button:" + label_ + ":" + std::to_string(width_)
               + ":" + std::to_string(height_);
    }
    
    bool deserialize(const std::string& data) override {
        return true;
    }
    
    void draw() const override {
        std::cout << "[ " << label_ << " ]\n";
    }
    
    void resize(double factor) override {
        width_ *= factor;
        height_ *= factor;
    }
};

int main() {
    Button btn("Click Me", 100, 30);
    btn.draw();
    std::cout << btn.serialize() << "\n";
}

Pure Virtual Destructor

#include <iostream>

class AbstractBase {
public:
    virtual ~AbstractBase() = 0;  // pure virtual destructor
};

AbstractBase::~AbstractBase() {
    // Must provide body even though pure virtual
    // Derived destructors call this after their own cleanup
}

class Derived : public AbstractBase {
public:
    ~Derived() override {
        std::cout << "Derived destroyed\n";
    }
};

int main() {
    Derived d;
}

A pure virtual destructor makes a class abstract but still needs a body because all derived destructors call the base destructor.

Abstract Classes and Factory Functions

#include <iostream>
#include <memory>
#include <string>

class Document {
public:
    virtual ~Document() = default;
    virtual void open() = 0;
    virtual void save() = 0;
    virtual void close() = 0;
};

// Factory function returning abstract type
std::unique_ptr<Document> createDocument(const std::string& type);

class TextDocument : public Document {
public:
    void open() override { std::cout << "Opening text document\n"; }
    void save() override { std::cout << "Saving text document\n"; }
    void close() override { std::cout << "Closing text document\n"; }
};

class SpreadsheetDocument : public Document {
public:
    void open() override { std::cout << "Opening spreadsheet\n"; }
    void save() override { std::cout << "Saving spreadsheet\n"; }
    void close() override { std::cout << "Closing spreadsheet\n"; }
};

std::unique_ptr<Document> createDocument(const std::string& type) {
    if (type == "text") return std::make_unique<TextDocument>();
    if (type == "spreadsheet") return std::make_unique<SpreadsheetDocument>();
    return nullptr;
}

int main() {
    auto doc = createDocument("text");
    if (doc) {
        doc->open();
        doc->save();
        doc->close();
    }
}

When to Use Abstract Classes

Use abstract classes when:

  • You want to define a common interface for a family of related classes
  • You want to provide partial implementation (abstract classes can have data members and implemented functions)
  • You need non-virtual interface (NVI) pattern with public non-virtual and private virtual functions

Common Mistakes

Mistake 1: Forgetting to Implement All Pure Virtuals

class Derived : public Shape {
    double area() const override { return 0; }
    // perimeter() not implemented — Derived is still abstract
};

You cannot instantiate Derived until all pure virtuals are implemented.

Mistake 2: Non-Virtual Destructor in Abstract Class

class Abstract {
    virtual void f() = 0;
    ~Abstract() {}  // should be virtual
};

Always make destructors virtual in classes that are intended as base classes.

Mistake 3: Calling Virtual Functions from Constructor

AbstractBase() {
    f();  // calls AbstractBase::f() or undefined if pure virtual
}

During construction, virtual calls do not reach derived class implementations.

Mistake 4: Slicing Abstract Classes

You cannot slice abstract classes because you cannot create instances of them. But passing by value to a non-abstract base can still slice.

Mistake 5: Over-Engineering with Too Many Interfaces

Not everything needs to be abstract. Simple utility classes often work better as concrete types.

Practice Questions

  1. What makes a class abstract in C++?
  2. Can an abstract class have a constructor? If so, when is it called?
  3. Why does a pure virtual destructor need a body?
  4. Write an <a href="/design-patterns/iterator/">Iterator</a> abstract class with next(), hasNext(), and reset() pure virtual functions.
  5. Implement a concrete ArrayIterator that iterates over a C-style array.

Challenge

Design a plugin system: create an abstract Effect class with pure virtual apply(std::vector<int>&). Implement AmplifyEffect (multiply by factor), InvertEffect (negate values), and DelayEffect (shift values right by N positions). Apply all effects to sample data.

FAQ

What is the difference between a pure virtual function and a virtual function with a default implementation?

A pure virtual function has = 0 and makes the class abstract. A virtual function with a body provides a default implementation that derived classes may override.

Can I have a pure virtual function with a body?

Yes. You can define a body for a pure virtual function: void f() = 0 { /* body */ }. Derived classes must still override it but can call the base implementation.

How are abstract classes different from Java interfaces?

Java interfaces are purely abstract with no data members. C++ abstract classes can have data members and implemented member functions. C++20 introduced concepts which are more analogous to Java interfaces in some ways.

Can I create a pointer or reference to an abstract class?

Yes. You cannot create objects of abstract classes, but you can have pointers and references to them. This is how polymorphism works with abstract types.

What is the 'Non-Virtual Interface' (NVI) pattern?

Public non-virtual functions call private virtual functions. The public function provides common behavior (logging, locking) and delegates the customizable part to the private virtual.

Should I make all my classes abstract?

No. Abstract classes are useful for defining interfaces and base class hierarchies, but concrete classes are simpler and often sufficient. Premature abstraction adds complexity.

Mini Project

Build a media player plugin system:

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

class MediaPlugin {
public:
    virtual ~MediaPlugin() = default;
    virtual std::string name() const = 0;
    virtual bool canPlay(const std::string& fileExtension) const = 0;
    virtual void play(const std::string& filePath) = 0;
};

class MP3Plugin : public MediaPlugin {
public:
    std::string name() const override { return "MP3 Player"; }
    bool canPlay(const std::string& ext) const override {
        return ext == "mp3";
    }
    void play(const std::string& path) override {
        std::cout << "Playing MP3: " << path << "\n";
    }
};

class VideoPlugin : public MediaPlugin {
public:
    std::string name() const override { return "Video Player"; }
    bool canPlay(const std::string& ext) const override {
        return ext == "mp4" || ext == "avi";
    }
    void play(const std::string& path) override {
        std::cout << "Playing video: " << path << "\n";
    }
};

class MediaPlayer {
private:
    std::vector<std::unique_ptr<MediaPlugin>> plugins_;
    
public:
    void registerPlugin(std::unique_ptr<MediaPlugin> plugin) {
        plugins_.push_back(std::move(plugin));
    }
    
    void playFile(const std::string& path) {
        std::string ext = path.substr(path.find_last_of('.') + 1);
        for (const auto& p : plugins_) {
            if (p->canPlay(ext)) {
                p->play(path);
                return;
            }
        }
        std::cout << "No plugin available for ." << ext << "\n";
    }
};

int main() {
    MediaPlayer player;
    player.registerPlugin(std::make_unique<MP3Plugin>());
    player.registerPlugin(std::make_unique<VideoPlugin>());
    
    player.playFile("song.mp3");
    player.playFile("movie.mp4");
    player.playFile("document.pdf");
}

What's Next

Abstract classes define interfaces. The next lesson covers multiple inheritance and the diamond problem, showing how virtual inheritance resolves ambiguities in complex class hierarchies.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro