C Pointers: Memory Addressing, Arithmetic, and Function Dispatch
Memory Addressing and Pointer Fundamentals
In C, memory is organized as a contiguous sequence of bytes, each assgined a unique identifier known as a memory address. A pointer is simply a variable designed to store these memory addresses rather than conventional data values like integers or charcaters.
Declaring and Initializing Pointers
The declaration of a pointer specifies the data type of the value it points to, followed by the dereference operator * and the pointer's name. The type informs the compiler how many bytes to read during dereference and how pointer arithmetic should behave.
int value = 50;
int* ptr_value = &value; // Assigning the address of 'value' to the pointer
To interact with pointers, two core operators are used:
- Address-of operator (
&): Retrieves the memory address of a variable. - Dereference operator (
*): Accesses the value stored at the memory address held by the pointer.
printf("Address: %p\n", ptr_value);
printf("Value: %d\n", *ptr_value); // Outputs 50
Pointer Size and Architecture
The size of a pointer is not determined by the data type it points to, but rather by the system architecture. On a 32-bit system, memory addresses are 32 bits (4 bytes), while 64-bit systems use 64-bit addresses (8 bytes). All pointer types share the same size on a given platform.
printf("Size of char*: %zu\n", sizeof(char*));
printf("Size of int*: %zu\n", sizeof(int*));
printf("Size of double*: %zu\n", sizeof(double*));
Pointer Step Size and Type Significance
The data type of a pointer dictates the step size during pointer arithmetic. When you increment a pointer, its internal address increases by the sizeof its pointed type.
int num = 10;
char* char_ptr = (char*)#
int* int_ptr = #
printf("char_ptr: %p, char_ptr+1: %p\n", char_ptr, char_ptr + 1); // Increments by 1 byte
printf("int_ptr: %p, int_ptr+1: %p\n", int_ptr, int_ptr + 1); // Increments by 4 bytes
Generic Pointers (void*)
A void* pointer is a generic pointer capable of holding the address of any data type. However, because it lacks type information, you cannot perform pointer arithmetic or direct dereferencing on a void*. It must be cast to a specific type before accessing the underlying data.
float pi = 3.14f;
void* generic_ptr = π
// float value = *generic_ptr; // Error: cannot dereference void*
float value = *(float*)generic_ptr; // Correct: explicit cast
Pointer Arithmetic and Operations
Pointers support three primary arithmetic operations:
Pointer +/- Integer
Adding or subtracting an integer shifts the pointer forward or backward by that multiple of the pointed type's size. This is commonly used for array traversal.
int dataset[5] = {10, 20, 30, 40, 50};
int* cursor = dataset; // Points to the first element
for (int i = 0; i < 5; i++) {
printf("%d ", *(cursor + i)); // Sequentially prints array elements
}
Pointer - Pointer
Subtracting two pointers yields the number of elements (of the pointed type) that exist between them. Both pointers must point into the same array object.
int calculate_length(const char* str) {
const char* start = str;
while (*str != '\0') {
str++;
}
return str - start; // Difference in addresses equals string length
}
Pointer Relational Operations
Pointers can be compared using relational operators (<, >, <=, >=) as long as they point within the same logical array structure.
int elements[6] = {1, 2, 3, 4, 5, 6};
int* end_ptr = elements + 6; // Past-the-end pointer
int* current = elements;
while (current < end_ptr) {
printf("%d ", *current);
current++;
}
Invalid Memory Access (Dangling and Wild Pointers)
Pointers that do not point to valid, accessible memory are extremely dangerous. They typically arise from three scenarios:
1. Uninitialized Pointers
A local pointer variable declared without an initializer contains garbage memory addresses.
int* wild_ptr; // Uninitialized
// *wild_ptr = 100; // Undefined behavior: writing to a random memory location
2. Out-of-Bounds Access
Traversing past the allocated boundaries of an array results in accessing unowned memory.
int buffer[4] = {0};
int* idx = buffer;
for (int i = 0; i <= 4; i++) { // Accesses one element past the array
*(idx + i) = i;
}
3. Dangling Pointers (Freed Memory)
Returning the address of a local variable or accessing memory after it has been freed.
int* create_value() {
int local_var = 42;
return &local_var; // Warning: address of local variable returned
}
Mitigating Invalid Access
Always initialize pointers. If a pointer cannot be immediately assigned a valid address, set it to NULL. Before dereferencing, verify that the pointer is not NULL. The assert macro from <assert.h> is useful for trapping invalid states during development.
#include <assert.h>
void process(int* data) {
assert(data != NULL); // Terminates program if data is NULL
*data = 99;
}
Multiple Indirection and Pass-by-Reference
Since pointers are variables themselves, they have memory addresses. A pointer that stores the address of another pointer is called a pointer-to-pointer, or a double pointer.
int quantity = 100;
int* ptr1 = &quantity;
int** ptr2 = &ptr1; // Double pointer
Modifying Caller Variables (Pass-by-Reference)
C passes arguments by value, meaning functions receive local copies. To alter a variable from the caller's scope, pass its address instead.
void reset_status(int* status_ref) {
*status_ref = 0; // Modifies the original variable
}
int main() {
int code = 1;
reset_status(&code); // Pass address
// code is now 0
return 0;
}
Function Pointers and Dispatch Tables
Functions reside in memory and have entry point addresses. A function pointer stores this address, allowing functions to be passed as arguments or stored in arrays. A "dispatch table" (or jump table) replaces lengthy switch statements by mapping indices to function pointers.
Dispatch Table Implementation
Consider an arithmetic calculation module. Instead of a sprawling switch block, define an array of function pointers indexed by operation choice.
#include <stdio.h>
typedef int (*MathOp)(int, int);
int add_func(int a, int b) { return a + b; }
int sub_func(int a, int b) { return a - b; }
int mul_func(int a, int b) { return a * b; }
int div_func(int a, int b) { return a / b; }
int main() {
// Dispatch table: index 0 is unused to match menu choices 1-4
MathOp operations[] = { NULL, add_func, sub_func, mul_func, div_func };
int choice = 2; // Corresponds to subtraction
int x = 20, y = 5;
if (choice >= 1 && choice <= 4) {
int result = operations[choice](x, y);
printf("Result: %d\n", result); // Outputs 15
}
return 0;
}