Scan the code to follow Chip Dynamics and say goodbye to “chip” congestion!

Search WeChat
Chip Dynamics
In the world of C language, pointers are the “soul tool”, but ordinary pointers (like int* and char*) are like “custom keys”—a key can only open one lock. However, the void* pointer is like a “universal key”: it can open any type of “lock” without needing to be “labeled” in advance. Why has this “mysterious key” become a necessity in C language? What are its little-known “superpowers”? What fatal taboos should be avoided when using it? This article will provide a detailed breakdown.
Why do we need void pointers?
The core design philosophy of C language is efficiency and flexibility. It allows programmers to directly manipulate memory but also requires the code to adapt to different scenarios—for example, a function may need to handle int arrays, char strings, or even custom structures; a block of memory may be used for storing float values or may be reallocated for an int array.
If only ordinary pointers are available, the code will fall into the dilemma of “reinventing the wheel”:
-
To write a generic memory copy function, versions must be implemented for int, char, and structures (int_memcpy, char_memcpy, etc.), otherwise, it cannot handle different types of data.
-
When dynamically allocating memory, if malloc can only return int* or char*, then every time allocating other types (like struct Node*) would require redesigning the function, leading to explosive code redundancy.
The emergence of void* perfectly solves this problem. It is the only type of pointer in C language that is untyped, not bound to any specific data type, yet can point to any type of memory address. This “untyped” feature makes void* a key tool for achieving “generality” in C language.
What exactly is a void pointer?
To understand void*, one must first recall what ordinary pointers look like. For example, int* p clearly tells the compiler: “I point to an int type data, you should process me as 4 bytes (assuming a 32-bit system)!” But what about void*? It is like a “blank check”—the compiler has no idea whether it points to int, char, or struct, or even whether it points to “data” or “function” (though this is generally not used).
For example:
int num = 100;char c = 'A';void* ptr1 = # // void pointer points to int variablevoid* ptr2 = &c; // void pointer points to char variable// At this point, ptr1 and ptr2 are both "untyped pointers", and the compiler does not care who they point to!
But note: void* pointers cannot be dereferenced directly (cannot directly *ptr1). Because the compiler does not know whether you want to retrieve an int or a char, just like you have a key without a label, directly inserting it into the lock may break it (resulting in an error). To use it, you must first “label” it—by performing a type cast!
int num = 100;void* ptr = #// Force cast to int*, equivalent to labeling the key with "int lock hole" labelprintf("%d", *((int*)ptr)); // Outputs 100, successfully opened the lock!
Superpowers of void pointers
The “untyped” nature of void* is not a defect, but its core advantage. With this feature, it derives three major “superpowers” in C language, almost permeating all scenarios that require “type independence”.
Generic containers: Making functions compatible with all data types
In the C standard library, many functions need to handle “unknown types” of data. void* serves as their “generic container”, allowing functions to focus solely on “operating memory” itself without worrying about specific types.
Typical example: memcpy (memory copy function)
The prototype of memcpy is:
void* memcpy(void* dest, const void* src, size_t n);
Its function is to copy the first n bytes of the memory block pointed to by src to the location pointed to by dest. Here, both dest and src are void*, meaning that regardless of whether they point to int arrays, char strings, or structures, memcpy can work directly—because it only performs “byte-level copying” and does not care about the specific meaning of the data.
For a practical example:
// Copy int arrayint arr1[] = {1, 2, 3};int arr2[3];memcpy(arr2, arr1, sizeof(arr1)); // Correct! sizeof(arr1) is 12 bytes (3 ints)// Copy stringchar str1[] = "hello";char str2[6];memcpy(str2, str1, strlen(str1)+1); // Correct! Includes '\0' terminator
If there were no void*, memcpy would have to write a version for each type (like int_memcpy, char_memcpy), which clearly contradicts the C language’s design goal of “simplicity and efficiency”.
Dynamic memory allocation
The malloc function we commonly use also returns void*:
void* malloc(size_t size);
It returns a void* pointer, indicating “a block of unspecified type of memory”. Programmers need to convert it to a specific type of pointer (like int* or char*) based on its intended use.
Why doesn’t malloc use int* or char* as a return value? Because its responsibility is to “allocate a block of raw memory”, and this memory may store any type of data in the future. If malloc could only return int*, then allocating a char array would require a forced conversion, making the code cumbersome; more critically, this would limit the generality of malloc—it would not be able to allocate memory for unknown types of structures or custom types.
// Allocate space for 3 ints (12 bytes), convert to int*int* p_int = (int*)malloc(3 * sizeof(int)); // Allocate space for 10 chars (10 bytes), convert to char*char* p_char = (char*)malloc(10 * sizeof(char)); // Allocate space for a structure, convert to structure pointerstruct Node { int data; char flag; };struct Node* p_node = (struct Node*)malloc(sizeof(struct Node));
Callback function type buffer
In C language, callback functions (like the comparison function for the sorting function qsort) need to handle “unknown types” of data. void* serves as a “buffer”, allowing callback functions to receive pointers of any type and convert them based on actual needs.
Typical example: qsort (quick sort function)
The prototype of qsort is:
void qsort(void* base, size_t nmemb, size_t size, int (*compar)(const void*, const void*));
Here, base is the starting address of the array to be sorted (of type void*), nmemb is the number of elements, size is the size (in bytes) of a single element, and compar is the comparison function, with parameters being two const void* (pointers to the elements to be compared).
Through void*, qsort can sort arrays of any type—whether int, float, or arrays of custom structures. The comparison function only needs to convert void* to the corresponding type of pointer to complete the comparison logic.
For example, sorting an int array:
int int_compare(const void* a, const void* b) { int num1 = *(const int*)a; // Convert to int* and dereference int num2 = *(const int*)b; return num1 - num2;}int main() { int arr[] = {3, 1, 4, 2}; qsort(arr, 4, sizeof(int), int_compare); // Use void* to accommodate int array // After sorting, arr becomes {1,2,3,4} return 0;}
If qsort did not use void* and required the comparison function to only handle int*, then sorting float arrays or structure arrays would require re-implementing a set of qsort_float, qsort_struct, which clearly contradicts the principle of “code reuse”.
Hidden rules of void pointers
Although void* is powerful, it is not a “cure-all”. Due to its “untyped” nature, incorrect usage can lead to compilation warnings, runtime crashes, or even data errors. Here are the three most common pitfalls for beginners:
Taboo 1: Directly dereferencing void pointers
void* pointers store memory addresses, but the compiler does not know the data type they point to, so they cannot be dereferenced directly (i.e., cannot get value through *ptr).
Error example:
void* ptr = malloc(sizeof(int)); // Allocate 4 bytes of memory*ptr = 100; // Error! The compiler does not know whether ptr points to int, char, or other types
Reason: When dereferencing a pointer, the compiler needs to know the size of the data (e.g., int occupies 4 bytes, char occupies 1 byte) to generate the correct memory access instructions. void* has no type information, so the compiler cannot determine how many bytes to read, making direct dereferencing illegal.
Correct approach: First convert void* to a specific type of pointer, then dereference:
int* int_ptr = (int*)ptr; // Convert to int* (clearly pointing to 4 bytes of data)*int_ptr = 100; // Correct!
Taboo 2: Performing arithmetic operations on void pointers
Ordinary pointers (like int*) can perform arithmetic operations (p++, p + n) because the compiler knows the size of the type they point to. For example, when executing p++ on int* p, the actual address will shift by sizeof(int) (usually 4 bytes).
However, void* pointers cannot perform arithmetic operations because the compiler does not know the size of the type they point to and cannot calculate the offset.
Error example:
// Allocate space for 3 ints (12 bytes)void* ptr = malloc(3 * sizeof(int)); ptr++; // Error! void* cannot be incremented
Reason: ptr++ is equivalent to ptr = ptr + 1, but what is the unit of 1? Is it 1 byte? Or the size of 1 int (4 bytes)? The compiler cannot determine, hence this operation is prohibited.
Correct approach: If you need to move the pointer, first convert it to a specific type of pointer:
int* int_ptr = (int*)ptr;int_ptr++; // Correct! Moves 4 bytes (assuming int is 4 bytes)
Taboo 3: Mismatched type conversions
void* can be converted to any type of pointer, but if the type being converted does not match the original data type, it can lead to data errors or even undefined behavior (such as out-of-bounds access or parsing errors).
Error example:
int num = 0x12345678; void* ptr = #char* c_ptr = (char*)ptr; // Convert to char*, only took the first byteprintf("%x", *c_ptr); // Outputs 0x78 (if in little-endian mode)
Reason: int type occupies 4 bytes, while char type occupies 1 byte. After converting int* to char*, dereferencing will only read the first byte of the int (depending on the system’s byte order). If the programmer mistakenly believes c_ptr points to the entire int, it will lead to logical errors.
Correct approach: Ensure that the type being converted matches the original data type. If you need to access partial bytes (like parsing binary protocols), you must clearly understand the data storage format (like big-endian/little-endian) and handle offsets carefully.
Conclusion
The void* pointer symbolizes “flexibility” in C language; its existence allows C language to achieve generic memory operations, dynamic memory management, and type-independent algorithms (like qsort, memcpy). However, this flexibility also means responsibility—the programmer must clearly know the data type pointed to by void* and perform the correct type conversion before use.
The next time you need to write a generic function, operate on unknown types of memory, or use generic interfaces in the standard library, remember void*—but keep the following principles in mind:
-
Untyped but has an address: void* stores a memory address but does not care about what type is stored at that address.
-
Must explicitly convert: Always convert to a specific type of pointer (like (int*)ptr) before use.
-
Prohibit arithmetic operations: Do not perform ++, +n, and other operations on void*.
-
Type matching is important: The type converted back must match the original data type, otherwise it will lead to errors.

If you find this article useful, click “Like”, “Share”, “Recommend”!