CMPUT-201 · Lesson 5 of 12 · 31 min
Pointers and addresses
Pointers are the feature that most distinguishes C from Python, and the one that gives C both its power and its danger. A pointer is a variable whose value is a memory address: the location of some other piece of data. With pointers a function can change its caller's variables, as the swap function in lesson 4 did; large structures can be passed around without copying; dynamic data structures such as linked lists and trees can be built; and programs can work directly with memory-mapped hardware. Python uses pointers too, internally, for every name and every list element, but it hides them. In C they are explicit, and you are responsible for making sure every pointer you follow points somewhere valid.
This lesson builds the mental model you need: memory as a long row of numbered bytes, the address-of operator & and the dereference operator *, pointer types, the null pointer, pointer arithmetic, pointers to pointers, const with pointers, and the generic pointer void *. It finishes with the ways pointers go wrong and how the sanitizers report them. Arrays and strings, which are closely tied to pointers, follow in lesson 6.
Memory as numbered bytes
From a C program's point of view, memory is a very long sequence of bytes, each with a numeric address. A 64-bit Linux process can in principle address 2 to the power 48 bytes, although only the parts the operating system has mapped for it can actually be used. Every variable occupies one or more consecutive bytes: a char occupies one, an int four, a double eight, and the variable's address is the address of its first byte. The operating system gives each process its own virtual address space, so the same address in two different programs refers to different physical memory, a topic for CMPUT-379. Modern systems also randomize where the stack, heap and program are placed each time a program runs, a security measure called address space layout randomization, so the addresses you print will change from run to run.
In the figure, score is an int holding 42, stored at some address. p is a pointer variable, itself stored at another address, whose value is the address of score. Following the pointer, called dereferencing, gets you to score. Keep three things separate in your mind: the pointer's own address, the pointer's value (an address), and the value at that address.
The & and * operators
The unary operator & gives the address of a variable: &score is the address of score. A pointer variable is declared with * between the type and the name: int *p; declares p as a pointer to int, meaning its value will be the address of an int. The unary operator * applied to a pointer dereferences it: *p is the int that p points to, and it can be read or assigned. So after p = &score;, writing *p = 50; changes score to 50. The %p conversion prints an address; the C standard requires the argument to be converted to void *.
#include <stdio.h>
int main(void) {
int score = 42;
int *p = &score; /* p holds the address of score */
printf("score = %d\n", score);
printf("*p = %d\n", *p);
*p = 50; /* change score through the pointer */
printf("score = %d after *p = 50\n", score);
printf("&score = %p\n", (void *)&score);
printf("p = %p (the same address)\n", (void *)p);
printf("&p = %p (where p itself is stored)\n", (void *)&p);
printf("sizeof p = %zu bytes, sizeof *p = %zu bytes\n", sizeof p, sizeof *p);
return 0;
}score = 42 *p = 42 score = 50 after *p = 50 &score = 0x7f2108a00030 p = 0x7f2108a00030 (the same address) &p = 0x7f2108a00040 (where p itself is stored) sizeof p = 8 bytes, sizeof *p = 4 bytes
The * in a declaration and the * operator in an expression are related but different uses of the same symbol. In a declaration, int *p means "p is a pointer to int"; a way to remember it is that the expression *p has type int. The * binds to the name, not the type, so int *a, b; declares a as a pointer and b as a plain int. Declare one pointer per line to avoid that trap. Every pointer on a 64-bit system is 8 bytes, whatever it points to, because it only holds an address.
Pointer types matter even though all addresses look alike. The type tells the compiler how many bytes to read or write when you dereference and how to interpret them: *p for an int pointer reads four bytes as an integer, while *q for a double pointer reads eight bytes as a floating-point number. Assigning a double * to an int * variable is a type error the compiler reports, because using one as the other would misinterpret memory.
The null pointer
A pointer that points to nothing is given the special value NULL, defined in stddef.h, stdio.h and stdlib.h (C23 adds the keyword nullptr). Functions use NULL to signal failure or absence: fopen returns NULL if a file cannot be opened, malloc returns NULL if memory runs out, and the last node of a linked list has a NULL next pointer. Dereferencing a null pointer is undefined behavior; on Linux it almost always crashes the program with a segmentation fault, the kernel's response to an access to an unmapped address. Check any pointer that might be NULL before using it, and initialize pointer variables to NULL when you do not yet have a valid address for them, so that a mistaken use crashes predictably rather than using a random address.
#include <stdio.h>
static int first_or_default(const int *values, int n, int fallback) {
if (values == NULL || n <= 0) {
return fallback; /* guard before dereferencing */
}
return values[0];
}
int main(void) {
int data[] = {7, 8, 9};
printf("%d\n", first_or_default(data, 3, -1));
printf("%d\n", first_or_default(NULL, 0, -1));
int *missing = NULL;
printf("about to dereference NULL\n");
fflush(stdout);
printf("%d\n", *missing); /* UB: crashes */
return 0;
}7
-1
about to dereference NULL
null.c:18:5: runtime error: load of null pointer of type 'int'
AddressSanitizer:DEADLYSIGNAL
=================================================================
==4242==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x56420f06d5a5 bp 0x7ffdc35bdbc0 sp 0x7ffdc35bdb30 T0)
==4242==The signal is caused by a READ memory access.
==4242==Hint: address points to the zero page.
#0 0x56420f06d5a5 in main null.c:18
#1 0x7f193c02a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#2 0x7f193c02a28a in __libc_start_main_impl ../csu/libc-start.c:360
#3 0x56420f06d204 in _start (prog+0x1204) (BuildId: 538827350ae509eddc36298027abd8bfa3cafd2c)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV null.c:18 in main
==4242==ABORTINGThe sanitizers turn the crash into precise reports. UndefinedBehaviorSanitizer speaks first, naming the line where a null int pointer was loaded; then AddressSanitizer reports a SEGV (segmentation violation) caused by a read at address 0x000000000000 in the zero page, with a stack trace leading to the faulty statement. That last part is what makes the sanitizers so valuable; without them you would see only "Segmentation fault (core dumped)" and have to find the line yourself with gdb.
Pointers as parameters
Because C passes everything by value, pointers are how a function produces more than one result or modifies its caller's data. The caller passes addresses, and the function writes through them. Such pointer parameters are often called output parameters. The standard library uses the pattern widely; for example, strtol reports through a pointer where parsing stopped, and lesson 10 uses it. A function can also return a status code and deliver its real results through pointers, which is the usual C style for operations that can fail.
#include <stdio.h>
#include <stdbool.h>
/* Divide a by b, storing quotient and remainder through the pointers.
Returns false (and stores nothing) if b is zero. */
static bool divide(int a, int b, int *quotient, int *remainder) {
if (b == 0) {
return false;
}
*quotient = a / b;
*remainder = a % b;
return true;
}
int main(void) {
int q, r;
if (divide(47, 5, &q, &r)) {
printf("47 = 5 * %d + %d\n", q, r);
}
if (!divide(1, 0, &q, &r)) {
printf("cannot divide by zero\n");
}
int minutes = 135, hours, mins;
divide(minutes, 60, &hours, &mins);
printf("135 minutes is %d h %02d min\n", hours, mins);
return 0;
}47 = 5 * 9 + 2 cannot divide by zero 135 minutes is 2 h 15 min
Pointer arithmetic
You can add an integer to a pointer or subtract one, and the result is measured in elements, not bytes. If p points to an int at address 1000, then p + 1 points to the next int, at address 1004, because an int is four bytes; for a double pointer p + 1 would be 1008. Subtracting two pointers into the same array gives the number of elements between them, as a value of type ptrdiff_t. Comparing pointers with < or > is meaningful only for pointers into the same array. This arithmetic is what makes pointers and arrays work together, as lesson 6 explains: a[i] is defined as *(a + i).
#include <stdio.h>
#include <stddef.h>
int main(void) {
int temps[5] = {12, 15, 9, 21, 18};
int *p = &temps[0];
double rates[3] = {1.5, 2.5, 3.5};
double *d = &rates[0];
printf("p = %p, *p = %d\n", (void *)p, *p);
printf("p + 1 = %p, *(p + 1) = %d\n", (void *)(p + 1), *(p + 1));
printf("int step: %td bytes\n", (char *)(p + 1) - (char *)p);
printf("double step: %td bytes\n", (char *)(d + 1) - (char *)d);
int *end = &temps[4];
printf("elements from p to end: %td\n", end - p);
int max = *p;
for (int *q = temps; q < temps + 5; q++) { /* walk the array with a pointer */
if (*q > max) max = *q;
}
printf("maximum = %d\n", max);
return 0;
}p = 0x7feef9900020, *p = 12 p + 1 = 0x7feef9900024, *(p + 1) = 15 int step: 4 bytes double step: 8 bytes elements from p to end: 4 maximum = 21
Pointer arithmetic is only defined inside an array, or one position past its last element. Forming a pointer two elements past the end, or before the beginning, is undefined behavior even if you never dereference it, and dereferencing the one-past-the-end pointer is undefined too. Loops such as for (q = temps; q < temps + 5; q++) are correct because they stop at the one-past-the-end address without reading it.
Worked problem
Computing addresses by hand
A double array prices starts at address 0x5000. A pointer p = &prices[2]. What are the addresses p, p + 3 and p - 1, and what is (p + 3) - p?
Show solution
- A double is 8 bytes, so element i is at 0x5000 + 8i.
- p = &prices[2] = 0x5000 + 16 = 0x5010.
- p + 3 points to prices[5]: 0x5000 + 40 = 0x5028. p - 1 points to prices[1]: 0x5008.
- Pointer subtraction counts elements, so (p + 3) - p is 3, even though the addresses differ by 24 bytes.
Answer: 0x5010, 0x5028 and 0x5008; the difference is 3 elements.
Pointers to pointers, const and void *
A pointer can point to another pointer. int **pp is a pointer to a pointer to int; *pp is an int *, and **pp is an int. You will meet double pointers in three common situations: a function that must change the caller's pointer, such as one that inserts at the head of a linked list (lesson 9); arrays of strings, such as the argv parameter of main (lesson 6); and two-dimensional dynamic arrays.
The const qualifier can apply to the pointed-to data, to the pointer itself or to both, depending on where it is written. Read the declaration from right to left. const int *p (or int const *p) is a pointer to constant int: you can change p to point elsewhere but cannot change the int through p. int *const p is a constant pointer to int: p must always point to the same place, but the int can change. const int *const p is constant in both ways. The first form is by far the most common, for parameters that a function only reads.
Finally, void * is a generic pointer: it can hold the address of any object type, but it cannot be dereferenced or used in arithmetic until it is converted to a specific type. In C, a void * converts implicitly to and from any other object pointer type, which is why malloc, which returns void *, can be assigned directly to an int * variable. Generic library functions such as qsort and memcpy take void * parameters so they can work with any data; lesson 9 shows how qsort uses them.
How pointers go wrong
Almost every serious C bug involves a pointer that does not point where the programmer thinks. A null pointer points nowhere. An uninitialized pointer contains whatever bits were in its memory, so following it reads or writes a random location. A dangling pointer once pointed to valid memory that has since gone away, such as a local variable of a function that has returned or a heap block that has been freed. An out-of-bounds pointer has been moved past the end of its array. And a pointer of the wrong type misinterprets the bytes it points to. None of these is detected by C itself; the tools are compiler warnings, the sanitizers, Valgrind and careful habits.
Worked problem
Fixing a function that should allocate for its caller
A function void make_zero(int *p) { int z = 0; p = &z; } is meant to make the caller's pointer point at a zero. After int *q = NULL; make_zero(q); the caller still has q == NULL. Explain, and say what a correct design would need.
Show solution
- p is a copy of q's value (NULL). Assigning to p changes only the copy, so q is untouched.
- Even if it did change q, it would point to z, a local variable that disappears when make_zero returns: a dangling pointer.
- To change the caller's pointer, the function needs the pointer's address: void make_zero(int **pp), called as make_zero(&q), assigning *pp = something.
- The something must outlive the call: a static variable, the caller's own variable, or heap memory from malloc (lesson 7).
Answer: It changes only its copy and points at a dying local; pass int ** and point it at storage that outlives the call.
Common mistakes
MistakeDeclaring int *a, b; and expecting two pointers.
InsteadThe * binds to each name; write int *a; int *b; on separate lines.
MistakeDereferencing a pointer without checking for NULL when the function that produced it can fail.
InsteadCheck results of fopen, malloc and similar functions before using them.
MistakePrinting a pointer with %d or %x.
InsteadUse %p with a (void *) cast.
MistakeThinking p + 1 adds one byte to the address.
InsteadPointer arithmetic moves by whole elements: sizeof(*p) bytes.
MistakeLeaving pointers uninitialized.
InsteadInitialize every pointer when it is declared, to a valid address or NULL.
MistakeConfusing const int *p with int *const p.
InsteadRead right to left: the first protects the data, the second fixes the pointer.
Summary and key terms
- Memory is a sequence of addressed bytes; a variable's address is that of its first byte.
- & takes an address and * dereferences a pointer; int *p declares a pointer to int.
- Pointer types tell the compiler how to read the memory they point to; all pointers are 8 bytes on 64-bit systems.
- NULL means points to nothing; dereferencing it is undefined and usually crashes.
- Functions modify caller data and return several results through pointer parameters.
- Pointer arithmetic counts in elements and is valid only within an array and one past its end.
- const can protect the data, the pointer or both; void * is a generic pointer that must be converted before use.
- Pointer
- A variable whose value is the address of another object.
- Address-of operator
- The unary & operator, which gives the address of an object.
- Dereference
- To follow a pointer to the object it points to, using the unary * operator.
- Null pointer
- A pointer value, written NULL, that points to no object.
- Segmentation fault
- A crash caused by accessing memory the process may not use.
- Output parameter
- A pointer parameter through which a function stores a result.
- Pointer arithmetic
- Adding or subtracting integers to pointers, measured in elements.
- Dangling pointer
- A pointer to memory whose lifetime has ended.
- void pointer
- A generic pointer type, void *, that can hold any object address.
- Address space layout randomization
- A security measure that places program regions at random addresses on each run.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Given int x = 5; int *p = &x;, what do *p, p and &p mean?
Show answer
*p is the int 5; p is the address of x; &p is the address of the pointer p.
Write code that sets x to 10 using only p.
Show answer
*p = 10;
What does int *a, b; declare?
Show answer
a is a pointer to int; b is an int.
If an int pointer p holds 0x1000, what does p + 4 hold?
Show answer
0x1010, because 4 ints of 4 bytes is 16 bytes.
Write a function void min_max(const int *v, int n, int *lo, int *hi) body.
Show answer
Set *lo = *hi = v[0]; then for i from 1 to n - 1 update *lo if v[i] < *lo and *hi if v[i] > *hi (assuming n >= 1).
Why must you check fopen's result before using it?
Show answer
It returns NULL on failure, and using a null pointer is undefined behavior.
Which declaration lets a function read but not modify an int through its parameter?
Show answer
const int *p
What is a dangling pointer? Give two causes.
Show answer
A pointer to memory that no longer exists, caused by returning the address of a local or using memory after free.
Why can't you dereference a void *?
Show answer
The compiler does not know the type, so it does not know how many bytes to read or how to interpret them.
Why do printed addresses change every time you run a program?
Show answer
Address space layout randomization places the stack, heap and code at random addresses.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.