Why this matters
Pointers are the idea that makes C C. Every array you passed to a function, every string, every scanf("%d", &x) already used them behind the scenes. They are how a function changes its caller's variables, how strings and arrays are handed around without copying, how memory from malloc is reached, and how linked lists, trees and every other dynamic data structure are built. They are also the source of C's most feared bugs. The good news is that the core idea is small: a pointer is a variable that holds an address. This module takes that idea slowly and draws a lot of pictures.
Every variable has an address
Recall from the first module that memory is a long row of bytes, each with a numeric address. When you declare int x = 42;, the compiler reserves 4 bytes somewhere (on the stack, for a local variable) and from then on "x" means "the int stored at that address".
The address-of operator & gives you that address. You have used it with scanf since the start: scanf("%d", &x) hands scanf the address of x so it can store the number there.
#include <stdio.h>
int main(void)
{
int x = 42;
int *p = &x; /* p holds the address of x */
printf("x = %d\n", x);
printf("&x = %p\n", (void *)&x);
printf("p = %p\n", (void *)p);
printf("*p = %d\n", *p);
*p = 100; /* write through the pointer */
printf("x is now %d\n", x);
printf("sizeof x = %zu, sizeof p = %zu\n", sizeof x, sizeof p);
return 0;
}
One run in the sandbox printed:
x = 42
&x = 0x7ffe50d43544
p = 0x7ffe50d43544
*p = 42
x is now 100
sizeof x = 4, sizeof p = 8
The addresses are different on every run (Linux deliberately randomizes where the stack goes, a security feature called ASLR), so never write code or tests that depend on their exact value. %p prints an address in hexadecimal; it expects a void *, hence the (void *) cast, which the last lesson of this module explains.
Declaring, assigning, dereferencing
int *p;
declares p as a pointer to int: a variable whose value is the address of an int. Read the declaration as "*p is an int". The * here is part of the declaration syntax.
p = &x;
stores the address of x in p. We say "p points to x".
*p
in an expression is the dereference (or indirection) operator: "the thing p points to". *p can be read (int y = *p;) or assigned (*p = 100;), exactly as if you had written x.
So the same symbol * has two jobs, which confuses everyone at first:
| where | meaning |
|---|---|
in a declaration: int *p; | p is a pointer to int |
in an expression: *p = 5; | the int that p points to |
between two values: a * b | multiplication (unrelated) |
& and * are opposites: *&x is x, and &*p is p.
The picture
Here is the program above as boxes in memory. A pointer is just another box; its contents happen to be an address.
address contents name
+------------+ +--------------+
| 0x...3544 | | 42 | x (int, 4 bytes)
+------------+ +--------------+
^
| p points to x
+------------+ +------+-------+
| 0x...3548 | | 0x...3544 | p (int *, 8 bytes)
+------------+ +--------------+
Most of the time we leave the numbers out and draw an arrow:
p x
+----+ +-----+
| *-+------->| 42 |
+----+ +-----+
After *p = 100; the arrow is unchanged and the box it leads to holds 100. After p = &y; the arrow would move to y and x would not change at all. Assigning to p moves the arrow; assigning to *p changes what is at the end of it.
#include <stdio.h>
int main(void)
{
int a = 1, b = 2;
int *p = &a;
*p += 10; /* a is now 11 */
p = &b; /* p now points at b; a is untouched */
*p *= 3; /* b is now 6 */
printf("a = %d, b = %d, *p = %d\n", a, b, *p);
return 0;
}
a = 11, b = 6, *p = 6
Pointers have types
A pointer's type says what kind of object is at the address, which tells * how many bytes to read and how to interpret them. Dereferencing an int * reads 4 bytes as an integer; a double * reads 8 bytes as a floating-point number. Every pointer on the sandbox is 8 bytes (64-bit addresses), whatever it points to, but the types are not interchangeable:
double d = 1.5;
int *p = &d;
main.c:5:14: error: initialization of 'int *' from incompatible pointer type 'double *' [-Wincompatible-pointer-types]
Likewise, an address is not an integer:
int y = &x;
main.c:7:13: error: initialization of 'int' from 'int *' makes integer from pointer without a cast [-Wint-conversion]
Older compilers only warned about these; GCC 14 makes them errors, which is a good thing.
NULL: pointing at nothing
NULL (defined in <stdio.h>, <stdlib.h> and others) is a special pointer value meaning "no object". Use it to initialize pointers that do not point anywhere yet, and to signal "not found" or "failed": you have already seen fgets return NULL at end of input.
Dereferencing NULL is undefined behavior. On Linux the lowest pages of memory are never mapped, so the CPU faults and the kernel kills the process:
#include <stdio.h>
int main(void)
{
int *p = NULL;
printf("about to dereference NULL\n");
*p = 5;
printf("never printed\n");
return 0;
}
$ ./prog
about to dereference NULL
Segmentation fault (core dumped)
$ echo $?
139
A segmentation fault means the program touched memory it is not allowed to touch. The exit status 139 is 128 + 11, where 11 is the number of the signal SIGSEGV. (If the output goes to a pipe or file instead of a terminal, even the first line can be lost, because it was still waiting in an output buffer when the process died. Output buffering is covered in the I/O module.)
Before dereferencing a pointer that might be NULL, check: if (p != NULL) ..., or simply if (p) ..., since a null pointer counts as false.
Uninitialized pointers
A local pointer that you never assign holds whatever bytes were there before, which is some random address:
int *p;
*p = 1;
gcc catches the simple case: warning: 'p' is used uninitialized [-Wuninitialized]. In general it cannot, and writing through a garbage pointer may crash, or may silently overwrite some other variable, which is far worse. Rule: every pointer is initialized when declared, to a real address or to NULL.
Common mistakes
- Mixing up
pand*p.p = 5;tries to store 5 as an address (an error in GCC 14:assignment to 'int *' from 'int' makes pointer from integer without a cast); you meant*p = 5;. And*p = &x;stores an address into an int; you meantp = &x;. int *p, q;declares one pointer and one int.- Dereferencing
NULLor an uninitialized pointer: segmentation fault, or worse, silent corruption. - Printing a pointer with
%dinstead of%p: gcc warnsformat '%d' expects argument of type 'int', but argument 2 has type 'int *', and only half the address is printed. - Expecting addresses to be the same between runs. They are not, by design.
Recap
- A pointer is a variable holding an address.
int *pdeclares a pointer to int. &xis the address ofx;*pis the objectppoints to, for reading or writing.- Assigning
pchanges where it points; assigning*pchanges the pointed-to object. - A pointer's type determines how
*reads memory; types do not mix without a cast. NULLmeans "points nowhere"; dereferencing it (or an uninitialized pointer) is undefined behavior, usually a segmentation fault.- Initialize every pointer when you declare it.
Going further: in the sandbox terminal, compile the first example with -g and run it in gdb: break 9, run, then print x, print &x, print p, print *p. gdb understands pointers and shows the same relationships you drew on paper. (The debugging module covers gdb properly.)