A Practical Guide to Dynamic Polymorphism in C Programming
2024-5-27 04:32:40 Author: hackernoon.com(查看原文) 阅读量:0 收藏

Dynamic polymorphism is a programming paradigm that enhances code flexibility and maintainability by allowing objects to be treated uniformly while exhibiting different behaviors. This concept is particularly useful in scenarios involving collections of related objects that need to perform specific actions. In this article, we compare two implementations of a toy simulation in C to demonstrate the benefits of dynamic polymorphism

Initial Implementation without Polymorphism

Here we define a Toy struct with a name field and create three instances representing Barbie and Superman toys. Each toy is initialized, and an array of pointers to these toy instances is created. The main function iterates through the array and prints the sound associated with each toy based on its name.

#include <stdio.h>
#include <stdlib.h>

// Function prototypes for the sounds made by different toys
char const* barbieSound(void){return "Love and imagination can change the world.";}
char const* supermanSound(void){return "Up, up, and away!";}

// Struct definition for Toy with an character array representing the name of the toy
typedef struct Toy{
    char const* name;  
}Toy;

int main(void){
    //Alocate memory for three Toy instances
    Toy* toy1 = (Toy*)malloc(sizeof(Toy));
    Toy* toy2= (Toy*)malloc(sizeof(Toy));
    Toy* toy3= (Toy*)malloc(sizeof(Toy));

    // Initialize the name members of the Toy instances
    toy1->name = "Barbie";
    toy2->name = "Barbie";
    toy3->name = "Superman";
    
    // Create an array of pointers to the Toy instances
    Toy* toys[] = {barbie1, barbie2, superman1};
    
    // Output the corresponding sound of each toy given its name
    for(int i=0; i < 3; i++){
        if (toys[i]->name == "Barbie"){printf("%s\n",toys[i]->name,barbieSound());}
        if (toys[i]->name == "SuperMan"){printf("%s\n",toys[i]->name,supermanSound());}
    }

    // Free the allocated memory for the Toy instances
    free(toy1);
    free(toy2);
    free(toy3);
    
    return 0;
}

While this is functional, it doesn't scale. Whenever we want to add a new toy we need to update the code to handle a new toy type and its sound function which could raise maintenance issues.

Enhanced Implementation of Dynamic Polymorphism:

The second code sample uses dynamic polymorphism for a more flexible, scalable application. Here Toy has its function pointer for the sound function. Factory functions(createBarbie(), createSuperMan()) are used to create Barbie and Superman instances, assigning the appropriate sound function to each toy. The makeSound() function demonstrates dynamic polymorphism by calling the appropriate sound function for each toy at run time.

#include <stdio.h>
#include <stdlib.h>

// Function prototypes for the sounds made by different toys
char const* barbieSound(void) {return "Love and imagination can change the world.";}
char const* supermanSound(void) {return "I’m here to fight for truth and justice, and the American way.";}

// Struct definition for Toy with a function pointer for the sound function
typedef struct Toy {
    char const* (*makeSound)();
} Toy;

// Function to call the sound function of a Toy and print the result
// Demonstrates dynamic polymorphism by calling the appropriate function for each toy
void makeSound(Toy* self) {
    printf("%s\n", self->makeSound());
}

// Function to create a Superman toy
// Uses dynamic polymorphism by assigning the appropriate sound function to the function pointer
Toy* createSuperMan() {
    Toy* superman = (Toy*)malloc(sizeof(Toy));
    superman->makeSound = supermanSound; // Assigns Superman's sound function
    return superman;
}

// Function to create a Barbie toy
// Uses dynamic polymorphism by assigning the appropriate sound function to the function pointer
Toy* createBarbie() {
    Toy* barbie = (Toy*)malloc(sizeof(Toy)); 
    barbie->makeSound = barbieSound; // Assigns Barbie's sound function
    return barbie;
}

int main(void) {
    // Create toy instances using factory functions
    Toy* barbie1 = createBarbie();
    Toy* barbie2 = createBarbie();
    Toy* superman1 = createSuperMan();
    
    // Array of toy pointers
    Toy* toys[] = { barbie1, barbie2, superman1 };
    
    // Loop through the toys and make them sound
    // Dynamic polymorphism allows us to treat all toys uniformly
    // without needing to know their specific types
    for (int i = 0; i < 3; i++) {
        makeSound(toys[i]);
    }
    
    // Free allocated memory
    free(barbie1);
    free(barbie2);
    free(superman1);

    return 0;
}

