C++ Abstract Classes
An abstract class in C++ is a class that cannot be instantiated on its own — it exists purely to define a common interface that derived classes must implement. You mark a class abstract by giving it at least one pure virtual function: a member function with no implementation that every concrete subclass is forced to override. Abstract classes are the backbone of polymorphic design in C++ — they let you write code against a shared interface (like Shape or Employee) while leaving the actual behavior to whichever concrete type gets plugged in at runtime.
Overview / How It Works
A class becomes abstract the instant it declares at least one pure virtual function — a virtual member function whose declarator ends in = 0 instead of a function body, for example virtual double area() const = 0;. This tells the compiler that every concrete subclass must supply its own implementation; the base class has no generic implementation to offer.
Because the base class is missing at least one piece of behavior, the compiler refuses to let you create an object of that type directly. Attempting Shape s; where Shape has an unimplemented pure virtual function produces a compile-time error such as \”cannot declare variable to be of abstract type\”. What you can do is use the abstract type as an interface: declare pointers or references of that type and have them refer to fully-implemented derived objects. That is the essence of runtime polymorphism in C++ — code operating on a Shape* does not need to know whether it points at a Circle or a Rectangle; it simply calls area(), and the correct override runs.
Abstract classes are not limited to pure virtual functions alone. They can also have data members, constructors (so derived classes can initialize shared state), and ordinary concrete methods that every derived class inherits as-is. A class with only pure virtual functions and no implementation at all is often called a \”pure interface\”, similar to an interface in Java or C#. If a derived class inherits from an abstract class but fails to override every pure virtual function, that derived class is itself still abstract — it simply inherits the unfulfilled obligation. Only once every pure virtual function in the inheritance chain has a concrete override does a class become instantiable.
Internally, C++ implements virtual dispatch using a vtable (virtual table): a hidden array of function pointers generated once per polymorphic class. Every object of a polymorphic type carries a hidden pointer (the vptr) to its class’s vtable. When you call a virtual function through a base-class pointer, the compiler does not jump to a fixed address; it dereferences the vptr, indexes into the vtable, and jumps to whichever function pointer is stored there — the most-derived override for that object’s actual type. For a pure virtual function, the base class’s vtable slot typically holds a null pointer or a special \”pure virtual called\” stub, which is exactly why the compiler must stop you from creating a plain base object that could end up invoking it.
Syntax
class BaseName {
public:
// Pure virtual function - makes BaseName abstract
virtual ReturnType functionName(ParamList) = 0;
// Abstract classes can still have concrete methods
ReturnType otherFunction(ParamList) { /* ... */ }
// Always give abstract classes a virtual destructor
virtual ~BaseName() {}
};
virtualmarks the function as overridable through dynamic dispatch.= 0is the \”pure specifier\” — it replaces the function body and tells the compiler no default implementation exists, which makes the enclosing class abstract.- Any class derived from
BaseNamemust override every pure virtual function to become a concrete, instantiable type. - A virtual destructor (
virtual ~BaseName()) ensures that deleting a derived object through a base pointer runs the correct derived destructor first, then the base one.
Examples
Example 1: A Basic Abstract Interface — Shape
This is the classic use of an abstract class: define a common interface (area(), perimeter()), then store different concrete shapes behind base-class pointers and call the interface uniformly.
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14159265 * radius * radius; }
double perimeter() const override { return 2 * 3.14159265 * radius; }
};
class Rectangle : public Shape {
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 * (width + height); }
};
int main() {
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(5.0));
shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
for (const auto& s : shapes) {
cout << \"Area: \" << s->area() << \", Perimeter: \" << s->perimeter() << endl;
}
return 0;
}
Output:
Area: 78.5398, Perimeter: 31.4159
Area: 24, Perimeter: 20
Neither Circle nor Rectangle could be created without implementing both pure virtual functions from Shape. Because shapes holds unique_ptr<Shape>, the loop calls area() and perimeter() through the base type, yet each call resolves to the correct derived implementation via the vtable.
Example 2: Mixing Pure Virtual and Concrete Methods — Employee Payroll
Abstract classes can also hold shared, fully-implemented logic alongside the parts that must vary. Here every employee type must define calculateBonus(), but totalPay() and printPaySlip() are written once, in the base class, and reused by all subclasses.
#include <iostream>
#include <string>
using namespace std;
class Employee {
protected:
string name;
double baseSalary;
public:
Employee(const string& n, double salary) : name(n), baseSalary(salary) {}
virtual ~Employee() {}
// Pure virtual - every derived class defines its own bonus rule
virtual double calculateBonus() const = 0;
// Concrete method shared by all employees
double totalPay() const {
return baseSalary + calculateBonus();
}
void printPaySlip() const {
cout << name << \": base=\" << baseSalary
<< \", bonus=\" << calculateBonus()
<< \", total=\" << totalPay() << endl;
}
};
class Manager : public Employee {
public:
Manager(const string& n, double salary) : Employee(n, salary) {}
double calculateBonus() const override {
return baseSalary * 0.20;
}
};
class Intern : public Employee {
public:
Intern(const string& n, double salary) : Employee(n, salary) {}
double calculateBonus() const override {
return 100.0;
}
};
int main() {
Manager m(\"Alice\", 5000);
Intern i(\"Bob\", 1500);
m.printPaySlip();
i.printPaySlip();
return 0;
}
Output:
Alice: base=5000, bonus=1000, total=6000
Bob: base=1500, bonus=100, total=1600
totalPay() and printPaySlip() are defined only once in Employee, yet they behave differently for a Manager versus an Intern because they call the pure virtual calculateBonus() internally, and that call is dispatched to whichever override matches the object’s real type.
Example 3: An Abstract Class as a Pure Interface — Drawable
When an abstract class has no data and only pure virtual functions, it acts as a pure interface. Unrelated classes can all implement it and be processed generically by the same function.
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() {}
};
class Square : public Drawable {
double side;
public:
Square(double s) : side(s) {}
void draw() const override {
cout << \"Square with side \" << side << endl;
}
};
class Triangle : public Drawable {
double base, height;
public:
Triangle(double b, double h) : base(b), height(h) {}
void draw() const override {
cout << \"Triangle with base \" << base << \" and height \" << height << endl;
}
};
void renderAll(const vector<unique_ptr<Drawable>>& items) {
for (const auto& item : items) {
item->draw();
}
}
int main() {
vector<unique_ptr<Drawable>> canvas;
canvas.push_back(make_unique<Square>(4.0));
canvas.push_back(make_unique<Triangle>(3.0, 6.0));
renderAll(canvas);
return 0;
}
Output:
Square with side 4
Triangle with base 3 and height 6
renderAll never mentions Square or Triangle by name — it only knows about the Drawable interface. This is what makes abstract classes so powerful: new shapes can be added later without changing renderAll at all.
Under the Hood: What Happens at Compile Time
- The compiler scans a class’s own and inherited virtual functions. If any pure virtual function lacks an override anywhere in the chain, the class is flagged abstract internally.
- Any attempt to define a variable, pass-by-value parameter, or
newexpression of that abstract type is rejected at compile time — this check happens before code generation, so there is zero runtime cost for the \”cannot instantiate\” rule. - For a concrete (non-abstract) class, the compiler builds one vtable containing a function pointer for every virtual function, using the most-derived override available.
- Every object of that class stores a hidden vptr (usually as the first few bytes) set up automatically by the constructor.
- A call like
shapePtr->area()compiles into: load the vptr from the object, load the function pointer from the known vtable slot forarea(), then call through that pointer — an indirect call, which is why virtual dispatch has a small runtime cost compared to a plain function call.
Common Mistakes
Mistake 1: Overriding Only Some of the Pure Virtual Functions
If a derived class overrides some but not all inherited pure virtual functions, it remains abstract itself, and trying to instantiate it fails to compile.
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
// perimeter() was never overridden!
};
int main() {
Circle c(5.0); // Error: cannot instantiate abstract class 'Circle'
return 0;
}
Because perimeter() is still unimplemented, Circle inherits that obligation and stays abstract. The fix is to override every pure virtual function inherited from the base:
#include <iostream>
using namespace std;
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
double perimeter() const override { return 2 * 3.14159 * radius; }
};
int main() {
Circle c(5.0);
cout << \"Area: \" << c.area() << \", Perimeter: \" << c.perimeter() << endl;
return 0;
}
Output:
Area: 78.5398, Perimeter: 31.4159
Mistake 2: Trying to Create a Variable of the Abstract Type Directly
Beginners often try to declare a variable of the base type itself, forgetting that the base is only meant to be used through pointers or references bound to a derived object.
class Shape {
public:
virtual double area() const = 0;
};
int main() {
Shape genericShape; // Error: cannot declare variable of abstract type 'Shape'
return 0;
}
The fix is to work with the abstract type only through a pointer or reference, bound to a concrete derived object:
#include <iostream>
#include <memory>
using namespace std;
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() {}
};
class Square : public Shape {
double side;
public:
Square(double s) : side(s) {}
double area() const override { return side * side; }
};
int main() {
unique_ptr<Shape> genericShape = make_unique<Square>(4.0);
cout << \"Area: \" << genericShape->area() << endl;
return 0;
}
Output:
Area: 16
Mistake 3: Forgetting a Virtual Destructor
If an abstract base’s destructor is not virtual, deleting a derived object through a base pointer is undefined behavior — in practice, most compilers simply skip the derived class’s destructor, silently leaking any resources it owns. Always declare the destructor virtual in any class meant to be used polymorphically, even if the body is empty.
#include <iostream>
using namespace std;
class Resource {
public:
virtual void release() const = 0;
virtual ~Resource() { cout << \"Resource destructor\" << endl; }
};
class FileHandle : public Resource {
public:
void release() const override {
cout << \"Releasing file handle\" << endl;
}
~FileHandle() override {
cout << \"FileHandle destructor\" << endl;
}
};
int main() {
Resource* r = new FileHandle();
r->release();
delete r;
return 0;
}
Output:
Releasing file handle
FileHandle destructor
Resource destructor
Because ~Resource() is virtual, delete r correctly runs ~FileHandle() first and then chains up to ~Resource(). Without virtual, only ~Resource() would run, and FileHandle‘s cleanup would never happen.
Best Practices
- Always give a class with virtual functions a virtual destructor, even an empty one, so cleanup through a base pointer works correctly.
- Mark every overriding function with
overrideso the compiler catches signature mismatches (a common source of silent bugs) instead of accidentally creating an unrelated new function. - Prefer smart pointers (
unique_ptr,shared_ptr) over raw pointers when managing polymorphic objects, to avoid manualdeleteand memory leaks. - Pass and return abstract types only by pointer or reference, never by value — passing by value causes \”object slicing\”, where the derived-specific parts are silently discarded.
- Keep an abstract interface small and focused (interface segregation) rather than piling unrelated pure virtual functions into one base class.
- Put truly shared logic (like
totalPay()in the payroll example) directly in the abstract base as a concrete method, instead of duplicating it in every derived class. - Document the expected behavior (the \”contract\”) of each pure virtual function clearly, since the compiler only enforces the signature, not what the function is supposed to do.
Practice Exercises
Exercise 1: Define an abstract class Animal with a pure virtual string makeSound() const and a concrete method describe() that prints the animal’s name followed by the result of makeSound(). Implement Dog and Cat subclasses and call describe() on each through a base pointer.
Exercise 2: Define an abstract class Vehicle with pure virtual functions string fuelType() const and int maxSpeed() const. Implement Car and Motorcycle, store several in a vector<unique_ptr<Vehicle>>, and print each vehicle’s fuel type and max speed in a loop.
Exercise 3: Start from the Shape hierarchy in Example 1. Add a third pure virtual function, string name() const, to Shape, but only implement it in Circle, not Rectangle. Try to compile and observe the error; then fix Rectangle so the program compiles and runs again.
Summary
- A class becomes abstract as soon as it declares at least one pure virtual function, written as
virtual ReturnType name(...) = 0;. - Abstract classes cannot be instantiated directly; they can only be used through pointers or references bound to fully-implemented derived objects.
- A derived class remains abstract until it overrides every inherited pure virtual function.
- Abstract classes may still contain data members, constructors, and fully-implemented concrete methods shared by all derived classes.
- Virtual dispatch is implemented via a hidden vtable and vptr per object, which is why calling a virtual function has a small indirection cost compared to a normal function call.
- Always declare a virtual destructor on a polymorphic base class to avoid resource leaks when deleting through a base pointer.
- Use
override, smart pointers, and pointer/reference semantics to avoid the most common abstract-class pitfalls: partial overrides, accidental instantiation, and object slicing.
