Programming Pandit

c/c++/c#/Javav/Python


Latest Update

Friday, October 23, 2026

Polymorphism and Virtual Functions

 

Polymorphism and Virtual Functions

 

Learning Objectives

After completing this module, students should be able to:

•      Define polymorphism and distinguish compile-time from run-time polymorphism.

•      Use base-class pointers and references to derived objects and explain why a non-virtual call uses the pointer type.

•      Declare virtual functions, override them correctly with override, and explain the virtual attribute in inheritance.

•      Explain overriding versus hiding and overloading, covariant return types, default arguments and object slicing.

•      Describe how virtual functions are implemented with a vptr and a vtable.

 

1. Introduction to Polymorphism

Definition: Polymorphism (Greek: “many forms”) is the ability of one name, interface or call to have different behaviour depending on the type of object involved.

 

Kind

When the function is chosen

Mechanism

Example

Compile-time (static) polymorphism

During compilation

Function overloading, operator overloading, templates

add(int, int) and add(double, double)

Run-time (dynamic) polymorphism

While the program runs, from the real type of the object

Virtual functions and inheritance

animal->sound() runs Dog::sound() or Cat::sound()

 

Program 5.1: Compile-time polymorphism by overloading.

#include <iostream>

using namespace std;

 

class Calculator {

public:

    int add(int a, int b) { return a + b; }

    double add(double a, double b) { return a + b; }

};

 

int main()

{

    Calculator c;

    cout << c.add(10, 20) << '\n';           // add(int, int)

    cout << c.add(2.5, 3.5) << '\n';         // add(double, double)

    return 0;

}

 

Output:

30

6

 

The compiler selects add(int, int) or add(double, double) from the argument types, before the program runs. The rest of this unit is about run-time polymorphism, which needs inheritance and virtual functions.

2. Base-Class Pointers and References to Derived Objects

A pointer or reference to a base class may refer to an object of any publicly derived class (Module 1). This is what makes it possible to write code that works with a whole family of classes through one type.

Dog dog;

Animal *ptr = &dog;     // base pointer, derived object

Animal &ref = dog;      // base reference, derived object

 

Two types are involved:

•      the static type – the declared type of the pointer or reference (Animal), known at compile time;

•      the dynamic type – the real type of the object it refers to (Dog), known only at run time.

 

Which function runs when the pointer is used for a call depends on whether the function is virtual.

Title: One call through a base pointer, many possible functions. - Description: One call through a base pointer, many possible functions.

Figure 5.1: One call through a base pointer, many possible functions.

Program 5.2: A virtual and a non-virtual function called through a base pointer.

#include <iostream>

using namespace std;

 

class Base {

public:

    virtual void show() { cout << "Base show()" << '\n'; }

    void display() { cout << "Base display()" << '\n'; }

    virtual ~Base() = default;

};

 

class Derived : public Base {

public:

    void show() override { cout << "Derived show()" << '\n'; }

    void display() { cout << "Derived display()" << '\n'; }

};

 

int main()

{

    Derived d;

    Base *ptr = &d;

    ptr->show();                  // virtual: Derived::show()

    ptr->display();               // non-virtual: Base::display()

    return 0;

}

 

Output:

Derived show()

Base display()

 

•      show() is virtual: the call through the Base * uses the dynamic type and runs Derived::show().

•      display() is not virtual: the call uses the static type Base and runs Base::display(), although the object is a Derived.

 

Important: Without virtual, a function declared in the derived class only hides the base function; calls through a base pointer still use the base version. virtual is what turns hiding into overriding.

 

3. Virtual Functions

Definition: A virtual function is a member function declared with the keyword virtual in a base class, which a derived class can override. When it is called through a base-class pointer or reference, the version belonging to the actual object is executed.

 

class Base {

public:

    virtual void show();            // may be overridden

    virtual ~Base();                // virtual destructor

};

class Derived : public Base {

public:

    void show() override;           // overrides Base::show

};

 

Program 5.3: Animals that make different sounds.

#include <iostream>

using namespace std;

 

class Animal {

public:

    virtual void sound() { cout << "Animal makes a sound" << '\n'; }

    virtual ~Animal() = default;

};

 

class Dog : public Animal {

public:

    void sound() override { cout << "Dog barks" << '\n'; }

};

 

class Cat : public Animal {

public:

    void sound() override { cout << "Cat meows" << '\n'; }

};

 

