Dynamic polymorphism is a fundamental principle in object-oriented programming, allowing objects to be treated uniformly while exhibiting different behaviors. This feature is crucial for achieving flexibility and reusability in software design. In this article, we'll explore dynamic polymorphism with a sample program, explain heap and stack memory, delve into the virtual table (vtable), and discuss memory consumption details.
To demonstrate dynamic polymorphism, consider the following example in C++:
#include <iostream>
// Base class
class Animal {
public:
virtual void makeSound() const {
std::cout << "Animal sound" << std::endl;
}
virtual ~Animal() = default; // Virtual destructor
};
// Derived class
class Dog : public Animal {
public:
void makeSound() const override {
std::cout << "Woof" << std::endl;
}
};
// Another derived class
class Cat : public Animal {
public:
void makeSound() const override {
std::cout << "Meow" << std::endl;
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound(); // Outputs: Woof
animal2->makeSound(); // Outputs: Meow
return 0;
}
In this example, Animal
is the base class with a virtual method makeSound()
. The Dog
and Cat
classes override this method. At runtime, the appropriate method is called based on the actual object type (Dog
or Cat
), demonstrating dynamic polymorphism.
To understand how this is achieved in C, let's recreate the same program:
#include <stdio.h>
#include <stdlib.h>
char const* dogSound(void){return "Woof!";}
char const* catSound(void){return "Meow!";}
typedef struct Animal{
char const * (*makeSound)(); //function pointer
}Animal;
void makeSound(Animal * self){
printf("%s\n",self->makeSound());
}
Animal * constructDog(){
Animal * dog = malloc(sizeof(Animal));
dog->makeSound = dogSound;
return dog;
}
Animal * constructCat(){
Animal * cat = malloc(sizeof(Animal));
cat->makeSound = catSound;
return cat;
}
int main(void){
Animal * cat = constructCat();
Animal * dog = constructDog();
makeSound(cat); // Outputs: Woof
makeSound(dog); // Outputs: Meow
return 0;
}
Here, function pointers are used to achieve polymorphism in C. The Animal
struct contains a function pointer makeSound
, which is assigned appropriate functions (dogSound
or catSound
) at runtime.
In the sample program, objects animal1
and animal2
are created using the new
keyword. This means they are allocated on the heap, which is a region of memory used for dynamic memory allocation. The delete
keyword is used to deallocate memory, preventing memory leaks.
Conversely, if we had created objects without the new
keyword (i.e., Dog dog;
), they would be allocated on the stack. The stack is used for static memory allocation, which is automatically managed and has a smaller, faster access compared to the heap.
The vtable (virtual table) is a mechanism used by C++ to support dynamic polymorphism. It is a table of function pointers maintained per class. Each class with virtual functions has a corresponding vtable, and each object has a pointer to this vtable, known as the vptr (virtual pointer).
When a virtual function is called on an object, the compiler uses the vptr to look up the correct function in the vtable and invoke it. This lookup allows C++ to resolve the function call at runtime, enabling dynamic polymorphism.
Memory consumption in dynamic polymorphism involves both the heap (for dynamic allocation) and the memory overhead of maintaining vtables and vptrs. Each object with virtual functions contains an additional pointer (vptr), and each class with virtual functions has an associated vtable.
While the heap allocation allows flexibility, it comes with a cost: dynamic memory management can lead to fragmentation, and improper deallocation can cause memory leaks. Additionally, the indirection through vtables can introduce a slight performance overhead due to the extra pointer dereference.
Dynamic polymorphism in C++ is a powerful feature that enables flexibility and reusability in object-oriented design. It is achieved through virtual functions, heap allocation, and the use of vtables. Understanding the memory implications and how the heap and stack work is essential for efficient programming in C++.
By following best practices in memory management and being mindful of the costs associated with dynamic polymorphism, developers can harness their full potential to create robust and maintainable software.