
When you write Base* ptr = new Derived(); ptr->func(); in your code, have you ever stopped to think: how does the compiler know to execute the func() of the Derived class instead of the Base class? Clearly, the pointer type is Base, yet it can accurately find the implementation in the derived class — this is the most fascinating “magic” of C++ polymorphism. This magic relies on a carefully designed underlying mechanism that allows the program to “see” the true type of the object at runtime and find the function to call. The core of this is what we commonly refer to as the virtual function table and dynamic binding. Next, we will peel back the layers: from how the compiler “secretly” prepares data structures for classes with virtual functions, to how hidden pointers in objects navigate to the correct function implementations, and see how polymorphism allows C++ code to maintain abstraction while flexibly responding to different scenarios.
1. What is Polymorphism?
1.1 Overview of C++ Polymorphism
In daily life, we often encounter a phenomenon where the same behavior exhibits different performances on different objects. For example, the behavior of “driving” can be exhilarating when a race car driver is behind the wheel, while a novice driver in a family car may exhibit more caution and care. In the world of C++ programming, there is a similar concept known as polymorphism.
By definition, polymorphism refers to the ability of the same behavior to have multiple different forms or manifestations. In C++, polymorphism is primarily achieved through virtual functions. Simply put, when a pointer or reference of a base class points to different derived class objects, calling the same virtual function will exhibit different behaviors, which is the charm of polymorphism. For instance, if there is an animal class with a “speak” function, and both dog and cat classes inherit from the animal class and override the “speak” function, when a pointer of the animal class points to objects of the dog and cat classes respectively, calling the “speak” function will result in hearing a dog bark and a cat meow.
In C++, polymorphism can be further divided into static polymorphism and dynamic polymorphism. Static polymorphism is mainly achieved through function overloading and templates, which determine the function version to call at compile time; while dynamic polymorphism is based on virtual functions, determining which function to call at runtime based on the actual type of the object, which is the focus of our subsequent discussions.
Generally speaking, polymorphism is divided into two types: static polymorphism and dynamic polymorphism. Static polymorphism, also known as compile-time polymorphism, mainly includes templates and overloading. Dynamic polymorphism is achieved through class inheritance and virtual functions. When both the base class and derived class have methods with the same name, parameters, and return types, and the method is declared as virtual, when a base class object, pointer, or reference points to a derived class object, calling the base class’s virtual function actually invokes the derived class’s function. This is dynamic polymorphism.
(1) Implementation of Static Polymorphism
Static polymorphism is implemented by the compiler, which essentially modifies the original function names. In C, functions cannot be overloaded because the C compiler simply adds an underscore “_” before the function name when modifying it. However, after compiling with the GCC compiler, it is found that the function names do not change. In contrast, the C++ compiler modifies function names based on the types and number of function parameters, allowing function overloading. Similarly, templates can also be implemented to generate corresponding specialized functions for different types of actual parameters, distinguishing functions with different type parameters through added modifications. The following code segment serves as an example:
#include <iostream>
using namespace std;
template <typename T1, typename T2>
int fun(T1 t1, T2 t2){}
int foofun(){}
int foofun(int){}
int foofun(int, float){}
int foofun(int, float, double){}
int main(int argc, char *argv[])
{
fun(1, 2);
fun(1, 1.1);
foofun();
foofun(1);
foofun(1, 1.1);
foofun(1, 1.1, 1.11);
return 0;
}
(2) Implementation of Dynamic Polymorphism
When declaring a class with virtual methods, a virtual function pointer is automatically added to the class, which points to a virtual function table that stores the actual addresses of each virtual function. Dynamic polymorphism employs a technique called late binding. In ordinary function calls, the address of the function to be called is determined at compile time, so no matter how it is called, it is always that function. However, for classes with virtual functions, when calling a virtual function, the program first checks the virtual function table to determine which function to call, meaning the function being called is only determined at runtime.
When declaring a base class object, if the base class has virtual functions, a virtual function pointer is automatically generated, pointing to the corresponding virtual function table of the base class. When declaring a derived class object, the virtual function pointer points to the corresponding virtual function table of the derived class. After the object is created (for example, using a pointer), regardless of whether the base class pointer or derived class pointer points to this object, the virtual function table will not change, nor will the pointer to the virtual table.
The following code segment serves as an example:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun()
{
cout << "this is base fun" << endl;
}
};
class Derived : public Base
{
public:
void fun()
{
cout << "this is Derived fun" << endl;
}
};
int main(int argc, char *argv[])
{
Base b1;
Derived d1;
Base *pb = &d1
Derived *pd = (Derived *)&b1
b1.fun();
pd->fun();
d1.fun();
pb->fun();
return 0;
}
1.2 How Polymorphism Solves Code Reusability Challenges
In software development, code reuse is key to improving development efficiency and reducing maintenance costs. However, without polymorphism, achieving code reuse often faces many challenges. For example, if we are developing a graphics drawing system that includes various shapes such as circles, rectangles, and triangles, without using polymorphism, we may need to write a lot of repetitive code to draw these different shapes.
class Circle {
public:
void drawCircle() {
// Code to draw a circle
std::cout << "Drawing a circle" << std::endl;
}
};
class Rectangle {
public:
void drawRectangle() {
// Code to draw a rectangle
std::cout << "Drawing a rectangle" << std::endl;
}
};
class Triangle {
public:
void drawTriangle() {
// Code to draw a triangle
std::cout << "Drawing a triangle" << std::endl;
}
};
int main() {
Circle circle;
Rectangle rectangle;
Triangle triangle;
circle.drawCircle();
rectangle.drawRectangle();
triangle.drawTriangle();
return 0;
}
In this code, each shape class has its own independent drawing function. When we need to draw different shapes, we must call different functions separately. If we later want to add a new shape, such as a trapezoid, we need to write a new drawing function again and add additional calling logic, resulting in poor extensibility and reusability of the code.
However, when we introduce polymorphism, the situation changes significantly. We can define a base class, such as Shape, declare a virtual function draw within it, and let each shape class inherit from the Shape class and override the draw function.
class Shape {
public:
virtual void draw() = 0; // Pure virtual function, making Shape an abstract class
};
class Circle : public Shape {
public:
void draw() override {
// Code to draw a circle
std::cout << "Drawing a circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
// Code to draw a rectangle
std::cout << "Drawing a rectangle" << std::endl;
}
};
class Triangle : public Shape {
public:
void draw() override {
// Code to draw a triangle
std::cout << "Drawing a triangle" << std::endl;
}
};
void drawShapes(Shape* shapes[], int count) {
for (int i = 0; i < count; ++i) {
shapes[i]->draw();
}
}
int main() {
Circle circle;
Rectangle rectangle;
Triangle triangle;
Shape* shapes[] = {&circle, &rectangle, ▵};
int count = sizeof(shapes) / sizeof(shapes[0]);
drawShapes(shapes, count);
return 0;
}
In this improved code, the drawShapes function can accept an array of pointers of type Shape. Regardless of whether the elements in the array point to Circle, Rectangle, or Triangle objects, the correct drawing can be achieved by calling the draw function. Thus, when we need to add a new shape, we only need to create a new derived class and override the draw function, while the code of the drawShapes function does not need to be modified, greatly enhancing the reusability and extensibility of the code.
1.3 Polymorphism Makes Code Extension Easier
In the process of software development, we often face the challenge of constantly changing requirements and ongoing feature expansions. A good program design should be able to easily cope with these changes, and polymorphism plays a crucial role in this, making code extension much easier.
For example, in game development, suppose we are developing a role-playing game with different types of characters, such as warriors, mages, and assassins. Each character has its own unique attack and movement methods.
If we do not use polymorphism, we might write independent classes for each character, each containing its own attack and movement methods. When we need to add a new character type, such as a priest, we would need to modify the code in multiple places. Not only would we need to create a new priest class and write its unique skill methods, but we might also need to add a lot of conditional statements in the logic that handles character behavior to accommodate the priest’s actions. For example:
class Warrior {
public:
void attackWarrior() {
std::cout << "Warrior attacks with a sword" << std::endl;
}
void moveWarrior() {
std::cout << "Warrior moves quickly" << std::endl;
}
};
class Mage {
public:
void attackMage() {
std::cout << "Mage casts a spell" << std::endl;
}
void moveMage() {
std::cout << "Mage moves slowly" << std::endl;
}
};
class Assassin {
public:
void attackAssassin() {
std::cout << "Assassin attacks with a dagger" << std::endl;
}
void moveAssassin() {
std::cout << "Assassin moves stealthily" << std::endl;
}
};
void handleCharacterAction() {
// Assume there is a variable representing character type
int characterType = 1; // 1 represents warrior, 2 represents mage, 3 represents assassin
Warrior warrior;
Mage mage;
Assassin assassin;
if (characterType == 1) {
warrior.attackWarrior();
warrior.moveWarrior();
}
else if (characterType == 2) {
mage.attackMage();
mage.moveMage();
}
else if (characterType == 3) {
assassin.attackAssassin();
assassin.moveAssassin();
}
}
As can be seen, this approach results in lengthy code that is very difficult to maintain. Each time a new character type is added, a lot of if-else statements need to be added to the handleCharacterAction function, which makes the code less readable and maintainable.
By utilizing the features of polymorphism, we can define a base class Character, declare virtual functions attack and move within it, and let the warrior, mage, and assassin classes inherit from the Character class and override these virtual functions. Thus, when we need to add a new character type, we only need to create a new derived class and override the corresponding virtual functions without modifying the existing core code. For example:
class Character {
public:
virtual void attack() = 0;
virtual void move() = 0;
};
class Warrior : public Character {
public:
void attack() override {
std::cout << "Warrior attacks with a sword" << std::endl;
}
void move() override {
std::cout << "Warrior moves quickly" << std::endl;
}
};
class Mage : public Character {
public:
void attack() override {
std::cout << "Mage casts a spell" << std::endl;
}
void move() override {
std::cout << "Mage moves slowly" << std::endl;
}
};
class Assassin : public Character {
public:
void attack() override {
std::cout << "Assassin attacks with a dagger" << std::endl;
}
void move() override {
std::cout << "Assassin moves stealthily" << std::endl;
}
};
void handleCharacterAction(Character* character) {
character->attack();
character->move();
}
int main() {
Warrior warrior;
Mage mage;
Assassin assassin;
handleCharacterAction(&warrior);
handleCharacterAction(&mage);
handleCharacterAction(&assassin);
return 0;
}
In this improved code, the handleCharacterAction function only needs to accept a pointer of type Character. Regardless of whether the pointer passed in is for a warrior, mage, or assassin object, the corresponding attack and movement methods can be correctly called. When we want to add a priest character, we only need to create a Priest class that inherits from the Character class and overrides the attack and move methods, and then we can directly use the handleCharacterAction function to handle the priest character’s behavior without modifying the code of the handleCharacterAction function.
1.4 Implementation Principles of Polymorphism
The main ways C++ implements polymorphism are:
(1) Overloading: Implementing different behaviors through multiple functions with the same name but different parameters. The function to call is determined at compile time based on parameter types.
void add(int a, int b) { ... }
void add(double a, double b) { ... }
(2) Overriding: Allowing derived classes to reimplement the base class’s virtual functions through inheritance. The corresponding function is called at runtime based on the actual type of the pointer/reference.
class Base {
public:
virtual void func() { ... }
};
class Derived : public Base {
public:
virtual void func() { ... }
};
Base* b = new Derived();
b->func(); // Calls Derived::func()
(3) Compile-time Polymorphism: Achieving functions with different implementations for different types through templates and generics. The specific implementation is determined at compile time based on the types passed in.
template <typename T>
void func(T t) { ... }
func(1); // Calls func<int>
func(3.2); // Calls func<double>
(4) Conditional Compilation: Using preprocessor commands like #ifdef/#elif to compile different code implementations based on different conditions. The specific implementation is determined at compile time based on defined macros.
#ifdef _WIN32
void func() { ... } // Windows version
#elif __linux__
void func() { ... } // Linux version
#endif
In summary, C++ achieves polymorphism through overloading, overriding, templates, conditional compilation, and other means. Among them, overriding based on inheritance and virtual functions implements true runtime polymorphism, enhancing the flexibility and extensibility of programs.
One Interface, Multiple Methods:
- Functions declared with the
virtualkeyword are called virtual functions, and virtual functions must be member functions of a class. - Classes with virtual functions have a one-dimensional virtual function table called a vtable. When a class declares a virtual function, the compiler generates a virtual function table within the class.
- Objects of the class have a pointer to the start of the virtual table. The virtual table corresponds to the class, while the virtual table pointer corresponds to the object.
- The virtual function table is a data structure that stores pointers to class member functions.
- The virtual function table is automatically generated and maintained by the compiler.
- Virtual member functions are placed in the virtual function table by the compiler.
- When there are virtual functions, each object has a pointer to the virtual function (the C++ compiler pre-allocates the vptr pointer for both parent and child class objects). When executing
test(parent *base), the C++ compiler does not need to distinguish between child or parent class objects; it only needs to find the vptr pointer in the base pointer. - The vptr is generally the first member of the class object.
2. What is a Virtual Function Table?
2.1 What is a Virtual Function Table
The virtual function table, abbreviated as vtable, is an array generated by the compiler during the compilation phase for classes containing virtual functions, storing the addresses of virtual functions. It is a key mechanism for implementing polymorphism in C++. The virtual function table can be thought of as a “function address directory”; in this special “directory”, each entry records the entry address of the corresponding virtual function in memory. When a virtual function needs to be called during program execution, this “directory” can quickly locate the specific position of the function, allowing the function code to be executed smoothly.
For example, in a game development scenario, we define a base class Character that contains a virtual function Attack:
class Character {
public:
virtual void Attack() {
std::cout << "Character attacks in a general way." << std::endl;
}
};
Then, we derive subclasses Warrior and Mage, each overriding the Attack function to implement their unique attack methods:
class Warrior : public Character {
public:
void Attack() override {
std::cout << "Warrior attacks with a sword!" << std::endl;
}
};
class Mage : public Character {
public:
void Attack() override {
std::cout << "Mage casts a fireball!" << std::endl;
}
};
In this example, the compiler generates a separate virtual function table for the Character, Warrior, and Mage classes. The virtual function table of the Character class contains the address of the Attack function pointing to the implementation code in the base class; the virtual function table of the Warrior class, having overridden the Attack function, points to the implementation code of the overridden Attack function in the Warrior class, and similarly for the Mage class. Thus, at runtime, the program can accurately find and call the corresponding attack function based on the actual type of the object through the virtual function table.
2.2 Why is a Virtual Function Table Needed?
In C++, the virtual function table plays a crucial role in implementing runtime polymorphism. When using a base class pointer or reference to point to different derived class objects, the program needs to determine which version of the virtual function to call at runtime based on the actual type of the object, and the virtual function table is the core of this dynamic binding process.
Suppose there is no virtual function table. When a base class pointer points to a derived class object and calls an overridden function, the compiler can only determine which version of the base class function to call based on the static type of the pointer (i.e., the base class type), and cannot achieve the polymorphic effect of calling the corresponding function based on the actual type of the object. For example, in the previous game character example, if there is no virtual function table, when Character* ptr = new Warrior(); is executed, calling ptr->Attack() would always invoke the Attack function of the Character class instead of the overridden attack function in the Warrior class, which clearly does not meet the diverse needs of different characters having different attack methods in the game.
With the virtual function table, when calling a virtual function through a base class pointer or reference, the program first finds the corresponding virtual function table based on the virtual pointer (vptr) stored in the object’s memory (each object containing virtual functions has a virtual pointer pointing to its corresponding class’s virtual function table, usually located at the front of the object’s memory layout), and then finds the actual function address to call based on the function index in the virtual function table, ultimately invoking that function.
Thus, regardless of which derived class object the base class pointer points to, it can accurately call the overridden version of the virtual function in the derived class, achieving runtime polymorphism. The virtual function table acts like an intelligent “navigator” that guides the program in the complex inheritance system to correctly call functions, allowing C++ polymorphism to be perfectly presented, greatly enhancing the flexibility, extensibility, and maintainability of the code.
2.3 Memory Layout
(1) Virtual Function Table Pointer in Object Memory Layout
In C++, when a class contains virtual functions, the memory layout of the class’s objects will have a special member — the virtual function table pointer (vptr). This pointer acts like a “key” pointing to the virtual function table and is a crucial link for implementing polymorphism.
In most compiler implementations, the virtual function table pointer is usually located at the start of the object’s memory. For example, in a 32-bit compiler, the pointer occupies 4 bytes of memory; in a 64-bit compiler, the pointer occupies 8 bytes. Suppose we have the following simple class definition:
class Animal {
public:
virtual void Speak() {
std::cout << "Animal makes a sound." << std::endl;
}
int m_age;
};
When creating an object of the Animal class, such as Animal dog;, in memory, the first 4 bytes (in a 32-bit compiler) or the first 8 bytes (in a 64-bit compiler) of the dog object will be the virtual function table pointer. We can verify this with the following code:
#include <iostream>
class Animal {
public:
virtual void Speak() {
std::cout << "Animal makes a sound." << std::endl;
}
int m_age;
};
int main() {
Animal dog;
dog.m_age = 5;
// Get the address of the object and convert it to an integer pointer to read data in memory
int* ptr = reinterpret_cast<int*>(&dog);
// Read the first 4 bytes of the object's memory, which is the value of the virtual function table pointer
int vptr_value = *ptr;
std::cout << "The value of vptr in the dog object: " << std::hex << vptr_value << std::endl;
return 0;
}
In this code, reinterpret_cast<int*>(&dog) converts the address of the dog object to an integer pointer, allowing us to read data in the object’s memory through pointer operations. The *ptr reads the first 4 bytes of the object’s memory, which is the value of the virtual function table pointer. By outputting this value, we can intuitively see the position of the virtual function table pointer in the object’s memory and the address it points to in the virtual function table.
(2) The Location of the Virtual Function Table in Memory
The location of the virtual function table in memory is also a key knowledge point. Typically, the virtual function table is located in the read-only data segment (.rodata), which is the constant area in the C++ memory model. This is because the contents of the virtual function table do not change during program execution, placing it in the read-only data segment ensures the safety and stability of the data, preventing accidental modifications to the virtual function table contents that could lead to runtime errors.
To verify this conclusion, let’s look at the following code example:
#include <iostream>
class Base {
public:
virtual void Func1() {
std::cout << "Base::Func1" << std::endl;
}
virtual void Func2() {
std::cout << "Base::Func2" << std::endl;
}
};
int main() {
Base obj;
// Get the virtual function table pointer of the object
int* vptr = reinterpret_cast<int*>(&obj);
// Get the address of the virtual function table through the virtual function table pointer
int vtable_address = *vptr;
std::cout << "The address of the virtual function table: " << std::hex << vtable_address << std::endl;
return 0;
}
After compiling and running this code, we obtain the address of the virtual function table. Next, we can use tools (such as the objdump -s command in Linux to parse the segment information in the ELF format executable file) to check which memory segment this address belongs to. Suppose the virtual function table address obtained after running the program is 0x400b40, we can execute objdump -s your_executable_file (where your_executable_file is the name of the generated executable file) in the terminal and look for the memory segment containing 0x400b40. Typically, we will find that this address is located in the .rodata segment, which verifies the conclusion that the virtual function table is located in the read-only data segment.
2.4 Dynamic Changes of the Virtual Function Table
(1) Single Inheritance without Overriding
In the case of single inheritance where the subclass does not override the parent class’s virtual functions, the structure of the subclass’s virtual function table is relatively straightforward. Let’s look at the following code example:
class Base {
public:
virtual void Func1() {
std::cout << "Base::Func1" << std::endl;
}
virtual void Func2() {
std::cout << "Base::Func2" << std::endl;
}
};
class Derived : public Base {
public:
virtual void Func3() {
std::cout << "Derived::Func3" << std::endl;
}
virtual void Func4() {
std::cout << "Derived::Func4" << std::endl;
}
};
In this example, the Base class contains two virtual functions Func1 and Func2, and the Derived class inherits from the Base class and adds two new virtual functions Func3 and Func4. At this point, the virtual function table of the Derived class will first list the addresses of the parent class’s virtual functions Func1 and Func2 in order of declaration, followed by the addresses of the newly added virtual functions Func3 and Func4.
We can verify this structure through some techniques. In a 32-bit system, suppose the starting memory address of the Derived class object is 0x1000. Since the virtual function table pointer (vptr) is usually located at the start of the object’s memory and occupies 4 bytes, we can obtain the virtual function table address through *(int*)0x1000, assuming it is 0x2000.
The virtual function table is an array storing pointers to virtual functions, with each pointer occupying 4 bytes. Therefore, *(int*)0x2000 is the function address of Func1, *(int*)(0x2000 + 4) is the function address of Func2, *(int*)(0x2000 + 8) is the function address of Func3, and *(int*)(0x2000 + 12) is the function address of Func4. This way, we can clearly see the order of the parent class’s virtual functions and the newly added virtual functions in the subclass’s virtual function table.
(2) Single Inheritance with Overriding
When the subclass overrides the parent class’s virtual function, the virtual function table undergoes significant changes. Continuing with the previous code, suppose the Derived class overrides the Func1 function of the Base class:
class Base {
public:
virtual void Func1() {
std::cout << "Base::Func1" << std::endl;
}
virtual void Func2() {
std::cout << "Base::Func2" << std::endl;
}
};
class Derived : public Base {
public:
virtual void Func1() {
std::cout << "Derived::Func1" << std::endl;
}
virtual void Func3() {
std::cout << "Derived::Func3" << std::endl;
}
virtual void Func4() {
std::cout << "Derived::Func4" << std::endl;
}
};
In this case, the virtual function table of the Derived class will replace the address that originally pointed to Base::Func1 with the address of Derived::Func1. The address of Func2 remains unchanged since it has not been overridden. The newly added virtual functions Func3 and Func4 are still added to the end of the table.
Again, in a 32-bit system, suppose the starting memory address of the Derived class object is 0x1000 and the virtual function table address is 0x2000. At this point, *(int*)0x2000 points to the function address of Derived::Func1, *(int*)(0x2000 + 4) is the function address of Base::Func2, *(int*)(0x2000 + 8) is the function address of Derived::Func3, and *(int*)(0x2000 + 12) is the function address of Derived::Func4. This overriding mechanism ensures that when calling virtual functions through base class pointers or references, the overridden function in the subclass can be accurately invoked, achieving polymorphism.
(3) Multiple Inheritance
In multiple inheritance, the structure of the virtual function table becomes more complex. Suppose we have the following code:
class Base1 {
public:
virtual void Func1() {
std::cout << "Base1::Func1" << std::endl;
}
virtual void Func2() {
std::cout << "Base1::Func2" << std::endl;
}
};
class Base2 {
public:
virtual void Func3() {
std::cout << "Base2::Func3" << std::endl;
}
virtual void Func4() {
std::cout << "Base2::Func4" << std::endl;
}
};
class Derived : public Base1, public Base2 {
public:
virtual void Func1() {
std::cout << "Derived::Func1" << std::endl;
}
virtual void Func5() {
std::cout << "Derived::Func5" << std::endl;
}
};
In multiple inheritance, the Derived class will have two virtual function tables, one corresponding to Base1 and the other to Base2. In the memory layout of the Derived class object, the pointer corresponding to Base1 will be first, followed by any other members of the Base1 class (if any), then the pointer corresponding to Base2, followed by any other members of the Base2 class (if any), and finally the members of the Derived class itself.
For the virtual function table corresponding to Base1, the address of Func1 will be replaced with the address of Derived::Func1 (since the Derived class overrides Func1), while the address of Func2 remains as Base1::Func2 since it has not been overridden. The addresses of Func3 and Func4 in the virtual function table corresponding to Base2 will be the addresses of Base2::Func3 and Base2::Func4 since the Derived class has not overridden these two functions.
Assuming in a 64-bit system, the starting memory address of the Derived class object is 0x1000:
The first virtual function table pointer (corresponding to Base1) is located at 0x1000. By using *(int*)0x1000, we can obtain its virtual function table address, assuming it is 0x2000. In this virtual function table, *(int*)0x2000 is the function address of Derived::Func1, *(int*)(0x2000 + 8) is the function address of Base1::Func2, and *(int*)(0x2000 + 16) is the function address of Derived::Func5.
The second virtual function table pointer (corresponding to Base2) is located at 0x1008 (since pointers occupy 8 bytes in a 64-bit system). By using *(int*)0x1008, we can obtain its virtual function table address, assuming it is 0x3000. In this virtual function table, *(int*)0x3000 is the function address of Base2::Func3, and *(int*)(0x3000 + 8) is the function address of Base2::Func4. This complex structure makes multiple inheritance powerful but also increases the difficulty of understanding and maintaining it.
3. Practical Case Analysis
3.1 Application of Polymorphism in Design Patterns
Polymorphism, as one of the core features of object-oriented programming, plays a crucial role in various design patterns. It provides more flexible and powerful solutions for design patterns, making the structure of software systems clearer and more maintainable. Below, we will explore the specific applications of polymorphism in the Strategy Pattern and Factory Method Pattern.
(1) Application of Polymorphism in the Strategy Pattern
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each algorithm, and makes them interchangeable. The core of the Strategy Pattern is to separate the selection and use of algorithms from the specific implementation of the algorithms, and polymorphism is the key to achieving this separation.
For example, in a simple calculator program, we can use the Strategy Pattern and polymorphism to implement different calculation logic. First, define an abstract operation strategy interface that contains a method for performing calculations:
class OperationStrategy {
public:
virtual double execute(double num1, double num2) = 0;
};
Then, create specific operation strategy classes, such as addition strategy class, subtraction strategy class, multiplication strategy class, and division strategy class, all inheriting from the OperationStrategy interface and implementing the execute method:
class AddStrategy : public OperationStrategy {
public:
double execute(double num1, double num2) override {
return num1 + num2;
}
};
class SubtractStrategy : public OperationStrategy {
public:
double execute(double num1, double num2) override {
return num1 - num2;
}
};
class MultiplyStrategy : public OperationStrategy {
public:
double execute(double num1, double num2) override {
return num1 * num2;
}
};
class DivideStrategy : public OperationStrategy {
public:
double execute(double num1, double num2) override {
if (num2 != 0) {
return num1 / num2;
}
// Here, an exception can be thrown or a special value can be returned to indicate an error
return 0;
}
};
Next, define a calculator class that holds a pointer to an OperationStrategy and calls the specific operation strategy through that pointer:
class Calculator {
private:
OperationStrategy* strategy;
public:
Calculator(OperationStrategy* s) : strategy(s) {}
double calculate(double num1, double num2) {
return strategy->execute(num1, num2);
}
};
In the client code, we can choose different operation strategies as needed and pass them to the calculator object to achieve different calculations:
int main() {
OperationStrategy* addStrategy = new AddStrategy();
Calculator addCalculator(addStrategy);
double result1 = addCalculator.calculate(5, 3);
std::cout << "5 + 3 = " << result1 << std::endl;
OperationStrategy* subtractStrategy = new SubtractStrategy();
Calculator subtractCalculator(subtractStrategy);
double result2 = subtractCalculator.calculate(5, 3);
std::cout << "5 - 3 = " << result2 << std::endl;
OperationStrategy* multiplyStrategy = new MultiplyStrategy();
Calculator multiplyCalculator(multiplyStrategy);
double result3 = multiplyCalculator.calculate(5, 3);
std::cout << "5 * 3 = " << result3 << std::endl;
OperationStrategy* divideStrategy = new DivideStrategy();
Calculator divideCalculator(divideStrategy);
double result4 = divideCalculator.calculate(5, 3);
std::cout << "5 / 3 = " << result4 << std::endl;
// Free memory
delete addStrategy;
delete subtractStrategy;
delete multiplyStrategy;
delete divideStrategy;
return 0;
}
In this example, polymorphism allows us to dynamically select different operation strategies at runtime without modifying the code of the calculator class. If we later need to add a new operation, such as exponentiation, we only need to create a new strategy class and implement the execute method, and then use the new strategy class in the client code, greatly enhancing the flexibility and extensibility of the system.
(2) Application of Polymorphism in the Factory Method Pattern
The Factory Method Pattern is a creational design pattern that defines an interface for creating objects, but allows subclasses to decide which class to instantiate. The Factory Method Pattern separates the creation and use of objects, making the code more flexible and maintainable, and polymorphism plays a crucial role in this.
Suppose we are developing a game with different types of characters, such as warriors, mages, and assassins. We can use the Factory Method Pattern and polymorphism to create these characters. First, define an abstract character class as the base class for all specific character classes:
class Character {
public:
virtual void display() = 0;
};
Then, create specific character classes, such as warrior class, mage class, and assassin class, all inheriting from the Character class and implementing the display method:
class Warrior : public Character {
public:
void display() override {
std::cout << "This is a warrior" << std::endl;
}
};
class Mage : public Character {
public:
void display() override {
std::cout << "This is a mage" << std::endl;
}
};
class Assassin : public Character {
public:
void display() override {
std::cout << "This is an assassin" << std::endl;
}
};
Next, define an abstract character factory class that contains a pure virtual factory method for creating character objects:
class CharacterFactory {
public:
virtual Character* createCharacter() = 0;
};
Then, create specific character factory classes, such as warrior factory class, mage factory class, and assassin factory class, all inheriting from the CharacterFactory class and implementing the createCharacter method:
class WarriorFactory : public CharacterFactory {
public:
Character* createCharacter() override {
return new Warrior();
}
};
class MageFactory : public CharacterFactory {
public:
Character* createCharacter() override {
return new Mage();
}
};
class AssassinFactory : public CharacterFactory {
public:
Character* createCharacter() override {
return new Assassin();
}
};
In the client code, we can create different types of character objects through specific character factory classes:
int main() {
CharacterFactory* warriorFactory = new WarriorFactory();
Character* warrior = warriorFactory->createCharacter();
warrior->display();
CharacterFactory* mageFactory = new MageFactory();
Character* mage = mageFactory->createCharacter();
mage->display();
CharacterFactory* assassinFactory = new AssassinFactory();
Character* assassin = assassinFactory->createCharacter();
assassin->display();
// Free memory
delete warrior;
delete mage;
delete assassin;
delete warriorFactory;
delete mageFactory;
delete assassinFactory;
return 0;
}
In this example, polymorphism allows us to create different types of character objects through the abstract CharacterFactory class without directly instantiating specific character classes in the client code. When we need to add a new character type, we only need to create a new specific character class and the corresponding character factory class, while the client code requires minimal modification, improving the maintainability and extensibility of the code.
3.2 Practical Use of Virtual Function Tables in Programming
(1) Accessing the Virtual Function Table through Code
In C++, while directly accessing the virtual function table is not a common operation, understanding how to access the virtual function table can provide deeper insights into the implementation mechanism of polymorphism. Here is a simple code example demonstrating how to obtain the virtual function table address and virtual function addresses through pointer operations and call virtual functions:
#include <iostream>
class Base {
public:
virtual void Func1() {
std::cout << "Base::Func1" << std::endl;
}
virtual void Func2() {
std::cout << "Base::Func2" << std::endl;
}
};
typedef void(*FunPtr)(); // Define a function pointer type for pointing to virtual functions
int main() {
Base obj;
// Get the virtual function table pointer of the object, as the virtual function table pointer is usually located at the start of the object's memory, we convert the object address to an integer pointer and dereference it to get the virtual function table pointer
int* vptr = reinterpret_cast<int*>(&obj);
// Get the address of the virtual function table through the virtual function table pointer
int vtable_address = *vptr;
std::cout << "The address of the virtual function table: " << std::hex << vtable_address << std::endl;
// Get the address of the first virtual function (Func1), the virtual function table is an array storing virtual function pointers, each pointer occupies 4 bytes (in a 32-bit system), so we convert the virtual function table address to an integer pointer and dereference it to get the address of the first virtual function
FunPtr func1_ptr = reinterpret_cast<FunPtr>(*(int*)vtable_address);
// Call the first virtual function
func1_ptr();
// Get the address of the second virtual function (Func2), offset the pointer to the first virtual function address by 4 bytes (in a 32-bit system) to dereference and get the address of the second virtual function
FunPtr func2_ptr = reinterpret_cast<FunPtr>(*((int*)vtable_address + 1));
// Call the second virtual function
func2_ptr();
return 0;
}
In this code, we first convert &obj to an integer pointer using reinterpret_cast<int*>, then dereference it to obtain the virtual function table pointer vptr. By using *vptr, we get the address of the virtual function table vtable_address. Next, by converting vtable_address to a function pointer of type FunPtr, we obtain and call the virtual functions Func1 and Func2 from the virtual function table.
This method, while allowing direct manipulation of the virtual function table, is generally not recommended in actual development, as it relies on compiler implementation details that may reduce code portability. However, this approach provides a more intuitive understanding of the layout and working principles of the virtual function table in memory.
(2) Application Scenarios of the Virtual Function Table in Polymorphic Programming
The virtual function table has wide applications in polymorphic programming, enabling C++ to achieve unified interface calls for different types of objects, greatly enhancing the extensibility and flexibility of the code. Let’s illustrate the application of the virtual function table in a simple graphics drawing system.
Suppose we are developing a simple graphics drawing system that needs to draw different types of shapes, such as circles, rectangles, and triangles. We can define an abstract base class Shape that contains a virtual function Draw for drawing shapes:
#include <iostream>
class Shape {
public:
virtual void Draw() const = 0;
virtual ~Shape() = default;
};
Then, we define classes Circle, Rectangle, and Triangle, inheriting from the Shape class and implementing their respective Draw functions:
class Circle : public Shape {
private:
int m_radius;
public:
Circle(int radius) : m_radius(radius) {}
void Draw() const override {
std::cout << "Drawing a circle with radius " << m_radius << std::endl;
}
};
class Rectangle : public Shape {
private:
int m_width;
int m_height;
public:
Rectangle(int width, int height) : m_width(width), m_height(height) {}
void Draw() const override {
std::cout << "Drawing a rectangle with width " << m_width << " and height " << m_height << std::endl;
}
};
class Triangle : public Shape {
private:
int m_base;
int m_height;
public:
Triangle(int base, int height) : m_base(base), m_height(height) {}
void Draw() const override {
std::cout << "Drawing a triangle with base " << m_base << " and height " << m_height << std::endl;
}
};
In the client code, we can use pointers or references of type Shape to operate on different types of shape objects without caring about the specific shape type:
void DrawShapes(const Shape* shapes[], int count) {
for (int i = 0; i < count; ++i) {
shapes[i]->Draw();
}
}
int main() {
Circle circle(5);
Rectangle rectangle(10, 5);
Triangle triangle(8, 6);
const Shape* shapes[] = { &circle, &rectangle, ▵ };
int count = sizeof(shapes) / sizeof(shapes[0]);
DrawShapes(shapes, count);
return 0;
}
In this example, the DrawShapes function accepts an array of pointers of type Shape and the size of the array. By iterating through the array and calling the Draw function of each Shape object, we achieve a unified drawing operation for different types of shapes. At runtime, based on the actual type of each pointer (whether it points to a Circle, Rectangle, or Triangle), the virtual function table dynamically determines which class’s Draw function to call, thus achieving polymorphism.
If we later need to add a new shape type, such as Square, we only need to define a new class inheriting from the Shape class and implement the Draw function, without modifying the DrawShapes function and other existing code, greatly enhancing the extensibility and flexibility of the code. This is the power of the virtual function table in polymorphic programming, allowing the code to handle various different types of objects in an elegant and flexible manner.
4. Virtual Destructors (Preventing Memory Leaks from Not Destructing Subclasses)
We know that sometimes a base class pointer points to a derived class object dynamically generated using the new operator; at the same time, objects dynamically generated using the new operator are released through the pointer pointing to them using delete. If a base class pointer points to a derived class object dynamically generated using the new operator, and the object is released through the base class pointer, it may lead to incorrect program behavior.
For example, consider the following program:
#include <iostream>
using namespace std;
class CShape // Base class
{
public:
~CShape() { cout << "CShape::destructor" << endl; }
};
class CRectangle : public CShape // Derived class
{
public:
int w, h; // Width and height
~CRectangle() { cout << "CRectangle::destructor" << endl; }
};
int main()
{
CShape* p = new CRectangle;
delete p; // Only the destructor of CShape is called, not CRectangle
return 0;
}
The output of the program is as follows:
CShape::destructor
The output indicates that delete p; only triggers the destructor of the CShape class and does not invoke the destructor of the CRectangle class. This is because this statement is statically bound; at compile time, the compiler cannot know which type of object p points to; it only decides to call the destructor of the CShape class based on the type of p.
Logically, delete p; should invoke the destructor of the CRectangle class since it is supposed to destroy an object of that class; otherwise, it may lead to program issues.
For instance, if the program needs to count the objects of the CRectangle class, failing to call the destructor of the CRectangle class would lead to incorrect counting. Additionally, if the object of the CRectangle class allocated dynamic memory during its lifetime, and the memory release operations are all performed in the destructor, failing to call the destructor of the CRectangle class would mean that the dynamically allocated memory would never be reclaimed.
In summary, we want the statement delete p; to intelligently execute the corresponding destructor based on the object pointed to by p. In fact, this is also polymorphism. To achieve polymorphism in this case, C++ requires that the base class’s destructor be declared as a virtual function, i.e., a virtual destructor.
We can modify the CShape class in the above program by adding the virtual keyword before the destructor declaration:
class CShape {
public:
virtual ~CShape() { cout << "CShape::destructor" << endl; }
};
Then the program’s output changes to:
CRectangle::destructor
CShape::destructor
This indicates that the destructor of the CRectangle class is called. In fact, the destructor of the derived class will automatically call the destructor of the base class as long as the base class’s destructor is a virtual function, meaning that the destructor of the derived class, regardless of whether it is declared with the virtual keyword, automatically becomes a virtual destructor.
Generally, if a class defines virtual functions, it is best to also define the destructor as a virtual function. While destructors can be virtual functions, constructors cannot be virtual functions.