Question

Why Virtual Functions Matter in C++: Polymorphism Explained

cpppolymorphismvirtual-functions

Question

In C++, I understand that a derived class can define a function with the same name as one in its base class, which seems like overriding. However, I was also taught that virtual functions are what allow overriding in inheritance.

So what is the difference?

If I can already redefine a base-class function in a derived class without using virtual, what exactly does virtual add, and why is it important?

I want to understand what problem virtual functions solve and when they are actually needed.

Short Answer

By the end of this page, you will understand the real purpose of virtual functions in C++: they enable runtime polymorphism. You will see the difference between simply redefining a function in a derived class and calling the correct function through a base-class pointer or reference. You will also learn when virtual is necessary, how it affects program behavior, and common mistakes beginners make.

Concept

In C++, a derived class can declare a function with the same name and signature as a base-class function. At first glance, this looks like overriding whether or not virtual is used.

The key difference is how the function call is chosen.

Without virtual

The function call is resolved based on the static type of the variable, pointer, or reference at compile time.

This is called static binding or early binding.

With virtual

The function call is resolved based on the actual object type at runtime.

This is called dynamic binding, late binding, or runtime polymorphism.

That is the real reason virtual functions exist.

Why this matters

Inheritance becomes much more useful when you can write code that works with a base type, but still gets derived-specific behavior.

For example:

  • a Shape* can point to a Circle or Rectangle
  • an Animal& can refer to a Dog or Cat
  • a Logger* can hold a ConsoleLogger or FileLogger

Without virtual, calling a function through the base type uses the base version, even if the object is actually a derived object.

With virtual, C++ chooses the derived version automatically.

Important distinction

Redefining a function in a derived class is not the full story. The question is:

What happens when you access the object through a base-class interface?

That is exactly what virtual controls.

Example

#include <iostream>
using namespace std;

class Base {
public:
    void speak() {
        cout << "Base speaks\n";
    }
};

class Derived : public Base {
public:
    void speak() {
        cout << "Derived speaks\n";
    }
};

int main() {
    Derived d;
    Base* ptr = &d;

    d.speak();      // Derived speaks
    ptr->speak();   // Base speaks
}

Even though d is a Derived, ptr is a Base*, so without virtual, the compiler chooses Base::speak().

Now with virtual:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void speak() {
        cout << "Base speaks\n";
    }
};

class Derived : public Base {
public:
    void speak() override {
        cout << "Derived speaks\n";
    }
};

int main() {
    Derived d;
    Base* ptr = &d;

    d.speak();      // Derived speaks
    ptr->speak();   // Derived speaks
}

Now the function call depends on the real object type at runtime.

Why it matters in real programming

Virtual functions let you:

  • write reusable code against interfaces or base classes
  • swap implementations without changing caller code
  • build plugin-style systems
  • handle many object types through one common API

This is a major foundation of object-oriented design in C++.

Mental Model

Think of a base-class pointer like a remote control labeled Base.

Without virtual, the remote only looks at the label on the remote, not the actual device it is connected to. If the remote says Base, it always sends the Base command.

With virtual, the remote checks the real device connected at runtime. If it is actually a Derived object, it sends the Derived version of the command.

So:

  • non-virtual function = "use the function based on the variable type"
  • virtual function = "use the function based on the actual object type"

Another way to think about it:

  • Without virtual, inheritance is mostly about code reuse.
  • With virtual, inheritance also becomes a way to express interchangeable behavior.
Take Quiz

Syntax and Examples

Basic syntax

class Base {
public:
    virtual void show() {
        // base implementation
    }
};

class Derived : public Base {
public:
    void show() override {
        // derived implementation
    }
};

Notes

  • virtual is written in the base class.
  • override is optional but strongly recommended in the derived class.
  • Once a function is virtual in the base class, it stays virtual in derived classes.

Example 1: Without virtual

#include <iostream>
using namespace std;

class Animal {
public:
    void makeSound() {
        cout << "Animal sound\n";
    }
};

class Dog : public Animal {
public:
    void makeSound() {
        cout << "Bark\n";
    }
};

int main() {
    Dog dog;
    Animal* a = &dog;

    dog.makeSound(); // Bark
    a->makeSound();  // Animal sound
}

The object is a Dog, but the pointer type is Animal*, so the base function is called.

Example 2: With virtual

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void makeSound() {
        cout << "Animal sound\n";
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        cout << "Bark\n";
    }
};

int main() {
    Dog dog;
    Animal* a = &dog;

    dog.makeSound(); // Bark
    a->makeSound();  // Bark
}

Now C++ uses dynamic dispatch and calls Dog::makeSound().

Example 3: Using references

Virtual dispatch also works with references:

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() {
        cout << "Drawing shape\n";
    }
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing circle\n";
    }
};

void render(Shape& shape) {
    shape.draw();
}

int main() {
    Circle c;
    render(c); // Drawing circle
}

This is one of the most common uses of virtual functions: a general function works with a base type, while each derived type provides its own behavior.

Step by Step Execution

Consider this example:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void greet() {
        cout << "Hello from Base\n";
    }
};

class Derived : public Base {
public:
    void greet() override {
        cout << "Hello from Derived\n";
    }
};

int main() {
    Derived d;
    Base* ptr = &d;
    ptr->greet();
}

Step by step

  1. Derived d;

    • A Derived object is created.
    • Because Derived inherits from Base, it contains a Base part too.
  2. Base* ptr = &d;

    • A base-class pointer is created.
    • It points to the Derived object.
    • The pointer type is Base*, but the real object type is Derived.
  3. ptr->greet();

    • The compiler sees that greet() is virtual.
    • Instead of forcing Base::greet(), the program checks the actual object at runtime.
    • The actual object is Derived.
    • So Derived::greet() is called.
  4. Output:

Hello from Derived

What if virtual were removed?

Then step 3 changes:

  • The compiler binds the call using the pointer type Base*
  • It calls Base::greet()
  • Output becomes:
Hello from Base

That single difference is the whole point of virtual functions.

Real World Use Cases

1. Drawing different UI elements

A graphics system may store many objects as Widget* or Shape*:

  • Button
  • TextBox
  • Checkbox
  • Circle
  • Rectangle

Each one implements its own draw() behavior. A render loop can call draw() on the base type and let virtual dispatch choose the right implementation.

2. Game development

Games often have a base class like Entity or Character with virtual functions such as:

  • update()
  • render()
  • takeDamage()

A game loop can work with Entity* while each derived class behaves differently.

3. Logging systems

A program may define a base Logger class and derived classes such as:

  • ConsoleLogger
  • FileLogger
  • NetworkLogger

Application code can call log() through a Logger* without caring which concrete logger is used.

4. File and stream abstractions

Code that reads from a base stream interface can work with:

  • files
  • memory buffers
  • network sockets

The same function calls can behave differently depending on the real object.

5. Plugin architectures

A system can load different implementations behind a shared base interface. Virtual functions let the main program call common operations without knowing the exact plugin type.

Take Quiz

Real Codebase Usage

In real projects, virtual functions are usually used as part of a base interface or abstract class.

Common patterns

1. Interface-style base classes

class Logger {
public:
    virtual void log(const std::string& message) = 0;
    virtual ~Logger() = default;
};

This says every logger must provide log().

2. Abstract base classes

Using = 0 creates a pure virtual function, making the class abstract:

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

You cannot create a Shape directly, but derived classes like Circle or Rectangle can implement area().

3. Polymorphic containers

Developers often store different derived objects together:

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class Animal {
public:
    virtual void sound() const = 0;
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void sound() const override {
        cout << "Bark\n";
    }
};

class Cat : public Animal {
public:
    void sound() const override {
        cout << "Meow\n";
    }
};

int main() {
    vector<unique_ptr<Animal>> animals;
    animals.push_back(make_unique<Dog>());
    animals.push_back(make_unique<Cat>());

    for (const auto& animal : animals) {
        animal->sound();
    }
}

This pattern is common in large C++ codebases.

4. Virtual destructors

If a class is meant to be used polymorphically, its destructor should usually be virtual:

class Base {
public:
    virtual ~Base() = default;
};

This ensures correct cleanup when deleting through a base pointer.

5. override for safety

Real codebases often require override because it helps catch mistakes like wrong parameter types or missing const.

class Base {
public:
    virtual void print() const;
};

class Derived : public Base {
public:
    void print() const override;
};

Common Mistakes

1. Thinking same-name functions always give polymorphism

A derived class can hide or redefine a base function, but that alone does not create runtime polymorphism.

Broken expectation

class Base {
public:
    void show() {
    }
};

class Derived : public Base {
public:
    void show() {
    }
};

Calling show() through a Base* still uses Base::show().

Fix

Make the base function virtual if you want runtime dispatch.

