OOP · ENCT 151

Ch 6. Virtual Functions

4 hours · 5 marks

6.1 What is a Virtual Function

A virtual function is a member function declared in a base class using the keyword virtual, which is expected to be redefined in derived classes. When called through a base class pointer/reference, the actual (derived) object's version is invoked — this is resolved at run time (late binding), not compile time.

class Shape {
public:
    virtual void draw() { cout << "Drawing shape"; }
};
class Circle : public Shape {
public:
    void draw() override { cout << "Drawing circle"; }   // overrides base version
};

6.2 Need of Virtual Function

Without virtual, calling a function through a base class pointer always invokes the base class's version (early/static binding), regardless of the actual object type — this defeats runtime polymorphism.

Shape *s = new Circle();
s->draw();   // WITHOUT virtual: calls Shape::draw()
              // WITH virtual:    calls Circle::draw()  -- correct runtime polymorphism

Virtual functions let a single interface (base class pointer) work correctly for objects of many derived types — the foundation of runtime polymorphism.

6.3 Pointer to Derived Class

A base class pointer can point to a derived class object (upcasting is implicit and safe), but a derived class pointer cannot directly point to a base object without an explicit (and potentially unsafe) downcast.

Shape *sp;
Circle c;
sp = &c;        // OK -- base pointer to derived object (upcasting)
sp->draw();     // calls Circle::draw() if draw() is virtual
// Circle *cp = sp;          -> error, needs explicit cast
Circle *cp = static_cast<Circle*>(sp);  // downcast (use dynamic_cast for safety)

6.4 Array of Pointers to Base Class

A common OOP pattern: store an array of base class pointers, each pointing to a different derived object, then call the virtual function uniformly on each — every call automatically dispatches to the correct derived version.

Shape *shapes[3];
shapes[0] = new Circle();
shapes[1] = new Square();
shapes[2] = new Triangle();
for (int i = 0; i < 3; i++)
    shapes[i]->draw();     // each call resolves to the correct derived draw()

6.5 Pure Virtual Functions and Abstract Class

A pure virtual function has no implementation in the base class and is declared with = 0. A class containing at least one pure virtual function becomes an abstract class — it cannot be instantiated directly, and serves only as a base for derived classes, which must override the pure virtual function(s) to become concrete (instantiable).

class Shape {
public:
    virtual void draw() = 0;    // pure virtual function
};
// Shape s;  -> ERROR: cannot instantiate abstract class
class Circle : public Shape {
public:
    void draw() override { cout << "Circle"; }   // must override to be concrete
};

6.6 Virtual Destructors

If a base class is meant to be used polymorphically (deleted through a base pointer), its destructor must be declared virtual. Otherwise, deleting a derived object through a base pointer invokes only the base destructor, causing the derived part's resources to leak (undefined behaviour).

class Base {
public:
    virtual ~Base() { cout << "Base dtor"; }   // virtual destructor
};
class Derived : public Base {
public:
    ~Derived() { cout << "Derived dtor"; }
};
Base *p = new Derived();
delete p;   // with virtual ~Base(): calls Derived dtor then Base dtor (correct)
             // without virtual: would call ONLY Base dtor -- leak/bug

6.7 Reinterpret Cast Operator

reinterpret_cast performs a low-level reinterpretation of the bit pattern of an object — e.g., converting a pointer of one unrelated type to another, or a pointer to an integer. It performs no type checking or conversion logic and should be used sparingly since it can produce unsafe/undefined results if misused.

int a = 65;
int *p = &a;
char *c = reinterpret_cast<char*>(p);   // reinterprets the pointer's bit pattern

6.8 Run-time Type Information (RTTI)

RTTI allows the type of an object to be determined during program execution. C++ provides two RTTI facilities:

Shape *s = new Circle();
Circle *c = dynamic_cast<Circle*>(s);
if (c) cout << "Successfully cast to Circle";
else cout << "Cast failed";

Most Probable Questions

Q1. What is a virtual function? Explain with a program how it achieves run-time polymorphism. 6 marks
A virtual function is a base class function marked virtual that is overridden in derived classes; calling it through a base pointer/reference invokes the actual object's version at run time.
class Animal {
public:
    virtual void sound() { cout << "Some sound"; }
};
class Dog : public Animal {
public:
    void sound() override { cout << "Bark"; }
};
class Cat : public Animal {
public:
    void sound() override { cout << "Meow"; }
};
int main() {
    Animal *a;
    Dog d; Cat c;
    a = &d; a->sound();  // Bark
    a = &c; a->sound();  // Meow
}
Q2. What is a pure virtual function? What is an abstract class? Why can't an abstract class be instantiated? 5 marks
A pure virtual function is declared with = 0 and has no body in the base class. A class with at least one pure virtual function is abstract. It cannot be instantiated because it is logically incomplete — some of its behaviour is undefined until a derived class supplies an implementation; the compiler enforces this to prevent calling an undefined function. See section 6.5.
Q3. Why are virtual destructors necessary? Illustrate with an example of what goes wrong without one. 5 marks
See section 6.6: without a virtual destructor, deleting a derived object through a base class pointer calls only the base class's destructor, skipping the derived class's cleanup — this causes resource/memory leaks and undefined behaviour. Rule of thumb: any base class intended for polymorphic use should have a virtual destructor.
Q4. Explain dynamic_cast and its use in run-time type identification, with an example. 5 marks
dynamic_cast safely converts a pointer/reference from a base class type to a derived class type at run time, checking the actual object type; it returns nullptr for an invalid pointer conversion (or throws bad_cast for references). It requires the base class to be polymorphic. See code in section 6.8.
Q5. Differentiate between early binding and late binding. 4 marks
Early (static) binding: the function call is resolved at compile time based on the declared (static) type of the pointer/reference — used for normal (non-virtual) function calls, function overloading. Late (dynamic) binding: the function call is resolved at run time based on the actual object type — used for virtual function calls, enabling run-time polymorphism.
Q6. Write a program using an array of base class pointers to call overridden functions of different derived classes. 6 marks
See the general pattern in section 6.4 — create an abstract Shape base class with a pure virtual draw(), derive Circle, Square, Triangle, store their pointers in a Shape* array, and loop calling draw() on each; each call correctly dispatches to the matching derived class implementation.
Q7. What is the typeid operator used for? 3 marks
typeid returns type information about an expression at run time (part of RTTI), commonly used to identify the actual dynamic type of an object accessed through a base class pointer/reference, e.g. typeid(*ptr).name() or to compare types with typeid(a) == typeid(b).
← Chapter 5 Next: Chapter 7 — Stream Computation →