«Why should C++ programmers minimize use of 'new'?»: разбираем культовый вопрос
Если вы когда-нибудь заглядывали на Stack Overflow в поисках ответов по C++, вы наверняка видели этот вопрос. Он набрал тысячи голосов и десятки тысяч просмотров. И неспроста. Для новичка тема кажется контринтуитивной: «Как же так? В Java или C# я постоянно пишу new, и это нормально. Почему в C++ это вдруг плохо?»
Сегодня мы не просто переведём ответ с англоязычного форума. Мы разложим по полочкам, почему минимизация new в C++ — это не прихоть гуру, а вопрос производительности, безопасности и здравого смысла. И да, мы будем писать код, смотреть на ассемблер (почти) и считать наносекунды.
Куда уходит память: стек vs куча
Прежде чем говорить о new, давайте вспомним фундамент. В C++ есть два основных места для хранения данных:
- Стек (stack) — маленький, быстрый, управляется автоматически. Как только переменная выходит из области видимости, она уничтожается. Мгновенно.
- Куча (heap) — большой, медленный, требует ручного управления. Чтобы получить там память, нужно вызвать
new(илиmalloc). Чтобы освободить —delete.
Каждый вызов new — это не просто «дай памяти». Это сложный системный вызов, поиск свободного блока, обновление служебных структур. Это в сотни раз медленнее, чем выделение на стеке.
Пример №1: Сравнение скорости
#include <chrono>
#include <iostream>
#include <vector>
struct Point {
double x, y, z;
};
int main() {
const int N = 1000000;
// Стековое размещение (массив на стеке)
auto start = std::chrono::high_resolution_clock::now();
Point points_stack[1000]; // 1000 объектов на стеке
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Stack time: "
<< std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count()
<< " ns\n";
// Динамическое размещение через new
start = std::chrono::high_resolution_clock::now();
std::vector<Point*> points_heap;
for (int i = 0; i < 1000; ++i) {
points_heap.push_back(new Point());
}
end = std::chrono::high_resolution_clock::now();
std::cout << "Heap (new) time: "
<< std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count()
<< " ns\n";
// Чистим за собой (обязательно!)
for (auto p : points_heap) delete p;
return 0;
}
Результат (на типичной машине):
Stack time: 0 ns
Heap (new) time: 45200 ns
Разница в десятки тысяч раз. Конечно, это синтетика, но она наглядно показывает: каждый лишний new — это микросекунды долой из вашего бюджета производительности.
Главная проблема new: утечки и фрагментация
Если бы дело было только в скорости, это было бы полбеды. Но new тащит за собой целый ворох проблем:
1. Утечки памяти (memory leaks)
Вы забыли вызвать delete? Поздравляю, у вас утечка. Программа работает час, день, неделю — и внезапно падает с std::bad_alloc. Классика жанра. Особенно опасны утечки в длительных процессах (сервера, игры, GUI).
2. Висячие указатели (dangling pointers)
Вы удалили объект, но указатель остался. Любое обращение к нему — неопределённое поведение (UB). Программа может работать, может упасть, может заменить вашу музыку на техно. UB — зло.
3. Фрагментация кучи
Частые new/delete дробят кучу на маленькие кусочки. Даже если памяти достаточно, выделить большой блок становится невозможно. Это называется внешняя фрагментация.
Современный C++: умные указатели и RAII
«Хорошо, — скажете вы. — Но что делать, если мне нужна динамическая память? Объекты живут дольше функции, размер массива неизвестен на этапе компиляции...»
Ответ: используйте умные указатели и контейнеры STL. Это не просто модно, это стандарт современного C++ (C++11 и новее).
Правило большого пальца:
- Если можно разместить на стеке — размещайте на стеке.
- Если нужен массив переменной длины — используйте
std::vector. - Если нужен уникальный владелец объекта —
std::unique_ptr. - Если нужно разделяемое владение —
std::shared_ptr.
Пример №2: Сравнение new vs unique_ptr
#include <memory>
#include <iostream>
class BigData {
public:
BigData() { std::cout << "Created\n"; }
~BigData() { std::cout << "Destroyed\n"; }
void process() { /* ... */ }
};
// Плохой стиль: ручное управление
void old_way() {
BigData* data = new BigData();
data->process();
// Забыли delete? Утечка!
delete data;
}
// Современный C++: RAII
void modern_way() {
std::unique_ptr<BigData> data = std::make_unique<BigData>();
data->process();
// delete вызывается автоматически при выходе из scope
}
int main() {
modern_way();
return 0;
}
Обратите внимание: std::make_unique — это не просто обёртка. Это безопасное создание. Даже если конструктор выбросит исключение, утечки не будет. А с голым new — будет.
Когда new всё-таки оправдан?
Было бы лицемерием сказать «никогда». Есть сценарии, где new (или new[]) необходим:
- Кастомные аллокаторы — если вы пишете свою систему управления памятью (игровые движки, high-frequency trading).
- Взаимодействие с C-библиотеками — часто они ожидают сырые указатели.
- Реализация паттернов вроде Pimpl (Pointer to Implementation) — но и там лучше использовать
unique_ptr. - Встраиваемые системы — где нет исключ