Pointer arithmetic

An array is a block of elements, and a pointer to its first element can move across it. Adding 1 to an int* moves four bytes, because the unit of pointer arithmetic is the pointed-to type, not a byte.

The range [begin, end) of lesson 049 is the same idea with a class. This lesson uses the raw form: a loop with ++, the distance between two pointers, the end pointer that may be formed but not read, and the arithmetic the compiler refuses.

Walking an array

An array name converts to a pointer to its first element, so int* p = values needs no cast. Adding one to the pointer moves to the next element, and dereferencing reads it.

The program sums four ints with a pointer loop and prints the sum and the number of elements, which is end - begin. The subtraction of two pointers gives the distance in elements.

The loop condition compares pointers, not indices, which is the form the standard algorithms use internally. The same loop works for a vector with values.data() and values.data() + values.size().

#include <iostream> int main() { int values[]{10, 20, 30, 40}; int* begin = values; int* end = values + 4; int sum = 0; for (int* p = begin; p != end; ++p) { sum += *p; } std::cout << sum << ' ' << (end - begin) << '\n'; }

g++ -std=c++23 -Wall -Wextra walk.cpp -o walk
./walk

100 4

Distance and size

The size of an array is sizeof(values) / sizeof(values[0]), which only works where the array has not decayed to a pointer. The difference of two pointers is a std::ptrdiff_t, a signed type.

The program prints the size in bytes, the element count and the distance as a ptrdiff_t. The three numbers are related by the size of one element.

Inside a function that takes a pointer, the size is gone: the pointer holds no length. That is why a function over a raw array needs a count as a second parameter, and why lesson 020 introduced std::span.

#include <cstddef> #include <iostream> int main() { int values[]{10, 20, 30, 40}; std::cout << sizeof(values) << ' ' << sizeof(values[0]) << '\n'; std::ptrdiff_t count = (values + 4) - values; std::cout << count << '\n'; }

g++ -std=c++23 -Wall -Wextra distance.cpp -o distance
./distance

16 4
4

One past the end

A pointer may point one past the last element; it may not be dereferenced. The range [begin, end) uses that position as its terminator, and every loop in the library stops when it reaches it.

The program forms the end pointer, prints how many elements it is from the start and prints whether the loop reached it. The end pointer is never read.

Going further is undefined: values + 5 for a four-element array has no defined value. The compiler will not stop you, which is why the range form is easier to get right.

#include <iostream> int main() { int values[]{1, 2, 3, 4}; int* begin = values; int* end = values + 4; int* p = begin; while (p != end) { ++p; } std::cout << (end - begin) << ' ' << (p == end) << '\n'; }

g++ -std=c++23 -Wall -Wextra one_past.cpp -o one_past
./one_past

4 1

p[i] is *(p + i)

Subscripting a pointer is defined as pointer arithmetic followed by a dereference: p[i] means *(p + i). The two forms are interchangeable, and the bracket one is easier to read.

The program reads the same three elements in three ways and prints them. All three reach the same bytes, and the compiler generates the same code.

The equivalence is also why i[p] compiles: it means *(i + p). Nobody writes that, and the fact that it works is the clearest sign that the bracket is arithmetic, not a container operation.

The bracket does no bounds check, as the vector version of operator[] does no bounds check either. A pointer that is one past the end is a valid pointer value to compare and an invalid one to read.

#include <iostream> int main() { int values[]{10, 20, 30}; int* p = values; std::cout << p[0] << ' ' << *(p + 1) << ' ' << p[2] << '\n'; }

g++ -std=c++23 -Wall -Wextra index.cpp -o index_ptr
./index_ptr

10 20 30

Pointers add, they do not multiply

Pointer arithmetic is addition and subtraction: moving by a number of elements, or measuring the distance between two addresses. Multiplying two addresses, or a pointer by a number, has no meaning and the compiler rejects it.

The program multiplies a pointer by 2 and the compiler reports invalid operands to the multiplication. The message names the types, and the fix is to multiply the value the pointer points at, with *p * 2.

The same rule stops p / 2, p % 2 and p * p. The one arithmetic operation the language defines on pointers is the step, and every other one is a sign that the code is working with the wrong type.

int main() { int values[]{1, 2, 3}; int* pointer = values; int* doubled = pointer * 2; return *doubled; }

g++ -std=c++23 -Wall -Wextra multiply.cpp -o multiply

multiply.cpp: In function 'int main()':
multiply.cpp:5:28: error: invalid operands of types 'int*' and 'int' to binary 'operator*'
5 | int* doubled = pointer * 2;
| ~~~~~~~ ^ ~
| | |
| int* int

Exercises

  1. Walk an array backwards with a pointer and print the elements in reverse.
  2. Write a function that takes a pointer and a count and returns the sum, then call it with the array and its size.
  3. Print the distance between the first and the last element of a vector's data and compare it with size().

Questions

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

  1. How far does p + 1 move when p is an int*?

    Answer

    Four bytes on this build

    Pointer arithmetic is in units of the pointed-to type, and sizeof(int) is 4 here.

  2. What does this program print? int v[]{10, 20, 30, 40}; int* e = v + 4; std::cout << (e - v);

    Answer

    4

    Subtracting two pointers gives the number of elements between them, not bytes.

  3. What does p[i] mean for a pointer p?

    Answer

    *(p + i)

    The bracket is defined as the dereference of the advanced pointer.

  4. May a pointer point one past the last element of an array?

    Answer

    Yes, it may be formed and compared but not dereferenced

    The one-past-the-end pointer is the terminator of every range; dereferencing it is undefined.

  5. What does this program print? int v[]{1, 2, 3}; int* p = v; std::cout << sizeof(v) / sizeof(v[0]);

    Answer

    3

    sizeof(v) is 12 bytes and sizeof(v[0]) is 4, so the quotient is 3.

  6. Why does int values[4]; values + 5 have no defined meaning?

    Answer

    It goes past the one-past-the-end position

    Arithmetic is only defined up to one past the end; anything beyond is undefined behaviour.

  7. What does the compiler do with int* p = values; int* q = p * 2;?

    Answer

    It refuses: there is no multiplication for pointers

    Only addition and subtraction of an integer are defined on pointers; the multiplication of a pointer by a number is an error.

  8. Inside a function that receives int* values and no count, what can the function know about the array?

    Answer

    Nothing about its length

    The pointer holds only the address of the first element; the length was lost when the array decayed to a pointer.

  9. What is the type of the difference of two pointers?

    Answer

    std::ptrdiff_t

    The distance may be negative when the pointers are in the other order, so the standard uses a signed type.

  10. What does this loop do? for (int* p = v; p != v + 3; ++p) std::cout << *p; with int v[]{1,2,3,4};

    Answer

    Prints 1 2 3

    The loop stops when p reaches v + 3, which is the element holding 4, so only the first three are printed.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev