Abstract Classes — Pure Virtual Functions, Interfaces, Virtual Destructors
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
- What makes a class abstract in C++?
- Can an abstract class have a constructor? If so, when is it called?
- Why does a pure virtual destructor need a body?
- Write an
<a href="/design-patterns/iterator/">Iterator</a>abstract class withnext(),hasNext(), andreset()pure virtual functions. - Implement a concrete
ArrayIteratorthat 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
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