int main()

{

    Dog dog;

    Cat cat;

    Animal *ptr;

 

    ptr = &dog;

    ptr->sound();

    ptr = &cat;

    ptr->sound();

    return 0;

}

 

Output:

Dog barks

Cat meows

 

1.      sound() is virtual in Animal and overridden in Dog and Cat.

2.      The same statement ptr->sound(); runs Dog::sound() when ptr points to a Dog and Cat::sound() when it points to a Cat.

3.      A new animal class can be added later without changing the code that calls sound() – the main benefit of polymorphism.

 

4. The Virtual Attribute Is Inherited

Once a function is virtual in a base class, it is virtual in every derived class, whether or not the keyword virtual is repeated. Each class in a chain may override it, and a class that does not override it simply inherits the nearest version.

Program 5.4: A three-level chain; one class does not override.

#include <iostream>

using namespace std;

 

class Base {

public:

    virtual void display() { cout << "Base class" << '\n'; }

    virtual ~Base() = default;

};

 

class Intermediate : public Base {

public:

    void display() override { cout << "Intermediate class" << '\n'; }

};

 

class Derived : public Intermediate {

public:

    void display() override { cout << "Derived class" << '\n'; }

};

 

class Plain : public Intermediate {};            // does not override display()

 

int main()

{

    Derived obj;

    Base &ref = obj;

    ref.display();                       // final override: Derived::display()

 

    Plain p;

    Base &ref2 = p;

    // nearest override: Intermediate::display()

    ref2.display();

    return 0;

}

 

Output:

Derived class

Intermediate class

 

•      For the Derived object the most derived override runs.

•      Plain does not override display(), so a call through a Base & runs the nearest version above it, Intermediate::display().

•      Writing virtual again in the derived class is allowed but unnecessary; the keyword override documents the intention and lets the compiler check it (Section 5).

 

Hierarchies of several levels, and the case in which a function is not defined at all in the base class (pure virtual functions), are discussed in Module 6.

5. Overriding, Overloading and Hiding

Term

Meaning

Resolved

Overloading

Same name, different parameters, in the same scope

At compile time

Overriding

A derived class redefines a virtual function with the same signature

At run time

Hiding

A derived function with the same name (any signature, virtual or not) hides base functions of that name

At compile time

 

5.1 The signature must match

A derived function overrides a virtual function only if its name, parameter types and const-qualification are the same. If the signature differs even slightly, the derived function is a new, unrelated function and nothing is overridden.

Program 5.5: A missing const silently breaks overriding.

#include <iostream>

using namespace std;

 

class Base {

public:

    virtual void show() const { cout << "Base::show() const" << '\n'; }

    virtual ~Base() = default;

};

 

class Derived : public Base {

public:

    // not const: a NEW function

    void show() { cout << "Derived::show()" << '\n'; }

};

 

int main()

{

    Derived d;

    Base *p = &d;

    p->show();                 // calls Base::show() const: nothing was overridden

    d.show();                  // calls Derived::show()

    return 0;

}

 

Output:

Base::show() const

Derived::show()

 

Base::show() const and Derived::show() have different signatures, so the call through the base pointer runs the base version. Some compilers warn about it:

warning: ‘virtual void Base::show() const’ was hidden [-Woverloaded-virtual=]

 

5.2 The keyword override

Writing override after the declaration tells the compiler that the function is meant to override a virtual function. If it does not, the compiler reports an error instead of silently creating a new function:

error: ‘void Derived::show()’ marked ‘override’, but does not override

 

Version Note: override (and final) are C++11 features. In C++98/03 they do not exist: write the function with the same signature and take care by hand. GCC in C++98 mode accepts the keyword only as an extension, with a warning:

 

warning: override controls (override/final) only available with ‘-std=c++11’ or ‘-std=gnu++11’ [-Wc++11-extensions]

 

Use override on every overriding function in modern C++.

6. Rules for Virtual Functions

Rule

Explanation

Only member functions

Non-member functions and friend functions cannot be virtual

Not static

A static function has no object, so it cannot be virtual

Not constructors

A constructor builds the object, so its type is not yet known; it cannot be virtual

Destructors can (and should) be

Make the destructor virtual in a base class that is used polymorphically

Same signature

Name, parameters and const must match to override

Return type

Must be the same, or a covariant pointer/reference type (Section 7)

