Function pointers are a powerful feature in the C programming language, allowing programs to pass, store, and dynamically call functions as data, providing great flexibility in code design. This article will systematically explain function pointers from their definition syntax, basic usage to advanced applications (callback mechanisms, state machines, plugin systems, object-oriented simulation), combining core insights from “Expert C Programming” and a wealth of practical code to help developers thoroughly master this technology.
1. Definition of Function Pointers: Understanding “Pointers to Functions”
A function pointer is essentially a pointer variable that points to the starting address of function code in memory. Unlike regular pointers, the type of a function pointer is determined by its return type and parameter type list, which is why the syntax for defining function pointers is relatively complex.
1.1 Basic Definition Syntax
The declaration format for a function pointer is:
return_type (*pointer_variable_name)(parameter_type1, parameter_type2, ...);
<span>(*pointer_variable_name)</span>: Parentheses are mandatory to ensure that<span>*</span>is associated with the variable name; otherwise, it will be parsed as a function returning a pointer.- The parameter type list must exactly match the parameter types and order of the function being pointed to.
Example: Definitions of Function Pointers of Different Types
// 1. Function pointer for a function with no parameters and no return value
void (*func_void_void)(void);
// 2. Function pointer for a function that takes two int parameters and returns an int
int (*func_int_int)(int, int);
// 3. Function pointer for a function that takes a char* parameter and returns a const char*
const char* (*func_str_str)(char*);
// 4. Function pointer as a parameter (higher-order function pointer)
void (*func_with_func_ptr)(int (*)(int, int));
1.2 Relationship Between Function Names and Function Pointers
In C, function names are implicitly converted to pointers to that function in expressions (similar to how array names decay to pointers). Therefore, a function name can be directly assigned to a function pointer variable of the same type:
// Define a normal function
int add(int a, int b) { return a + b; }
int main() {
// Assign the function name to the function pointer (the function name is implicitly converted to a pointer)
int (*calc)(int, int) = add; // Equivalent to &add, but & can be omitted
// Verify: the addresses of the function name and function pointer are the same
printf("add function address: %p\n", (void*)add);
printf("calc pointer value: %p\n", (void*)calc); // Output is the same as the previous line
return 0;
}
2. Basic Usage of Function Pointers: Assignment, Calling, and Parameter Passing
Mastering the basic operations of function pointers is the foundation for subsequent advanced applications, including assignment, calling, and passing as function parameters.
2.1 Assignment and Calling
When assigning a function pointer, ensure that the types match exactly (return type, parameter types, and number of parameters must be consistent). There are two ways to call a function pointer: directly through the pointer name or by dereferencing it (both methods are equivalent).
Example: Assignment and Calling of Function Pointers
#include <stdio.h>
// Define an addition function
int add(int a, int b) { return a + b; }
// Define a subtraction function (same type as add)
int sub(int a, int b) { return a - b; }
int main() {
// 1. Assignment: function pointer points to add
int (*calc)(int, int) = add;
// 2. Calling method 1: directly call through pointer name (recommended)
int sum = calc(3, 5); // Equivalent to add(3,5)
printf("3 + 5 = %d\n", sum); // Output: 8
// 3. Calling method 2: call after dereferencing (the * can be omitted, syntactic sugar)
sum = (*calc)(3, 5); // Completely equivalent to calc(3,5)
printf("3 + 5 = %d\n", sum); // Output: 8
// 4. Reassign: point to sub function (type matches)
calc = sub;
int diff = calc(3, 5); // Equivalent to sub(3,5)
printf("3 - 5 = %d\n", diff); // Output: -2
return 0;
}
2.2 Passing as Function Parameters (Basics of Callback Functions)
One of the most common scenarios for function pointers is passing as parameters to other functions, where the passed function is called a “callback function”. This method allows for “decoupling the caller and the callee”, which is a core idea in framework design.
Example: Implementing a Generic Calculator with Function Pointers
#include <stdio.h>
// Callback function type: takes two ints, returns int
typedef int (*Operation)(int, int); // Use typedef to simplify type name
// Generic calculation function: dynamically selects operation through function pointer parameter
int calculate(int a, int b, Operation op) {
return op(a, b); // Call the callback function
}
// Specific operation functions (matching Operation type)
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int max(int a, int b) { return a > b ? a : b; }
int main() {
int a = 10, b = 5;
// Pass different function pointers to implement different calculation logic
printf("%d + %d = %d\n", a, b, calculate(a, b, add)); // 10 + 5 = 15
printf("%d * %d = %d\n", a, b, calculate(a, b, mul)); // 10 * 5 = 50
printf("max(%d, %d) = %d\n", a, b, calculate(a, b, max));// max(10,5)=10
return 0;
}
3. Advanced Application 1: Callback Mechanism – Example with qsort Function
The callback mechanism is the most classic application of function pointers, and the <span>qsort</span> (quick sort) function in the C standard library implements generic sorting logic through function pointers, requiring users to provide a custom comparison function.
3.1 Callback Principle of qsort Function
<span>qsort</span> function prototype:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
<span>base</span>: Address of the first element of the array to be sorted;<span>nmemb</span>: Number of elements in the array;<span>size</span>: Size in bytes of each element;<span>compar</span>: Comparison function pointer, where the user must implement the logic for comparing two elements, returning:- Positive number: the first element is greater than the second;
- Negative number: the first element is less than the second;
- 0: the two elements are equal.
3.2 Practical Example: Using qsort to Sort an Array of Custom Structures
Requirement: Sort an array of <span>Student</span> structures in ascending order by score (<span>score</span>), and if scores are the same, sort by name (<span>name</span>) in dictionary order.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Custom structure
typedef struct {
char name[20];
int score;
} Student;
// Comparison function (callback function): sort by score in ascending order, then by name in dictionary order
int compare_student(const void *a, const void *b) {
// Convert void* to Student* (must match actual type)
Student *s1 = (Student*)a;
Student *s2 = (Student*)b;
// First compare scores
if (s1->score != s2->score) {
return s1->score - s2->score; // Ascending (small → large)
} else {
// If scores are the same, compare names (strcmp returns the dictionary order difference)
return strcmp(s1->name, s2->name);
}
}
int main() {
Student students[] = {
{"Bob", 85}, {"Alice", 90}, {"Bob", 85}, {"Charlie", 75}
};
int n = sizeof(students) / sizeof(students[0]);
// Call qsort, passing the comparison function pointer
qsort(students, n, sizeof(Student), compare_student);
// Output sorted results
printf("Sorted results:\n");
for (int i = 0; i < n; i++) {
printf("%s: %d\n", students[i].name, students[i].score);
}
return 0;
}
Output Result:
Sorted results:
Charlie: 75
Alice: 90
Bob: 85
Bob: 85 // Same score, sorted by name in dictionary order (Alice < Bob)
4. Advanced Application 2: Function Table – State Machine and Command Processor
An array of function pointers (“function table”) organizes multiple function pointers of the same type into an array, allowing for quick calls to different functions via indexing. This method can replace complex <span>switch-case</span> branches and is commonly used in state machines and command processors.
4.1 State Machine: Managing State Transitions with Function Table
Scenario: Implement a simple traffic light state machine, cycling through “red light → green light → yellow light → red light”, with each state corresponding to a different handling function.
#include <stdio.h>
#include <unistd.h> // for sleep
// State enumeration
typedef enum {
RED, // Red light
GREEN, // Green light
YELLOW // Yellow light
} TrafficLightState;
// State handling function type (no parameters, returns next state)
typedef TrafficLightState (*StateHandler)(void);
// Red light handling function: on for 3 seconds, switch to green light
TrafficLightState red_handler() {
printf("Red light on (3 seconds)\n");
sleep(3);
return GREEN;
}
// Green light handling function: on for 3 seconds, switch to yellow light
TrafficLightState green_handler() {
printf("Green light on (3 seconds)\n");
sleep(3);
return YELLOW;
}
// Yellow light handling function: on for 1 second, switch to red light
TrafficLightState yellow_handler() {
printf("Yellow light on (1 second)\n");
sleep(1);
return RED;
}
// Function table: mapping states to handling functions (index corresponds to enumeration values)
StateHandler state_table[] = {
red_handler, // RED(0) → red_handler
green_handler, // GREEN(1) → green_handler
yellow_handler // YELLOW(2) → yellow_handler
};
int main() {
TrafficLightState current_state = RED; // Initial state: red light
while (1) { // Infinite loop
// Call the handling function for the current state via the function table, getting the next state
current_state = state_table[current_state]();
}
return 0;
}
Output Result (executed in a loop):
Red light on (3 seconds)
Green light on (3 seconds)
Yellow light on (1 second)
Red light on (3 seconds)
...
4.2 Command Processor: Parsing Commands with Function Table
Scenario: Implement a simple command-line tool that supports <span>add</span> (addition), <span>sub</span> (subtraction), and <span>exit</span> commands, matching commands to handling functions via a function table.
#include <stdio.h>
#include <string.h>
// Command handling function type: takes parameters, returns whether to exit (0-continue, 1-exit)
typedef int (*CmdHandler)(int a, int b);
// Command structure: contains command name and corresponding handling function
typedef struct {
const char *name; // Command name
CmdHandler handler; // Handling function
} Command;
// Addition command handling function
int add_handler(int a, int b) {
printf("%d + %d = %d\n", a, b, a + b);
return 0; // Do not exit
}
// Subtraction command handling function
int sub_handler(int a, int b) {
printf("%d - %d = %d\n", a, b, a - b);
return 0; // Do not exit
}
// Exit command handling function
int exit_handler(int a, int b) {
printf("Exiting program\n");
return 1; // Exit
}
// Command table: mapping command names to handling functions
Command cmd_table[] = {
{"add", add_handler},
{"sub", sub_handler},
{"exit", exit_handler},
{NULL, NULL} // End marker
};
// Find the handling function corresponding to the command
CmdHandler find_handler(const char *cmd) {
for (int i = 0; cmd_table[i].name != NULL; i++) {
if (strcmp(cmd, cmd_table[i].name) == 0) {
return cmd_table[i].handler;
}
}
return NULL; // Command does not exist
}
int main() {
char cmd[20];
int a, b;
while (1) {
printf("Input command (add/sub/exit) and parameters: ");
scanf("%s %d %d", cmd, &a, &b);
CmdHandler handler = find_handler(cmd);
if (handler) {
if (handler(a, b)) { // Call handling function, check if exit
break;
}
} else {
printf("Unknown command: %s\n", cmd);
}
}
return 0;
}
Interactive Example:
Input command (add/sub/exit) and parameters: add 10 20
10 + 20 = 30
Input command (add/sub/exit) and parameters: sub 5 3
5 - 3 = 2
Input command (add/sub/exit) and parameters: exit 0 0
Exiting program
5. Advanced Application 3: Dynamic Loading (Plugin System)
Function pointers combined with dynamic link libraries (<span>.so</span>/<span>.dll</span>) can implement a plugin system: the main program dynamically loads plugin libraries at runtime and calls functions within the plugins via function pointers, extending functionality without recompiling the main program.
5.1 Core API for Dynamic Loading in Linux
In Linux, dynamic loading functionality is provided through <span>dlfcn.h</span>, with core functions:
<span>void* dlopen(const char *filename, int flag)</span>: Loads a dynamic library and returns a library handle;<span>flag</span>:<span>RTLD_LAZY</span>(deferred symbol resolution) or<span>RTLD_NOW</span>(immediate resolution).<span>void* dlsym(void *handle, const char *symbol)</span>: Retrieves the address of a symbol (function/variable) from the library, returning a function pointer;<span>int dlclose(void *handle)</span>: Closes the dynamic library;<span>const char* dlerror(void)</span>: Retrieves the last error message.
5.2 Practical Example: Implementing a Simple Plugin System
Step 1: Define Plugin Interface (Plugins and Main Program Must Follow the Same Interface)
// plugin_api.h (plugin interface header file)
#ifndef PLUGIN_API_H
#define PLUGIN_API_H
// Plugin handling function type: takes a string, returns the processed string
typedef const char* (*PluginHandler)(const char* input);
#endif
Step 2: Write Plugin Implementation (Compile as Dynamic Library)
// plugin_uppercase.c (plugin that converts input string to uppercase)
#include "plugin_api.h"
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
// Plugin function implementation: convert to uppercase
const char* uppercase_handler(const char* input) {
if (!input) return NULL;
char* result = strdup(input); // Copy input string
for (int i = 0; result[i]; i++) {
result[i] = toupper(result[i]); // Convert to uppercase
}
return result; // Caller must free memory
}
// Export plugin function (C-style to avoid C++ name mangling)
__attribute__((visibility("default")))
PluginHandler get_plugin_handler() {
return uppercase_handler; // Return plugin function pointer
}
Compile Plugin as Dynamic Library:
gcc -shared -fPIC -o libuppercase.so plugin_uppercase.c
Step 3: Main Program Dynamically Loads Plugin and Calls
// main.c (main program)
#include <stdio.h>
#include <dlfcn.h>
#include "plugin_api.h"
int main() {
// 1. Load dynamic library
void* handle = dlopen("./libuppercase.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Failed to load plugin: %s\n", dlerror());
return 1;
}
// 2. Get the exported function pointer (get_plugin_handler)
typedef PluginHandler (*GetHandlerFunc)();
GetHandlerFunc get_handler = dlsym(handle, "get_plugin_handler");
const char* error = dlerror();
if (error) {
fprintf(stderr, "Failed to get function: %s\n", error);
dlclose(handle);
return 1;
}
// 3. Call the plugin function
PluginHandler plugin_func = get_handler();
const char* input = "hello plugin system";
const char* output = plugin_func(input);
printf("Input: %s\nOutput: %s\n", input, output);
// 4. Free resources
free((void*)output); // Free memory allocated by plugin
dlclose(handle); // Close dynamic library
return 0;
}
Compile Main Program and Run:
gcc -o main main.c -ldl # -ldl links the dynamic loading library
./main
Output Result:
Input: hello plugin system
Output: HELLO PLUGIN SYSTEM
6. Advanced Application 4: Simulating Object-Oriented Programming (OOP)
Although C does not natively support object-oriented programming, it can simulate the three major features of encapsulation, inheritance, and polymorphism through structures (data members) + function pointers (member methods).
6.1 Encapsulation: Structures + Function Pointers Simulating “Classes”
Encapsulate data and functions that operate on the data within a structure, exposing only the structure pointer and initialization function to hide the internal implementation.
Example: Simulating a “Stack” Class
#include <stdio.h>
#include <stdlib.h>
// Stack structure (class): encapsulates data and methods
typedef struct {
int *data; // Data member: stack element array
int top; // Data member: stack top pointer
int capacity; // Data member: capacity
// Member methods (function pointers)
void (*push)(struct Stack*, int); // Push
int (*pop)(struct Stack*); // Pop
int (*is_empty)(struct Stack*); // Check if empty
void (*destroy)(struct Stack*); // Destroy stack
} Stack;
// Push implementation
static void stack_push(Stack* stack, int value) {
if (stack->top >= stack->capacity) {
fprintf(stderr, "Stack overflow\n");
return;
}
stack->data[stack->top++] = value;
}
// Pop implementation
static int stack_pop(Stack* stack) {
if (stack->is_empty(stack)) {
fprintf(stderr, "Stack is empty\n");
return -1;
}
return stack->data[--stack->top];
}
// Check if empty implementation
static int stack_is_empty(Stack* stack) {
return stack->top == 0;
}
// Destroy stack implementation
static void stack_destroy(Stack* stack) {
free(stack->data);
free(stack);
}
// Constructor: create and initialize stack
Stack* stack_create(int capacity) {
Stack* stack = malloc(sizeof(Stack));
stack->data = malloc(sizeof(int) * capacity);
stack->top = 0;
stack->capacity = capacity;
// Bind member methods
stack->push = stack_push;
stack->pop = stack_pop;
stack->is_empty = stack_is_empty;
stack->destroy = stack_destroy;
return stack;
}
int main() {
// Create stack object (call constructor)
Stack* stack = stack_create(5);
// Call member methods (via function pointers)
stack->push(stack, 10);
stack->push(stack, 20);
stack->push(stack, 30);
printf("Popped: %d\n", stack->pop(stack)); // 30
printf("Popped: %d\n", stack->pop(stack)); // 20
printf("Is empty: %s\n", stack->is_empty(stack) ? "Yes" : "No"); // No
// Destroy stack (free resources)
stack->destroy(stack);
return 0;
}
6.2 Polymorphism: Implementing Dynamic Behavior via Function Pointers
Different “subclass” structures implement the same set of function pointer interfaces, allowing the main program to dynamically execute the subclass’s implementation when called through a base class pointer (similar to C++ virtual functions).
Example: Simulating Polymorphism in Graphic Drawing
#include <stdio.h>
#include <stdlib.h>
// Base class: Shape (defines interface)
typedef struct Shape {
// Virtual function: draw (subclass must implement)
void (*draw)(struct Shape*);
int x, y; // Common properties: coordinates
} Shape;
// Subclass: Circle (inherits Shape)
typedef struct {
Shape base; // Inherit base class (must be the first member)
int radius; // Unique property: radius
} Circle;
// Subclass: Rectangle (inherits Shape)
typedef struct {
Shape base; // Inherit base class
int width, height;// Unique properties: width, height
} Rectangle;
// Circle drawing implementation
static void circle_draw(Shape* shape) {
Circle* circle = (Circle*)shape; // Base class pointer to subclass pointer
printf("Drawing circle: coordinates(%d,%d), radius%d\n",
shape->x, shape->y, circle->radius);
}
// Rectangle drawing implementation
static void rectangle_draw(Shape* shape) {
Rectangle* rect = (Rectangle*)shape;
printf("Drawing rectangle: coordinates(%d,%d), width%d, height%d\n",
shape->x, shape->y, rect->width, rect->height);
}
// Circle constructor
Circle* circle_create(int x, int y, int radius) {
Circle* circle = malloc(sizeof(Circle));
circle->base.x = x;
circle->base.y = y;
circle->base.draw = circle_draw; // Bind circle drawing method
circle->radius = radius;
return circle;
}
// Rectangle constructor
Rectangle* rectangle_create(int x, int y, int width, int height) {
Rectangle* rect = malloc(sizeof(Rectangle));
rect->base.x = x;
rect->base.y = y;
rect->base.draw = rectangle_draw; // Bind rectangle drawing method
rect->width = width;
rect->height = height;
return rect;
}
// Polymorphic call: call different subclass methods via base class pointer
void draw_shape(Shape* shape) {
shape->draw(shape); // Dynamic binding: call actual subclass's draw
}
int main() {
// Create different subclass objects
Shape* shapes[2];
shapes[0] = (Shape*)circle_create(10, 20, 5); // Circle
shapes[1] = (Shape*)rectangle_create(30, 40, 10, 8); // Rectangle
// Loop to call the same interface, exhibiting different behaviors (polymorphism)
for (int i = 0; i < 2; i++) {
draw_shape(shapes[i]);
}
// Free resources
free(shapes[0]);
free(shapes[1]);
return 0;
}
Output Result:
Drawing circle: coordinates(10,20), radius5
Drawing rectangle: coordinates(30,40), width10, height8
7. Precautions and Best Practices
- Type Matching: The return value, parameter types, and number of parameters of function pointers must exactly match the function they point to, otherwise it may lead to undefined behavior (such as crashes or data corruption).
- It is recommended to use
<span>typedef</span>to define function pointer types for improved readability and to avoid type errors.
- After calling
<span>dlsym</span>, always check for errors (<span>dlerror()</span><span>), to avoid dangling pointers caused by symbols not found.</span> - Memory allocated by plugin functions must have clear responsibility for release (as shown in the example where the main program releases the string returned by the plugin).
8. Conclusion
Function pointers are the “Swiss Army knife” that C language provides to developers, breaking the limitation that “functions can only be called” and allowing functions to participate in program flow as data, providing strong support for dynamic logic, decoupled design, and modular development.
From the basics of definition and calling, to callback mechanisms (such as <span>qsort</span>), function tables (state machines/command processors), dynamic loading (plugin systems), and finally to simulating object-oriented programming, the application of function pointers runs through all levels of C language development. Mastering function pointers not only enables handling complex programming needs but also deepens the understanding of the C language’s philosophy of “everything is an address”, laying a solid foundation for system-level development (such as operating systems, embedded systems, and drivers).
