int i–int sum–$ g++ -Wall -Wextra sum.cpp# no warnings, no errors$ ./a.out# a wrong total?# a crash?# 60, by luck?$ g++ -O2 sum.cpp$ ./a.out# may act differentlyERROR: AddressSanitizer:stack-buffer-overflowREAD of size 4in main sum.cpp:6601#include <iostream>2int main() {3 int a[3] = {10, 20, 30};4 int sum = 0;5 for (int i = 0; i <= 3; ++i) for (int i = 0; i < 3; ++i)6 sum += a[i];7 std::cout << sum << '\n';8}a[0]10a[1]20a[2]30a[3]?102030?!-O2
Violet card: the program; slate cells: the array a; khaki cells: i and sumRed cell: the memory just past the end of a (a[3]); gold fence: the sanitizerTokens: values read from the cells, a red ? for the bad read, a red ! for the reportIn front: the code card sum.cpp and the array a as three memory cells, then a red cell just past its end. Behind them: the variables i and sum. At the back: the compiler and the terminal. The loop's first three rounds carry 10, 20 and 30 into sum. Because the test is i <= 3, a fourth round reads a[3], memory that is not part of a: undefined behaviour, so sum becomes a question mark. Nothing checks the read and the compiler may optimise on the promise it never happens. AddressSanitizer fences off the array and reports the read at line 6; the fix, i < 3, prints 60. The figure does not show a value for a[3], because there is none to show: what a plain build prints is not defined and differs between compilers, settings and runs.

The program

  1. This program keeps three ints in an array called a, then adds them up. An array's elements sit side by side in memory, one after another: a[0] holds 10, a[1] holds 20, a[2] holds 30.

Past the end

Find it, fix it

What happens

;; step 1/10 · program
sum.cpp
  1  #include <iostream>
  2  int main() {
  3    int a[3] = {10, 20, 30};
  4    int sum = 0;
  5    for (int i = 0; i <= 3; ++i)
  6      sum += a[i];
  7    std::cout << sum << '\n';
  8  }
line 3: int a[3]  an array of 3 ints, stored next to each other

The C++ standard calls these cases undefined behaviour and places no requirements on them. cppreference's examples include memory access outside an array's bounds, signed integer overflow, dereferencing a null pointer and reading an uninitialised variable; using an object after its lifetime has ended (for example after delete) is undefined too. The figure does not show a value for a[3], because there is none to show: what a plain build prints is not defined and differs between compilers, settings and runs.