By using a function pointer within the Toy struct, the code can easily accommodate new toy types without modifying the core logic.

Factory function is a design pattern used to create objects without specifying the exact class or type of the object that will be created. In the context of C programming and especially in embedded software, a factory function helps in managing the creation and initialization of various types of objects (e.g., sensors, peripherals) in a modular and flexible manner. This pattern is particularly useful when dealing with dynamic polymorphism, as it abstracts the instantiation logic and allows the system to treat objects uniformly

A function pointer is a variable that stores the address of a function, allowing the function to be called through the pointer. This enables dynamic function calls, which are particularly useful when the function to be executed needs to be determined at runtime.

Defining a Function Pointer

A function pointer is defined using the following syntax:

<return type> (*<pointer name>)(<parameter types>);

Example

For example, to declare a pointer to a function that returns an int and takes two int parameters, you would write:

int (*functionPointer)(int, int);

Performance Considerations

  • Memory Overhead: Each structure that uses function pointers requires additional memory to store these pointers. This can slightly increase your program's memory footprint, especially if many such structures are used.

  • Function Call Overhead: Indirect function calls through pointers can introduce a slight performance overhead compared to direct function calls. This is due to the additional level of indirection required to resolve the function pointer at runtime.

    How the Overheads Occur

    When a function is called directly, the assembly code typically contains a direct jump to the function's address. For example, let’s take a simple C program:

#include <stdio.h>

void function(){
    printf("Hello Barbie");
}

int main(void){
    function();
    void (*myFunction)() = function;
    myFunction();
    return 0; 
}

The assembly code of the C code compiled with an x86-64 GCC 14.1 compiler looks like this:




.LC0:
        .string "Hello Barbie"
function:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        nop
        pop     rbp
        ret
main:
        push    rbp        # Save the base pointer of the previous stack frame by pushing it on the stack
        mov     rbp, rsp   # Set up a new stack frame for the current function
        mov     eax, 0     # Clear the eax register
        call    function   # Call the function 'function'
        mov     eax, 0     # Set eax register to 0, indicating the return value of the main function
        pop     rbp        # Pop the saved value from stack into rbp, restoring the previous stack frame
        ret                # Return from main function

We can see that in this case, calling the function takes only one instruction in the assembly code (call function).

If we add a function pointer called myFunction to point to the function:

void (*myFunction)() = function;
myFunction();

The assembly code of the main function will look like this:

main:    
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16      # Allocate 16 bytes on the stack for local variables 
        mov     eax, 0
        call    function
        mov     QWORD PTR [rbp-8], OFFSET FLAT:function  # Store the address of function into the memory location [rbp-8]
        mov     rdx, QWORD PTR [rbp-8]                   # Load the function pointer stored at [rbp-8] into rdx register
        mov     eax, 0                                   # Clear the eax register before function call, beacause the function might use it
        call    rdx                                      # Call the function stored in rdx
        mov     eax, 0                                   # Set the return value of main to 0
        leave                                            # Release the stack space used by the current stack frame
        ret

We can see that when using a function pointer, we have one additional assembly instruction for storing the function address in the function pointer and three additional instructions for calling the function pointed to by the function pointer. Also, 16 bytes had to be allocated for storing local variables, while only 8 bytes of the stack memory were used by the function pointer. This is because the stack size typically needs to be a multiple of 16, so if we were to use 20 bytes in our function, the compiler would allocate 32 bytes for the stack frame.

Benefits of Dynamic Polymorphism

  • Flexibility: Allows new types to be added with minimal changes to existing code.
  • Maintainability: Reduces the need for extensive conditional logic.
  • Scalability: Supports the extension of the system by adding new object types and behaviors.

Practical Use Cases

Dynamic polymorphism is often used in various software design patterns, such as the Strategy Pattern, State Pattern, and Command Pattern. These patterns make use of polymorphism to allow different behaviors and states to be easily swapped in and out. This makes the code more flexible and easier to maintain.


文章来源: https://hackernoon.com/a-practical-guide-to-dynamic-polymorphism-in-c-programming?source=rss
如有侵权请联系:admin#unsafe.sh