In this lesson:
- what an address is, and what it means to "hold" one
- the two operators that define pointers:
&(address-of) and*(dereference) - declaring pointers and reading their types right-to-left
NULL, wild pointers, and dangling pointers, the three ways a pointer goes wrong- why pass-by-pointer is how a C function modifies the caller's variable
Every variable has an address
RAM is a giant array of bytes, each with a numeric address. When you declare int x = 42;, the compiler picks an address for x and stores 42 there. The & operator hands you that address:
int x = 42;
printf("%p\n", (void *)&x); // e.g. 0x7ffe... β the address of xA pointer is a variable whose value is an address. Instead of holding 42, it holds "the location where 42 lives."
x p
ββββββββββββ ββββββββββββ
β 42 β βββββββββ 0x1000 β
ββββββββββββ ββββββββββββ
addr 0x1000 p holds the address of xThe two operators
int x = 42;
int *p = &x; // p holds the address of x (& = "address of")
printf("%d\n", *p); // 42 (* = "value at this address" β dereference)
*p = 99; // write through the pointer: x is now 99
printf("%d\n", x); // 99&x, "address ofx." Turns a variable into a pointer.*p, "the thingppoints at." Turns a pointer back into the variable it references. Reading*preadsx; writing*pwritesx.
They're inverses: *&x is just x.
Reading pointer declarations
The * in a declaration binds to the variable, not the type. This trips people up:
int *a, b; // a is a pointer-to-int; b is a plain int (NOT a pointer!)
int *a, *b; // both pointers β you must repeat the *For complex types, read right to left from the variable name:
int *p; // p is a pointer to int
const int *p; // p points to a const int (can't modify *p)
int * const p; // p is a const pointer (can't change p) to int
char **pp; // pp is a pointer to a pointer to charThe type of the pointer matters: it tells the compiler how many bytes *p reads, and how p + 1 advances (covered next lesson).
NULL, the "points at nothing" value
NULL is a pointer that deliberately points nowhere. Use it to mean "no valid target yet" and always check before dereferencing:
int *p = NULL; // explicitly "no target"
if (p != NULL) {
*p = 5; // safe β only deref when non-null
}Dereferencing NULL (*p when p == NULL) is undefined behavior, on a desktop it usually segfaults; on an MCU it may read address 0 (often the start of flash or the vector table) and silently misbehave.
A handy idiom combines the null check with short-circuit evaluation:
if (p != NULL && *p > 0) { ... } // *p is never evaluated when p is NULLThe three ways a pointer goes bad
// 1. NULL pointer β points at nothing. Deref = crash.
int *a = NULL;
*a = 5; // π₯
// 2. Wild (uninitialized) pointer β holds garbage. Deref = anything.
int *b; // uninitialized: holds whatever was on the stack
*b = 5; // π₯ writes to a random address
// 3. Dangling pointer β pointed at something that no longer exists.
int *c = get_ptr(); // returns address of a freed/out-of-scope variable
*c = 5; // π₯ use-after-free / use-after-scopeThe fixes: initialize pointers (to NULL if you have no target yet), never return the address of a local (next lessons cover lifetimes), and set a pointer to NULL after freeing what it pointed to.
Why pointers exist: modifying the caller's data
Recall from the Functions lesson that C is pass-by-value, a function gets copies of its arguments, so it can't change the caller's variables. Pointers are the workaround. Pass the address, and the function can write through it:
// Doesn't work β operates on copies
void bad_swap(int a, int b) {
int t = a; a = b; b = t; // swaps the local copies only
}
// Works β operates on the originals via their addresses
void swap(int *a, int *b) {
int t = *a; // read the value at a
*a = *b; // write b's value into a's location
*b = t; // write the saved value into b's location
}
int main(void) {
int x = 3, y = 7;
swap(&x, &y); // pass addresses, not values
printf("%d %d\n", x, y); // 7 3
return 0;
}This is also how scanf("%d", &x) works, you hand it the address of x so it can write the parsed number back into your variable. The same mechanism underlies "output parameters," returning multiple values, and operating on big structures without copying them.
An embedded angle: pointing at a register
On a microcontroller, a hardware register lives at a fixed memory address. You access it by making a pointer to that address and dereferencing:
// Write 0x01 to the GPIOA output register on a hypothetical MCU.
// 'volatile' (covered later) tells the compiler not to optimize the access away.
#define GPIOA_ODR (*(volatile uint32_t *)0x48000014u)
GPIOA_ODR = 0x01; // dereference-and-write straight to the peripheral(volatile uint32_t *)0x48000014u is "treat this integer as the address of a volatile 32-bit register," and the outer * dereferences it. Memory-mapped I/O is pointers all the way down, which is why firmware engineers must be fluent here.
Gotchas
int *a, b;makes onlyaa pointer. The*binds to the declarator, not the type. Repeat it:int *a, *b;.- Uninitialized pointers hold garbage, not NULL. A bare
int *p;is a wild pointer. Initialize toNULLor a real target before use. - Dereferencing NULL or a dangling pointer is undefined behavior. On desktop it segfaults; on bare metal it may corrupt silently. Null-check, and null out pointers after free.
%pexpects avoid *. Cast when printing addresses:printf("%p", (void *)p);. Mismatched format specifiers are UB.
TL;DR
- A pointer holds an address.
&xgets a variable's address;*paccesses the value at an address (read or write). - The
*in a declaration binds to the variable:int *a, b;declares one pointer and one int. Read complex types right-to-left. NULLmeans "points at nothing", always null-check before dereferencing. The bad states are NULL, wild (uninitialized), and dangling pointers.- Passing a pointer lets a function modify the caller's variable, because the function receives the address, not a copy. This is how
swap,scanf, and output parameters work. - Memory-mapped registers are accessed by dereferencing a pointer to a fixed address, pointers are the foundation of embedded I/O.