Указатели и арифметика адресов

Урок раскрывает арифметику указателей: почему p+1 сдвигается не на байт, а на размер типа, и как эта механика связывает указатели с массивами.
Прибавление единицы к указателю сдвигает его не на один байт, а на размер одного элемента того типа, на который он указывает. Это и делает указатели удобными для прохода по массивам.

Над указателями можно выполнять арифметику, но она «умная». Если p — указатель на int (4 байта), то p + 1 указывает не на следующий байт, а на следующее целое — то есть сдвигается на 4 байта. Компилятор сам учитывает размер типа:

int arr[3] = {10, 20, 30};
int *p = arr;          // p указывает на arr[0]

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20 — следующий int
printf("%d\n", *(p + 2)); // 30

Здесь же раскрывается глубокая связь: имя массива arr само по себе ведёт себя как указатель на первый элемент. Поэтому arr[i] и *(arr + i) — это буквально одно и то же. Индексация массива — синтаксический сахар над арифметикой указателей.

Как работает под капотом

Массив в памяти — это непрерывная цепочка элементов. Указатель шагает по ней с шагом в размер типа:

int arr[3] = {10, 20, 30};   sizeof(int) = 4 байта

Адрес:   0x1000   0x1004   0x1008
         +------+ +------+ +------+
         |  10  | |  20  | |  30  |
         +------+ +------+ +------+
            ^        ^        ^
            p      p+1      p+2     (шаг = 4 байта, не 1!)

*(p)   = arr[0] = 10
*(p+1) = arr[1] = 20
*(p+2) = arr[2] = 30

arr[i]  тождественно  *(arr + i)

Вот почему один и тот же массив можно обойти и через индекс, и через движущийся указатель. Разность двух указателей тоже осмысленна: &arr[2] - &arr[0] даёт 2 — число элементов между ними, а не байтов.

// Проход по массиву движущимся указателем
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
int sum = 0;
for (int i = 0; i < 5; i++) {
    sum += *p;    // берём текущий элемент
    p++;          // сдвигаемся на следующий int
}
printf("Сумма: %d\n", sum);  // 15

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

  • Расчёт сдвига в байтах. p + 1 — это плюс один элемент, а не один байт. Путаница ведёт к неверным адресам.
  • Выход за границы. Указатель легко увести за конец массива (p + 100); разыменование такого указателя — чтение чужой памяти.
  • Сложение двух указателей. Складывать указатели бессмысленно и запрещено; вычитать — можно (разность в элементах).
  • Смешение типов. Арифметика указателя на char и на int даёт разный шаг; нельзя их путать.

Best practices

  • Предпочитайте индексацию arr[i], когда она яснее: она читается проще, чем *(arr + i).
  • Всегда держите в голове границы. Указатель не знает, где кончается массив — это знаете только вы.
  • Для размера массива используйте отдельную переменную или константу и передавайте её в функции вместе с указателем.

В Python индексы по списку — естественный способ показать ту же логику. Смоделируем «движущийся указатель» через индекс, который шагает на 1 элемент.

arr = [10, 20, 30, 40, 50]

# arr[i] эквивалентно *(arr + i) в C
i = 0                  # "указатель" как индекс на элемент
total = 0
while i < len(arr):
    total += arr[i]    # *p
    i += 1             # p++ — на следующий ЭЛЕМЕНТ, не байт
print("Сумма:", total)

# *(arr + 2) — это просто arr[2]
print("*(arr+2) =", arr[2])

Та же логика на Python ▶ — индекс шагает по элементам, ровно как указатель в C шагает на размер типа. Связь arr[i] == *(arr+i) — фундамент работы с массивами.

Указатель const и строки в аргументах

Арифметика указателей особенно естественна при обработке строк, ведь строка — это и есть массив, по которому удобно идти указателем до нулевого символа. Многие классические реализации строковых функций написаны именно так: указатель ползёт по символам, пока не упрётся в '\0'. При этом важно различать два вида «константности» указателя. const char *p — указатель на неизменяемые символы: сам p двигать можно, а вот *p менять нельзя. А char * const p — наоборот: символы менять можно, а сам указатель нельзя сдвинуть. Эта разница кажется придиркой, но именно она позволяет компилятору ловить ошибки и оптимизировать код, а вам — точно выражать намерение: «я только читаю эти данные» или «я не сдвину этот указатель».

Итоги

Арифметика указателей учитывает размер типа: p + 1 сдвигается на один элемент, а не байт. Имя массива ведёт себя как указатель на первый элемент, поэтому arr[i] тождественно *(arr + i) — это и есть связь массивов и указателей. По данным можно ходить движущимся указателем. Главная опасность — выход за границы, ведь указатель сам их не знает.

Проверьте себя
1. На сколько сдвинется указатель int *p при выполнении p + 1, если sizeof(int) равен 4?
AНа 1 байт
BНа 4 байта — то есть на один элемент типа int
CНа 8 байт
DУказатель не изменится
2. Чему эквивалентно выражение arr[i] в C?
A&arr + i
B*(arr + i)
Carr * i
Di * sizeof(arr)