Pointer Fundamentals
A pointer is a variable that holds a memory address. That is the entire definition. There is no hidden complexity, no runtime magic — a pointer is a number that refers to a location in memory. Pointers are the single most important concept in C. They enable indirection, pass-by-reference, dynamic data structures, and direct memory manipulation. Every serious C program uses pointers extensively.
What Is a Pointer?
Every variable in your program occupies memory at a specific address. A pointer stores that address.
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x; // p holds the address of x
printf("x lives at address: %p\n", (void *)&x);
printf("p holds the value: %p\n", (void *)p);
printf("x has the value: %d\n", x);
printf("*p dereferences to: %d\n", *p);
return 0;
}
x lives at address: 0x7ffd3c4a1b54
p holds the value: 0x7ffd3c4a1b54
x has the value: 42
*p dereferences to: 42
The * in int *p means "p is a pointer to an int." The & in &x means "the address of x." The * in *p means "the value at the address p holds" (dereference).
The Two Operators: & and *
& (Address-of)
&x gives you the memory address where x is stored. You can take the address of any lvalue (a named memory location).
int x = 10;
int *p = &x; // p now holds the address of x
double d = 3.14;
double *dp = &d; // dp holds the address of d
char c = 'A';
char *cp = &c; // cp holds the address of c
* (Dereference)
*p follows the pointer to the address it holds and gives you the value there. You can read or write through a dereferenced pointer.
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x;
// Read through the pointer
printf("value: %d\n", *p); // 42
// Write through the pointer
*p = 99;
printf("x is now: %d\n", x); // 99
return 0;
}
value: 42
x is now: 99
Writing *p = 99 modifies x because p points to x. The pointer provides indirection — you are modifying a variable without using its name.
Pointer Types & sizeof
Pointers have types. An int * points to an int. A double * points to a double. The type tells the compiler how many bytes to read or write when you dereference the pointer.
#include <stdio.h>
int main(void) {
int i = 42;
double d = 3.14;
int *ip = &i;
double *dp = &d;
// All pointers are the same size (the size of a memory address)
printf("sizeof(int *): %zu\n", sizeof(int *));
printf("sizeof(double *): %zu\n", sizeof(double *));
printf("sizeof(char *): %zu\n", sizeof(char *));
// But they point to different-sized data
printf("sizeof(*ip): %zu (reads %zu bytes)\n", sizeof(*ip), sizeof(*ip));
printf("sizeof(*dp): %zu (reads %zu bytes)\n", sizeof(*dp), sizeof(*dp));
return 0;
}
sizeof(int *): 8
sizeof(double *): 8
sizeof(char *): 8
sizeof(*ip): 4 (reads 4 bytes)
sizeof(*dp): 8 (reads 8 bytes)
On a 64-bit system, all pointers are 8 bytes — they hold a 64-bit memory address. The type determines what happens when you dereference, not how big the pointer itself is.
Pointer Arithmetic
Adding an integer to a pointer moves it by that many elements, not that many bytes. The compiler multiplies by sizeof(*ptr) automatically.
#include <stdio.h>
int main(void) {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // points to arr[0]
printf("p points to: %d\n", *p); // 10
printf("p + 1 points to: %d\n", *(p + 1)); // 20
printf("p + 2 points to: %d\n", *(p + 2)); // 30
// Pointer difference: number of elements between two pointers
int *start = &arr[0];
int *end = &arr[4];
printf("elements between: %td\n", end - start); // 4
return 0;
}
p points to: 10
p + 1 points to: 20
p + 2 points to: 30
elements between: 4
p + 1 does not add 1 byte — it adds sizeof(int) bytes (4 on most platforms). This is why pointer types matter: int *p + 1 moves 4 bytes, char *p + 1 moves 1 byte, double *p + 1 moves 8 bytes.
NULL: The "Points to Nothing" Value
NULL is a pointer value that means "this pointer does not point to anything." Dereferencing NULL is undefined behavior — on most systems, it causes a segmentation fault.
#include <stdio.h>
#include <stdlib.h>
int *find_value(int *arr, size_t len, int target) {
for (size_t i = 0; i < len; i++) {
if (arr[i] == target) {
return &arr[i]; // found: return pointer to element
}
}
return NULL; // not found: return NULL
}
int main(void) {
int data[] = {5, 10, 15, 20, 25};
size_t len = sizeof(data) / sizeof(data[0]);
int *result = find_value(data, len, 15);
if (result != NULL) {
printf("found: %d\n", *result);
}
result = find_value(data, len, 99);
if (result == NULL) {
printf("not found\n");
}
return 0;
}
found: 15
not found
Always check for NULL before dereferencing a pointer that might be null. This includes the return value of malloc, fopen, and any function documented to return NULL on failure.
Why Pointers Exist
Indirection
Pointers let you refer to data without copying it. A function can receive a pointer to a large struct instead of copying the entire struct:
#include <stdio.h>
typedef struct {
char name[256];
int scores[100];
double gpa;
} Student;
// Takes a pointer — no copy of the 1000+ byte struct
void print_gpa(const Student *s) {
printf("GPA: %.2f\n", s->gpa);
}
Pass by Reference
C passes all arguments by value. Pointers simulate pass-by-reference:
#include <stdio.h>
// Without pointer: cannot modify caller's variable
void try_to_swap_wrong(int a, int b) {
int temp = a;
a = b;
b = temp;
// This modifies local copies, not the originals
}
// With pointers: modifies the caller's variables
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(void) {
int x = 10, y = 20;
try_to_swap_wrong(x, y);
printf("after wrong swap: x=%d, y=%d\n", x, y); // unchanged
swap(&x, &y);
printf("after real swap: x=%d, y=%d\n", x, y); // swapped
return 0;
}
after wrong swap: x=10, y=20
after real swap: x=20, y=10
Dynamic Data Structures
Pointers enable linked lists, trees, graphs, and any data structure where elements refer to other elements:
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int value;
struct Node *next; // pointer to next node
} Node;
int main(void) {
// Build a linked list
Node *head = malloc(sizeof(Node));
head->value = 1;
head->next = malloc(sizeof(Node));
head->next->value = 2;
head->next->next = NULL;
// Walk the list
for (Node *cur = head; cur != NULL; cur = cur->next) {
printf("%d -> ", cur->value);
}
printf("NULL\n");
// Clean up (free each node)
while (head != NULL) {
Node *next = head->next;
free(head);
head = next;
}
return 0;
}
1 -> 2 -> NULL
The Mental Model
Think of memory as a long row of numbered boxes. Each box holds one byte. A variable like int x = 42 occupies four consecutive boxes. A pointer int *p = &x is another variable (occupying 8 boxes on a 64-bit system) whose value is the box number where x starts.
Dereferencing (*p) means: go to the box number stored in p, read 4 bytes (because p is an int *), and interpret them as an integer.
Pointer arithmetic (p + 1) means: take the box number in p, add 4 (the size of an int), and you have the box number of the next integer.
This is not an analogy — it is literally what happens at the hardware level.
Common Pitfalls
- Forgetting to initialize pointers — an uninitialized pointer holds garbage. Dereferencing it reads or writes random memory. Always initialize pointers to a valid address or
NULL. - Confusing
*in declarations and expressions — inint *p, the*is part of the type declaration. In*p = 42, the*is the dereference operator. Same character, different meaning. - Returning a pointer to a local variable — local variables are deallocated when the function returns. A pointer to a local variable becomes a dangling pointer. Return heap-allocated memory or pass a buffer as a parameter.
- Not checking for NULL —
mallocreturnsNULLwhen allocation fails. DereferencingNULLis a crash. Always check. - Confusing pointers and arrays — arrays decay to pointers in most contexts, but they are not the same thing.
sizeof(array)gives the total size;sizeof(pointer)gives the pointer size.
Key Takeaways
- A pointer is a variable that holds a memory address.
&gets the address,*follows the address. - Pointer types determine how many bytes to read/write on dereference and how pointer arithmetic scales.
NULLis a pointer that points to nothing. Always check before dereferencing.- Pointers enable indirection (avoid copies), pass-by-reference (modify caller's data), and dynamic data structures (linked lists, trees).
- Pointer arithmetic moves by elements, not bytes.
int *p + 1advances bysizeof(int)bytes. - The mental model: pointers are box numbers in a row of numbered memory boxes. Everything in C reduces to this.