virtual functions in C++


Introduction

In C++, a virtual function is a function in the base class that is declared using the virtual keyword and is intended to be overridden in derived classes. The purpose of virtual functions is to enable runtime polymorphism, where the function that is called depends on the actual object type (not the type of the pointer or reference used to call it).

Virtual functions are a key feature in object-oriented programming (OOP) because they allow the behavior of derived classes to be invoked through base class pointers or references. This allows you to write more flexible and reusable code, especially in scenarios where the actual object type may not be known at compile-time.


1. What Are Virtual Functions?

A virtual function in the base class is one that can be overridden in a derived class to provide a custom implementation. When a base class pointer or reference is used to call the function, C++ uses dynamic dispatch (or late binding) to determine which function to call, based on the actual type of the object being pointed to, rather than the type of the pointer or reference.

Syntax of a Virtual Function

class BaseClass {
public:
    virtual void functionName() {
        // Base class implementation
    }
};
  • The virtual keyword is used to declare a function as virtual in the base class.
  • The function in the derived class should have the same signature to override the base class function.

2. Example of Virtual Function

Let's see how virtual functions work in practice with an example:

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void sound() {  // Virtual function
        cout << "Animal makes a sound!" << endl;
    }
};

class Dog : public Animal {
public:
    void sound() override {  // Overriding the virtual function
        cout << "Dog barks!" << endl;
    }
};

class Cat : public Animal {
public:
    void sound() override {  // Overriding the virtual function
        cout << "Cat meows!" << endl;
    }
};

int main() {
    Animal* animal1 = new Dog();  // Base class pointer, but object is of type Dog
    Animal* animal2 = new Cat();  // Base class pointer, but object is of type Cat

    animal1->sound();  // Output: Dog barks!
    animal2->sound();  // Output: Cat meows!

    delete animal1;
    delete animal2;

    return 0;
}

Explanation:

  • The sound() function is declared as virtual in the Animal base class.
  • The Dog and Cat classes override the sound() function to provide their specific implementations.
  • Even though animal1 and animal2 are pointers of type Animal*, the appropriate version of sound() is called at runtime, based on the type of object (Dog or Cat) they are pointing to. This is runtime polymorphism in action.

3. How Virtual Functions Work

  • When you declare a function as virtual in the base class, the function call is resolved at runtime using the vtable (virtual table) mechanism.
  • Each object of a class that contains virtual functions has a vtable, a table of function pointers that point to the functions that can be called for that object. The vtable is used to determine which function to invoke when a virtual function is called.
  • Dynamic dispatch occurs when the program decides at runtime which version of the function to call based on the actual type of the object.

4. Virtual Destructor

A common use of virtual functions is for destructors. If you have a class hierarchy where objects are dynamically allocated, and you are using base class pointers to delete derived class objects, you need to ensure that the correct destructor is called. This is done by declaring the destructor in the base class as virtual.

Example of Virtual Destructor:

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() {  // Virtual destructor
        cout << "Base class destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() override {  // Overriding the destructor
        cout << "Derived class destructor" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // Correct destructor called due to virtual destructor

    return 0;
}

Explanation:

  • If the destructor in the Base class is declared virtual, deleting a Derived class object through a Base* pointer will call the Derived class destructor first, followed by the Base class destructor. This ensures that all resources allocated by the derived class are properly cleaned up.
  • If the destructor in the Base class were not virtual, only the Base class destructor would be called, potentially causing a resource leak in the derived class.

5. Pure Virtual Functions and Abstract Classes

A pure virtual function is a function that is declared in the base class, but has no implementation. It makes the base class abstract, meaning you cannot instantiate objects of that class directly. Derived classes are required to provide an implementation for pure virtual functions.

Syntax for Pure Virtual Functions:

class Base {
public:
    virtual void functionName() = 0;  // Pure virtual function
};
  • A pure virtual function is declared by using = 0 at the end of the function declaration.

Example of Pure Virtual Function:

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() = 0;  // Pure virtual function
};

class Circle : public Shape {
public:
    void draw() override {  // Override pure virtual function
        cout << "Drawing a circle!" << endl;
    }
};

class Square : public Shape {
public:
    void draw() override {  // Override pure virtual function
        cout << "Drawing a square!" << endl;
    }
};

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Square();

    shape1->draw();  // Output: Drawing a circle!
    shape2->draw();  // Output: Drawing a square!

    delete shape1;
    delete shape2;

    return 0;
}

Explanation:

  • The Shape class is abstract because it contains a pure virtual function draw().
  • Both the Circle and Square classes provide their own implementations of the draw() function.
  • This approach allows you to work with Shape pointers or references, but ensures that the actual object type determines the specific behavior at runtime.

6. When to Use Virtual Functions

You should use virtual functions when:

  • You want to achieve runtime polymorphism and allow derived classes to provide their own specific implementation of a function.
  • You are dealing with base class pointers or references that refer to objects of derived classes, and you want the correct function to be invoked based on the actual object type.
  • You want to allow derived classes to override the behavior of base class methods, while maintaining a common interface for all derived classes.