Pointers & Memory · beginner · ~12 min
By the end of this lesson you will be able to: - Explain what a pointer is: a variable whose value is the *address* of another value. - Use the address-of operator `&` to obtain the address of a variable. - Use the dereference operator `*` to read and write the value stored at an address. - Declare pointers of the correct type (`int *`, `char *`, `double *`) and explain why the type matters. - Pass a pointer to a function so the function can change the caller's data in place. - Recognise and avoid the three deadly pointer bugs: NULL dereference, uninitialised (wild) pointers, and use-after-free.
So far, in Variables, you learned that a variable is a named box in memory that holds a value: int x = 7; reserves space and puts the number 7 in it. But that box also lives at a specific location in memory — an address — just like a house on a street has a number.
A pointer is simply a variable that stores one of those addresses instead of a plain value. If a normal variable answers the question "what is the value?", a pointer answers "where does the value live?".
Three small pieces of notation do almost all the work:
&x — the address-of operator. It gives you the address where x lives.int *p = &x; — a pointer declaration. It creates a pointer named p and stores the address of x in it. We say "p points to x."*p — the dereference operator. It follows the pointer and gives you the value at that address. You can read it (int y = *p;) or write it (*p = 42;).Why bother? Because pointers let different parts of a program share and modify the same data without copying it around. A function normally receives copies of its arguments, so it cannot change the originals. Hand it a pointer instead, and it can reach back and edit the caller's variable directly. Pointers are also how you walk through linked lists, trees, arrays, and text — anything where you follow one piece of data to the next.
In plain terms first: a pointer is a note that says "the thing you want is over there." In C terminology: a pointer is a typed variable holding a memory address, and dereferencing it accesses the object at that address.
Pointers are the mechanism that lets the parts of a C program cooperate. Almost every non-trivial C function takes or returns a pointer.
scanf("%d", &n) fills in your n, and how a swap function actually swaps.FILE *) whose internals you never see, so they can change them without breaking your code.Understanding pointers is the dividing line between writing toy C programs and writing real ones.
Definition. Memory is a huge array of bytes, each with a numbered position called an address. Every variable occupies one or more bytes starting at some address.
Plain explanation. Think of memory as a street of numbered mailboxes. int x = 7; puts the value 7 into a group of mailboxes and remembers where they start. The address is that starting mailbox number.
How it works internally. When you write int x = 7;, the compiler picks a location (say address 0x7ffe10) and reserves 4 bytes there (a typical int size). The name x is just a convenient label for that address.
Address Contents Name
0x7ffe10 --> [ 0x07 0x00 0x00 0x00 ] x (the int 7, little-endian)
0x7ffe18 --> [ .. .. .. .. .. .. .. .. ] p (will hold an address)
Knowledge check. In your own words, what is the difference between the value of a variable and the address of a variable?
&Definition. &x evaluates to the address of x. Its result is a pointer.
Plain explanation. & means "give me the location of", not the value. x is 7; &x is where 7 is stored.
How it works. The compiler already knows where x lives, so &x simply produces that number, typed as "pointer to the same type as x". For an int x, &x has type int *.
When to use it / when not. Use & when a function or pointer needs the location of your variable (e.g. scanf("%d", &n)). You cannot take the address of a temporary expression: &(a + b) is illegal because a + b has no home in memory.
Common pitfall. Forgetting & in scanf: writing scanf("%d", n) passes the value of n as if it were an address, which corrupts memory.
*Definition. int *p; declares p as a pointer to int. *p (on an existing pointer) is the value stored at the address p holds.
Plain explanation. The * symbol wears two hats. In a declaration (int *p;) it means "p is a pointer". In an expression (*p) it means "follow the pointer and use what's there". This is the single most confusing thing about C pointers, so read the two uses carefully.
How it works internally. int *p = &x; copies the address of x into p. Later, *p looks up that address and, because p is typed int *, reads 4 bytes and interprets them as an int. Writing *p = 42; stores 42 into those same bytes — so x becomes 42, because p points at x.
int x = 7;
int *p = &x;
p x
[ 0x7ffe10 ] ------> [ 7 ]
holds an the int p
address points to
*p reads/writes the box on the right
When to use it / when not. Dereference only when the pointer holds a valid address of a live object. Never dereference a pointer that is NULL, uninitialised, or points to memory that has been freed.
Common pitfall. Confusing p with *p. p is the address; *p is the value there. p = 42; overwrites the address (dangerous); *p = 42; writes 42 into the pointed-to variable (usually what you want).
Knowledge check. Predict the output:
int a = 3;
int *q = &a;
*q = *q + 1;
printf("%d\n", a);
Definition. int *, char *, and double * are distinct types. A pointer's type records what kind of thing it points to.
Plain explanation. The type answers "how many bytes at that address, and how should they be interpreted?" An int * reads 4 bytes as an integer; a char * reads 1 byte as a character.
Why it matters (two jobs).
| Job | What the type controls |
|---|---|
| Dereferencing | How many bytes *p reads/writes and how it interprets them |
| Pointer arithmetic | How far p + 1 moves — by the size of the pointed-to type (covered in the next lesson) |
When to use / not. Keep pointer types matched to their targets. The compiler will warn you if you assign an int * to a char *. Do not "fix" a warning with a cast unless you truly understand the memory layout — a wrong cast reinterprets bytes and produces garbage or crashes.
Common pitfall. Placement of * in multi-declarations: int *a, b; declares a as int * but b as a plain int, not a pointer. To get two pointers: int *a, *b;.
Definition. NULL is a special pointer value meaning "points to nothing." It is guaranteed distinct from the address of any real object.
Plain explanation. NULL is the pointer equivalent of a blank note — there is no address written on it, so you must not follow it.
How it works. NULL compares equal to 0 and is falsy in an if, so if (p) means "if p is not NULL". Functions that can fail (like malloc) return NULL to signal failure.
When to use / not. Initialise pointers you cannot yet point somewhere to NULL, and always check before dereferencing. Never dereference NULL — it triggers a segmentation fault.
Common pitfall. Assuming a function succeeded. Always check if (p == NULL) { /* handle error */ } after allocation or lookups before using the result.
Knowledge check. Find the bug:
int *p = NULL;
*p = 5;
Why does this crash, and what one line before *p = 5; would fix it?
int x = 7; // an ordinary int variable
int *p; // declare p as "pointer to int" (the * binds to the name)
p = &x; // store the ADDRESS of x in p -> "p points to x"
int *q = &x; // declare and initialise in one step (preferred)
printf("%d\n", *p); // 7 — dereference: READ the int at p's address
*p = 42; // dereference: WRITE 42 into that int (x is now 42)
printf("%p\n", (void *)p); // print the address itself, with %p and a void* cast
int *n = NULL; // a pointer that points to nothing yet
if (n != NULL) { // ALWAYS guard before dereferencing
*n = 1;
}
Key points:
* marks the variable as a pointer. In an expression, * dereferences.& takes an address; * follows one. They are opposites.int * for an int.%p prints an address; cast the pointer to void * for a portable, warning-free print.A pointer holds the address of another value.
Use the form type *name;.
& (address-of) gives you a pointer to its operand.* (dereference, when used on a value rather than in a declaration) gives you the value stored at an address.The pointer's type tells the compiler what it points at. For example, int *p knows its target is an int. That controls how *p reads from and writes to memory.
#include <stdio.h>
/* Adds 'amount' to the int that 'value' points to.
Because we receive a pointer, the change is visible to the caller. */
void add_to(int *value, int amount) {
if (value == NULL) { /* defensive: never dereference NULL */
return;
}
*value += amount; /* write through the pointer: caller's variable changes */
}
int main(void) {
int score = 7;
int *p = &score; /* p now points to score */
printf("score = %d\n", score); /* 7 */
printf("*p (value) = %d\n", *p); /* 7 — same data, read via pointer */
printf("p (addr) = %p\n", (void *)p);
printf("&score = %p\n", (void *)&score); /* same address as p */
*p = 20; /* write through the pointer */
printf("after *p=20, score = %d\n", score); /* 20 */
add_to(&score, 5); /* pass the ADDRESS so the function can edit score */
printf("after add_to(+5), score = %d\n", score); /* 25 */
int *n = NULL; /* a pointer to nothing */
add_to(n, 100); /* safe: the NULL check inside returns early */
printf("score is still = %d\n", score); /* 25, unchanged */
return 0;
}
What it does. It creates score, points p at it, and shows that reading *p gives the same value as score and that p holds the same address as &score. Writing through *p changes score. The add_to function receives a pointer and edits the caller's variable in place — and safely ignores a NULL pointer.
Expected output (the two addresses match each other; their exact hex value varies per run):
score = 7
*p (value) = 7
p (addr) = 0x7ffde1a2c4bc
&score = 0x7ffde1a2c4bc
after *p=20, score = 20
after add_to(+5), score = 25
score is still = 25
Edge cases. The actual addresses differ every run (and with address-space layout randomisation) — only their equality is guaranteed. Passing NULL to add_to is handled gracefully; without the guard it would segfault.
void add_to(int *value, int amount) — declares a function taking a pointer to int and an int. Whatever address the caller passes, value receives a copy of that address.if (value == NULL) return; — a defensive guard. If the caller passed NULL, we return immediately instead of crashing.*value += amount; — dereferences value and adds amount to the pointed-to int. This edits the caller's variable, not a local copy.int score = 7; — allocates an int on the stack and stores 7.int *p = &score; — &score produces score's address; p now holds it. p and &score are equal from here on.printf(... *p ...) — dereferences p, reads 4 bytes as an int, prints 7.%p lines print p and &score — the same address, proving p really points at score.*p = 20; — writes 20 into the bytes at p's address, i.e. into score. So score becomes 20.add_to(&score, 5); — passes the address of score. Inside, *value += 5 turns 20 into 25.int *n = NULL; then add_to(n, 100); — the guard returns early, so nothing is written and score stays 25.Trace of score:
| Step | Statement | score before |
score after |
|---|---|---|---|
| 4 | int score = 7; |
— | 7 |
| 8 | *p = 20; |
7 | 20 |
| 9 | add_to(&score, 5); |
20 | 25 |
| 10 | add_to(NULL, 100); |
25 | 25 |
The key idea: whenever code writes through a pointer that points at score, score itself changes — there is only one copy of the data.
1. Forgetting & in scanf.
int n;
scanf("%d", n); // WRONG: passes the (garbage) value of n as an address
Why it's wrong: scanf needs to know where to store the input, so it needs an address. Passing n's value makes it write to a random location.
scanf("%d", &n); // CORRECT: pass the address of n
Prevent it: whenever a function must fill in your variable, pass &variable. Enable compiler warnings (-Wall) — most compilers flag this.
2. Confusing declaration * with dereference *.
int *p = &x;
*p = 5; // writes 5 into x (dereference)
p = 5; // WRONG: overwrites the address with the number 5
Why it's wrong: p = 5; makes p point at address 5, which you don't own. Recognise it: assigning a plain integer to a pointer draws a warning like "assignment makes pointer from integer without a cast".
3. The multi-declaration trap.
int *a, b; // a is int*, but b is a plain int — probably not what you meant
Corrected:
int *a, *b; // both are pointers
Prevent it: declare pointers one per line, or repeat the *.
4. Dereferencing an uninitialised pointer.
int *p; // p holds garbage
*p = 10; // WRONG: writes to a random address -> crash or corruption
Corrected:
int x;
int *p = &x; // p points somewhere valid
*p = 10; // fine
Prevent it: initialise every pointer to NULL or a valid address the moment you declare it.
5. Expecting a by-value function to change the caller.
void reset(int v) { v = 0; } // edits only the local copy
int x = 9; reset(x); // x is still 9
Corrected: pass a pointer.
void reset(int *v) { *v = 0; }
int x = 9; reset(&x); // x is now 0
Compiler errors and warnings (turn on -Wall -Wextra):
p = 5; instead of *p = 5;.char *p = &some_int;).&n where a value was expected, or vice versa.Runtime errors:
Segmentation fault (Linux/macOS) means you dereferenced a bad pointer — usually NULL, uninitialised, or freed. Add a print of the pointer just before the crash line: printf("p=%p\n", (void *)p);. If it prints (nil) or 0x0, it's a NULL dereference.Inspecting pointers in a debugger (gdb / lldb):
print p # shows the address stored in p
print *p # shows the value at that address
print &x # shows x's address (compare with p)
info locals # lists all local variables and their values
If print p shows 0x0, you found your NULL. If it shows a wild-looking value that isn't near your other stack addresses, p was never initialised.
Questions to ask when it doesn't work:
p actually hold a valid address? Print it.p (the address) with *p (the value)?&x where an address was needed, or x where a value was needed?p be NULL because an allocation or lookup failed?An Address Sanitizer build (gcc -fsanitize=address) will pinpoint the exact line for many pointer bugs.
Pointers are powerful because they touch memory directly — which is also why they cause most C crashes and security bugs. Three classic hazards, and how to avoid each:
1. NULL dereference. Reading or writing through NULL is undefined behaviour and typically crashes.
if (p != NULL) { // guard first
*p = 1;
}
Always check the result of anything that can return NULL (like malloc, fopen, list lookups) before using it.
2. Uninitialised / wild pointers. A pointer declared without a value holds whatever bytes were on the stack. Dereferencing it reads or writes a random address.
int *p = NULL; // give it a known value immediately
3. Use-after-free and dangling pointers. After free(p), the memory is gone; using p is undefined behaviour. Also, returning the address of a local variable creates a dangling pointer, because the local dies when the function returns.
free(p);
p = NULL; // so a later accidental *p fails loudly instead of silently
int *bad(void) {
int local = 5;
return &local; // WRONG: local ceases to exist after return -> dangling pointer
}
Bounds and initialisation. Dereferencing a pointer reads/writes based on its type, so the pointer must point at a real object of that type. Never dereference past the object you were given.
Rules of thumb:
NULL or a valid address at declaration.NULL right after free.-Wall -Wextra -fsanitize=address while learning.Where pointers show up in real software:
scanf("%d", &n) fills your variable through a pointer; strtol reports where it stopped parsing via an out-pointer; FILE * is an opaque pointer to file state.qsort takes one).Professional best-practice habits:
| Habit | Beginner focus | Advanced focus |
|---|---|---|
| Naming | p/q are fine for tiny scopes |
Name by role: head, cursor, out_len |
| Initialisation | Always init to NULL or a valid address |
Use const T * for read-only params to document intent |
| Validation | Null-check before dereferencing | Validate ownership/lifetime; document who frees what |
| Error handling | Check malloc/fopen for NULL |
Fail fast, clean up partial state, no leaks |
| Cleanup | free then set to NULL |
Track ownership so each allocation is freed exactly once |
A good habit from day one: mark pointers you won't modify through as const int * — it tells readers (and the compiler) that the function only reads the data. Exercise deref uses exactly this pattern.
Beginner 1 — Point and read.
Objective: prove to yourself that a pointer and its target share data.
Requirements: declare int x = 42; and int *p = &x;. Print x, *p, &x, and p (cast addresses to void * and use %p). Confirm the two addresses match.
Expected: the two addresses print identically; x and *p both print 42.
Concepts: &, *, pointer declaration. Hint: use %d for values and %p for addresses.
Beginner 2 — Write through a pointer.
Objective: change a variable without naming it directly.
Requirements: given int count = 0; and a pointer to it, use only the pointer (never count on the left-hand side) to set the value to 10, then to add 5. Print count at the end.
Expected output: 15.
Concepts: dereference-and-write *p = ..., *p += ....
Intermediate 1 — Swap two ints.
Objective: write a function that swaps its caller's variables.
Requirements: implement void swap(int *a, int *b); that exchanges the two ints. In main, set x=1, y=2, call swap(&x, &y), and print them.
Expected output: x=2 y=1.
Constraints: no global variables; use a temporary local. Concepts: pointers as out-parameters. Hint: you cannot swap by value — that's the whole point.
Intermediate 2 — Min and max via out-parameters.
Objective: return two results from one function.
Requirements: implement void min_max(const int *arr, int n, int *out_min, int *out_max);. It scans n elements and writes the smallest to *out_min and largest to *out_max. Guard against n <= 0 and any NULL output pointer.
Input/Output example: for {3, 9, -1, 4}, *out_min = -1, *out_max = 9.
Concepts: const input pointer, multiple out-parameters, NULL checks. Hint: initialise both from arr[0] before the loop.
Challenge — Safe reset with cleanup.
Objective: combine everything with defensive habits.
Requirements: write int *make_counter(void) that mallocs one int, sets it to 0 via a pointer, and returns it (return NULL on allocation failure). Write void bump(int *c) that adds 1 through the pointer if c is non-NULL. In main, create a counter, bump it three times, print it (expect 3), then free it and set the pointer to NULL.
Constraints: check malloc for NULL; no memory leaks (build with -fsanitize=address to verify); never dereference after freeing.
Concepts: malloc/free, NULL checks, dereference read/write, use-after-free avoidance. Hint: the freeing code and the NULL-after-free assignment belong together.
&x gives you an address; *p follows one.& (address-of) and * (dereference) are opposites. In a declaration, int *p marks p as a pointer; in an expression, *p reads or writes the value it points to.int *p = &x; (point at x), *p (the value), p (the address). Writing *p = v; changes the pointed-to variable — there is only one copy of the data.int * vs char * control how bytes are interpreted and (next lesson) how far arithmetic moves. Watch the int *a, b; multi-declaration trap.&x), avoid big copies, and return multiple values — which is why they are everywhere in real C.NULL or a valid address, null-check before dereferencing, and set to NULL right after free. Build with -Wall -Wextra -fsanitize=address while you learn.