Я привык инициализировать переменные-члены в конструкторах классов, но решил проверить, устанавливаются ли значения по умолчанию конструкторами по умолчанию. Мои тесты проводились с Visual Studio 2022 с использованием языкового стандарта C++ 20. Результаты меня смутили:
#include <iostream>
class A
{
public:
double r;
};
class B
{
public:
B() = default;
double r;
};
class C
{
public:
C() {}
double r;
};
int main()
{
A a1;
std::cout << a1.r << std::endl; // ERROR: uninitialized local variable 'a1' used
A a2();
std::cout << a2.r << std::endl; // ERROR: left of '.r' must have class/struct/union
A* pa1 = new A;
std::cout << pa1->r << std::endl; // output: -6.27744e+66
A* pa2 = new A();
std::cout << pa2->r << std::endl; // output: 0
B b1;
std::cout << b1.r << std::endl; // ERROR: uninitialized local variable 'b1' used
B b2();
std::cout << b2.r << std::endl; // ERROR: left of '.r' must have class/struct/union
B* pb1 = new B;
std::cout << pb1->r << std::endl; // output: -6.27744e+66
B* pb2 = new B();
std::cout << pb2->r << std::endl; // output: 0
C c1;
std::cout << c1.r << std::endl; // output: -9.25596e+61
C c2();
std::cout << c2.r << std::endl; // ERROR: left of '.r' must have class/struct/union
C* pc1 = new C;
std::cout << pc1->r << std::endl; // output: -6.27744e+66
C* pc2 = new C();
std::cout << pc2->r << std::endl; // output: -6.27744e+66
}
Спасибо всем, кто может просветить меня.
MyType name(); // This is treated as a function declaration
MyType name{}; // This is the correct way
A a2();
, B b2();
и C c2();
также могут быть проанализированы как объявления функций, возвращающих A
/B
/C
с пустым списком параметров. Эта интерпретация предпочтительнее, поэтому вы объявляете функции, а не переменные. Это также известно как проблема "самый неприятный разбор".
Ни один из конструкторов по умолчанию (включая неявный из A
) не инициализирует r
, поэтому он будет иметь неопределенное значение. Чтение этого значения приведет к неопределенному поведению.
Исключение составляют A* pa2 = new A();
и B* pb2 = new B();
. Инициализатор ()
выполняет инициализация значения. Эффект инициализации значения в этих двух случаях заключается в том, что весь объект будет инициализирован нулями, потому что и A
, и B
имеют конструктор по умолчанию, который не предоставляется пользователем. (По умолчанию при первом объявлении не считается предоставленным пользователем.)
В случае C
это неприменимо, потому что конструктор C
по умолчанию предоставляется пользователем, и поэтому инициализация значения приведет только к инициализации по умолчанию, вызывая конструктор по умолчанию, который не инициализирует r
.
Спасибо. «Самый неприятный вопрос разбора» назван правильно!
Давайте посмотрим, что происходит в каждом конкретном случае в вашем примере.
Здесь мы рассмотрим утверждения:
A a1; //this creates a variable named a1 of type A using the default constrcutor
std::cout << a1.r << std::endl; //this uses the uninitialized data member r which leads to undefined behavior
В приведенном выше фрагменте первый оператор создает переменную с именем a1
типа A
, используя по умолчаниюA::A()
синтезированный компилятором. Это означает, что элемент данных r
будет инициализировано по умолчанию. А так как r
встроенного типа, то у него будет неопределенное значение. Использование этой переменной неинициализированный, которую вы делаете, когда пишете второй оператор, показанный в приведенном выше фрагменте, — это неопределенное поведение.
Здесь мы рассмотрим утверждения:
A a2(); //this is a function declaration
std::cout << a2.r << std::endl; //this is not valid since a2 is the name of a function
Первый оператор в приведенном выше фрагменте, объявляет, представляет собой функцию с именем a2
, которая не принимает параметров и имеет возвращаемый тип A
. То есть первый оператор на самом деле является объявление функции. Теперь во втором выражении вы пытаетесь получить доступ к члену данных r
функции с именем a2
, что не имеет никакого смысла, и, следовательно, вы получаете упомянутую ошибку.
Здесь мы рассмотрим утверждения:
A* pa1 = new A;
std::cout << pa1->r << std::endl; // output: -6.27744e+66
Первый оператор в приведенном выше фрагменте имеет следующие эффекты:
A
создается в куче из-за new A
использования конструктор по умолчаниюA::A()
синтезированный компилятором. Более того, в результате мы получаем еще и указатель на этот безымянный объект.pa1
. То есть создается указатель на A
с именем pa1
и инициализируется указателем на безымянный объект, который мы получили на шаге 1 выше.Поскольку использовался конструктор по умолчанию (см. шаг 1), это означает, что элемент данных r
безымянного объекта — инициализация по умолчанию. А поскольку член данных r
имеет встроенный тип, это означает, что он имеет неопределенное значение. И использование этого неинициализированного члена данных r
, которое вы делаете во втором операторе приведенного выше фрагмента кода, — это неопределенное поведение. Вот почему вы получаете какое-то мусорное значение в качестве вывода.
Здесь мы рассмотрим утверждения:
A* pa2 = new A();
std::cout << pa2->r << std::endl;
Первый оператор приведенного выше фрагмента имеет следующие эффекты:
Безымянный объект типа A
создается благодаря выражению new A()
. Но на этот раз, поскольку вы использовали круглые скобки ()
и поскольку класс A
не имеет предоставленного пользователем конструктора по умолчанию, это означает, что произойдет инициализация значения. По сути, это означает, что элемент данных r
будет ноль инициализирован. Вот почему / как вы получаете вывод как 0
во втором утверждении приведенного выше фрагмента. Более того, в качестве результата возвращается указатель на этот безымянный объект.
Далее создается указатель на A
с именем pa2
, который инициализируется с помощью указателя на безымянный объект, который мы получили на шаге 1 выше.
Точно то же самое происходит со следующими 4 операторами, относящимися к классу B
. Поэтому я не буду обсуждать следующие 4 утверждения, относящиеся к классу B
, так как мы не узнаем из них ничего нового. Для них произойдет то же самое, что и для предыдущих 4 утверждений, описанных выше.
Теперь рассмотрим утверждения, относящиеся к классу C
. Мы не пропускаем эти 4 утверждения, потому что для класса C
существует определяемый пользователем конструктор по умолчанию.
Здесь мы рассмотрим утверждения:
C c1;
std::cout << c1.r << std::endl;
Первый оператор приведенного выше фрагмента кода создает переменную с именем c1
типа C
, используя предоставленный пользователем конструктор по умолчаниюA::A()
. Поскольку этот предоставленный пользователем конструктор по умолчанию ничего не делает, член данных r
остается неинициализированным, и мы получаем то же поведение, что и для A a1;
. То есть использование этой неинициализированной переменной, которую вы делаете во втором операторе, — это неопределенное поведение.
Здесь мы рассмотрим утверждения:
C c2();
std::cout << c2.r << std::endl;
Первый оператор в приведенном выше фрагменте — это объявление функции. Таким образом, вы получите то же поведение/ошибку, что и для класса A
.
Здесь мы рассмотрим утверждения:
C* pc1 = new C;
std::cout << pc1->r << std::endl;
Первый оператор в приведенном выше фрагменте имеет следующие эффекты:
Безымянный объект типа C
создается в куче с использованием предоставленного пользователем конструктора по умолчанию A::A()
из-за выражения new A
. И поскольку пользователь предоставляет конструктор по умолчанию, ничего не делает, член данных r
остается неинициализированным. Более того, в результате мы получаем указатель на этот безымянный объект.
Далее создается указатель на C
с именем pc1
, который инициализируется pionter для безымянного объекта, который мы получили на шаге 1.
Теперь второй оператор в приведенном выше фрагменте использует неинициализированный элемент данных r
, что является неопределенным поведением и объясняет, почему вы получаете какое-то мусорное значение в качестве вывода.
Здесь мы рассмотрим утверждения:
C* pc2 = new C();
std::cout << pc2->r << std::endl;
Первый оператор приведенного выше фрагмента имеет следующие эффекты:
Безымянный объект типа C
создается в куче из-за new C()
. Теперь, поскольку вы указали скобки ()
, это будет выполнять инициализацию значения. Но поскольку на этот раз у нас есть конструктор по умолчанию, предоставляемый пользователем, инициализация значения аналогична инициализации по умолчанию, которая будет выполняться с использованием конструктора по умолчанию, предоставляемого пользователем. И поскольку пользователь предоставляет конструктор по умолчанию, ничего не делает, элемент данных r
останется неинициализированным. Более того, в результате мы получаем указатель на безымянный объект.
Затем создается указатель на C
с именем pc2
, который инициализируется pionter для безымянного объекта, который мы получили на шаге 1 выше.
Теперь второй оператор в приведенном выше фрагменте использует неинициализированный элемент данных r
, который является неопределенное поведение, и объясняет, почему вы получаете какое-то мусорное значение в качестве вывода.
Спасибо, что нашли время, чтобы объяснить так подробно. Это очень полезно.
@KeithM Пожалуйста, взгляните на Что мне делать, когда кто-то отвечает на мой вопрос?.
Какая часть сбивает с толку? Есть 2 разные вещи - про унитилизированные значения и
left of '.r' must have class/struct/union
, что самое неприятное в разборе.