C · Intermediate
Pointers in C
In short: A pointer is a variable that holds the address of another object. &x gives the address of x, *p reads or writes whatever p points at, and because an array's name converts to the address of its first element, p + 1 steps to the next element. Pointers let a function change its caller's variables and let code walk arrays without copying them.
Addresses as values
Every variable lives somewhere in memory, and that location has an address. A pointer is a variable whose value is such an address. int *p declares p as a pointer to an int; p = &stock stores the address of stock in it; and *p is the int that p points at, usable on either side of an assignment. The type in the declaration matters because *p must know how many bytes to read and how to interpret them.
Why does C need pointers when other languages hide them? Because C passes function arguments by value: a function receives copies, and changing a copy changes nothing outside. To let a function change a variable in the caller, you give it the variable's address. That is why scanf("%d", &n) needs &, and why a function that produces two results, such as hours and minutes, takes two pointers. Addresses also avoid copying: passing a 10,000-element array by address costs one machine word.
Arrays and pointers are closely linked. In almost every expression an array's name converts to a pointer to its first element, so readings means &readings[0]. Pointer arithmetic is measured in elements, not bytes: p + 1 on an int * moves forward by sizeof(int) bytes, and p[i] is defined as *(p + i). Two pointers into the same array can be subtracted to give the number of elements between them, and compared with < to drive a loop. Computing the address one past the last element is allowed, so p < arr + n is the idiomatic bound; reading through that address is not.
A pointer that holds no valid address should be set to NULL, a macro defined in <stddef.h>, <stdio.h> and <stdlib.h>. Dereferencing NULL, an uninitialised pointer, or the address of a local variable after its function has returned is undefined behaviour; the usual symptom is a crash, but nothing is guaranteed. const int *p declares that the pointed-at value will not be modified through p, which is the right type for a parameter that only reads.
Printing an address with %p shows a number that differs from run to run, so the examples here print values and differences instead. A pointer's size depends on the platform, not on what it points at: on 64-bit Linux every data pointer is 8 bytes.
Syntax
int x = 5;
int *p = &x; /* p holds the address of x */
*p = 6; /* writes through p: x is now 6 */
int *q = NULL; /* points nowhere; never dereference */
int arr[4] = {1, 2, 3, 4};
int *first = arr; /* same as &arr[0] */
first[2]; *(first + 2); /* the same element, 3 */
first++; /* now points at arr[1] */
const int *r = &x; /* x cannot be changed through r */
void swap(int *a, int *b); /* receives addresses, can change the caller's ints */In a declaration the * belongs to the name that follows it: int *p, n; declares one pointer and one plain int. In an expression, *p means the value p points at.
Reading pointer expressions
| Expression | Meaning |
|---|---|
| &x | the address of x |
| *p | the object p points at (read or write) |
| p + 1 | the address one element further on, scaled by the element size |
| p[i] | the same as *(p + i) |
| p - q | number of elements between two pointers into the same array |
| *p++ | the value at p; afterwards p points at the next element |
| (*p)++ | adds 1 to the value p points at; p itself stays |
Reading and writing through a pointer
One int, one pointer to it, and the effect of each assignment.
#include <stdio.h>
int main(void) {
int stock = 40;
int *p = &stock;
printf("stock = %d, *p = %d\n", stock, *p);
*p = *p - 15;
printf("after *p = *p - 15: stock = %d\n", stock);
stock = 100;
printf("after stock = 100: *p = %d\n", *p);
int other = 7;
p = &other;
*p += 1;
printf("other = %d, stock = %d\n", other, stock);
printf("p == &other: %d, p == &stock: %d\n", p == &other, p == &stock);
return 0;
}Output
stock = 40, *p = 40 after *p = *p - 15: stock = 25 after stock = 100: *p = 100 other = 8, stock = 100 p == &other: 1, p == &stock: 0
After p = &stock, *p and stock are two names for the same four bytes: subtracting through the pointer changes stock, and assigning to stock changes what *p reads. Reassigning p itself makes it point at a different variable, so the increment through it reaches other while stock is untouched. Comparing pointers with == asks whether they hold the same address, which is how you test what a pointer currently refers to.
Letting a function change its caller's variables
Two results out of one function, a swap that works, and one that does not.
#include <stdio.h>
void split_minutes(int total, int *hours, int *minutes) {
*hours = total / 60;
*minutes = total % 60;
}
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
void no_effect(int a, int b) {
int tmp = a;
a = b;
b = tmp;
printf("inside no_effect: %d %d\n", a, b);
}
int main(void) {
int h, m;
split_minutes(135, &h, &m);
printf("135 minutes = %dh %02dm\n", h, m);
int first = 3, second = 9;
no_effect(first, second);
printf("after no_effect: %d %d\n", first, second);
swap(&first, &second);
printf("after swap: %d %d\n", first, second);
return 0;
}Output
135 minutes = 2h 15m inside no_effect: 9 3 after no_effect: 3 9 after swap: 9 3
split_minutes cannot return two numbers, so main passes the addresses of h and m and the function writes through them. no_effect receives copies: inside, the swap plainly happens (it prints 9 3), but first and second in main are unchanged. swap takes pointers and exchanges the values they point at, which is why main sees the result. The rule is simple: a function that must change a variable it did not declare needs that variable's address.
Walking an array with a pointer
Indexing and pointer arithmetic on the same five readings.
#include <stdio.h>
int sum(const int *values, int count) {
int total = 0;
for (const int *p = values; p < values + count; p++) {
total += *p;
}
return total;
}
int main(void) {
int readings[5] = {12, 15, 11, 18, 14};
int *p = readings;
printf("first: %d, third: %d, third again: %d\n", *p, *(p + 2), p[2]);
p += 3;
printf("after p += 3: value %d at index %d\n", *p, (int) (p - readings));
printf("last: %d\n", *(readings + 4));
printf("sum of all: %d\n", sum(readings, 5));
printf("sum of last two: %d\n", sum(readings + 3, 2));
printf("elements: %zu\n", sizeof readings / sizeof readings[0]);
return 0;
}Output
first: 12, third: 11, third again: 11 after p += 3: value 18 at index 3 last: 14 sum of all: 70 sum of last two: 32 elements: 5
p starts at the first element, so *(p + 2) and p[2] are the same element as readings[2]. p += 3 moves three elements, and p - readings recovers the index. Inside sum, the loop pointer advances until it reaches values + count, the address one past the last element, which may be computed and compared but not read. Passing readings + 3 sums only the last two elements: the function sees a two-element array that happens to live inside a larger one. sizeof works on the real array in main; inside sum it would give the size of a pointer, which is why the count travels as a separate parameter.
Common mistakes
Using a pointer that points nowhere
Why it goes wrong:
int *p; *p = 5;writes through whatever garbage the uninitialised pointer holds. It may crash at once, or corrupt another variable and fail much later.Fix: Initialise every pointer, to a real address or to NULL, and check for NULL before dereferencing a pointer that might be unset.
C · fixint value = 0; int *p = &value; /* or: int *p = NULL; */Returning the address of a local variable
Why it goes wrong:
int *make(void) { int n = 3; return &n; }returns a pointer to storage that is released when the function returns. Reading it later is undefined behaviour.Fix: Return the value itself, have the caller pass an address to fill, or allocate with malloc as the dynamic memory lesson shows.
Confusing the declaration star with the dereference star
Why it goes wrong:
int *p = &x;is one declaration in which*is part of the type. Writing*p = &x;later tries to store an address in an int, andint *p = x;tries to store an int in a pointer; both are type errors.Fix: In a declaration,
*says p is a pointer. In a statement,*pis the pointed-at value andp = &xchanges where p points.Using sizeof on an array parameter
Why it goes wrong: A parameter declared
int a[]is reallyint *a, sosizeof a / sizeof a[0]gives the size of a pointer divided by the size of an int, not the element count.Fix: Pass the length as a separate argument, as
sum(readings, 5)does.
Where you use this
The C standard library is built on this idea. scanf fills variables through the addresses you pass; strtol(text, &end, 10) converts a number and reports where it stopped by writing into end; fgets writes into the array you supply. Your own parsing functions work well in the same shape: return a status that says whether the parse succeeded and deliver the value through a pointer parameter, so callers can write if (parse_temperature(line, &t)) and use t only when it is valid.
int parse_temperature(const char *text, double *out) {
char *end;
double value = strtod(text, &end);
if (end == text) {
return 0; /* nothing was parsed; *out untouched */
}
*out = value;
return 1;
}
double t;
if (parse_temperature(line, &t)) {
/* use t */
}Key points
&xis the address of x;*pis the object at the address stored in p.- Functions receive copies, so pass an address when a function must change the caller's variable.
- An array's name converts to a pointer to its first element;
p[i]means*(p + i). - Pointer arithmetic counts elements, and
p < arr + nis the idiomatic loop bound. - Initialise pointers, use
NULLfor "nothing", and never return the address of a local. const T *ppromises not to modify what p points at; use it for read-only parameters.
Try it yourself
Complete clamp so that it limits the value through the pointer: below low becomes low, above high becomes high, anything else is left alone. For the input 150 0 100 the program prints clamped: 100.
#include <stdio.h>
void clamp(int *value, int low, int high) {
/* if *value is below low, set it to low; if above high, set it to high */
}
int main(void) {
int value, low, high;
if (scanf("%d %d %d", &value, &low, &high) != 3) {
return 1;
}
clamp(&value, low, high);
printf("clamped: %d\n", value);
return 0;
}150 0 100clamped: 100#include <stdio.h>
void clamp(int *value, int low, int high) {
if (*value < low) {
*value = low;
} else if (*value > high) {
*value = high;
}
}
int main(void) {
int value, low, high;
if (scanf("%d %d %d", &value, &low, &high) != 3) {
return 1;
}
clamp(&value, low, high);
printf("clamped: %d\n", value);
return 0;
}Practise this
Exercises for this lesson are in the C practice set.
Related lessons
- Functions in CHow to define and call C functions: prototypes before the first call, arguments copied by value, void functions, early returns and local variables.10 min
- Arrays in CHow C arrays store same-typed values by zero-based index: initialiser lists, the sizeof length idiom, passing arrays to functions and out-of-range dangers.10 min
- Dynamic Memory in CRequesting memory at run time in C with malloc, calloc and realloc, checking for NULL, sizing allocations correctly, and freeing every block exactly once.10 min
- Structures in CHow a C struct bundles related values into one record, how to initialise, copy and pass structs to functions, and when to use -> instead of the dot.9 min
Frequently asked questions
Why does scanf need & but printf does not?
printf only reads its arguments, so a copy of each value is enough. scanf has to store what it reads into your variable, and since C passes arguments by value it can only do that when given the variable's address. Arrays are the exception: scanf("%15s", name) needs no & because an array name already converts to the address of its first element.
Are arrays and pointers the same thing in C?
No, although they are easy to confuse. An array is a block of elements with a size known to sizeof; a pointer is a single variable holding an address. The link is that an array name converts to a pointer to its first element in most expressions, and indexing is defined through pointer arithmetic. The differences show when you take sizeof, try to assign to an array name, or declare a function parameter, where int a[] silently becomes int *a.
What is a NULL pointer?
A pointer value guaranteed not to be the address of any object. It is the conventional way for a function to say "nothing found" or "this failed": fopen, malloc, strchr and fgets all return NULL in that case. Dereferencing NULL is undefined behaviour, so code that receives a pointer from such a function must test it before use.
How this page was checked. Every program on it was run with GCC 13 (C11) at build time by the publishing checks, and the output shown is what it printed. Running C inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with GCC 13 (C11) locally.