Стек и куча: два вида памяти
Переменная, объявленная в функции, живёт на стеке — память выделяется мгновенно при входе и освобождается при выходе из скоупа. Размер ограничен (обычно мегабайты), жизнь — время жизни скоупа.
Куча — память, которую вы выделяете сами через new/malloc и сами же обязаны освобождать. Больше размером, живёт сколько угодно, но управление — ваша ответственность.
void f() {
int stackVar = 5; // стек — умрёт при выходе
int* heapVar = new int(5); // куча — живёт пока не delete
delete heapVar; // без этого — утечка
}
Сырые указатели: синтаксис и семантика
Указатель — переменная, хранящая адрес другого объекта:
int x = 42;
int* p = &x; // p хранит адрес x
*p = 10; // разыменование — чтение/запись по адресу: теперь x = 10
int** pp = &p; // указатель на указатель
nullptr; // нулевой указатель (вместо NULL/0)
Разыменование nullptr или висячего указателя — неопределённое поведение. Программа может упасть, а может «работать» с мусором — самый коварный тип бага.
Указатель vs ссылка
int x = 5;
int* ptr = &x; // может быть nullptr, можно переназначить, нужно разыменование
int& ref = x; // псевдоним, всегда «настоящий» объект, обязан быть привязан при создании
Ссылки используют в параметрах функций и возвратах. Указатели — когда нужен «отсутствующий объект» (nullptr) или пересадка на другой объект.
new/delete и почему их почти не пишут
int* p = new int(42);
delete p;
int* arr = new int[100];
delete[] arr; // для массивов — delete[], иначе UB
Каждый new обязан встретить свой delete ровно один раз. Забыл — утечка. Удалил дважды — double free и UB. Удалил через delete вместо delete[] — UB. Исключение между new и delete — утечка.
Поэтому современный C++ заменяет new/delete на RAII:
// вместо
Widget* w = new Widget;
use(w);
delete w;
// пишут
auto w = std::make_unique<Widget>();
use(*w); // удалится сам
// или вообще без кучи
Widget w;
use(w);
Арифметика указателей и массивы
Указатель на элемент массива можно сдвигать: p+1 — следующий элемент, не байт! Компилятор умножает на sizeof(T):
int a[5] = {10, 20, 30, 40, 50};
int* p = a; // a само распадается в указатель на первый элемент
*(p + 2) == a[2]; // то же самое
p[3] == a[3]; // индексный синтаксис на указателе
Из этого следует знаменитая ошибка: размер массива через sizeof(a)/sizeof(a[0]) работает только в скоупе объявления. После распада в указатель sizeof(p) вернёт размер указателя (8), а не массива.
Висячие указатели — главная боль
int* dangling() {
int local = 5;
return &local; // local умрёт — указатель висячий
}
int* p = new int(7);
delete p;
*p = 10; // use-after-free
Правила безопасности:
- не возвращать указатели/ссылки на локальные переменные;
- после
deleteприсваиватьnullptr— хотя бы детектируемо; std::vectorиstd::stringвместо ручных массивов;- умные указатели для владения; сырые — только как «наблюдатели» внутри scope.
Двойные указатели и передача в функции
Чтобы функция могла изменить сам указатель (выделить память внутри), передают указатель на указатель или ссылку на указатель:
// C-стиль
bool allocate(int** out) { *out = new int(5); return *out != nullptr; }
// C++ стиль — проще
bool allocate(int*& out) { out = new int(5); return out != nullptr; }
// лучше — вернуть объект владения
std::unique_ptr<int> allocate() { return std::make_unique<int>(5); }
Умные указатели как ответ
unique_ptr — единоличное владение, размер сырого указателя, ноль оверхеда:
std::unique_ptr<Widget> w = std::make_unique<Widget>();
w->draw();
// delete не нужен
shared_ptr — разделяемое владение со счётчиком ссылок; weak_ptr — наблюдатель против циклов. Полный разбор — в статье про умные указатели.
Диагностика: ASan и Valgrind
На этапе разработки включайте санитайзеры — они ловят use-after-free, утечки и выходы за границы:
g++ -fsanitize=address,undefined -g main.cpp -o app
./app
# AddressSanitizer покажет: где выделено, где освобождено, где обратились после смерти
Итог
Сырые указатели нужны для понимания языка и работы со старым кодом, но в новом коде: объекты на стеке по умолчанию, unique_ptr для владения в куче, T&/T* только как невладеющие параметры. Помните про висячие ссылки, парность new/delete и -fsanitize при отладке.
Полный пример жизненного цикла
struct Node {
int value;
Node* next;
Node(int v) : value(v), next(nullptr) {}
};
Node* head = new Node(1); // ручное управление — опасно
head->next = new Node(2);
head->next->next = new Node(3);
// обход и печать
for (Node* p = head; p; p = p->next)
std::cout << p->value << ' ';
// освобождение — не забудьте каждый!
while (head) {
Node* next = head->next;
delete head;
head = next;
}
Каждая ошибка в этом коде — потенциальная утечка или краш. Именно поэтому современный C++ предпочитает std::unique_ptr и контейнеры.
Проверка на утечки
g++ -g -fsanitize=address app.cpp -o app
./app
# AddressSanitizer сообщит о каждой утечке с местом выделения
valgrind --leak-check=full ./app # альтернатива без пересборки
new[] и delete[]: массивы в куче
int* arr = new int[100]; // массив из 100 int
arr[5] = 42;
delete[] arr; // именно delete[], не delete!
new[]/delete[] — парная операция для массивов. delete без [] на массиве — UB. В реальном коде вместо сырого массива — std::vector<int>(100).
Память стека и кучи: числа
- Стек на поток: обычно 1–8 МБ, выделяется мгновенно (декремент указателя).
- Аллокация в куче: десятки-сотни наносекунд, метаданные аллокатора, фрагментация.
- Рекурсия глубиной миллион — гарантированный stack overflow; структура на мегабайт в стеке — то же самое.
Правила выживания с указателями
- Инициализируйте указатели (
nullptr) — висячий указатель неотличим от валидного. - После
delete—p = nullptr, если указатель ещё живёт в области видимости. - Не
deleteдважды — только nullptr можно удалять многократно. - Возвращайте из функций
unique_ptr/shared_ptr, а не сырые указатели. - Проверяйте анализаторами: ASan, UBSan, Valgrind — они находят ошибки памяти за вас.
Итог
Ручная память в C++ — это инструмент низкого уровня: понимать обязательно, использовать через обёртки по возможности. Стек vs куча, указатели и их опасности, RAII-обёртки вместо new/delete, санитайзеры для проверки — и ошибки памяти перестают быть самым частым багом в вашем коде.