Указатели и динамическая память в C++
Что такое указатель, чем отличается от ссылки, как работают new и delete, откуда берутся утечки памяти и почему на защите спрашивают про деструктор.
Указатель — это переменная, которая хранит не значение, а адрес другой переменной в памяти. Всё остальное в теме указателей — следствие этого одного факта.
int x = 42;
int* p = &x; // p хранит адрес x
std::cout << *p; // 42 — разыменование: "значение по адресу"
*p = 100; // меняем x через указатель
std::cout << x; // 100
Указатель, ссылка и массив
- Указатель можно переназначить на другой объект и сделать нулевым (nullptr). Ссылка привязывается один раз при создании и всегда указывает на существующий объект.
- Имя массива в выражениях превращается в указатель на первый элемент, поэтому a[i] и *(a + i) — одно и то же.
- Ссылку удобнее передавать в функции: не нужно проверять на nullptr и разыменовывать.
Стек и куча
Локальные переменные живут на стеке и умирают при выходе из функции — размер должен быть известен на этапе компиляции. Если размер известен только во время выполнения (например, пользователь ввёл n), память берут в куче через new и обязаны вернуть через delete.
int n;
std::cin >> n;
int* arr = new int[n]; // выделили
// ... работа с массивом ...
delete[] arr; // освободили; для new — delete, для new[] — delete[]
arr = nullptr; // хорошая привычка
Правило трёх и почему про него спрашивают
Если класс владеет динамической памятью, компилятор по умолчанию копирует только сам указатель. Два объекта начинают ссылаться на одну область памяти, и при разрушении второго происходит повторное удаление. Поэтому такому классу нужны деструктор, конструктор копирования и оператор присваивания — это и есть «правило трёх».
class Vec {
int* data; size_t n;
public:
Vec(size_t n) : data(new int[n]), n(n) {}
Vec(const Vec& o) : data(new int[o.n]), n(o.n) { // копирование
std::copy(o.data, o.data + o.n, data);
}
Vec& operator=(const Vec& o) { // присваивание
if (this == &o) return *this;
delete[] data;
n = o.n; data = new int[n];
std::copy(o.data, o.data + n, data);
return *this;
}
~Vec() { delete[] data; } // деструктор
};
Современный подход: умные указатели
- std::unique_ptr — единственный владелец, память освобождается автоматически при выходе из области видимости.
- std::shared_ptr — счётчик ссылок, память освобождается, когда исчезает последний владелец.
- std::vector вместо new[] в 95% учебных задач — и никаких утечек.
Ошибки, за которые снижают оценку
| Ошибка | Что происходит |
|---|---|
| Возврат адреса локальной переменной | Висячий указатель: память уже освобождена, поведение непредсказуемо |
| delete вместо delete[] | Деструкторы остальных элементов не вызываются, поведение не определено |
| Отсутствие проверки на nullptr | Падение при разыменовании |
| Выход за границы массива | Порча чужой памяти, ошибка проявляется в другом месте программы |
Частые вопросы
Чем указатель отличается от ссылки?
Указатель — самостоятельная переменная с адресом, её можно переназначить и обнулить. Ссылка — это второе имя существующего объекта, она не может быть «пустой» и не переназначается.
Зачем нужен виртуальный деструктор?
Если удалять объект наследника через указатель на базовый класс, без virtual вызовется только деструктор базы — память наследника утечёт.