06 · Pointers Basics¶
🎥 Video walkthrough¶
Every variable lives somewhere in memory. A pointer is just a variable whose value is a memory address — instead of holding a number or a character, it holds the location where a number or character lives. Pointers are the foundation for arrays, strings, dynamic memory, and passing data efficiently between functions in C.
This module only covers the basics — declaring, reading, and using pointers safely. Pointer arithmetic and function pointers get a full deep dive in Level 2, Module 1.
The address-of operator (&)¶
Every variable has an address. The & operator gives you that address:
#include <stdio.h>
int main(void) {
int age = 30;
printf("Value of age: %d\n", age);
printf("Address of age: %p\n", (void *)&age);
return 0;
}
// Output (address will vary each run):
// Value of age: 30
// Address of age: 0x7ffee3a1c9ac
%p is the format specifier for printing addresses; casting to (void *) is
the conventional, portable way to pass a pointer to printf.
Declaring a pointer and the dereference operator (*)¶
A pointer variable is declared with a type and a *, and it must be told
what type of thing it points to:
#include <stdio.h>
int main(void) {
int age = 30;
int *agePtr = &age; // agePtr holds the address of age
printf("agePtr holds address: %p\n", (void *)agePtr);
printf("Value at that address (dereferenced): %d\n", *agePtr);
*agePtr = 31; // changes age itself, through the pointer
printf("age is now: %d\n", age);
return 0;
}
// Output:
// agePtr holds address: 0x7ffee3a1c9ac
// Value at that address (dereferenced): 30
// age is now: 31
Two very different meanings for * show up here:
- In a declaration (
int *agePtr),*says "this variable is a pointer." - In an expression (
*agePtr = 31),*means "dereference — go to the address this pointer holds, and read/write the value there."
| Operator | Name | Meaning |
|---|---|---|
&x |
Address-of | "Give me the memory address of x" |
*p |
Dereference | "Give me the value stored at the address p holds" |
int *p |
Pointer declaration | "p is a pointer to an int" |
NULL pointers¶
An uninitialized pointer holds a garbage address — dereferencing it is
undefined behavior and a common source of crashes. It's good practice to
initialize a pointer to NULL when it doesn't yet point anywhere, and to
check before dereferencing:
#include <stdio.h>
#include <stddef.h> // defines NULL
int main(void) {
int *ptr = NULL;
if (ptr == NULL) {
printf("ptr does not point to anything yet.\n");
}
int value = 42;
ptr = &value;
if (ptr != NULL) {
printf("ptr now points to a value: %d\n", *ptr);
}
return 0;
}
// Output:
// ptr does not point to anything yet.
// ptr now points to a value: 42
Checking for NULL before dereferencing is one of the most important habits
in C — functions like malloc (covered in Level 2) return NULL on failure,
and dereferencing that NULL without checking crashes the program.
Pointers and arrays¶
An array name, when used in most expressions, "decays" into a pointer to its first element. This is why arrays and pointers feel closely related in C:
#include <stdio.h>
int main(void) {
int numbers[] = {10, 20, 30, 40};
printf("numbers itself: %p\n", (void *)numbers);
printf("&numbers[0]: %p\n", (void *)&numbers[0]);
printf("First element: %d\n", *numbers); // same as numbers[0]
printf("Second via pointer: %d\n", *(numbers + 1)); // same as numbers[1]
return 0;
}
// Output:
// numbers itself: 0x7ffee3a1c990
// &numbers[0]: 0x7ffee3a1c990 (identical address)
// First element: 10
// Second via pointer: 20
numbers and &numbers[0] print the same address — the array name decays to
a pointer to its first element. numbers[i] and *(numbers + i) are
equivalent ways of writing the same access. The full rules of pointer
arithmetic (why +1 moves by sizeof(int) bytes, not one byte) are covered in
Level 2, Module 1; for now, just
recognize that arrays and pointers are closely related.
Pass-by-reference with pointers¶
C passes arguments to functions by value — a function normally gets a copy of the argument and can't modify the caller's variable. Pointers let a function reach back and modify the original, simulating pass-by-reference:
#include <stdio.h>
// Without a pointer, this would only swap the local copies
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(void) {
int x = 5;
int y = 10;
printf("Before swap: x=%d, y=%d\n", x, y);
swap(&x, &y); // pass addresses, not values
printf("After swap: x=%d, y=%d\n", x, y);
return 0;
}
// Output:
// Before swap: x=5, y=10
// After swap: x=10, y=5
Without pointers, swap would receive copies of x and y; changes inside
the function would vanish when it returned. By passing &x and &y, swap
receives the addresses, dereferences them, and modifies the caller's actual
variables.
Pointer to pointer (brief look)¶
A pointer can itself be pointed to, using **. This shows up when a function
needs to modify a pointer that lives in the caller (for example, allocating
memory and handing the new address back):
#include <stdio.h>
int main(void) {
int value = 100;
int *ptr = &value; // ptr points to value
int **ptrToPtr = &ptr; // ptrToPtr points to ptr
printf("value: %d\n", value);
printf("*ptr: %d\n", *ptr);
printf("**ptrToPtr: %d\n", **ptrToPtr);
return 0;
}
// Output:
// value: 100
// *ptr: 100
// **ptrToPtr: 100
This is just a preview — you'll use pointer-to-pointer patterns more deliberately once dynamic memory allocation is introduced in Level 2.
How It Actually Works¶
A pointer variable itself is not magic — it's an ordinary chunk of memory
that happens to store a number: the address of another byte in the
process's address space. int *agePtr = &age; allocates 8 bytes on a
64-bit machine (a pointer's size equals the machine's address width, not
the size of what it points to) and stores age's address in them. &age
is computed by the compiler at compile time if age is a known stack
offset — it's literally "current frame base plus this variable's fixed
offset," not a runtime lookup.
Dereferencing (*agePtr) compiles into a two-step memory access at the
hardware level: first load the address value out of agePtr's own storage,
then issue a second load/store using that value as the address. This is
exactly why an uninitialized pointer is dangerous — its bytes hold whatever
garbage was left on the stack from a previous function call, and
dereferencing it means asking the CPU to read/write at a essentially random
address, which either corrupts unrelated memory silently or hits a page the
OS hasn't mapped into your process, triggering a segmentation fault
(the kernel's memory-management unit rejects the access and sends
SIGSEGV). NULL is conventionally address 0, which the OS deliberately
leaves unmapped specifically so dereferencing a null pointer reliably
crashes instead of corrupting memory silently — that reliability is why
if (ptr == NULL) checks are effective as a safety net.
Array decay is a compile-time rule, not a runtime conversion: whenever
an array name appears in most expressions, the compiler substitutes the
address of element 0 in its place, because arrays and pointers use
compatible representations at the machine level — an array's "value" in an
expression context is a base address. This is also why pointer
arithmetic is type-aware: numbers + 1 doesn't add 1 byte, it adds
1 * sizeof(int) bytes (4, typically), because the compiler scales the
offset by the pointee type's size so that *(numbers + i) lands exactly on
element i's first byte — the same address arithmetic explained for
arrays in Module 5.
swap(&x, &y) demonstrates why passing addresses defeats pass-by-value:
a and b inside swap are still copies (of the addresses), but
dereferencing a copied address still reaches the original memory it points
to — *a = *b is a memory write at x's actual stack location in main's
frame, not a write to any copy. A pointer-to-pointer (int **ptrToPtr)
simply repeats this once more: it's a variable holding the address of
another variable (ptr) that itself holds an address — **ptrToPtr
dereferences twice, following two address hops in sequence to finally land
on value's actual storage.
🔀 See this in another language¶
Exercise¶
Write a program that declares an array of 5 integers. Using only pointer
notation (no [] indexing), write a loop that prints every element and its
memory address. Then write a function void doubleValue(int *n) that doubles
whatever integer its pointer points to, and call it on one of the array
elements (by passing &array[i]) to confirm the array itself changed.