Access specifier

Is not part of the override rule: a private virtual function can be overridden, and the access checked is that of the static type

Inheritance

Virtual functions are inherited; a derived class need not override them

Declaration in the base

A function must be declared virtual in some base class to be called polymorphically

 

Note: Calling through an object (not a pointer or reference) is resolved at compile time, because the type of the object is known: d.show() uses Derived::show() directly.

 

7. Covariant Return Types

An overriding function may return a pointer or reference to a more derived class than the base function returns. The classic use is a virtual copy function, clone().

Program 5.6: Derived::clone() returns Derived *.

#include <iostream>

using namespace std;

 

class Base {

public:

    virtual Base *clone() const { cout << "Base::clone"

        << '\n'; return new Base(*this); }

    virtual const char *name() const { return "Base"; }

    virtual ~Base() = default;

};

 

class Derived : public Base {

public:

    /* covariant return type: Derived* instead of Base* */

    Derived *clone() const override

    {

        cout << "Derived::clone" << '\n';

        return new Derived(*this);

    }

    const char *name() const override { return "Derived"; }

};

 

int main()

{

    Derived d;

    Base *b = &d;

    Base *copy1 = b->clone();                // dynamic: Derived::clone

    Derived *copy2 = d.clone();              // no cast needed: returns Derived*

    cout << copy1->name() << " " << copy2->name() << '\n';

    delete copy1;

    delete copy2;

    return 0;

}

 

Output:

Derived::clone

Derived::clone

Derived Derived

 

Through the base pointer, clone() still runs Derived::clone() and returns a Base * to the new object. Called on a Derived directly, it returns a Derived * with no cast. This relaxation of the return type is allowed only for pointers or references to classes.

8. Virtual Functions and Default Arguments

Default argument values are not overridden: they are bound at compile time from the static type of the expression, while the function body is selected at run time.

Program 5.7: Different defaults in base and derived classes.

#include <iostream>

using namespace std;

 

class Base {

public:

    virtual void print(int x = 10) { cout << "Base::print " << x << '\n'; }

    virtual ~Base() = default;

};

 

class Derived : public Base {

public:

    void print(int x = 20) override { cout << "Derived::print " << x << '\n'; }

};

 

int main()

{

    Derived d;

    Base *p = &d;

    // body of Derived::print, default value of Base::print

    p->print();

    d.print();                 // body and default value of Derived::print

    return 0;

}

 

Output:

Derived::print 10

Derived::print 20

 

Through the Base * the body of Derived::print runs, but with the default value 10 from Base. Do not give different default arguments to overriding functions.

9. Object Slicing

Polymorphism works only through pointers and references. If a derived object is copied into a base object or passed to a function by value, only the base part is copied (sliced) and the new object is a genuine Base; its virtual calls cannot reach the derived functions.

Program 5.8: Slicing by assignment and by passing by value.

#include <iostream>

using namespace std;

 

class Base {

public:

    virtual void show() const { cout << "Base::show()" << '\n'; }

    virtual ~Base() = default;

};

 

class Derived : public Base {

public:

    void show() const override { cout << "Derived::show()" << '\n'; }

};

 

// the argument is a copy of Base part

void byValue(Base b) { b.show(); }

void byReference(const Base &b) { b.show(); }

 

int main()

{

    Derived d;

    Base sliced = d;           // slicing: only the Base part is copied

    sliced.show();

    byValue(d);

    byReference(d);

    return 0;

}

 

Output:

Base::show()

Base::show()

Derived::show()

 

The first two lines show slicing; the reference version shows the correct behaviour. In polymorphic code pass objects by reference or pointer (or smart pointer), and store pointers, not objects, in containers.

10. Preventing Overriding: final

A virtual function declared final cannot be overridden any more, and a class declared final cannot be used as a base class.

Program 5.9: A final function and a final class.

#include <iostream>

using namespace std;

 

class Base {

public:

    virtual void run() { cout << "Base::run" << '\n'; }

    virtual void stop() { cout << "Base::stop" << '\n'; }

    virtual ~Base() = default;

};

 

class Middle : public Base {

public:

    void run() override final { cout << "Middle::run (final)" << '\n'; }

    void stop() override { cout << "Middle::stop" << '\n'; }

};

 

class Last final : public Middle {               // no class may derive from Last

public:

    void stop() override { cout << "Last::stop" << '\n'; }

