Арифметика указателей (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, хотя на практике строки лежат подряд и пример работает.