Указатели и арифметика адресов
Урок раскрывает арифметику указателей: почему 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) — это и есть связь массивов и указателей. По данным можно ходить движущимся указателем. Главная опасность — выход за границы, ведь указатель сам их не знает.