Арифметика указателей (pointer arithmetic) — сдвиг указателя на целое число элементов и разность двух указателей в элементах. Единица измерения — не байт, а sizeof(T), где T — тип, на который указывают.

Зачем нужно

Адреса — целые числа, но обычная целочисленная арифметика к ним не применяется. Определены ровно две операции, и обе считают в элементах, а не в байтах. Зачем это нужно, становится ясно на массивах: элементы массива лежат подряд, и сдвиг на n — это переход к элементу с номером на n больше.

Как работает

Сдвиг. К указателю можно прибавить целое число или вычесть его. Результат — указатель, сдвинутый на n шагов размера sizeof(T):

int32_t* p = ...;
p + 5;    // + 20 байт
p - 4;    // - 16 байт
p += 10;
p -= 12;
++p;
p--;

Разность. Указатели одного типа можно вычитать. Результат — количество элементов типа T, помещающихся в промежутке: разница в байтах, делённая на sizeof(T).

Остальное запрещено: указатели нельзя складывать, умножать, делить.

Тонкости

  • Для указателей на отдельные переменные арифметика смысла не имеет: стандарт не говорит, как переменные расположены в памяти. В лонгриде:

    int x, y, z;
    std::cout << &z - &x << '\n';  // скорее всего, 2

    и там же оговорка, что разумного результата это не даёт, а разыменование такого указателя — UB.

Не из лекции

На GCC 16.2 этот пример печатает 1, а не 2 — и с -O0, и с -O2. По стандарту арифметика определена только внутри одного массива (плюс позиция сразу за последним элементом), поэтому UB здесь уже само вычитание указателей на разные переменные, а не только разыменование.

Частые ошибки

  • Считать, что p + 1 сдвигает на один байт. Сдвиг — на sizeof(T) байт.
  • Вычитать указатели разных типов или указатели на несвязанные объекты.

Углубление — L04

Обещанное применение — массивы. Элементы массива лежат подряд, и массив почти везде превращается в указатель на нулевой элемент. Поэтому индексирование — это и есть арифметика указателей:

int array[10];
int* p = array;
 
p[2];  // равносильно *(p + 2)
2[p];  // да, настолько равносильно *(2 + p)
 
p += 5;
p[2];  // array[7]

Внутри одного массива сдвиг и разность осмысленны: &a[i] - &a[j] == i - j.

У многомерного массива шаг зависит от уровня: для int b[2][3] выражение b + 1 сдвигается на целую строку, sizeof(int[3]) байт, а *(b + 1) + 1 — уже на один int:

b[1][1] <=> *(b[1] + 1) <=> *(*(b + 1) + 1) <=> *(&b[0][0] + 3 + 1);

Не из лекции

Последняя форма из лонгрида выходит за строку b[0] из трёх элементов. Формально арифметика разрешена только в пределах одного массива, поэтому строгий анализатор вправе считать это UB, хотя на практике строки лежат подряд и пример работает.