Pointer arithmetic and arrays

An array name in an expression is the address of its first element, and from that address the elements follow one after another. Adding one to a pointer moves it by one element, not by one byte, and subtracting two pointers gives the number of elements between them. Lesson 22 indexed the array; this lesson moves through it.

The array name becomes a pointer

In most expressions the name of an array is converted to a pointer to its first element, and that conversion is called decay. It is why int *p = values; compiles. The array itself keeps its size, but the pointer is only an address, so sizeof on the two gives different answers.

#include <stdio.h> int main(void) { int values[5] = {10, 20, 30, 40, 50}; int *p = values; printf("sizeof values: %zu\n", sizeof values); printf("sizeof p: %zu\n", sizeof p); printf("values[2]: %d\n", values[2]); printf("*(p + 2): %d\n", *(p + 2)); return 0; }

gcc -Wall -Wextra decay.c -o decay
./decay

sizeof values: 20
sizeof p: 8
values[2]: 30
*(p + 2): 30

Five ints take twenty bytes and the pointer takes eight, the size of an address from lesson 2. The two ways of naming the third element produce the same value, because indexing is defined as pointer arithmetic.

Walking with p++

p++ moves the pointer to the next element, and the loop can stop when it reaches the address just past the last one. The comparison uses the address of the end, not an index, so the loop does not need the count in a variable.

#include <stdio.h> int main(void) { int values[5] = {10, 20, 30, 40, 50}; int *p = values; int sum = 0; while (p < values + 5) { sum += *p; p++; } printf("sum: %d\n", sum); return 0; }

gcc -Wall -Wextra walk.c -o walk
./walk

sum: 150

The pointer does not move eight bytes: it moves four, the size of one int, because the compiler scales the addition by the element type. That is the whole point of typing pointers, and the reason p + 1 cannot be written as p + sizeof(int).

One past the end

C allows a pointer to the element just past the last one. It cannot be dereferenced, but it can be compared and subtracted, which is exactly what a loop needs. The difference of two pointers in the same array has type ptrdiff_t and is printed with %td.

#include <stdio.h> int main(void) { int values[4] = {3, 1, 4, 1}; for (int *p = values; p != values + 4; p++) { printf("%d ", *p); } printf("\n"); printf("count: %td\n", (values + 4) - values); return 0; }

gcc -Wall -Wextra end.c -o end
./end

3 1 4 1
count: 4

The loop condition p != values + 4 is the idiomatic form; writing p <= values + 4 would dereference the one-past-the-end address on the last pass, which is undefined behaviour. The trailing space after the last number is printed by the same statement as the others.

Two pointers

Two pointers moving toward each other reverse an array in place. One starts at the first element and one at the last, they swap values until they meet, and the loop ends in the middle. This is the same swap from lesson 27, applied to addresses instead of variables.

#include <stdio.h> static void reverse(int *first, int count) { int *last = first + count - 1; while (first < last) { int temporary = *first; *first = *last; *last = temporary; first++; last--; } } int main(void) { int values[6] = {1, 2, 3, 4, 5, 6}; reverse(values, 6); for (int i = 0; i < 6; i++) { printf("%d ", values[i]); } printf("\n"); return 0; }

gcc -Wall -Wextra reverse.c -o reverse
./reverse

6 5 4 3 2 1

The function receives a pointer and a count, because the array size is not part of a pointer. The call reverse(values, 6) passes the address of the first element and the number of elements, which is the interface every function in this course uses for arrays.

Past the end, wrong difference format

A pointer equal to values + 3 for an array of three is legal to form and illegal to dereference. At -O2 the compiler can see the constant offset and reports the dereference:

#include <stdio.h> int main(void) { int values[3] = {10, 20, 30}; int *p = values + 3; printf("%d\n", *p); return 0; }

gcc -O2 -Wall -Wextra bounds.c -o bounds

bounds.c: In function 'main':
bounds.c:8:5: warning: array subscript 3 is outside array bounds of 'int[3]' [-Warray-bounds=]
8 | printf("%d\n", *p);
| ^~~~~~~~~~~~~~~~~~
bounds.c:5:9: note: at offset 12 into object 'values' of size 12
5 | int values[3] = {10, 20, 30};
| ^~~~~~

