optional · CSS study notes
Polymorphism (extended, incl. Abstract Super Class distinction)
- Virtual function: a base-class function declared
virtual, allowing a derived class to override it such that the correct derived version is called through a base-class pointer/reference at runtime (dynamic binding) - Pure virtual function: declared with
= 0, has no implementation in the base class — forces derived classes to provide their own implementation - Abstract class: a class containing at least one pure virtual function — cannot be instantiated directly, only used as a base for derived classes
- Abstract super class: the topmost abstract class in a hierarchy defining a common interface for all derived classes
- Interface simulation: C++ has no
interfacekeyword (unlike Java/C#) — a class with only pure virtual functions and no data members serves the same role as an interface
Binding: Early vs Late
- Early (static) binding: the function to call is decided at compile time — used for ordinary (non-virtual) calls and for compile-time polymorphism (overloading)
- Late (dynamic) binding: the function to call is decided at runtime, based on the actual object type — happens when a
virtualfunction is called through a base-class pointer/reference
C++ Example — Virtual Function (Runtime Polymorphism)
class Shape
{
public:
virtual double area() const { return 0; } // virtual -> enables override
virtual ~Shape() {} // virtual destructor -- see below
};
class Circle : public Shape
{
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.1416 * radius * radius; }
};
class Square : public Shape
{
private:
double side;
public:
Square(double s) : side(s) {}
double area() const override { return side * side; }
};
int main()
{
Shape* shapes[2];
shapes[0] = new Circle(5);
shapes[1] = new Square(4);
for (Shape* s : shapes)
cout << s->area() << endl; // calls the CORRECT derived area() at runtime
for (Shape* s : shapes) delete s;
}
C++ Example — Pure Virtual Function & Abstract Class
class Shape
{
public:
virtual double area() const = 0; // pure virtual -> Shape is now abstract
};
// Shape s; -- ERROR: cannot instantiate an abstract class
class Circle : public Shape
{
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.1416 * radius * radius; } // MUST override
};
Why the Destructor Must Be virtual Here
Shape* s = new Circle(5);
delete s; // without a VIRTUAL destructor in Shape, only ~Shape() runs
// -- ~Circle() is skipped, leaking any Circle-specific resources
Rule: any class meant to be used polymorphically through base-class pointers must declare a virtual destructor (even if it does nothing itself).
How It Works Internally (brief)
- Each object of a class with virtual functions carries a hidden vptr (virtual pointer) to its class's vtable (virtual table) — an array of function-pointer entries, one per virtual function
- A call through a base pointer (
s->area()) is resolved by looking up the actual object's vtable at runtime — this small extra indirection is the "cost" of runtime polymorphism
Function Overloading vs Function Overriding
| Aspect | Overloading (compile-time) | Overriding (runtime) |
|---|---|---|
| Where | Same class (or same scope) | Base class and derived class |
| Signature | Must differ (params/types) | Must be identical |
| Binding | Early (compile-time) | Late (runtime), needs virtual |
| Purpose | Same name, different behavior per input | Derived class customizes inherited behavior |
MCQ Trap
- A pure virtual function (
= 0) has no base-class implementation and forces overriding; a plain virtual function has a default implementation that CAN be overridden but doesn't have to be. - An abstract class (one with a pure virtual function) CAN still have a constructor and ordinary member functions — only instantiation is blocked
- Forgetting
virtualon a base class destructor, thendelete-ing a derived object through a base pointer, is undefined behavior and a frequently tested trap - Object slicing: assigning a derived object to a base-class object by value (not pointer/reference) strips off the derived part — polymorphism only works through pointers/references, never through plain object assignment
Compile-time vs Runtime Polymorphism (both forms — not just virtual functions)
| Type | Mechanism | Resolved | Example |
|---|---|---|---|
| Compile-time (static) | Function overloading, Operator overloading | At compile time — compiler picks the matching version by signature | add(int,int) vs add(float,float) |
| Runtime (dynamic) | Virtual functions + inheritance | At runtime — via the vtable, based on the object's actual type | Shape* s = new Circle(); s->area(); calls Circle::area() |
This distinction matters for exams: "Polymorphism" as a pillar covers both — many students only associate it with virtual functions, but function/operator overloading is polymorphism too, just resolved earlier (at compile time instead of runtime).
Code example — virtual function, pure virtual, and abstract class together
#include <iostream>
using namespace std;
class Shape // Abstract class: has a pure virtual function
{
public:
virtual double area() = 0; // Pure virtual -- no implementation, forces override
virtual ~Shape() {} // virtual destructor -- required for polymorphic base classes
};
class Circle : public Shape
{
double radius;
public:
Circle(double r) : radius(r) {}
double area() override { return 3.1416 * radius * radius; }
};
class Square : public Shape
{
double side;
public:
Square(double s) : side(s) {}
double area() override { return side * side; }
};
int main()
{
Shape* shapes[2];
shapes[0] = new Circle(5);
shapes[1] = new Square(4);
for (int i = 0; i < 2; i++)
cout << "Area: " << shapes[i]->area() << endl; // calls the correct override at runtime
// Shape s; // ERROR -- cannot instantiate an abstract class directly
delete shapes[0];
delete shapes[1];
return 0;
}
Key exam points:
Shapeis abstract because it has at least one pure virtual function (area() = 0) — tryingShape s;is a compile error- The
forloop callsarea()through a base-class pointer, yet the correct derived version runs each time — this is runtime polymorphism / dynamic dispatch - The virtual destructor ensures
delete shapes[i]correctly runs each derived class's destructor, not just~Shape()
Self-Test Questions
- Give one example of compile-time polymorphism and one of runtime polymorphism.
- Why can't
Shape s;compile in the example above? - What breaks if
~Shape()is not declaredvirtual?
Abstract Class vs Abstract Super Class — the distinction, made concrete
These terms are related but not identical, and CSS examiners test the difference:
| Term | Definition |
|---|---|
| Abstract class | Any class with at least one pure virtual function — may still have some regular (non-pure) methods with real implementations |
| Abstract super class | An abstract class that sits at the top of a hierarchy, specifically designed as a common interface for all its derived classes — typically has most or all of its functions as pure virtual, providing little to no implementation of its own |
class Shape // Abstract SUPER class -- pure interface, top of hierarchy
{
public:
virtual double area() = 0; // pure virtual
virtual double perimeter() = 0; // pure virtual
virtual ~Shape() {}
};
class Polygon : public Shape // Abstract class too (still has an unimplemented function)
// but NOT the super class -- it's one level down
{
public:
double perimeter() override { return 0; /* shared logic for all polygons */ }
// area() is still pure virtual here -- Polygon remains abstract
};
class Square : public Polygon // Concrete class -- implements everything, CAN be instantiated
{
double side;
public:
Square(double s) : side(s) {}
double area() override { return side * side; }
};
Shape is the abstract super class (the root interface). Polygon is also abstract (still can't be instantiated — area() remains unimplemented) but is not the super class, since it sits below Shape and already provides one implementation. Only Square is concrete.