Имеют ли круглые скобки после имени типа значение при использовании new?
Заголовок: Разница между созданием экземпляров класса в C++: new Test
и new Test()
Текст проблемы:
Я изучаю C++ и столкнулся с вопросом о различиях между двумя способами создания экземпляра класса. Если Test
является обычным классом, есть ли какая-либо разница между следующими строками кода:
Test* test = new Test;
и
Test* test = new Test();
Интересно, повлияет ли выбор одного из этих способов на работу программы, например, на инициализацию переменных-членов класса или на производительность. Заранее спасибо за объяснения!
5 ответ(ов)
В общем, в первом случае у нас происходит инициализация по умолчанию, а во втором — инициализация значением.
Например, в случае с типом int
(POD-тип):
int* test = new int;
— здесь инициализация отсутствует, и значение*test
может быть любым.int* test = new int();
— в этом случае*test
будет равно 0.
Далее поведение зависит от вашего типа Test
. У нас есть разные сценарии: Test
может иметь конструктор по умолчанию, автоматически сгенерированный конструктор по умолчанию, содержать POD-члены или не POD-члены и т.д.
Нет, они одинаковые. Но между ними есть различие:
Test t; // создаёт объект Test с именем t
и
Test t(); // объявляет функцию с именем t, которая возвращает объект Test
Это связано с основным правилом C++ (и C): если что-то может быть объявлением, то это и есть объявление.
Правка: Что касается проблем инициализации для POD и не-POD данных, я согласен со всем, что было сказано, но хотел бы подчеркнуть, что эти проблемы применимы только в том случае, если объект, который создаётся с помощью new или каким-либо другим способом, не имеет пользовательского конструктора. Если такой конструктор есть, он будет использован. В 99,99% разумно спроектированных классов существует такой конструктор, и поэтому эти проблемы можно игнорировать.
Предположим, что Test
— это класс с определённым конструктором, в этом случае нет разницы. Последняя форма немного яснее указывает на то, что запускается конструктор класса Test
, но, в общем, это всё.
Правила для операторов 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-типов фактически вызывает тривиальный конструктор по умолчанию). Хотя это и не приводит к значительным изменениям в поведении, правила упрощаются.)
Вот перевод вашего вопроса на русский язык в стиле ответа на 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++. Как видно из вывода, разные способы инициализации могут давать неожиданные результаты, особенно если вы используете конструкторы по умолчанию и не по умолчанию.
В чем проблема с "using namespace std;"?
Что такое Правило трёх?
Как инициализировать значения HashSet при создании?
Каковы правила вызова конструктора базового класса?
Что означает 'const' в конце объявления метода класса?