We’re Officially Live β€” Lifetime Founding Membership Available for a Limited Time
Free preview

Pointers & Addresses

A pointer is just a variable that holds an address. Master & and *, NULL, and how passing a pointer lets a function reach back and modify the caller's data.

25 min read

In this lesson:


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 x

A 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 x

The 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

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 char

The 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 NULL

The 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-scope

The 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


TL;DR

This is just the start

Sign up free to track progress on this lesson, get coding problems with the Run button, and unlock the full interview Q&A.

More in C Programming

The C Compilation PipelinePointer Arithmetic & Arrays
Dynamic Memory: malloc & free
Stack vs Heap
Structs, Unions & Bitfields
Bit Manipulation & Masks