Given Shape* p = &rectangle; p->area();, which area() runs? The pointer says Shape, but the object is a Rectangle, and C++ answers with the object: the virtual call runs Rectangle::area(). That one rule decides every dispatch in a polymorphic hierarchy, and it is exactly what a missing const, a changed parameter, or a non-virtual base destructor quietly break, turning correct-looking code into a compiler error or undefined behaviour.
Virtual functions in C++ choose by dynamic type
In Shape* p = &rectangle, the expression p has the static type Shape*. The object it points to has the dynamic type Rectangle. For a virtual call, C++ selects the final overrider for that dynamic type, so p->area() calls Rectangle::area(). A non-virtual member call is instead resolved from the expression's static type.
The minimum pattern is simple. Declare the base member virtual, match its parameter types and qualifiers in the derived class, and add override to the derived declaration. override does not create dynamic dispatch. It asks the compiler to confirm that the function really overrides a base virtual. Once virtuality is introduced in the base, it is inherited.
For an inheritance refresh, start with OOP for Teaching CS Exams: Classes and Inheritance. C Programming for Teaching CS Exams gives the procedural baseline, but C++ virtual dispatch is a language feature, not a hand-written C function-pointer convention.
Virtual function worked example with two shapes
This complete C++17 program uses an abstract Shape interface and two concrete derived classes:
#include <iostream>
struct Shape {
virtual const char* name() const = 0;
virtual int area() const = 0;
virtual ~Shape() = default;
};
struct Rectangle final : Shape {
int w;
int h;
Rectangle(int width, int height) : w(width), h(height) {}
const char* name() const override { return "rectangle"; }
int area() const override { return w * h; }
};
struct Triangle final : Shape {
int base;
int height;
Triangle(int b, int h) : base(b), height(h) {}
const char* name() const override { return "triangle"; }
int area() const override { return (base * height) / 2; }
};
int main() {
Rectangle rectangle{6, 4};
Triangle triangle{6, 4};
Shape* shapes[]{&rectangle, &triangle};
int total = 0;
for (Shape* shape : shapes) {
int value = shape->area();
std::cout << shape->name() << " area = " << value << '\n';
total += value;
}
std::cout << "total area = " << total << '\n';
}The exact output is:
rectangle area = 24
triangle area = 12
total area = 36Work each value before trusting the output. The rectangle area is 6 * 4 = 24. The triangle area is (6 * 4) / 2 = 24 / 2 = 12. Therefore, the total is 24 + 12 = 36. The Coding & DSA course category places this example in a broader programming sequence.
VTable and vptr as a conceptual dispatch trace
C++ guarantees the observable rule: shape->area() invokes the final overrider for the dynamic object. The standard does not require anything named a VTable. Mainstream compilers commonly implement the rule by giving a polymorphic object a hidden vptr that refers to a table of virtual-function addresses shared by objects of that dynamic class.
First, shapes[0] points to rectangle. Its conceptual vptr leads to the Rectangle table and its area entry to Rectangle::area(). The stored data gives 6 * 4 = 24. Next, the lookup reaches Triangle::area(), returning (6 * 4) / 2 = 12. Both source expressions have type Shape*.
The useful model is that data members remain in each object while dispatch metadata identifies the correct final overriders. Table layout, destructor entries, thunks, byte counts, and the vptr's position are compiler and ABI details. Do not infer a portable object size from the conceptual model.
![A conceptual VTable and vptr trace: shapes[0] dispatches to Rectangle::area (24) and shapes[1] to Triangle::area (12), totalling 36.](https://cdn.knowledgegate.ai/blog-assets/blog_asset_1784460998519_68skak.jpg)
Override errors in signatures and qualifiers
Suppose the base declares virtual int area() const. This derived declaration does not override it:
int area() override; // wrong: missing const
int area() const override; // correctThe first line fails because override exposes the mismatch. Without it, the line declares a different function, potentially leaving the class abstract because the pure virtual base function lacks a final overrider.
Parameters must also match. With base virtual void scale(int factor), derived void scale(double factor) is a different signature. It can hide the base name during direct lookup, while a call through Shape* follows the base virtual signature. Use override for every intended override and final only to stop further overriding.
Return types generally must be compatible. Covariant pointer or reference returns are an advanced exception, not a reason to loosen ordinary signature checks.
Pure virtual functions, references, and object slicing
The = 0 declarations make Shape abstract, so Shape s; cannot compile. A derived type becomes concrete only after supplying final overriders for name() and area(). Although a pure virtual function can have an implementation, here it defines an interface contract.
Preserve polymorphism with Shape& or Shape*. In a non-abstract hierarchy, passing a derived object by base value copies only the base subobject: object slicing. Here, Shape copy = rectangle; is rejected because an abstract Shape cannot be created, so slicing does not occur.
For owned heterogeneous objects, use std::vector<std::unique_ptr<Shape>> with std::make_unique<Rectangle>(6, 4) and std::make_unique<Triangle>(6, 4). Virtual calls in constructors and destructors dispatch only within the class currently being constructed or destroyed, never to a more-derived override.
Virtual destructors make base-pointer deletion safe
Destruction obeys the same dispatch rule, and ignoring it costs more than a wrong answer:
#include <iostream>
struct Base {
virtual ~Base() { std::cout << "Base destructor\n"; }
};
struct Derived : Base {
~Derived() override { std::cout << "Derived destructor\n"; }
};
int main() {
Base* p = new Derived;
delete p;
}The output is Derived destructor, then Base destructor. Deleting a derived object through a base pointer with a non-virtual destructor is undefined behaviour. A base intended for polymorphic deletion needs a public virtual destructor. std::unique_ptr<Base> also relies on this rule when owning a Derived through the base type.
Compilers commonly keep destructor dispatch information alongside other virtual entries, but its representation is ABI-specific. The language rule is the safe deletion contract and derived-then-base order.
Virtual function questions test output and edge cases
Predict this output before reading the explanation:
#include <iostream>
struct B {
virtual int f() const { return 1; }
int g() const { return 2; }
};
struct D : B {
int f() const override { return 3; }
int g() const { return 4; }
};
int main() {
D d;
B* p = &d;
std::cout << p->f() << ' ' << p->g() << '\n';
}The exact output is 3 2. The virtual f() dispatches to D::f() using the dynamic type. The non-virtual g() resolves to B::g() using the static type B*.
Common questions ask you to predict output through a base pointer or reference, find a const or parameter mismatch, or decide whether a base destructor must be virtual. A question that asks for the exact size of a polymorphic object is unanswerable until the compiler, ABI, architecture, and alignment are fixed.
For more C++ and coding-round practice, work through Coding For Placements.
Virtual functions and VTable: the short version
Three checks catch almost every virtual-dispatch bug.
Overriding signatures match exactly, including
constand parameter types, withoverrideon each derived declaration.Calls travel through
Shape&orShape*, so no copy into a base value slices the object away.The base destructor is virtual wherever an object can be deleted through a base pointer.
Dynamic dispatch is guaranteed by the language. VTables and vptrs are the usual implementation model, not a mandated layout.
Add Square final : public Shape with side 5. Return "square" from name() and compute 5 * 5 = 25. Append &square to the array. Predict the output: a third line says square area = 25, and the total changes from 36 to 36 + 25 = 61.
Continue with C++ Programming: Concepts, MCQs, Coding Questions. Then compile the extension and explain each dispatched call by its static type, dynamic type, and final overrider.




