Pointers in C
October 10, 2026 • 9 min read

Table of contents
Part of the series:C Explained
Throughout the series a variable was described as “a named box in memory”. A pointer is the C feature that deals with those boxes directly: it is a variable that stores the address of another variable. Pointers are why you can write scanf("%d", &age) and why arrays seem to turn into pointers when passed to functions. They are also, honestly, the hardest mental step in C, and that is mostly because programmers are not told what they are for. This post takes it slowly: what an address is, what pointers do, and the two places they earn their keep (changing caller variables, and dynamic memory).
Addresses
Every variable lives at a memory address, like a house on a street. The address is just a number. The & operator (address-of) gives you a variable’s address:
int x = 42;
printf("%p\n", (void *)&x); // prints something like 0x7ffee4a3b8c0
%p is the format specifier for an address. The address is not random: it is where x sits in memory, decided by the system at runtime. Notice the &x in scanf: that is why scanf("%d", &age) works. The function must write to your age box, so you hand it the box’s address instead of a copy of its value.
Declaring and using pointers
A pointer variable is declared with * after the base type; it declares that the variable holds an address of something of that type:
int x = 42;
int *p = &x; // p holds the address of x
Read int *p as “p is a pointer to an int”. The address is stored, but to reach the value through the address you dereference with *:
printf("%d\n", *p); // 42: the int that p points to
*p = 99; // writes THROUGH p, changing x
printf("%d\n", x); // 99
Spelling matters: in a declaration, int *p declares a pointer; in an expression, *p means “the thing p points to”. The & and * are inverse operations: & produces an address from a variable, * produces the variable from an address, and applying one after the other gets you back where you started.
NULL: the empty pointer
Every pointer should either point at something real or at the special value NULL. Dereferencing NULL crashes your program, so check before use:
int *p = NULL;
if (p != NULL) {
printf("%d\n", *p); // only runs if p is real
}
NULL exists so pointers can say “nothing here” instead of holding a garbage address. The check p != NULL (or the shorthand if (p) when treating presence as truth) is the single most common defensive pattern in C, and it is where the error-handling post takes over later.
Pointers as function parameters
Recall from the functions post that C is pass-by-value: a function gets a copy and cannot change the caller’s variable. Pass a pointer and it can, because a copy of an address still names the original box:
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void) {
int x = 1, y = 2;
swap(&x, &y);
printf("%d %d\n", x, y); // 2 1
return 0;
}
swap never touches x and y by name; it writes through their addresses. Any function that must return a modified argument uses this pattern. It is the same reason scanf("%d", &age) asks for the address: it needs to write into your variable, not into a copy.
Pointers and arrays
C arrays and pointers are close relatives. The name of an array, used in an expression, is a pointer to its first element:
int scores[] = {92, 85, 88};
int *p = scores; // p points to scores[0]
printf("%d\n", *p); // 92
That is why the arrays post warned that passing an array to a function passes a pointer, and why sizeof inside the function fails: the compiler only sees a pointer there. Array indexing is defined in terms of pointers: scores[i] is just syntax for “the element at address scores + i”.
Pointer arithmetic
Adding an integer to a pointer moves by whole elements, not bytes. p + 1 points to the next element of the array:
printf("%d\n", *(p + 1)); // 85
printf("%d\n", p[1]); // same thing: 85
p[k] and *(p + k) are interchangeable. This is the whole secret behind array indexing, and it means you can walk an array with a pointer:
int total = 0;
int *p = scores;
for (int i = 0; i < 3; i++) {
total += *p; // read current element
p++; // move to the next one
}
Strings rely on the same idea: strlen is just “count characters while the current byte is not '\0'”. A char * pointing into a string and stepping one byte at a time until zero is the loop behind every string function.
Pointers to structs
The structs post showed ->, and now it makes sense. If s is a pointer to a struct, s->field is (*s).field; both dereference and pick the field:
typedef struct { int x; int y; } Point;
void move(Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
int main(void) {
Point p = {1, 1};
move(&p, 3, 2);
printf("(%d, %d)\n", p.x, p.y); // (4, 3)
return 0;
}
Pointer to pointer and const pointers
Pointers are variables, so they too have addresses, producing a pointer to a pointer: int ** (“pointer to pointer to int”). You rarely need one early on, but you will meet it when a function must return a pointer through a parameter, like the allocator functions below.
const in front of a pointer means “the data it points at cannot change through this pointer”:
void print(const int *p) {
// *p = 1; // error: read-only
printf("%d\n", *p);
}
Putting const after the star means the pointer itself cannot be reassigned: int *const p. The first form, const int *, is the common one for function parameters, and it is how the string helpers say “I will read this, never modify it”. The string literals you pass have type const char *, which is why printf("...", ...) and friends expect const char * parameters.
Function pointers
Because functions live in memory too, you can store a pointer to a function and call through it. The syntax is ugly and worth learning by pattern, not by heart:
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int apply(int (*op)(int, int), int a, int b) {
return op(a, b);
}
int main(void) {
printf("%d\n", apply(add, 3, 4)); // 7
printf("%d\n", apply(mul, 3, 4)); // 12
return 0;
}
int (*op)(int, int) declares op: a pointer to a function taking two ints and returning an int. Passing a function selected at runtime is how you write callbacks and tables of operations. If you see typedef int (*Operation)(int, int); in real code, it is this idea given a readable name.
Dynamic memory
Everything so far lived on the stack, sized at compile time. Real programs need memory whose size is only known at runtime, or that outlives the current function. That memory comes from the heap, and C hands it out with malloc:
int *p = malloc(10 * sizeof(int)); // 10 ints, on the heap
if (p == NULL) {
// the request failed; handle it
}
for (int i = 0; i < 10; i++) {
p[i] = i * i;
}
free(p); // give the memory back
The three function names to know, all from <stdlib.h>:
malloc(n)- returns a pointer tonbytes of uninitialized memory.calloc(count, size)- likemallocbut zero-initialized, and takes count and element size separately.realloc(p, newSize)- grows or shrinks a previously allocated block, moving it if needed.free(p)- returns the block to the system.
malloc returns void *, a pointer with no element type; C converts it to your pointer type automatically, so in C you do not need to cast the result. Always multiply by sizeof, because malloc counts bytes and int is not 1 byte.
The rules that keep memory sane
Dynamic memory has no garbage collector, so you are responsible for every byte:
- Check the result.
mallocreturnsNULLwhen out of memory. Treatp == NULLas the error check above. - Free exactly what you allocated, once. A memory leak is memory you allocated and never freed; the program slowly eats RAM. A double free is calling
free(p)twice, which corrupts the allocator’s bookkeeping. A use-after-free is reading*pafterfree(p); the bytes may be reused for something else. Leaks, double frees, and use-after-free are the classic C bugs, and the error-handling and testing posts will show how to catch them. - Never use freed memory. A pointer that was freed and then used is called a dangling pointer1:
char *p = malloc(16);
free(p);
strcpy(p, "boom"); // use-after-free: undefined behavior
A useful discipline: set a freed pointer to NULL so a later accidental dereference crashes loudly in debuggers instead of silently corrupting data:
free(p);
p = NULL;
Lifetime, in one rule
Stack memory belongs to the function that declared it and vanishes when it returns. Heap memory belongs to you until free. If a function must return data that survives the call, the memory must be heap-allocated (or a pointer to something the caller owns). The stack vs heap distinction is reasoned about concretely in the Computers Explained series; here the practical rule is: stack sizes are known at compile time, heap sizes can be decided at runtime, and every heap byte must be freed exactly once.
The big picture
A pointer is a variable holding an address. & produces addresses, * dereferences them, NULL marks “points at nothing”, p[k] and *(p+k) are the same thing, passing pointers lets functions modify caller variables and avoid copying big structs, const tames what a pointer may change, and function pointers store whole functions in variables. malloc/free at the end bring the heap into play, with the lifetime rules that keep memory safe. Pointers connect everything in C, and every later post in this series builds on them. With pointers mastered, the next stop is what happens when all of this goes wrong: error handling.
Footnotes
/sponsor
Enjoyed this post? You can sponsor me and this site.