- Understanding Polymorphism
Polymorphism means "having multiple forms." Simply put, it refers to the ability of different objects to respond to the same message or method call in different ways.
For example, in real life, when purchasing high-speed rail tickets, adult objects require full-price tickets, while student objects only need half-price tickets. In other words, two different objects executing the same action (buying tickets) produce different results.
Let's examine a code example before diving into the details:
class Passenger
{
public:
virtual void purchase_ticket()
{
cout << "adult -> full price" << endl;
}
};
class Student : public Passenger
{
public:
virtual void purchase_ticket()
{
cout << "student -> half price" << endl;
}
};
void process_ticket(Passenger* p)
{
p->purchase_ticket();
}
int main()
{
Passenger adult;
Student stu;
process_ticket(&adult);
process_ticket(&stu);
return 0;
}
Output:
adult -> full price
student -> half price
// We can see that the code above achieves "passing a base class pointer calls the base class function, passing a subclass pointer calls the subclass function"
// This implements polymorphism
- Implementing Polymorphism
To implement polymorphism, two conditions must be met: 1. The base class function and derived class function must form an override of a virtual function 2. The call must be through a polymorphic context
2.1 Virtual Function Overriding
2.1.1 Virtual Functions
A member function modified by the virtual keyword is a virtual function.
Several important points about virtual functions:
- Virtual functions can only be class member functions
- Static member functions don't have a this pointer, so they cannot be virtual functions
- A member function can be modified by both
inlineandvirtual, but it won't become an inline function (it loses the inline attribute)inlineis just a suggestion- Virtual functions' addresses are stored in the virtual table, but inline functions are expanded at compile time and don't have addresses >
- Constructors cannot be virtual functions
2.1.2 What is Overriding
When a virtual function in a base class and a virtual function in a derived class have the same return type, function name, and parameters, these two virtual functions form an override relationship.
Note: When the base class's virtual function and the derived class's virtual function have covariance, the return types can be different.
For example:
class Passenger
{
public:
virtual void purchase_ticket()
{
cout << "adult -> full price" << endl;
}
};
class Student : public Passenger
{
public:
virtual void purchase_ticket()
{
cout << "student -> half price" << endl;
}
};
The virtual function purchase_ticket() in base class Passenger and the virtual function purchase_ticket() in subclass Student have the same return type, function name, and parameters, thus forming an override relationship.
At the same time, we find that so-called overriding actually overrides the function implementation. Therefore, polymorphism's overriding is also called implementation overriding.
2.2 Polymorphic Calls
To achieve polymorphic calls, you must call virtual functions through base class pointers or references. Calls that don't meet this condition are regular calls.
For example:
class Passenger
{
public:
virtual void purchase_ticket()
{
cout << "adult -> full price" << endl;
}
};
class Student : public Passenger
{
public:
virtual void purchase_ticket()
{
cout << "student -> half price" << endl;
}
};
int main()
{
Passenger adult;
Student stu;
// Polymorphic call
cout << "Polymorphic call" << endl;
Passenger* ptr = nullptr;
ptr = &adult;
ptr->purchase_ticket();
ptr = &stu;
ptr->purchase_ticket();
// Regular call
cout << "Regular call" << endl;
adult.purchase_ticket();
adult = stu;
adult.purchase_ticket();
return 0;
}
Output:
Polymorphic call
adult -> full price
student -> half price
Regular call
adult -> full price
adult -> full price
From the above code results, we can summarize the differences between regular calls and polymorphic calls:
- Polymorphic calls can only be made through base class pointers or references to virtual functions. The result depends on the type of the object referenced or pointed to (pointing to the parent class calls the parent class virtual function, pointing to the child class calls the child class virtual function)
- Calls that don't satisfy polymorphic conditions are regular calls. Regular calls depend on the type of the calling object. If a parent class object/pointer/reference is used, you get the parent class slice and call the parent class function, and similarly for the child class
2.3 Special Cases of Polymorphism
2.3.1 Covariance
When a virtual function in a base class and a virtual function in a subclass form an override relationship, their return types can be different. However, the base class must return a base class pointer (reference) and the derived class must return a derived class pointer (reference). This is called covariance.
For example, the following two implementations are covariance:
// Base class returns base class pointer, derived class returns derived class pointer
class Passenger
{
public:
virtual Passenger* purchase_ticket()
{
cout << "adult -> full price" << endl;
return this;
}
};
class Student : public Passenger
{
public:
virtual Student* purchase_ticket()
{
cout << "student -> half price" << endl;
return this;
}
};
// Or base class returns base class reference, derived class returns derived class reference
class Passenger
{
public:
virtual Passenger& purchase_ticket()
{
cout << "adult -> full price" << endl;
return *this;
}
};
class Student : public Passenger
{
public:
virtual Student& purchase_ticket()
{
cout << "student -> half price" << endl;
return *this;
}
};
The following implementations are not covariance and are incorrect, causing compilation errors:
// Error example 1: Base class returns base class object, derived class returns derived class object
class Passenger
{
public:
virtual Passenger purchase_ticket()
{
cout << "adult -> full price" << endl;
return *this;
}
};
class Student : public Passenger
{
public:
virtual Student purchase_ticket()
{
cout << "student -> half price" << endl;
return *this;
}
};
// Error example 2: Base class returns base class reference, derived class returns derived class object
class Passenger
{
public:
virtual Passenger& purchase_ticket()
{
cout << "adult -> full price" << endl;
return *this;
}
};
class Student : public Passenger
{
public:
virtual Student purchase_ticket()
{
cout << "student -> half price" << endl;
return *this;
}
};
2.3.2 Virtual Keyword Modifiers
If a virtual function in a base class and a virtual function in a subclass form an override relationship, the virtual keyword in the subclass's virtual function can be omitted.
For example:
class Passenger
{
public:
virtual void purchase_ticket()
{
cout << "adult -> full price" << endl;
}
};
class Student : public Passenger
{
public:
void purchase_ticket()
{
cout << "student -> half price" << endl;
}
};
// The purchase_ticket() in base Passenger and purchase_ticket() in derived Student still form an override
2.3.3 Destructor Overriding
Let's look at the following code:
class Passenger
{
public:
~Passenger()
{
cout << "~Passenger()" << endl;
}
};
class Student : public Passenger
{
public:
~Student()
{
cout << "~Student()" << endl;
delete _ticket;
}
protected:
Ticket* _ticket;
};
int main()
{
Passenger* passenger = new Student;
delete passenger;
return 0;
}
Output:
~Passenger()
Here, we use a base class pointer pointing to a derived class object, then delete this pointer. We can see that the system only calls the base class destructor.
- This is understandable because
passengeractually points to the base class slice within the derived class, and this is a regular call, naturally calling the base class destructor - But this creates a problem—the derived class destructor is not called, so resources in the derived class cannot be cleaned up, leading to memory leaks
- To solve this problem, we should make this call polymorphic by making the base class and derived class destructors form an override relationship, i.e., making the destructor a virtual function with the
virtualkeyword
class Passenger
{
public:
virtual ~Passenger()
{
cout << "~Passenger()" << endl;
}
};
class Student : public Passenger
{
public:
~Student()
{
cout << "~Student()" << endl;
delete _ticket;
}
protected:
Ticket* _ticket;
};
int main()
{
Passenger* passenger = new Student;
delete passenger;
return 0;
}
Output:
~Student()
~Passenger()
Some might ask: *The base class and derived class destructors have different names, how do they form an override relationship?*
In fact, during the compilation phase, the compiler renames both the subclass and base class destructors to the same function name destructor(). This way, with the virtual keyword, they can successfully form an override relationship.
2.3.4 Final Keyword
Question: How to make a class non-inheritable?
One method—privatize the constructor of this class:
The derived class constructor calls the base class constructor. If the base class constructor is private, then the base class constructor is not visible in the derived class, and thus cannot be called.
class Base
{
protected:
int _data;
private:
Base()
{
_data = 1;
}
};
Another method—use the final keyword to modify this class.
A class modified by the final keyword is called a final class, which cannot be inherited.
class Base final
{
protected:
int _data = 1;
};
The final keyword can also modify virtual functions to indicate that the virtual function cannot be overridden.
class Base
{
public:
virtual void func() final {}
protected:
int _data = 1;
};
class Derived : public Base
{
public:
virtual void func()
{
cout << endl;
// Compilation error: "'Base::func': function declared as 'final' cannot be overridden by 'Derived::func'"
}
};
2.3.5 Override Keyword
The override keyword is used after a derived class virtual function to check whether the virtual function has completed the override.
class Base
{
public:
virtual void func() {}
protected:
int _data = 1;
};
class Derived : public Base
{
public:
virtual void func() override
{
cout << endl;
}
virtual void func1() override
{
// Compilation error: "'Derived::func1': a method marked 'override' does not override any base class method"
}
};
- Implementation Inheritance vs Interface Inheritance
3.1 Implementation Inheritance
Regular function inheritance is implementation inheritance. Implementation inheritance inherits the function implementation. After inheriting the base class function, the derived class can use it directly.
class Base
{
public:
void func()
{
cout << "hello world" << endl;
}
};
class Derived : public Base
{
};
int main()
{
Derived d;
d.func();
return 0;
}
3.2 Interface Inheritance
- Virtual function inheritance is interface inheritance, which inherits the interface of the base class function rather than the implementation
- The purpose of interface inheritance is to override (override the function implementation) to achieve polymorphism
- Therefore, if you don't implement polymorphism, don't set member functions as virtual functions
class Base
{
public:
virtual void func()
{
cout << "hello world" << endl;
}
};
class Derived : public Base
{
public:
virtual void func()
{
cout << "nice to meet you" << endl;
}
};
Next, let's use an example to better understand polymorphism's interface inheritance and implementation overriding:
class Base
{
public:
virtual void func(int val = 1)
{
std::cout << "Base->" << val << std::endl;
}
virtual void test()
{
func();
}
};
class Derived : public Base
{
public:
void func(int val = 0)
{
std::cout << "Derived->" << val << std::endl;
}
};
int main(int argc, char* argv[])
{
Derived* p = new Derived;
p->test();
return 0;
}
// What is the output?
Let's analyze:
- The derived class pointer p points to a derived class object and calls the function
test()through the pointer p - The function
test()is a function in base class Base, which nests the functionfunc() - Since the function
func()is in the scope of class Base, it's actually called like this:this->func(), where this pointer is the this pointer of class Base - Since the function
func()meets the virtual function override rules and is called through a base class pointer, this is a polymorphic call - It's important to note that all this happens in the derived class Derived, so the parent class pointer this actually points to the child class Derived, i.e., the function
func()is actually the derived class'sfunc() - Since virtual function inheritance is interface inheritance, the function
func()uses the interfacevoid func(int val = 1) - Overriding is implementation overriding, i.e., overridden to:
std::cout << "Derived->" << val << std::endl; - So the final output is
Derived->1
- Abstract Classes
4.1 Pure Virtual Functions
If you add = 0 after a virtual function, it becomes a pure virtual function:
class Vehicle
{
public:
virtual void display_name() = 0
{
cout << "vehicle" << endl;
}
};
4.2 Abstract Classes
A class with pure virtual functions is an abstract class. For example, the class Vehicle above is an abstract class.
- Abstract classes cannot instantiate objects
- Abstract classes force classes that inherit them to override pure virtual functions. Otherwise, the derived class still contains pure virtual functions and remains an abstract class, which cannot instantiate objects
class Vehicle
{
public:
virtual void display_name() = 0
{
cout << "vehicle" << endl;
}
};
class Car : public Vehicle
{
public:
virtual void display_name()
{
cout << "Car" << endl;
}
};
- Principles of Polymorphism
5.1 Virtual Function Table Pointer and Virtual Function Table
Let's think about the size of the following class:
class Data
{
public:
virtual void method1() {}
virtual void method2() {}
protected:
int _value = 1;
};
int main()
{
cout << sizeof(Data) << endl;
return 0;
}
Output:
8
This doesn't match our initial answer of 4.
The reason is that if a class has virtual functions, it will have an additional pointer _vfptr, called a virtual function table pointer.
We can debug to see this:
We can see that the virtual function table pointer _vfptr points to an area that stores the addresses of two virtual functions method1() and method2(). We call the area pointed to by _vfptr the virtual function table, also abbreviated as vtable. We can also see that the vtable is actually an array of function pointers, storing the addresses of virtual functions. In VS2019, the virtual function table ends with NULL.
So where is the vtable stored? Is it in the stack area, heap area, or constant area? We can use a comparison method to deduce:
class Data
{
public:
virtual void method1() {}
virtual void method2() {}
protected:
int _value;
};
int main()
{
Data d;
int num = 1;
int* ptr = new int[3];
const char* str = "xxxxx";
static char ch = 'a';
printf("Stack: %p\n", &num);
printf("Heap: %p\n", ptr);
printf("Static: %p\n", &ch);
printf("Constant: %p\n", str);
printf("VTable: %p\n", *(int*)&d);
return 0;
}
Output:
Stack: 00AFFC9C
Heap: 00E6B7F0
Static: 00D2A000
Constant: 00D27B40
VTable: 00D27B34
Through comparison, we can see that in VS2019, the vtable should be stored in the constant area.
Note: Some people might not understand the code *(int*)&d. Let's analyze it:
- To know which area the vtable is stored in, we need to know the address of the vtable
- In VS2019, the virtual function table pointer is stored at the beginning of the class. So we just need to get the address of the object, convert it to a 4-byte pointer
int*, and this way we get the address of the virtual function table pointer - Finally, by dereferencing the address of the virtual function table pointer, we get the virtual function table pointer, which is the address where the vtable is stored
At the same time, we need to know that the vtable is created at compile time, while the virtual function table pointer is initialized in the constructor.
Different objects instantiated from the same class share one vtable:
class Data
{
public:
virtual void method1() {}
protected:
int _value;
};
int main()
{
Data d1, d2, d3;
printf("%p\n", *(int*)&d1);
printf("%p\n", *(int*)&d2);
printf("%p\n", *(int*)&d3);
return 0;
}
Output:
00307B34
00307B34
00307B34
5.2 Single Inheritance and Multiple Inheritance VTables
5.2.1 Single Inheritance
Let's look at the following code:
class Base
{
public:
virtual void method1() {}
virtual void method2() {}
protected:
int _base_value = 1;
};
class Derived : public Base
{
public:
protected:
int _derived_value = 2;
};
int main()
{
Derived d;
return 0;
}
We can see that the derived class Derived inherits the vtable of the base class Base and doesn't create a separate vtable.
Now let's look at the following code:
/*
The derived class Derived overrides the base class Base's virtual function method1()
and adds a new virtual function method3()
Then defines a base class Base object and a derived class Derived object
*/
class Base
{
public:
virtual void method1() {}
virtual void method2() {}
protected:
int _base_value = 1;
};
class Derived : public Base
{
public:
virtual void method1() {}
virtual void method3() {}
protected:
int _derived_value = 2;
};
int main()
{
Base b;
Derived d;
return 0;
}
We can observe several phenomena:
- The vtable of the base class object and the derived class object are different
- The derived class Derived did not override the base class Base's virtual function
method2(), so in both classes' vtables, the address of functionmethod2()is the same. This means the base class and derived class use the samemethod2() - The derived class Derived overrode the base class Base's virtual function
method1(), so the address of the derived class's newmethod1()overwrites the original base classmethod1()address in the vtable. Therefore, overriding is also called overriding in terms of underlying principles. Overriding is reflected in implementation, while overriding is reflected in the underlying principles
But there's something strange: *The derived class Derived clearly added a new virtual function method3(), why doesn't it appear in the vtable of d in the watch window?*
Infact, the virtual function method3() is indeeed added to d's vtable, but due to some special reasons, the VS watch window doesn't display it. We can view it using the memory window:
We can summarize:
- Subclasses inherit the parent class's vtable
- Different classes have different vtables
- If the subclass's virtual function overrides the parent class's virtual function, then the address of the subclass's virtual function will overwrite the address of the parent class's virtual function in the vtable. Therefore, overriding is also called overriding
- If the subclass adds new virtual functions, their addresses are also added to the end of the vtable
5.2.2 Multiple Inheritance
Let's look at the size of the following derived class:
class BaseA
{
public:
virtual void method1() {}
protected:
int _a = 1;
};
class BaseB
{
public:
virtual void method2() {}
protected:
int _b = 2;
};
class Derived : public BaseA, public BaseB
{
public:
virtual void method3() {}
protected:
int _c = 3;
};
int main()
{
cout << sizeof(Derived) << endl;
return 0;
}
Output:
20
We can infer: The derived class Derived inherits two base classes BaseA and BaseB, containing 3 integer data, which is 12 bytes. The remaining 8 bytes should be the vtable pointers of class BaseA and BaseB.
That is, in multiple inheritance, the derived class inherits the vtables of the base classes.
In multiple inheritance, we face such a problem: For example, if the derived class Derived adds a new virtual function method3(), is this virtual function stored in class A's vtable, class B's vtable, or both vtables?
We can view it using the memory window:
We can see that method3() is placed in the vtable of the first inherited class BaseA.
We can summarize:
- If a derived class inherits multiple base classes, it will also inherit these base classes' vtables
- If the derived class adds new virtual functions, the address of this virtual function will be placed in the first inherited vtable
5.3 Principles of Polymorphism Implementation
Let's use a simple polymorphic call example to explain the implementation principles of polymorphism:
class Base
{
public:
virtual void method1()
{
cout << "hello\n";
}
protected:
int _base_value = 1;
};
class Derived : public Base
{
public:
virtual void method1()
{
cout << "world\n";
}
protected:
int _derived_value = 2;
};
int main()
{
Base* ptr = new Derived;
ptr->method1();
return 0;
}
- We use a base class pointer
ptrpointing to a derived class object and call the overridden virtual functionmethod1(), thus forming a polymorphic call - From our knowledge of inheritance, we know that at this time
ptrpoints to the base class slice within the derived class, so we can find the vtable throughmethod1()'s this pointer - Finally, we can achieve polymorphic calls by finding the address of the virtual function
method1()through the vtable
From this, we also know why static member functions cannot be virtual functions or implement polymorphic calls.
- This is because static member functions don't have a this pointer, so they cannot find the vtable through the this pointer, and thus cannot find the corresponding virtual function's address for calling
At the same time, we can better explain why constructors cannot be virtual functions.
- We said earlier that the vtable pointer is initialized during construction, while calling virtual functions requires the vtable pointer
- Then if the constructor is a virtual function, the vtable pointer is not initialized when calling the constructor, so how can we call the virtual function?
5.4 Static Polymorphism vs Dynamic Polymorphism
5.4.1 Static Polymorphism
Static polymorphism is also called static binding, where the program's behavior is determined at compile time, making it compile-time.
Function overloading is a typical example of static polymorphism. It implements calling functions with the same name but different functions through function name decoration rules. Because function overloading requires that function parameters must be different, the compiler can determine which function to call through function parameters at compile time.
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
In addition, templates are also a form of static polymorphism, where specific classes or functions are instantiated at compile time based on the passed-in types.
template<class t="">
T add(T a, T b)
{
return a + b;
}</class>
5.4.2 Dynamic Polymorphism
Dynamic polymorphism is also called dynamic binding, where the program's behavior is determined at runtime, making it runtime.
Polymorphism is dynamic polymorphism because virtual function overriding requires that functions have the same return type, parameters, and names. The compiler cannot determine which function to call, so it can only check the virtual function's address through the vtable at runtime to make the call.