What is a Pointer?
Every variable lives somewhere in RAM. Your computer's memory is like a long row of numbered boxes — each box has an address (a number) and holds a value. A pointer is a variable whose job is to store one of those address numbers — not a value itself, but where a value lives.
- Regular variable: holds a value —
int x = 42;→ box contains 42 - Pointer variable: holds an address —
int *p = &x;→ box contains the location of x
The house analogy — the mental model that clicks
Memory layout — int x = 42; int *p = &x;
#include <stdio.h> int main() { int x = 42; // regular int variable int *p = &x; // p is a pointer — stores the address of x printf("Value of x: %d\n", x); // 42 printf("Address of x: %p\n", &x); // 0x7fff... (changes every run) printf("Value of p: %p\n", p); // same address as &x printf("Value at *p: %d\n", *p); // 42 — follows the address return 0; }
Value of x: 42 Address of x: 0x7ffee4b2c8ac Value of p: 0x7ffee4b2c8ac ← same as &x Value at *p: 42 ← follows address, reads value
%p output will always look different — that is normal.
& and * — What Each Symbol Means
& always means one thing: "give me the address of". Simple, consistent, no exceptions.
* means two completely different things depending on context — this is where almost everyone gets confused:
- In a declaration:
int *p;— the * is part of the type — means "p is a pointer-to-int" - In an expression:
*p = 10;— the * is an action — means "go to the address stored in p and write 10"
The trick to reading pointer code — ask: is this a declaration or an expression?
| Code | Plain English — say it like this |
|---|---|
| int x = 42 | x is an int holding 42 |
| int *p | p is a pointer to an int (declaration — * = type marker) |
| p = &x | store x's address in p |
| *p | go to the address stored in p and read what's there (expression — * = dereference) |
| *p = 99 | go to that address and write 99 there (expression — * = dereference) |
| printf("%p", p) | print the address number itself — not what's there |
| printf("%d", *p) | go to the address, read the int value, print it |
#include <stdio.h> int main() { int x = 42; int *p; // DECLARATION — * means "p is a pointer to int" p = &x; // & means "address of x" // Three different things from same pointer: printf("x = %d\n", x); // 42 — value directly printf("*p = %d\n", *p); // 42 — EXPRESSION: follow address, read value printf("p = %p\n", p); // 0x... — the address NUMBER itself printf("&x = %p\n", &x); // same address — & gives address of x // x and *p are literally the same memory box! *p = 100; // EXPRESSION: write 100 through the pointer printf("After *p=100: x = %d\n", x); // x is 100 too! return 0; }
x = 42 *p = 42 p = 0x7ffee4b2c8ac &x = 0x7ffee4b2c8ac After *p=100: x = 100
Changing
*p changes x too — because p is just a second door into x's house. No copying happens. This is the entire power of pointers — you can modify a variable through its address from anywhere in the program.
Dereferencing — Following the Address
Dereferencing means following a pointer to the value it points to. The * operator in an expression is the dereference operator. It is the opposite of &:
&goes from value → address ("give me your address")*goes from address → value ("go to that address, show me what's there")
#include <stdio.h> int main() { int x = 42; int *p = &x; printf("Before: x = %d\n", x); // 42 *p = 30; // write 30 into x through pointer printf("After *p=30: x = %d\n", x); // 30 (*p)++; // increment through pointer (parentheses important!) printf("After (*p)++: x = %d\n", x); // 31 // Multiple pointers to same variable int *q = &x; // q also points to x *q = 999; printf("After *q=999: x=%d *p=%d\n", x, *p); // both 999 return 0; }
Before: x = 42 After *p=30: x = 30 After (*p)++: x = 31 After *q=999: x=999 *p=999
#include <stdio.h> // Function takes POINTER — can modify original variable void doubleIt(int *num) { *num = *num * 2; // modify the original through pointer } // Function takes VALUE — only gets a copy, cannot change original void tryDouble(int num) { num = num * 2; // changes only the LOCAL copy } int main() { int x = 10; tryDouble(x); printf("After tryDouble: x = %d\n", x); // still 10 doubleIt(&x); printf("After doubleIt: x = %d\n", x); // 20! return 0; }
After tryDouble: x = 10 ← value copy — original unchanged After doubleIt: x = 20 ← pointer — original changed!
scanf("%d", &age) — you pass the address of age so scanf can write the value directly into it from inside the function. Without the &, scanf would only receive a copy and the variable would never change.
Pointer Arithmetic — Moving Through Memory
Pointers understand the size of their type. Adding 1 to an int* moves it forward by sizeof(int) = 4 bytes — not just 1 byte. This is what links arrays and pointers so tightly in C.
p + 1→ advances 4 bytes (for int*) to the next int slotp + 2→ advances 8 bytes to the one after thatarr[i]is literally*(arr + i)— the compiler generates identical code for both
int arr[3] = {10, 20, 30} — memory layout with pointer positions
#include <stdio.h> int main() { int arr[3] = {10, 20, 30}; int *p = arr; // arr is already the address of arr[0] // Two ways — identical output printf("Array notation: %d %d %d\n", arr[0], arr[1], arr[2]); printf("Pointer notation: %d %d %d\n", *(p+0), *(p+1), *(p+2)); // Walking through array with pointer printf("Walking: "); for (int i = 0; i < 3; i++) { printf("%d ", *p); p++; // advance pointer to next element } printf("\n"); // arr[i] IS *(arr+i) — the compiler generates same code printf("arr[2] = %d | *(arr+2) = %d\n", arr[2], *(arr+2)); return 0; }
Array notation: 10 20 30 Pointer notation: 10 20 30 Walking: 10 20 30 arr[2] = 30 | *(arr+2) = 30
5 Common Pointer Bugs — Never Make These
Pointer bugs are the most dangerous in C — they often cause no compiler error but silently corrupt data, crash the program, or create security vulnerabilities. Know these five and you will avoid 90% of pointer mistakes.
// ❌ WRONG — wild pointer — undefined behaviour int *p; // p contains garbage — points anywhere *p = 10; // writing to a random address — CRASH or corruption! // ✅ CORRECT — always initialize int x = 0; int *p = &x; // p points to x — safe to use // OR if you don't have a variable yet: int *p2 = NULL; // NULL marks it as "not pointing to anything yet"
NULL is address 0 — which is never valid to read or write. Dereferencing a NULL pointer always crashes the program. Always check before dereferencing.
// ❌ WRONG — null dereference — always crashes int *p = NULL; *p = 10; // CRASH — address 0 is protected by the OS // ✅ CORRECT — check before using int *p = NULL; if (p != NULL) { *p = 10; // safe — only runs if p actually points somewhere }
free(p) the memory is returned to the OS. The pointer p still contains the old address — but that address is no longer yours. Set p = NULL immediately after freeing.
#include <stdlib.h> // ❌ WRONG — dangling pointer int *p = malloc(sizeof(int)); free(p); // memory returned to OS *p = 99; // UNDEFINED BEHAVIOUR — p is now "dangling" // ✅ CORRECT — null the pointer immediately after free int *p = malloc(sizeof(int)); free(p); p = NULL; // any accidental *p use now crashes visibly — better than silent corruption
int arr[3] = {1, 2, 3}; int *p = arr + 3; // valid: one-past-the-end address is fine to HOLD // ❌ WRONG — dereferencing one-past-the-end *p = 5; // UNDEFINED BEHAVIOUR — that memory is not part of arr // ✅ Use it only as a sentinel to detect end-of-array: int *end = arr + 3; for (int *q = arr; q < end; q++) { printf("%d ", *q); // safe — q never reaches end }
* symbol means two completely different things. In a declaration it is part of the type. In an expression it is the dereference operator. These look identical but behave entirely differently.
int x = 42; int *p = &x; // DECLARATION — * means "p is a pointer to int" // NOT "dereference" here *p = 10; // EXPRESSION — * means "dereference: follow address, write 10" // Ask yourself every time: // Am I DECLARING a new pointer variable? → * = type label // Am I USING an existing pointer? → * = dereference operator
A wild pointer or off-by-one might work fine 99 times and corrupt memory on the 100th run. Always initialize pointers, always check for NULL before dereferencing, always set to NULL after freeing.
Quick Reference — All Pointer Syntax
| Syntax | What it means | Output / Result |
|---|---|---|
| int *p; | Declare pointer to int (uninitialized) | garbage value — dangerous! |
| int *p = NULL; | Declare pointer, set to NULL (safe empty) | p = 0x0 |
| int *p = &x; | Declare pointer, point to x | p holds x's address |
| &x | Address of variable x | 0x7fff... (hex address) |
| *p | Dereference — read value at address p holds | Same as x |
| *p = 99 | Dereference — write 99 to address p holds | x becomes 99 |
| p + 1 | Pointer arithmetic — advance by sizeof(type) | next element in array |
| *(p + i) | Same as arr[i] | element at index i |
| printf("%p", p) | Print the address number itself | 0x7ffee4b2c8ac |
| printf("%d", *p) | Print the value at the address | 42 (or whatever is there) |
| if (p != NULL) | Check pointer is valid before using | safe dereference |
Quick Quiz
Given int x = 42; int *p = &x; — what does printf("%d", *p) output?
What does printf("%p", p) print?
In int *p = &x; — the * before p means:
Given int arr[3] = {10,20,30}; int *p = arr; — what is *(p+1)?
What is the safest way to declare a pointer when you don't have a variable to point to yet?
Lesson Checklist
- I understand RAM as numbered boxes — each has an address and a value
- I know a pointer stores an address, not a value
- I know & always means "address of" — no exceptions
- I know * in a declaration means "this is a pointer variable"
- I know * in an expression means "dereference — follow the address"
- I understand x and *p are the same memory box — same location
- I know why scanf needs & — it passes the address to write into
- I understand pointer arithmetic — p+1 moves by sizeof(type) bytes
- I know arr[i] and *(arr+i) are identical
- I know all 5 pointer bugs: wild, null deref, dangling, off-by-one, * confusion
- I always initialize pointers and set to NULL after free
- I completed the quiz
What to Learn Next
- 🏗️ Pointers and structs —
ptr->memberarrow operator Next - 📦 Dynamic memory — malloc, calloc, free Next
- 🔗 Linked lists — nodes pointing to next nodes Advanced
- 📋 Function pointers — pointers that store function addresses Advanced