    // void run() override {}                    // error: run() is final in Middle

};

 

int main()

{

    Last x;

    Base &b = x;

    b.run();

    b.stop();

    return 0;

}

 

Output:

Middle::run (final)

Last::stop

 

Attempting to override a final function or to derive from a final class gives errors:

error: virtual function ‘virtual void Last::run()’ overriding final function

 

error: cannot derive from ‘final’ base ‘A’ in derived type ‘B’

 

Version Note: final and override are contextual keywords introduced in C++11; they are not available in C++98/03.

 

11. How Virtual Functions Are Implemented

A typical compiler implements run-time polymorphism with two structures.

•      A vtable (virtual table): for each class that has virtual functions, one table of function pointers, one slot per virtual function (an overriding class stores its own function, an inheriting class the inherited one).

•      A hidden vptr (virtual pointer) stored in every object of such a class, which points to the vtable of the object’s real class.

 

Title: An object holds a vptr to the vtable of its class; a virtual call reads the function from the table. - Description: An object holds a vptr to the vtable of its class; a virtual call reads the function from the table.

Figure 5.2: An object holds a vptr to the vtable of its class; a virtual call reads the function from the table.

A virtual call ptr->show() is compiled as: read the vptr of the object, take the entry of the show slot from the vtable, and call it. A non-virtual call is a direct call fixed at compile time (Module 7).

Program 5.10: The hidden pointer enlarges the object.

#include <iostream>

using namespace std;

 

class Plain {

    int x;

public:

    void f() {}

};

 

class Poly {

    int x;

public:

    virtual void f() {}

};

 

int main()

{

    cout << "sizeof(Plain) = " << sizeof(Plain) << '\n';

    cout << "sizeof(Poly)  = " << sizeof(Poly) << "  (extra hidden vptr)\n";

    return 0;

}

 

Output (GCC, 64-bit; the exact sizes depend on the compiler and padding):

sizeof(Plain) = 4

sizeof(Poly)  = 16  (extra hidden vptr)

 

The class with a virtual function is larger by the hidden vptr (plus alignment padding). The vtable itself exists only once per class, not per object. The cost of a virtual call is one extra memory access and, usually, the loss of inlining.

Title: The compiler decides between early and late binding according to whether the function is virtual. - Description: The compiler decides between early and late binding according to whether the function is virtual.

Figure 5.3: The compiler decides between early and late binding according to whether the function is virtual.

Note: The implementation with a vptr and vtable is the usual one, but the C++ standard only specifies the behaviour, not how it is achieved.

 

12. Advantages and Limitations

Advantages

Limitations

One interface, many implementations; code works with new classes unchanged

Small run-time overhead (indirect call, larger objects)

Extensible design: add a derived class without editing the callers

Needs pointers or references (slicing otherwise)

Cleaner code: no long if/switch on the type of an object

Wrong signature silently fails (use override)

Basis of frameworks, plug-ins and interfaces

Hierarchies can be hard to follow in large programs

 

13. Quick Revision

•      Run-time polymorphism = virtual function called through a base pointer or reference; the object’s dynamic type decides.

•      Non-virtual member functions use the static type; a derived function with the same name only hides the base one.

•      Signature (including const) must match to override; use override (C++11). final stops further overriding.

•      Covariant returns are allowed for pointers/references to classes; default arguments are not overridden.

•      Copying a derived object into a base object slices it.

•      Implementation: vptr in the object, vtable per class.

 

14. Important Examination Questions

Short-answer questions

1.      Define polymorphism. Name its two types.

2.      What is a virtual function? Write its syntax.

3.      Why is a base-class pointer to a derived object useful?

4.      What happens if a function is not declared virtual and is called through a base pointer?

5.      Differentiate between overloading, overriding and hiding.

6.      What is the purpose of the keyword override? In which C++ version was it introduced?

7.      What is a covariant return type?

8.      What is object slicing?

9.      Explain the terms vptr and vtable.

 

Long-answer and programming questions

1.      Explain run-time polymorphism with a program using a base class Animal and derived classes Dog and Cat.

2.      Explain the rules for virtual functions.

3.      Write a program that shows the difference between a virtual and a non-virtual function called through a base pointer.

4.      Explain how virtual functions are implemented using a vtable.

5.      Write a program that demonstrates that a missing const prevents overriding.

 

 

 

No comments:

Post a Comment