CSS Prepare

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 interface keyword (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 virtual function 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

AspectOverloading (compile-time)Overriding (runtime)
WhereSame class (or same scope)Base class and derived class
SignatureMust differ (params/types)Must be identical
BindingEarly (compile-time)Late (runtime), needs virtual
PurposeSame name, different behavior per inputDerived 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 virtual on a base class destructor, then delete-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)

TypeMechanismResolvedExample
Compile-time (static)Function overloading, Operator overloadingAt compile time — compiler picks the matching version by signatureadd(int,int) vs add(float,float)
Runtime (dynamic)Virtual functions + inheritanceAt runtime — via the vtable, based on the object's actual typeShape* 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:

  • Shape is abstract because it has at least one pure virtual function (area() = 0) — trying Shape s; is a compile error
  • The for loop calls area() 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

  1. Give one example of compile-time polymorphism and one of runtime polymorphism.
  2. Why can't Shape s; compile in the example above?
  3. What breaks if ~Shape() is not declared virtual?

Abstract Class vs Abstract Super Class — the distinction, made concrete

These terms are related but not identical, and CSS examiners test the difference:

TermDefinition
Abstract classAny class with at least one pure virtual function — may still have some regular (non-pure) methods with real implementations
Abstract super classAn 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.