C Pointers & Memory Addresses: The Complete Beginner-to-Expert Guide (C23)

C Pointers & Memory Addresses: The Complete Beginner-to-Expert Guide (C23)
Table of Contents
- What Is a Memory Address?
- Declaring and Initializing Pointers
- The Address-Of Operator (&)
- The Dereference Operator (*)
- Pointer Types and Type Safety
- Modifying Variables Through Pointers
- NULL Pointers and Safety (Including C23 nullptr)
- Pointers to Structs: The Arrow Operator
- Double Pointers (Pointer to Pointer)
- Common Pointer Errors: Dangling, Wild, and NULL Dereference
- const Correctness with Pointers
- Frequently Asked Questions
- Key Takeaway
What Is a Memory Address?
Your computer's RAM is a long sequence of bytes, each with a unique numeric address. On a modern 64-bit system, addresses are 64-bit numbers (usually written in hexadecimal, like 0x7ffdf3a02b40). Every variable in your program occupies one or more consecutive bytes, and the address of a variable is the address of its first byte.
A pointer is a variable whose value is that numeric address. Instead of holding data like 42 or "hello", a pointer holds 0xAF01 — the location of data in memory.
Why is this useful? Because you can:
- Pass large data structures to functions without copying them (just pass their address — 8 bytes).
- Modify a variable from inside a function that didn't declare it.
- Build dynamic data structures (linked lists, trees) whose size is determined at runtime.
- Interact with hardware by writing to specific memory addresses (memory-mapped I/O).
Declaring and Initializing Pointers
The syntax for declaring a pointer uses * between the type and the name. The * here is part of the declaration syntax, not the dereference operator:
[!CAUTION] An uninitialized pointer contains garbage — whatever bytes happen to be in that memory location. Dereferencing an uninitialized pointer causes undefined behavior (usually a segfault). Always initialize pointers, even if just to
NULL.
Declaration style: A common C style question is whether to write int *ptr or int* ptr. Both are valid. The C community typically prefers int *ptr because * binds to the variable name, not the type — so int *a, b; declares a as a pointer and b as a plain int, which int* a, b; obscures.
The Address-Of Operator (&)
The unary & operator (read: "address of") applied to any variable returns the memory address of that variable:
The cast (void*) before &age is required by the C standard for %p — pointer values passed to printf must be void*.
The Dereference Operator (*)
When used as a unary prefix operator on a pointer variable, * means "give me the value at this address" — it follows the pointer to its target:
This is the fundamental power of pointers: you can change any variable's value from anywhere in the program, as long as you have its address.
Pointer Types and Type Safety
Pointers are typed. A int* pointer knows it points to an int. The type determines:
- How many bytes to read/write when dereferencing.
- How many bytes to skip when doing pointer arithmetic (Module 7).
This technique — reinterpreting the same bytes through a different pointer type — is called type punning and is used in network protocol parsing, binary file I/O, and serialization code.
Modifying Variables Through Pointers
The most common practical use of pointers is passing them to functions so those functions can modify the caller's variables — since C is pass-by-value, this is the only way to modify a variable from another scope:
This is the same reason that in C, scanf uses & — scanf("%d", &x) — it needs to write into x, so it needs x's address.
NULL Pointers and Safety (Including C23 nullptr)
NULL is a macro defined as (void*)0 — a pointer value that is guaranteed not to point to any valid object. It is the "safe" uninitialized state for a pointer:
C23: nullptr Keyword
C23 introduces nullptr as a typed null pointer constant (similar to C++11). Unlike NULL (which is (void*)0), nullptr has type nullptr_t and is implicitly convertible to any pointer type without a cast:
The advantage of nullptr over NULL is type safety: you cannot accidentally assign nullptr to an integer, but you can accidentally assign NULL (since NULL can be 0).
Pointers to Structs: The Arrow Operator
When you have a pointer to a struct, you typically access its members with the arrow operator ->, which is shorthand for "dereference then access":
The -> operator is syntactic sugar: emp->salary is exactly equivalent to (*emp).salary. The arrow form is universally preferred for readability.
Double Pointers (Pointer to Pointer)
A double pointer (**) stores the address of another pointer. This is needed when you want a function to modify a pointer variable in the caller's scope (just as a single * is needed to modify a regular int):
Double pointers are also fundamental for:
- Dynamically-allocated 2D arrays:
int **matrix - Arrays of strings:
char **argv(the command-line arguments parameter inmain) - Linked list operations that need to modify the head pointer
Common Pointer Errors
1. Dangling Pointer
2. Wild Pointer (Uninitialized)
3. NULL Dereference (Segfault)
4. Double Free
Best practice after every free:
const Correctness with Pointers
const with pointers has four permutations. Understanding each is essential for writing clean, safe C APIs:
The rule: Use const int * for function parameters when the function must not modify the data it receives. This documents intent, prevents bugs, and allows the compiler to pass the pointer to other const-correct functions.
Frequently Asked Questions
What is a Segmentation Fault?
A segfault occurs when your code accesses memory that the OS has not granted to your process — most commonly by dereferencing a NULL or garbage pointer, accessing an array out of bounds, or using a freed pointer. The OS detects this via the Memory Management Unit (MMU) and terminates your program with SIGSEGV. Use GDB's backtrace command after a segfault to find exactly which line caused it.
Why are C pointers 8 bytes on 64-bit systems?
On a 64-bit system, the CPU can address 2⁶⁴ memory locations. Each unique address requires 64 bits (8 bytes) to represent. This is why sizeof(int*) is 8 on a 64-bit system regardless of what the pointer points to — all pointers are the same size.
Is void* safe to use for generic containers?
void* is the C mechanism for generic programming (type-erased pointer). It is used in malloc (void*), qsort (comparator receives void*), and generic data structures. The trade-off is complete loss of type safety at the container boundary — you must cast carefully and document the contract.
What tool can I use to find pointer bugs automatically?
Use AddressSanitizer (-fsanitize=address) during development — it detects heap overflows, stack overflows, use-after-free, and NULL dereferences with minimal performance overhead. Valgrind provides deeper analysis. Both tools are free and invaluable for production C development.
Can I have a pointer to a function? Yes — and this is one of C's most powerful features. Function pointers allow you to store and call functions dynamically, build callback systems, implement virtual dispatch tables (essentially C's version of polymorphism), and write plugin architectures. We cover function pointers in depth in Module 13.
Key Takeaway
Pointers are not magic — they are just variables that hold memory addresses. Once you internalize that *ptr means "the value at the address stored in ptr" and &var means "the address of the variable var," the entire pointer system becomes intuitive.
Pointers are the bridge between Logic and Hardware. They are what allows C to be simultaneously a high-level and a machine-level language — writing the same code that runs in a browser's rendering engine and an automotive brake controller. Master them, and you master C.
Read next: Dynamic Memory & The Heap: Malloc Deep Dive →
Part of the C Mastery Course — 30 modules covering everything from toolchains to kernel-level systems programming.
