11

Имеют ли круглые скобки после имени типа значение при использовании new?

18

Заголовок: Разница между созданием экземпляров класса в C++: new Test и new Test()

Текст проблемы:

Я изучаю C++ и столкнулся с вопросом о различиях между двумя способами создания экземпляра класса. Если Test является обычным классом, есть ли какая-либо разница между следующими строками кода:

Test* test = new Test;

и

Test* test = new Test();

Интересно, повлияет ли выбор одного из этих способов на работу программы, например, на инициализацию переменных-членов класса или на производительность. Заранее спасибо за объяснения!

5 ответ(ов)

0

В общем, в первом случае у нас происходит инициализация по умолчанию, а во втором — инициализация значением.

Например, в случае с типом int (POD-тип):

  • int* test = new int; — здесь инициализация отсутствует, и значение *test может быть любым.
  • int* test = new int(); — в этом случае *test будет равно 0.

Далее поведение зависит от вашего типа Test. У нас есть разные сценарии: Test может иметь конструктор по умолчанию, автоматически сгенерированный конструктор по умолчанию, содержать POD-члены или не POD-члены и т.д.

0

Нет, они одинаковые. Но между ними есть различие:

Test t;      // создаёт объект Test с именем t

и

Test t();   // объявляет функцию с именем t, которая возвращает объект Test

Это связано с основным правилом C++ (и C): если что-то может быть объявлением, то это и есть объявление.

Правка: Что касается проблем инициализации для POD и не-POD данных, я согласен со всем, что было сказано, но хотел бы подчеркнуть, что эти проблемы применимы только в том случае, если объект, который создаётся с помощью new или каким-либо другим способом, не имеет пользовательского конструктора. Если такой конструктор есть, он будет использован. В 99,99% разумно спроектированных классов существует такой конструктор, и поэтому эти проблемы можно игнорировать.

0

Предположим, что Test — это класс с определённым конструктором, в этом случае нет разницы. Последняя форма немного яснее указывает на то, что запускается конструктор класса Test, но, в общем, это всё.

0

Правила для операторов new аналогичны тому, что происходит при инициализации объекта с автоматическим временем хранения (хотя, из-за "неприятного парсинга", синтаксис может быть несколько иным).

Если я напишу:

int my_int; // инициализация по умолчанию → неопределенное значение (тип не класса)

То у my_int будет неопределенное значение, поскольку это тип не класса. В качестве альтернативы я могу выполнить инициализацию значением для my_int (что для типов не класса означает обнуление) следующим образом:

int my_int{}; // инициализация значением → обнуление (тип не класса)

(Разумеется, я не могу использовать (), потому что это будет объявление функции, но int() работает так же, как int{}, чтобы создать временный объект.)

А для типов класса:

Thing my_thing; // инициализация по умолчанию → вызов конструктора по умолчанию (тип класса)
Thing my_thing{}; // инициализация значением → инициализация по умолчанию → вызов конструктора по умолчанию (тип класса)

Конструктор по умолчанию вызывается для создания Thing, никаких исключений.

Таким образом, правила выглядят следующим образом:

  • Это тип класса?
    • ДА: Конструктор по умолчанию вызывается, независимо от того, инициализирован ли объект значением (с помощью {}) или по умолчанию (без {}). (Существует дополнительное поведение предварительного обнуления при инициализации значением, но последнее слово всегда остается за конструктором по умолчанию.)
    • НЕТ: Использовались ли {}?
      • ДА: Объект инициализирован значением, что для типов не класса фактически просто означает обнуление.
      • НЕТ: Объект инициализирован по умолчанию, что для типов не класса оставляет его с неопределенным значением (по сути, он фактически не инициализирован).

Эти правила точно так же применяются и к синтаксису new, с добавленным правилом, что () может быть заменен на {}, поскольку new никогда не интерпретируется как объявление функции. Например:

int* my_new_int = new int; // инициализация по умолчанию → неопределенное значение (тип не класса)
Thing* my_new_thing = new Thing; // инициализация по умолчанию → вызов конструктора по умолчанию (тип класса)
int* my_new_zeroed_int = new int(); // инициализация значением → обнуление (тип не класса)
     my_new_zeroed_int = new int{}; // то же самое
       my_new_thing = new Thing(); // инициализация значением → инициализация по умолчанию → вызов конструктора по умолчанию (тип класса)

(Этот ответ включает концептуальные изменения в C++11, которые сейчас не охвачены в ответе наивысшего ранга; в частности, новый скалярный или POD-экземпляр, который в итоге окажется с неопределенным значением, нынче технически считается инициализированным по умолчанию (что для POD-типов фактически вызывает тривиальный конструктор по умолчанию). Хотя это и не приводит к значительным изменениям в поведении, правила упрощаются.)

0

Вот перевод вашего вопроса на русский язык в стиле ответа на StackOverflow:


Я написал несколько примеров кода, которые могут дополнить ответ Михаила Берра:

#include <iostream>

struct A1 {
    int i;
    int j;
};

struct B {
    int k;
    B() : k(4) {}
    B(int k_) : k(k_) {}
};

struct A2 {
    int i;
    int j;
    B b;
};

struct A3 {
    int i;
    int j;
    B b;
    A3() : i(1), j(2), b(5) {}
    A3(int i_, int j_, B b_) : i(i_), j(j_), b(b_) {}
};

int main() {
    {
        std::cout << "Случай #1: POD без ()\n";
        A1 a1 = {1, 2};
        std::cout << a1.i << " " << a1.j << std::endl;
        A1* a = new (&a1) A1;
        std::cout << a->i << " " << a->j  << std::endl;
    }
    {
        std::cout << "Случай #2: POD с ()\n";
        A1 a1 = {1, 2};
        std::cout << a1.i << " " << a1.j << std::endl;
        A1* a = new (&a1) A1();
        std::cout << a->i << " " << a->j  << std::endl;
    }
    {
        std::cout << "Случай #3: non-POD без ()\n";
        A2 a1 = {1, 2, {3}};
        std::cout << a1.i << " " << a1.j << " " << a1.b.k << std::endl;
        A2* a = new (&a1) A2;
        std::cout << a->i << " " << a->j << " " << a->b.k << std::endl;
    }
    {
        std::cout << "Случай #4: non-POD с ()\n";
        A2 a1 = {1, 2, {3}};
        std::cout << a1.i << " " << a1.j << " " << a1.b.k  << std::endl;
        A2* a = new (&a1) A2();
        std::cout << a->i << " " << a->j << " " << a1.b.k << std::endl;
    }
    {
        std::cout << "Случай #5: класс с определенным пользователем конструктором без ()\n";
        A3 a1 = {11, 22, {33}};
        std::cout << a1.i << " " << a1.j << " " << a1.b.k << std::endl;
        A3* a = new (&a1) A3;
        std::cout << a->i << " " << a->j << " " << a->b.k << std::endl;
    }
    {
        std::cout << "Случай #6: класс с определенным пользователем конструктором с ()\n";
        A3 a1 = {11, 22, {33}};
        std::cout << a1.i << " " << a1.j << " " << a1.b.k  << std::endl;
        A3* a = new (&a1) A3();
        std::cout << a->i << " " << a->j << " " << a1.b.k << std::endl;
    }
    return 0;
}

/*
Вывод с GCC11.1 (C++20)
Случай #1: POD без ()
1 2
1 2
Случай #2: POD с ()
1 2
0 0
Случай #3: non-POD без ()
1 2 3
1 2 4
Случай #4: non-POD с ()
1 2 3
0 0 4
Случай #5: класс с определенным пользователем конструктором без ()
11 22 33
1 2 5
Случай #6: класс с определенным пользователем конструктором с ()
11 22 33
1 2 5
*/

Этот код иллюстрирует различия в поведении, когда вы работаете с POD (Plain Old Data) и non-POD структурами в C++. Как видно из вывода, разные способы инициализации могут давать неожиданные результаты, особенно если вы используете конструкторы по умолчанию и не по умолчанию.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь