Introduction
Hello! In our last lesson, we contrasted compile-time (static) and run-time (dynamic) polymorphism. We learned that dynamic polymorphism is the mechanism that allows us to use a base class pointer or reference to interact with objects of various derived types, and that the virtual keyword is the key to enabling this behavior.
Today, we will move from theory to practice. This lesson is focused on how to correctly use virtual functions to achieve dynamic polymorphism. We'll start by seeing what happens when we don't use them, then we'll introduce the virtual keyword to fix the issue. Because you're interested in the "under the hood" details, we'll then take a deeper look at the v-table mechanism that makes this all possible, even inspecting object memory layouts.
By the end of this lesson, you'll be able to implement polymorphic behavior in your own class hierarchies.
1. The Problem: When Binding Happens Too Early
In C++, by default, function calls are resolved at compile time (static binding). When you have a base class pointer pointing to a derived class object, the compiler only knows the type of the pointer (Base*), not the type of the object it actually points to (Derived). This can lead to surprising behavior.
Let's watch a video that sets up a clear example of this problem.
Dynamic Binding (Polymorphism) With The Virtual Keyword | C++ Tutorial
This video from Portfolio Courses demonstrates what happens when you create a collection of base class pointers to different object types (students and medical students) and call an overridden method. It clearly shows the problem caused by static binding.
Watch from the beginning to 08:42. Pay close attention to the for loop that prints the student information. Notice that even for the MedicalStudent objects, only the base print() function is called, and the specialty information is missing. This is the core problem we need to solve.
As you saw, even though the array contains pointers to MedicalStudent objects, calling print() through a Student* pointer invokes Student::print(). The compiler binds the function call based on the pointer's static type, not the object's dynamic type.
2. The Solution: The virtual Keyword
To fix this and enable dynamic binding, we simply need to tell the compiler that a function might be overridden by derived classes and that the decision of which version to call must be deferred until runtime. We do this by marking the function with the virtual keyword in the base class.
Let's see how this one small change fixes our program.
Dynamic Binding (Polymorphism) With The Virtual Keyword | C++ Tutorial
The same video now shows the solution by adding the virtual keyword to the print() function in the Student base class.
Watch from 08:42 to 09:43. Observe how adding virtual to Student::print() is all that's needed to make the program call the correct MedicalStudent::print() for the derived class objects at runtime.
This is dynamic polymorphism in action. At runtime, when students[i]->print() is executed, the program checks the actual type of the object being pointed to and calls the most-derived version of the print() function available for that type.
For a more detailed written explanation and another clear example, please read the following article.
25.2 — Virtual functions and polymorphism
The excellent Learn C++ tutorial site provides a concise explanation of virtual functions, distinguishing them from regular functions and showing their effect with base and derived classes.
Read the first two sections of the article, from the beginning down to just before the 'Polymorphism' heading. This will reinforce what you just saw in the video with a different code example.
3. Under the Hood: The V-Table and V-Ptr
In the previous lesson, we briefly mentioned that C++ typically implements dynamic polymorphism with virtual tables (v-tables) and virtual pointers (v-ptrs). Now, let's explore this mechanism in more detail, as it's fundamental to understanding how this works.
- V-Table (Virtual Table): For any class that contains one or more virtual functions, the compiler creates a static, class-wide table. This table is essentially an array of function pointers, where each entry points to the memory address of one of the class's virtual functions.
- V-Ptr (Virtual Pointer): Each object instance of a class with virtual functions contains a hidden member variable: the v-ptr. This pointer, which is automatically set by the constructor, points to the v-table for that object's class.
When you make a virtual function call through a base class pointer (e.g., ptr->printInfo()), the following happens at runtime:
- The program follows the object's
v-ptrto find the correct v-table. - It looks up the address of the
printInfofunction in that v-table (at a fixed, compile-time-determined index). - It calls the function at that address.
A Derived object's v-ptr points to the Derived v-table, which correctly lists Derived::printInfo(). A Base object's v-ptr points to the Base v-table. This is how the runtime system dispatches the call to the right function.
The following video provides a great visual walkthrough of this concept.
Classes part 18 - Understanding the vtable (Popular interview question) | Modern Cpp Series Ep. 54
Mike Shah is known for his clear, diagram-based explanations. Here, he draws out the classes, the v-tables, and the v-ptrs to show how they all connect.
Watch from the beginning until 08:26. Focus on how the v-table pointer within an object instance points to the correct v-table for its class (Base or Derived), and how this table is used to find the right function to call.
Here is a static diagram illustrating a v-table for a derived class.

Given your interest in systems-level details, the following article uses the GDB debugger to actually inspect the memory of C++ objects, revealing the v-ptr and the addresses in the v-table. This is a practical look at the theory we've just discussed.
Dive into C++ Object Memory Layout with Examples
This article provides a fascinating look into the actual memory layout of objects with virtual functions. It confirms the existence and role of the v-ptr and v-table using a debugger.
Read the section 'Inheritance with Virtual Functions'. You don't need to run the GDB commands yourself, but pay attention to the output and the author's analysis. It shows how the base pointer ptr points to an object whose v-ptr directs calls to the derived class's v-table, while the sliced object dup has a v-ptr pointing to the base class's v-table.
4. The Power of Polymorphism: Heterogeneous Collections
The primary benefit of virtual functions is the ability to write generic code that can handle a variety of object types through a common interface. A classic example is creating a collection (like an array or vector) of base class pointers, where each pointer can refer to an object of any derived class.
The learncpp.com article demonstrates this beautifully with an Animal hierarchy.
25.2 — Virtual functions and polymorphism
Let's return to the Learn C++ article to see the practical payoff of virtual functions.
Read the section titled 'A more complex example'. Notice how the report function and the animals array work seamlessly with both Cat and Dog objects without any changes. This demonstrates the power and flexibility of dynamic polymorphism.
5. Best Practices and Gotchas
When using virtual functions, there are a few important rules and modern C++ features to keep in mind.
Use the override Specifier
When you intend for a function in a derived class to override a base class virtual function, you should explicitly mark it with the override specifier.
class Animal {
public:
virtual std::string_view speak() const { return "???"; }
};
class Cat : public Animal {
public:
// Good: The 'override' keyword tells the compiler our intent.
std::string_view speak() const override { return "Meow"; }
};
This is not just for documentation. The compiler will generate an error if a function marked override does not, in fact, override a base class virtual function. This can happen due to a typo in the function name or a mismatch in parameters, which are common and hard-to-find bugs.
Do Not Call Virtual Functions from Constructors or Destructors
This is a critical rule. When a Derived object is being constructed, the Base class constructor runs first. At that point, the Derived part of the object hasn't been initialized yet, so the object is still effectively of type Base. If you call a virtual function from the Base constructor, it will always resolve to the Base version of the function.
The same logic applies in reverse during destruction.
25.2 — Virtual functions and polymorphism
The Learn C++ article concludes with this very important best practice. Understanding the reason behind it is key to avoiding subtle bugs.
Read the final sections, 'Do not call virtual functions from constructors or destructors' and 'The downside of virtual functions'.
Conclusion
In this lesson, you've moved from the concept of dynamic polymorphism to its practical implementation. You now know how to use the virtual keyword to enable this powerful feature and understand the v-table mechanism that makes it work.
Key Takeaways:
- Dynamic polymorphism is enabled by declaring a member function as
virtualin the base class. - When a virtual function is called through a base class pointer or reference, the call is resolved at runtime to the version belonging to the object's actual type.
- This is implemented via v-tables (a class-level array of function pointers) and v-ptrs (a hidden pointer in each object instance).
- Always use the
overridespecifier in derived classes to prevent signature mismatch errors. - Never call virtual functions inside a constructor or destructor, as they will not behave polymorphically.
Next Up
We've seen the power of virtual functions for method calls. However, what happens when we delete a derived object through a base class pointer?
Animal* animal = new Cat("Fred");
delete animal; // Is this safe?
If the destructor is not virtual, this can lead to a serious problem called a memory leak. In our next lesson, we will explore why this happens and introduce the concept of virtual destructors to ensure proper resource cleanup in polymorphic hierarchies.