Menu
Coddy logo textTech

Virtual Functions & VTable

Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 52 of 104.

The virtual keyword solves the problem we saw in the previous lesson. When you declare a method as virtual, C++ determines which version to call based on the actual object type at runtime, not the pointer type.

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

Dog d;
Animal* ptr = &d;
ptr->speak();  // Output: Woof!

Now the correct method is called! The override keyword is optional but recommended. It tells the compiler you intend to override a virtual function, catching errors if the signatures don't match.

How does this work? When a class has virtual functions, the compiler creates a virtual table (vtable). This is a hidden lookup table containing pointers to the class's virtual functions. Each object with virtual functions stores a hidden pointer (vptr) to its class's vtable.

When you call a virtual function through a pointer, the program looks up the correct function address in the vtable at runtime. This is called dynamic dispatch. It adds a small overhead compared to regular function calls, but enables powerful polymorphic behavior.

One important rule: if a class has virtual functions and will be used as a base class, its destructor should also be virtual. This ensures proper cleanup when deleting derived objects through base pointers:

class Animal {
public:
    virtual ~Animal() {}  // Virtual destructor
    virtual void speak() {}
};
challenge icon

Challenge

Easy

Let's build a media player system that demonstrates the power of virtual functions and runtime polymorphism. You'll create a hierarchy of media types where each one plays differently, and see how virtual functions enable the correct behavior even when accessed through base class pointers.

You'll organize your code across three files:

  • Media.h: Define a base Media class that represents any playable media:
    • A protected std::string title member
    • A constructor that takes a title and stores it
    • A virtual play() method that prints: Playing media: <title>
    • A virtual destructor that prints: Media [<title>] destroyed
  • AudioTrack.h: Define an AudioTrack class that publicly inherits from Media:
    • A private std::string artist member
    • A constructor that takes a title and artist, passes the title to the base class, and stores the artist
    • Override play() using the override keyword to print: Playing audio: <title> by <artist>
    • A destructor that prints: AudioTrack [<title>] destroyed
  • main.cpp: Read three inputs (each on a separate line):
    1. Audio track title (string)
    2. Artist name (string)
    3. Video title (string)

    Define a VideoClip class directly in main.cpp that publicly inherits from Media:

    • A private int duration member (in seconds)
    • A constructor that takes a title and duration (default duration to 120)
    • Override play() to print: Playing video: <title> (<duration>s)
    • A destructor that prints: VideoClip [<title>] destroyed

    Create an array of three Media* pointers inside a block scope. Dynamically allocate:

    • A base Media object with title "Generic Media"
    • An AudioTrack object with the input title and artist
    • A VideoClip object with the input video title

    Loop through the array and call play() on each pointer. Then delete all objects in reverse order. After the block, print: Playback complete!

For example, with inputs Bohemian Rhapsody, Queen, and Nature Documentary:

Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!

Notice how calling play() through Media* pointers invokes the correct derived class method thanks to virtual functions. Also observe how the virtual destructor ensures proper cleanup — both the derived and base destructors run when deleting through a base pointer.

Cheat sheet

The virtual keyword enables runtime polymorphism by determining which method to call based on the actual object type, not the pointer type:

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

Dog d;
Animal* ptr = &d;
ptr->speak();  // Output: Woof!

The override keyword is optional but recommended. It tells the compiler you intend to override a virtual function, catching errors if the signatures don't match.

How virtual functions work: The compiler creates a virtual table (vtable) containing pointers to the class's virtual functions. Each object with virtual functions stores a hidden pointer (vptr) to its class's vtable. When calling a virtual function through a pointer, the program looks up the correct function address in the vtable at runtime. This is called dynamic dispatch.

Virtual destructors: If a class has virtual functions and will be used as a base class, its destructor should also be virtual. This ensures proper cleanup when deleting derived objects through base pointers:

class Animal {
public:
    virtual ~Animal() {}  // Virtual destructor
    virtual void speak() {}
};

Try it yourself

#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"

using namespace std;

// TODO: Define VideoClip class here that publicly inherits from Media
// - Private int duration member (in seconds)
// - Constructor takes title and duration (default duration to 120)
// - Override play() to print: Playing video: <title> (<duration>s)
// - Destructor prints: VideoClip [<title>] destroyed

class VideoClip : public Media {
private:
    int duration;

public:
    // TODO: Implement constructor
    VideoClip(const std::string& t, int d = 120) : Media(t) {
        // TODO: Store the duration
    }

    // TODO: Override play() method
    void play() override {
        // TODO: Implement this method
    }

    // TODO: Implement destructor
    ~VideoClip() {
        // TODO: Implement this destructor
    }
};

int main() {
    // Read inputs
    string audioTitle;
    string artist;
    string videoTitle;
    
    getline(cin, audioTitle);
    getline(cin, artist);
    getline(cin, videoTitle);

    // TODO: Create a block scope with curly braces
    // Inside the block:
    // 1. Create an array of three Media* pointers
    // 2. Dynamically allocate:
    //    - A base Media object with title "Generic Media"
    //    - An AudioTrack object with the input title and artist
    //    - A VideoClip object with the input video title
    // 3. Loop through the array and call play() on each pointer
    // 4. Delete all objects in reverse order

    // TODO: After the block, print: Playback complete!
    
    return 0;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming