Avatar ·

Скобки после имени типа в new: есть ли разница?

Всем привет! Я начал изучать C++ и наткнулся на странную вещь с оператором `new`. Когда я выделяю память для объекта, я вижу два варианта: ```cpp int* p1 = new int; // без скобок int* p2 = new int(); // со скобками int* p3 = new int(42); // со значением ``` Или для классов: ```cpp MyClass* obj1 = new MyClass; MyClass* obj2 = new MyClass(); MyClass* obj3 = new MyClass(42); ``` Вроде бы все три варианта работают, но я заметил, что в некоторых случаях `new int` и `new int()` ведут себя по-разному. Например, я читал, что для встроенных типов (int, double и т.д.) `new int()` инициализирует память нулём, а `new int` оставляет мусор. Это правда? А что насчёт пользовательских классов? Есть ли разница между `new MyClass` и `new MyClass()`, если у класса есть конструктор по умолчанию? Может ли такое быть, что скобки влияют на то, будет ли объект инициализирован или нет? И как это связано с агрегатной инициализацией и списками инициализации? Объясните, пожалуйста, подробно, потому что я путаюсь в этих тонкостях. Хочется понять, когда нужно ставить скобки, а когда можно без них, и какие могут быть последствия ошибки. <<>>

Разница между new T и new T() в C++

Короткий ответ: да, разница есть, и она критична для встроенных типов и POD-структур. Для классов с пользовательским конструктором — обычно нет разницы.

Для встроенных типов (int, double, char и т.д.)

Здесь разница проявляется наиболее явно:

int* p1 = new int;      // НЕ инициализируется — содержит мусор
int* p2 = new int();    // Инициализируется нулём → *p2 == 0
int* p3 = new int(42);  // Инициализируется значением 42

То же самое для массивов:

int* arr1 = new int[10];   // 10 неинициализированных int (мусор)
int* arr2 = new int[10](); // 10 int, инициализированных нулём

Это поведение описано в стандарте C++ как value-initialization (для ()) vs default-initialization (без скобок). Для встроенных типов default-initialization оставляет значение неопределённым.

Для пользовательских классов

Если у класса есть конструктор по умолчанию:

class MyClass {
public:
    int x;
    MyClass() : x(100) {} // пользовательский конструктор
};

MyClass* obj1 = new MyClass;   // Вызов конструктора по умолчанию
MyClass* obj2 = new MyClass(); // Тоже вызов конструктора по умолчанию

В этом случае разницы нет — оба варианта вызывают один и тот же конструктор. Скобки просто необязательны.

Для POD-типов и агрегатов

Если класс — POD (Plain Old Data) или агрегат без пользовательского конструктора:

struct Point {
    int x;
    int y;
};

Point* p1 = new Point;   // x и y не инициализированы (мусор)
Point* p2 = new Point(); // x и y инициализированы нулём (0, 0)

Это та же логика, что и для встроенных типов: () включает value-initialization, которая обнуляет все поля.

Важное исключение: списки инициализации (C++11 и новее)

Начиная с C++11, появилась uniform initialization с фигурными скобками:

int* p = new int{};      // value-initialization → 0
Point* pt = new Point{}; // value-initialization → {0, 0}
int* arr = new int[5]{}; // все элементы — 0

Фигурные скобки {} ведут себя так же, как () для value-initialization, но есть нюанс: если у класса есть конструктор, принимающий std::initializer_list, то {} может вызвать его, а () — нет.

Когда может быть опасность?

Самая частая ошибка новичков — использование new int и чтение мусора:

int* ptr = new int;
std::cout << *ptr; // НЕОПРЕДЕЛЁННОЕ ПОВЕДЕНИЕ! Может вывести 0, а может -123456

Аналогично для массивов структур:

struct Data { int id; float value; };
Data* arr = new Data[100];
// arr[0].id — мусор! Используйте new Data[100]() или new Data[100]{}

Рекомендация

  • Для встроенных типов и POD-структур всегда используйте () или {}, чтобы гарантировать инициализацию нулём.
  • Для классов с конструктором — скобки не обязательны, но их можно ставить для единообразия.
  • В современном C++ (C++11+) предпочитайте auto ptr = std::make_unique<T>() или std::make_shared<T>() — они всегда выполняют value-initialization.
  • Избегайте сырого new в пользу умных указателей — это снижает риск утечек и неопределённого поведения.

Таким образом, скобки после типа в new — это не просто синтаксический сахар, а способ явно указать компилятору: «инициализируй значением по умолчанию». Для встроенных типов это превращает мусор в ноль, что часто спасает от трудноотлавливаемых багов.

<<>>

Альтернативный взгляд: когда разница неожиданна

Есть ещё один тонкий момент, связанный с агрегатной инициализацией в C++11 и новее. Рассмотрим такой код:

struct Widget {
    int a;
    int b = 5; // in-class initializer (C++11)
};

Widget* w1 = new Widget;   // a — не инициализирован, b = 5
Widget* w2 = new Widget(); // a = 0, b = 5
Widget* w3 = new Widget{}; // a = 0, b = 5

Обратите внимание: new Widget оставляет a неинициализированным, хотя у b есть значение по умолчанию. Это может сбивать с толку. Если ваш класс — агрегат (все поля public, нет пользовательских конструкторов), то new Widget выполняет агрегатную инициализацию только если вы передаёте значения в скобках. Без скобок — это просто выделение памяти без инициализации членов, у которых нет in-class initializer.

Ещё один нюанс: для классов с удалённым конструктором по умолчанию:

struct NoDefault {
    NoDefault() = delete;
    int x;
};

NoDefault* p = new NoDefault;   // Ошибка компиляции
NoDefault* p = new NoDefault(); // Тоже ошибка — конструктор удалён
NoDefault* p = new NoDefault{}; // Тоже ошибка

Но если добавить явный конструктор:

struct WithDefault {
    WithDefault(int v = 0) : x(v) {}
    int x;
};

WithDefault* p = new WithDefault;   // OK, x = 0
WithDefault* p = new WithDefault(); // OK, x = 0

Мораль: всегда явно инициализируйте. Используйте () или {} для value-initialization, если не уверены. В современном C++ лучше вообще избегать сырого new — используйте std::make_unique и std::make_shared, которые гарантируют правильную инициализацию.