Learn C pointers through memory visualization. Addresses and dereferencing explained with diagrams.
What is a pointer?
A variable is a box in memory. Each box has an address. A pointer is a variable that stores an address.
Memory is a "numbered, contiguous warehouse"
A computer's memory (RAM) is like a long row of shelves, where each byte (8 bits) has a sequential number (address). When you declare a variable, the OS allocates a free slot and assigns it an address.
πΎ Memory (RAM) β each cell is 1 byte
int n = 10; (4 bytes)
int m = 20; (4 bytes)
char c = 'A'; (1 byte)
Unused
Key point:Start address of n = 0x1000 /
Start address of m = 0x1004 /
Address of c = 0x1008
Since int uses 4 bytes, the next variable is placed 4 addresses later. Memory is allocated contiguously, sized according to the type.
House and address analogy: A variable is a "house", the address is its "street number". A pointer is a "memo" that records that address. Knowing the address lets you read or rewrite what's inside the house.
Get an address with the & operator: Writing &n gives you the address of variable n (e.g., 0x1000). You can even print the address with printf("%p\n", &n);.
Regular variable vs pointer
Regular variable
int n = 10;
Stores the value (10) directly in the box
Pointer variable
int *p = &n;
Stores the address of n in the box
The declaration form is type *name;. The type is the type that the pointer points to.int *p means "pointer p that points to an int variable".
& operator and * operator
Two operators are essential when working with pointers.
& (address-of operator)
&n β address of n
Gets the address of a variable. Same & used in scanf!
* (dereference operator)
*p β value at the address p points to
Accesses the value the pointer points to. Can be used to both read and write.
int n = 10;
int *p = &n; // assign address of n to p
printf("%d", *p); // β 10 (value p points to)
*p = 99; // rewrites n!
printf("%d", n); // β 99
Address and value β Visualizer
Use the buttons to see the relationship between variable n and pointer p.
Address 0x1000
int n
β
β
Address 0x2000
int *p
β
Press the buttons in order...
Step through memory line by line
See how the pointer moves inside memory, one line at a time. Pointer p will switch between pointing to n and pointing to m.
Program
int n = 10;
int m = 20;
int *p;
p = &n;
*p = 99;
p = &m;
*p = 77;
πΎ Memory (RAM)
0x1000
int nβ
0x1004
int mβ
0x2000
int *pβ
Press "Execute next line"...
What to observe:
p = &n; and p = &m; switch where the arrow (target of p) points
*p = value; rewrites the value at the other end of the arrow
p itself contains an address (0x1000 or 0x1004); you need * to reach the value
The truth about & in scanf
Up to now you've been writing scanf("%d", &a); in scanf. That "&" is the operator that passes an address.
Why is & needed?
The scanf function needs to know where to write the value. That's why you pass the address of the variable.
β Internally, scanf uses a pointer to write the input value at that address.
β οΈ Common mistake: If you forget & and write scanf("%d", a);, scanf treats a's (indeterminate) contents as an address, writing to an unexpected location, which can crash the program.
swap function β a classic pointer example
A swap function that exchanges two variables is a classic use case for pointers.
Pass-by-value cannot swap! void swap(int a, int b){ ... } does not change the caller's variables. (Only local copies inside the function are swapped.)
// receive pointers and swap the values they point to
void swap(int *x, int *y){
int t = *x;
*x = *y;
*y = t;
}
int main(void){
int a = 5, b = 10;
swap(&a, &b); // pass addresses
printf("%d %d", a, b); // β 10 5
}
Key point: passing an address to a function is pass-by-reference. Arrays passed to functions are automatically pass-by-reference for the same reason.
Pointer arithmetic (adding/subtracting pointers)
Adding or subtracting an integer to a pointer moves the address by the size of the type. This is the key to understanding the relationship between arrays and pointers.
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // points to arr[0]printf("%d\n", *p); // 10 (arr[0])printf("%d\n", *(p + 1)); // 20 (arr[1])printf("%d\n", *(p + 3)); // 40 (arr[3])
p += 2; // p now points to arr[2]printf("%d\n", *p); // 30
p + 1 means "the next element", not "the next byte". With an int pointer (4 bytes), p + 1 advances the address by 4 bytes.
10
p+0
20
p+1
30
p+2
40
p+3
50
p+4
Array and pointer equivalence
In C, the following are all equivalent.
Array notation
Pointer notation
Meaning
arr[0]
*arr
First element
arr[i]
*(arr + i)
i-th element
&arr[i]
arr + i
Address of i-th element
Pointer subtraction
int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &arr[1];
int *p2 = &arr[4];
printf("%ld\n", p2 - p1); // 3 (difference in number of elements)
Subtracting two pointers returns "the number of elements between them", not the number of bytes.
The const qualifier β Mark values as "unchangeable"
const tells the compiler and other developers that a variable won't be modified. It prevents bugs and makes code intent clear.
Basic: const variables
constint MAX = 100;
printf("%d\n", MAX); // OK: read freely
MAX = 200; // β Compile error: cannot modify const
Use cases: Values that never change during execution β like pi, array sizes, configuration constants. The advantage over #define is type checking.
Pointers and const β 3 patterns
Combining pointers with const is a notoriously confusing topic. The meaning depends on which side of the asterisk the const appears.
Declaration
What can change
Meaning
const int *p or int const *p
β p can change β *p cannot change
Pointer to a constant value
int * const p
β p cannot change β *p can change
Constant pointer (can't point elsewhere)
const int * const p
β p cannot change β *p cannot change
Fully locked pointer
Concrete examples
int a = 10, b = 20;
// β const int *p : pointed-to value is read-onlyconstint *p1 = &a;
p1 = &b; // β OK: pointer itself can change
*p1 = 30; // β Error: pointed-to value is immutable// β‘ int * const p : pointer itself is read-onlyint * const p2 = &a;
*p2 = 30; // β OK: can modify 'a' through p2
p2 = &b; // β Error: p2 can't be redirected// β’ const int * const p : everything lockedconstint * const p3 = &a;
*p3 = 30; // β Error
p3 = &b; // β Error
Reading trick: Read right-to-left.
γ»const int *p = "p is a pointer to a const int"
γ»int * const p = "p is a const pointer to int"
const in function parameters
When passing arrays or strings to functions that won't modify them, use const to make that intent explicit.
// Just reads the string β mark as constintmy_strlen(constchar *s) {
int n = 0;
while (*s) { s++; n++; }
return n;
}
// Sums array elements, doesn't modify them β constintsum(constint arr[], int n) {
int total = 0;
for (int i = 0; i < n; i++) total += arr[i];
return total;
}
Best practice:Always mark parameters as const when the function doesn't modify them.
γ»Signals intent: "this function won't touch your data"
γ»Accidental modifications are caught at compile time
γ»Standard library functions like strlen, strcmp follow this convention
Important: String literals (strings written directly in source like "Hello") live in read-only memory. Always receive them via const char *.
Summary: const acts as a "bug-prevention barrier". Your code still works without it, but using it catches mistakes at compile time and makes your intent crystal clear.
Try it yourself β pointers
A program that rewrites a value through a pointer. Go ahead and run it.
pointer.c
Output
Press "Run"...
Confirm that *p = 99; rewrites the value of n. That is what it means to "indirectly change a value via a pointer".
Q. Why use pointers? Aren't regular variables enough?
A. Pointers are needed to: 1. Modify a variable's value from a function (pass-by-reference), 2. Dynamically allocate memory, 3. Build complex data structures (lists, trees, etc.), 4. Work with strings, and more. Many features are impossible without pointers.
Q. I keep confusing * and &. Which is which?
A. & (address-of) takes the address indicating "where the variable lives". * (dereference) goes to see "what's stored at this address". In a pointer declaration `int *p;`, the * means "p is a pointer to int".
Q. What is a NULL pointer?
A. A NULL pointer represents "a pointer that points to nothing", with value 0. An uninitialized pointer is called a dangling pointer and is dangerous. For safer code, get in the habit of always initializing pointers at declaration, e.g., `int *p = NULL;`.
Q. What does "a pointer to a pointer" mean?
A. A pointer is just a variable too, so the pointer variable itself lives in memory. That means you can make "a pointer to a pointer": `int **pp = &p;`. Multi-level pointers are tricky, but you're just applying the same principle repeatedly. In practice they're rarely needed.
Review Quiz
Check your understanding of this lesson!
Q1. What is stored in p of int *p;?
An integer value
The address of an int variable
A string
int *p is a pointer to int, which stores the memory address of an int variable.
Q2. With int x=10; int *p=&x;, what is the value of *p?
The address of x
10
The address of pointer p
*p dereferences the pointer. Since p points to x, *p = 10.
Q3. What happens when you dereference (*p) a NULL pointer?
Returns 0
Runtime error (segmentation fault)
Compile error
Dereferencing a NULL pointer causes a runtime error such as a segmentation fault. Always NULL-check before use.