Pointers, Addresses, and Memory: C's Most Crucial and Sharpened Tool
Pointers are the core of C.
They empower functions to modify external objects, unify the handling of arrays and dynamic memory, and unlock direct hardware interfaces and system calls.
However, pointers are also the primary catalyst for crashes, out-of-bounds access, dangling references, and security vulnerabilities.
Learning pointers is not merely memorizing the syntax of * and &; you must internalize their profound implications on memory and lifecycles.
What is an Address?
When a program runs, objects occupy segments of storage in memory. An address is used to locate this storage segment.
int x = 42;
printf("%p\n", (void*)&x);
&x acquires the address of x.
To print an address, use %p and cast it to void*.
The address value itself should never be treated as a casual integer for arbitrary math.
Pointer Variables Store Addresses
int x = 42;
int* p = &x;
p is a pointer to an int.
It stores the address of x.
p ─────> x
42
How to read it: p points to x.
The type int* signifies that when you dereference this pointer, the target storage will be interpreted as an int.
Dereferencing to Access the Target Object
int x = 42;
int* p = &x;
printf("%d\n", *p);
*p = 100;
*p dictates accessing the object pointed to by p.
The first instance reads x.
The second instance mutates x.
If p does not point to a valid int object, dereferencing it is a severe risk.
Null Pointers Indicate "Pointing to Nothing"
int* p = NULL;
Dereferencing a null pointer yields undefined behavior. You must check it first.
if (p != NULL) {
*p = 1;
}
Short-circuit logic can guard dereferences nicely.
if (p != NULL && *p > 0) {
use(*p);
}
The order must not be reversed.
Pointers and Function Parameters
C passes parameters by value. Passing a pointer allows a function to modify an object belonging to the caller.
void set_value(int* out, int value) {
if (out != NULL) {
*out = value;
}
}
Invocation:
int x = 0;
set_value(&x, 10);
The parameter name out communicates that this is an output parameter.
Naming conventions help express ownership and data flow direction.
Pointers and Arrays Are Intimate But Not Identical
An array name in many expressions decays into a pointer to its first element.
int values[3] = {1, 2, 3};
int* p = values;
p[1] is entirely equivalent to *(p + 1).
printf("%d\n", p[1]);
However, an array is not a pointer.
sizeof values and sizeof p produce completely different results.
Pointer Arithmetic Moves by Element Size
int values[3] = {1, 2, 3};
int* p = values;
++p;
p moves forward by one int, not by one byte.
If an int is 4 bytes, the address typically increments by 4.
Pointer arithmetic is only safe within the bounds of a single array object. An out-of-bounds pointer must never be dereferenced.
void* is a Typeless Address
void* can store a pointer to any object type.
However, it cannot be dereferenced directly because the compiler has no knowledge of the target type's size.
void* raw = &x;
int* p = raw;
printf("%d\n", *p);
malloc returns void*.
It essentially says: "Here is a block of storage; you need to cast it and interpret it with a specific type."
Pointers to Pointers (Double Pointers)
A pointer itself is an object and has its own address. A pointer pointing to another pointer is a double pointer.
int x = 1;
int* p = &x;
int** pp = &p;
A common use case is allowing a function to modify a caller's pointer.
int allocate_int(int** out) {
*out = malloc(sizeof **out);
return *out == NULL ? -1 : 0;
}
Double pointers reduce readability. When using them, ensure the naming and release protocols are crystal clear.
Pointers and const
const int* p = &x;
You cannot modify x through p.
int* const q = &x;
The pointer q itself cannot be repointed elsewhere.
const int* const r = &x;
Neither the pointer itself nor the target object can be modified via r.
Practice these declarations.
They are essential tools in C APIs for expressing read-only borrows.
Dangling Pointers
Once the object a pointer points to ends its lifecycle, the pointer becomes a dangling pointer.
int* bad(void) {
int x = 1;
return &x;
}
After the function returns, x ceases to exist.
The returned address must not be used.
The same applies after freeing heap memory:
int* p = malloc(sizeof *p);
free(p);
*p = 1; // use-after-free
This is a critical security vulnerability.
A Pointer is Not Ownership
A raw pointer merely states: "Here is an address." It does not automatically imply who is responsible for freeing it.
char* read_file(const char* path);
void free_buffer(char* buffer);
This pair of functions uses naming to express ownership. A pointer API without a defined release protocol is a ticking time bomb.
Debugging Pointers
When debugging pointer issues, ask yourself:
- Is the pointer initialized?
- Is it null?
- Does it point to the correct object type?
- Is the object's lifecycle still valid?
- Did it cross an array boundary?
- Has it been freed?
- Are there other aliased pointers modifying it simultaneously?
Tooling:
clang -std=c23 -g -O1 -fsanitize=address,undefined pointer.c
AddressSanitizer (ASan) quickly detects many out-of-bounds and use-after-free errors. However, it cannot design ownership architectures for you.
Common Pointer Patterns
Output parameters:
int parse_int(const char* text, int* out);
Read-only borrows:
void print_name(const char* name);
Writable buffers:
int read_bytes(unsigned char* buffer, size_t capacity);
Opaque handles:
typedef struct Engine Engine;
Engine* engine_create(void);
void engine_destroy(Engine* engine);
Every pattern demands explicit documentation of lifecycles and release responsibilities.
Engineering Risks
Common pointer risks:
- Uninitialized pointers.
- Null pointer dereferences.
- Array out-of-bounds access.
- Returning addresses of local variables.
- Use-after-free.
- Double-free.
- Pointer type casting violating aliasing rules.
- Passing pointers without lengths.
- APIs lacking ownership documentation.
- Concurrent multi-thread access to the pointed-to object.
These errors rarely manifest exactly when they occur. Once a pointer API crosses thread or module boundaries, you must establish strict concurrency limits, resource release duties, and observation logs. Otherwise, the crash point will inevitably be far removed from the actual erroneous write. Therefore, combine clear APIs, boundary checks, and sanitizers to build a robust defense.
Summary
Pointers are the strongly-typed expression of addresses.
& acquires an address.
* dereferences an address.
Pointer arithmetic moves by element size.
Null pointers, dangling pointers, and out-of-bounds pointers are inherently unsafe to use.
Mastering pointers isn't about mastering a few symbols; it's about verifying—before every single access—that the object still exists, the type is correct, the boundaries are respected, and ownership is unambiguous.