Прежде чем начать: зачем вообще нужны указатели
Почти каждый студент задаёт этот вопрос. Ответ — в трёх сценариях, где без указателей не обойтись.
Динамическая память
Мы не знаем заранее, сколько памяти нужно. Например, читаем числа из файла — их может быть 10 или 10000. Указатель позволяет запросить память во время работы программы.
Изменение аргументов
Функция swap(a, b) должна поменять значения в вызывающем коде.
Без указателей функция получит только копии и не сможет изменить оригиналы.
Эффективность
Передать функции большую структуру целиком — дорого. Передать указатель — дёшево: копируется всего 8 байт адреса, а не, скажем, 10 КБ данных.
Что такое память: сетка пронумерованных ячеек
Память компьютера — это огромный массив байтов. У каждого байта есть свой уникальный номер — адрес. Именно по адресам процессор и находит нужные данные.
x», мы на самом деле имеем в виду «жилец в квартире номер 0x1000».
0x7ffd3c2a4b10). Для наглядности здесь используются «красивые» адреса
вроде 0x1000. Логика от этого не меняется.
Переменная — это три вещи одновременно
Когда вы пишете int x = 42;, происходят три вещи:
- 1. Компилятор выбирает свободный участок памяти — например, 4 байта с адресом
0x1000. - 2. В эту область записывается значение
42. - 3. Имя
xсвязывается с адресом0x1000— компилятор впредь будет обращаться по этому адресу.
Демонстрация 1: операторы & и *
Самая важная и простая демонстрация. Здесь два оператора, но они путаются у всех — потому что
знак * играет разные роли в зависимости от контекста.
& — «взять адрес»
Применяется к переменной, возвращает её адрес в памяти.
int x = 42;
cout << &x; // напечатает адрес,
// например 0x7ffd...b10
* — «пойти по адресу»
Применяется к указателю, даёт доступ к значению по этому адресу.
int *p = &x; // p хранит адрес x
cout << *p; // 42 — значение по адресу
*p = 100; // изменили x через p
* в объявлении int *p; —
это часть типа («p — указатель на int»), а не разыменование. И только
в выражении *p = 100; это уже разыменование («пойти по адресу и записать 100»).
Это разные вещи, хоть и выглядят одинаково.
Пошаговая трассировка
Нажимайте «Шаг», чтобы увидеть, как меняется память на каждом этапе.
Демонстрация 2: зачем указатели в функциях
Классический пример — функция обмена значений. Здесь особенно наглядно, почему без указателей не обойтись.
Не работает
void swap(int a, int b) {
int t = a;
a = b;
b = t;
}
int main() {
int x = 1, y = 2;
swap(x, y);
// x = 1, y = 2 — НЕ поменялись!
}
Функция получает копии x и y. Меняет копии, а оригиналы остаются нетронутыми.
Работает
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
int main() {
int x = 1, y = 2;
swap(&x, &y); // передаём адреса
// x = 2, y = 1 — поменялись!
}
Функция получает адреса. По адресу меняет оригинальные x и y.
Пошаговая трассировка вызова swap(&x, &y)
Демонстрация 3: указатели и массивы
Массив и указатель очень тесно связаны — имя массива фактически является указателем
на первый элемент. Это объясняет, почему a[i] и *(a + i) —
одно и то же.
int занимает 4 байта.
p + 1 означает «следующий элемент массива», то есть адрес увеличивается
на sizeof(int) = 4 байта. Это называется арифметикой указателей.
Интерактивно: p[i] vs *(p + i)
Демонстрация 4: динамическая память — new и delete
Иногда размер данных заранее неизвестен. Тогда память запрашивается у системы во время работы программы. Именно так рождается динамический массив.
int n;
cin >> n; // пользователь вводит размер
int *arr = new int[n]; // выделить память под n целых
for (int i = 0; i < n; i++)
cin >> arr[i]; // заполнить массив
// ... работаем с массивом ...
delete[] arr; // освободить память
arr = nullptr; // обнулить указатель
new — освободите через delete.
Если new[] (массив) — delete[]. Забытый delete —
это утечка памяти: программа будет занимать память, пока не завершится.
Пошаговая трассировка
Четыре опасные ошибки с указателями
Каждая из них рано или поздно случится и у вас. Главное — узнать её сразу.
Ошибка 1: разыменование нулевого указателя
int *p = nullptr;
*p = 100; // ← segfault! Программа упадёт
Как избежать. Всегда проверяйте указатель перед разыменованием:
if (p != nullptr) *p = 100;
Ошибка 2: «висячий» указатель (dangling pointer)
int *p = new int(42);
delete p; // память освобождена
cout << *p; // ← читаем освобождённую память!
// или:
int *q;
{
int x = 10;
q = &x;
} // x уничтожена, но q всё ещё хранит её адрес
cout << *q; // ← тоже мусор
Как избежать. После delete сразу ставьте p = nullptr;.
Не сохраняйте адреса локальных переменных за пределами их блока.
Ошибка 3: утечка памяти
void process() {
int *buffer = new int[1000];
// ... работаем ...
// забыли delete[] buffer;
}
// при повторных вызовах память будет заканчиваться
Как избежать. Правило: сколько раз new — столько раз
delete. В современном C++ предпочитайте умные указатели
unique_ptr и shared_ptr — они освобождают память автоматически.
Ошибка 4: путаница * в объявлении и в выражении
int a = 10, b = 20;
int *p = &a; // здесь * — часть типа «указатель на int»
*p = b; // здесь * — разыменование: «по адресу p записать b»
// в итоге a стало равно 20, а b не изменилось
int x = *p; // читаем значение по адресу p → x = 20
Как читать. Смотрите на контекст: если * стоит
рядом с типом в объявлении — это «указатель на». Если в выражении перед именем
указателя — это «значение по адресу».
Шпаргалка: всё, что нужно запомнить
| Синтаксис | Что означает | Пример |
|---|---|---|
int x; |
Обычная переменная типа int | x = 42; |
int *p; |
Указатель на int. Хранит адрес, где лежит int | p = &x; |
&x |
Взять адрес переменной x | cout << &x; |
*p |
Разыменовать: получить значение по адресу p | cout << *p; |
*p = 100; |
Записать 100 по адресу p | изменит переменную, на которую указывает p |
int **pp; |
Указатель на указатель | pp = &p; |
int *arr = new int[n]; |
Выделить память под n целых | вернёт адрес первого элемента |
delete[] arr; |
Освободить память, выделенную через new[] | обязателен, если выделяли через new[] |
p = nullptr; |
Обнулить указатель — «никуда не указывает» | безопасное состояние |
p[i] или *(p + i) |
i-й элемент от адреса p | эквивалентные записи |
& берёт адрес, оператор * идёт по адресу.
Всё остальное — детали, которые приходят с практикой. Когда сомневаетесь, снова откройте
эту страницу и посмотрите на картинку памяти: она подскажет.
Практика
- Объявите
int x = 5;иint *p = &x;. Выведитеx,&x,p,*p. Объясните каждое значение. - Что выведет этот код? Проверьте на бумаге, потом в компиляторе:
int a = 10, b = 20; int *p = &a; *p = 15; p = &b; *p = 25; cout << a << " " << b; - Напишите функцию
void increment(int *p), которая увеличивает значение по адресу на 1. Проверьте её работу вmain. - Зачем в
swapнужны указатели? Объясните, почему передача по значению не работает. - Напишите функцию, которая принимает массив и его размер и возвращает сумму элементов через
арифметику указателей (
*(arr + i)вместоarr[i]). - Найдите ошибку в этом коде:
int *p; *p = 42;