C Pointers: A Complete Guide
Pointers are the most powerful and most dangerous feature of C. A pointer is a variable that stores a memory address. Understanding pointers is essential for systems programming, embedded development, and writing efficient code. Every non-trivial C program uses pointers — for dynamic memory, array traversal, function callbacks, and building data structures.
C gives you direct access to memory through addresses. This level of control is why C remains the language of choice for operating systems, embedded firmware, and performance-critical applications.
Pointer Basics
A pointer is declared with * and holds the address of another variable:
int x = 42;
int *ptr = &x; // ptr stores the address of x
printf("%d\n", *ptr); // Dereference: prints 42
*ptr = 100; // Modify x through ptr
printf("%d\n", x); // Prints 100
The & operator gets the address of a variable. The * operator dereferences a pointer — accessing the value at the stored address. A pointer has a type because the compiler needs to know how many bytes to read when dereferencing.
NULL Pointers
A pointer that does not point to valid memory should be set to NULL:
int *ptr = NULL;
if (ptr != NULL) {
*ptr = 42; // Safe: only dereference if not NULL
---Dereferencing a NULL pointer causes undefined behavior — typically a segmentation fault. Always initialize pointers and check for NULL before dereferencing. Calloc and malloc return NULL on failure, which means every allocation must be checked.
Pointer Arithmetic
You can perform arithmetic on pointers. The compiler scales the operation by the size of the pointed-to type:
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // Points to arr[0]
printf("%d\n", *p); // 10
p++; // Advances by sizeof(int) bytes
printf("%d\n", *p); // 20
p += 2; // Advance two more ints
printf("%d\n", *p); // 40
Pointer arithmetic works for any type. p + n gives the address of the n-th element after p. This is how array indexing works internally: arr[i] is syntactic sugar for *(arr + i).
Subtraction and Difference
int arr[] = {10, 20, 30, 40, 50};
int *start = &arr[0];
int *end = &arr[4];
ptrdiff_t count = end - start; // 4 (elements between)
Subtracting two pointers of the same type gives the number of elements between them. The result type is ptrdiff_t, defined in <stddef.h>. Only subtract pointers that point into the same array — subtraction between unrelated pointers is undefined behavior.
Pointers and Arrays
Arrays and pointers are closely related. An array name decays to a pointer to its first element in most contexts:
int arr[5] = {1, 2, 3, 4, 5};
// These are equivalent:
printf("%d\n", arr[2]); // 3
printf("%d\n", *(arr + 2)); // 3
// The array name is not a modifiable lvalue:
// arr = &x; // Compile error
The decay means that passing an array to a function passes a pointer — no copy occurs. This is efficient but means the function does not know the array size unless you pass it separately.
Array of Pointers vs Pointer to Array
int *arr_of_ptrs[5]; // Array of 5 int pointers
int (*ptr_to_arr)[5]; // Pointer to an array of 5 ints
The parentheses change everything. arr_of_ptrs is an array where each element is an int*. ptr_to_arr is a single pointer that points to an entire array of 5 ints. Understanding this distinction is critical when working with multi-dimensional arrays.
Function Pointers
Pointers can store the address of a function, enabling callbacks and dynamic dispatch:
#include <stdio.h>
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int compute(int (*op)(int, int), int x, int y) {
return op(x, y);
---
int main() {
int (*func_ptr)(int, int) = &add;
printf("%d\n", compute(func_ptr, 5, 3)); // 8
printf("%d\n", compute(multiply, 5, 3)); // 15
return 0;
---The & is optional when assigning a function name — function names decay to pointers just like arrays. Function pointers are typed by their signature: int (*)(int, int) is a pointer to a function taking two ints and returning an int. The parentheses around *func_ptr are mandatory — without them, int *func_ptr(int, int) declares a function returning an int*.
Arrays of Function Pointers
int (*operations[])(int, int) = {add, multiply, subtract, divide};
for (int i = 0; i < 4; i++) {
printf("%d\n", operations[i](10, 5));
---Arrays of function pointers power state machines, command dispatch tables, and plugin systems in C.
Dynamic Memory Allocation
Pointers are essential for working with heap memory:
#include <stdlib.h>
int *arr = malloc(10 * sizeof(int));
if (arr == NULL) {
// Handle allocation failure
return 1;
---
for (int i = 0; i < 10; i++) {
arr[i] = i * i;
---
free(arr); // Always free allocated memory
malloc returns a void*, which implicitly converts to any pointer type in C (unlike C++). Always check the return value — malloc returns NULL when memory is exhausted. Free memory exactly once and set the pointer to NULL after freeing to prevent use-after-free and double-free bugs.
Common Pointer Pitfalls
Dangling Pointers
A pointer that references freed or out-of-scope memory:
int *ptr = malloc(sizeof(int));
free(ptr);
*ptr = 42; // Undefined behavior — use-after-free
After free, the pointer still holds the old address. Using it is undefined behavior. Always NULL a pointer after freeing:
free(ptr);
ptr = NULL;Pointer to Local Variable
int *bad_func(void) {
int x = 42;
return &x; // Returns pointer to local — destroyed when function returns
---The local variable x goes out of scope when the function returns. The returned pointer is dangling. Either allocate on the heap or pass a pointer to a buffer owned by the caller.
Buffer Overflow
int arr[5];
*(arr + 100) = 42; // Writing past the end — undefined behavior
C does not perform bounds checking. Writing past the end of an array corrupts adjacent memory, leading to crashes or security vulnerabilities. Tools like AddressSanitizer and Valgrind detect these bugs at runtime.
Pointer Type Mismatch
int x = 42;
double *dp = (double*)&x; // Forcing a reinterpretation
*dp = 3.14; // Undefined behavior — strict aliasing violation
C’s strict aliasing rule says you should not access an object through a pointer of a different type (except through char*). Violating this rule can cause the optimizer to generate incorrect code.
Best Practices
Initialize all pointers — An uninitialized pointer holds a garbage address. Dereferencing it is undefined behavior. Always set pointers to NULL or a valid address at declaration.
Check return values — Every malloc, calloc, and realloc can fail. Every function that takes a pointer parameter should document whether NULL is valid or not.
Use const for read-only pointers — const int *p means the pointed-to value cannot be modified through p. This catches accidental writes at compile time.
Prefer sizeof on the variable — ptr = malloc(sizeof(*ptr)) is less error-prone than ptr = malloc(sizeof(int)) because it stays correct if the type changes.
Use tools — Compile with -Wall -Wextra -fsanitize=address and run Valgrind during testing. These catch most pointer-related bugs.
Pointers to Pointers
A pointer to a pointer (int **ptr) stores the address of another pointer variable. This is commonly used for:
- Modifying pointer arguments in functions — When a function needs to allocate memory and return it through a parameter, you pass a pointer to the pointer so the function can modify the original pointer.
void create_array(int **arr, int size) {
*arr = malloc(size * sizeof(int));
// caller's pointer now points to the allocated memory
---2D arrays and arrays of strings —
char **argvrepresents an array of strings. Each element points to a C string, and the array itself is terminated by a NULL pointer.Dynamic arrays of pointers — Useful for data structures like hash tables where each bucket contains a linked list accessible through a pointer.
Arrays and Pointer Arithmetic
Array indexing is defined in terms of pointer arithmetic: a[i] is equivalent to *(a + i). The array name decays to a pointer to its first element in most contexts:
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p points to arr[0]
p++; // p points to arr[1]
*(p + 2) = 99; // sets arr[3] to 99
Pointer arithmetic automatically scales by the size of the pointed-to type. p + n advances by n * sizeof(*p) bytes. This type-awareness is what distinguishes pointers from raw memory addresses.
Function Pointers
Function pointers store the address of a function and allow dynamic dispatch. They are essential for callbacks, plugin systems, and implementing polymorphic behavior in C:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int (*operation)(int, int); // function pointer declaration
operation = add;
printf("%d\n", operation(5, 3)); // 8
operation = sub;
printf("%d\n", operation(5, 3)); // 2
Function pointers are heavily used in the standard library — qsort() takes a comparison function pointer, and signal handlers use function pointers for callbacks.
FAQ
What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.
How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.
What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.
Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.
What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.
Related: Learn about C memory management, C++ smart pointers, and C vs C++.