2. Forgetting override

Without override, you might accidentally create a different function instead of overriding.

Broken code

class Base {
public:
    virtual void print() const {
    }
};

class Derived : public Base {
public:
    void print() {
    }
};

This does not override because the const is missing.

Fix

class Derived : public Base {
public:
    void print() const override {
    }
};

3. Forgetting a virtual destructor

If you delete a derived object through a base pointer and the base destructor is not virtual, behavior is incorrect.

Risky code

class Base {
public:
    ~Base() {}
};

class Derived : public Base {
public:
    ~Derived() {}
};

int main() {
    Base* ptr = new Derived();
    delete ptr;
}

Safer version

class Base {
public:
    virtual ~Base() = default;
};

4. Passing objects by value

Passing by value can slice the derived part away.

Broken code

void run(Base obj) {
    obj.greet();
}

If you pass a Derived, only the Base portion is copied.

Fix

Use a reference or pointer:

void run(Base& obj) {
    obj.greet();
}

5. Using virtual when polymorphism is not needed

Not every inherited function needs to be virtual. Use it when you need derived-specific behavior through a base interface.

Comparisons

ConceptWithout virtualWith virtual
Binding timeCompile timeRuntime
Based onStatic type of pointer/reference/objectActual object type
PolymorphismNo runtime polymorphismYes
Base pointer to derived objectCalls base versionCalls derived version
Typical useSimple inheritance, code reuseInterfaces, extensible behavior

Overriding vs hiding

SituationResult
Derived defines same function, base not virtualFunction hiding/redefinition for derived objects, not runtime polymorphism
Base function is virtual and derived matches signatureTrue runtime overriding

Pointer/reference vs object

Access styleVirtual dispatch works?
Base pointerYes
Base referenceYes
Base object by valueNo

virtual vs pure virtual

Keyword styleMeaning
virtual void f();Base provides a virtual function that derived classes may override
virtual void f() = 0;Pure virtual function; derived classes must implement it to be concrete
Take Quiz

Cheat Sheet

Quick rules

  • Use virtual in the base class when derived classes should provide different behavior.
  • Virtual dispatch works through base pointers and base references.
  • It does not help if you pass objects by value.
  • Use override in derived classes.
  • If a class is used polymorphically, give it a virtual destructor.

Basic syntax

class Base {
public:
    virtual void f();
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void f() override;
};

Pure virtual function

class Base {
public:
    virtual void f() = 0;
};

Remember

  • Same function name in derived class does not automatically mean runtime polymorphism.
  • virtual changes how calls are resolved.
  • override helps the compiler catch mistakes.

Common edge cases

  • Missing const means no override.
  • Different parameter list means no override.
  • Passing by value causes slicing.
  • Deleting through a base pointer needs a virtual destructor.

FAQ

Why can I redefine a function without virtual?

Because C++ allows a derived class to declare a function with the same name and signature. But without virtual, calls through a base pointer or reference still use the base version.

What problem do virtual functions solve in C++?

They let base-class pointers and references call the correct derived implementation at runtime. This is runtime polymorphism.

Do I always need virtual when using inheritance?

No. Use virtual when you want different derived behavior through a base interface. If inheritance is only for code reuse, it may not be needed.

What is the difference between virtual and override?

virtual enables runtime dispatch. override tells the compiler that a derived function is intended to override a base virtual function.

Do virtual functions work with objects, pointers, and references?

They matter mainly with pointers and references to base classes. Passing or storing by value loses polymorphic behavior.

Why should a base class destructor be virtual?

So deleting a derived object through a base pointer runs the full destruction chain correctly.

What is a pure virtual function?

A pure virtual function uses = 0 and makes the class abstract. Derived classes must implement it before objects of those classes can be created.

Take Quiz

Mini Project

Description

Build a small animal sound system that demonstrates why virtual functions are needed. You will create a base class and multiple derived classes, then call a shared function through base-class pointers. This shows the exact difference between ordinary inherited functions and virtual dispatch in a realistic, easy-to-test example.

Goal

Create a program where different animal objects respond correctly through a common base-class interface.

Requirements

  1. Create a base class named Animal with a sound-related member function.
  2. Create at least two derived classes such as Dog and Cat.
  3. Store derived objects using base-class pointers or smart pointers.
  4. Call the sound function through the base type and ensure the correct derived behavior happens.
  5. Add a virtual destructor to the base class.
Take Quiz

Keep learning

Related questions

View more