The program is not run here: the value after the array is whatever the stack happens to hold. The second mistake is the format for a pointer difference. The type is ptrdiff_t, which is a long on this machine, not an int:

#include <stdio.h> int main(void) { int values[4] = {1, 2, 3, 4}; int *end = values + 4; printf("%d\n", end - values); return 0; }

gcc -Wall -Wextra ptrfmt.c -o ptrfmt
./ptrfmt

ptrfmt.c: In function 'main':
ptrfmt.c:8:14: warning: format '%d' expects argument of type 'int', but argument 2 has type 'long int' [-Wformat=]
8 | printf("%d\n", end - values);
| ~^ ~~~~~~~~~~~~
| | |
| | long int
| int
| %ld
4

The result prints correctly here because the two types happen to travel in the same register, but the warning is right: use %td for ptrdiff_t. That is the conversion the end section used for the same expression.

Exercises

  1. Sum an array with a pointer loop, then again with p != values + count, and compare the two conditions.
  2. Write a function that finds the largest element and returns a pointer to it. What should it return for an empty array?
  3. Rotate an array of six elements left by two positions using index arithmetic, then repeat it with pointers only.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. By how many bytes does p + 1 move an int * on this machine?

    Answer

    4

    Pointer arithmetic is scaled by the element type, and an int is four bytes here.

  2. What is special about a pointer one past the last array element?

    Answer

    It can be compared and subtracted but not dereferenced

    Forming the one-past-the-end address is legal, and loops use it as a sentinel; dereferencing it is undefined behaviour.

  3. What happens to an array name in most expressions?

    Answer

    It becomes a pointer to its first element

    Array-to-pointer decay is why int *p = values; compiles, and why sizeof then measures a pointer instead of the array.

  4. Which conversion prints a pointer difference of type ptrdiff_t?

    Answer

    %td

    ptrdiff_t has its own conversion, %td; %d can warn because the type is a signed long here.

  5. What does this program print? #include <stdio.h> int main(void) { int values[5] = {10, 20, 30, 40, 50}; printf("%zu\n", sizeof values); return 0; }

    Answer

    20

    Five int elements of four bytes each take twenty bytes.

  6. What does this program print? #include <stdio.h> int main(void) { int values[5] = {10, 20, 30, 40, 50}; int *p = values; printf("%zu\n", sizeof p); return 0; }

    Answer

    8

    A pointer takes eight bytes on this 64-bit platform, whatever it points to.

  7. What does this program print? #include <stdio.h> int main(void) { int values[5] = {10, 20, 30, 40, 50}; int *p = values; printf("%d\n", *(p + 2)); return 0; }

    Answer

    30

    p + 2 points at the third element, and the dereference reads the same value as values[2].

  8. What does this program print? #include <stdio.h> int main(void) { int values[5] = {10, 20, 30, 40, 50}; int *p = values; int sum = 0; while (p < values + 5) { sum += *p; p++; } printf("%d\n", sum); return 0; }

    Answer

    150

    The loop adds all five values from 10 to 50, which makes 150.

  9. What does this program print? #include <stdio.h> static void reverse(int *first, int count) { int *last = first + count - 1; while (first < last) { int temporary = *first; *first = *last; *last = temporary; first++; last--; } } int main(void) { int values[6] = {1, 2, 3, 4, 5, 6}; reverse(values, 6); for (int i = 0; i < 6; i++) printf("%d ", values[i]); printf("\n"); return 0; }

    Answer

    6 5 4 3 2 1

    The two pointers swap the ends and move inward, so the array is reversed in place.

  10. This program is built with gcc -std=c17 -Wall -Wextra ptrfmt.c -o ptrfmt. Which warning flag does GCC print? #include <stdio.h> int main(void) { int values[4] = {1, 2, 3, 4}; int *end = values + 4; printf("%d\n", end - values); return 0; }

    Answer

    -Wformat=

    The pointer difference is a long printed with %d, so the flag is -Wformat=.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev