试图理解默认构造函数和成员初始化
Trying to understand default constructors and member initialisatioon
我习惯于在 class 构造函数中初始化成员变量,但我想我应该检查一下默认构造函数是否设置了默认值。我的测试是 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
且为空的函数声明参数列表。这种解释是首选,因此您声明的是函数,而不是变量。这也称为“最令人烦恼的解析”问题。
None 的默认构造函数(包括 A
的隐式构造函数)正在初始化 r
,因此它将具有不确定的值。读取该值将导致未定义的行为。
A* pa2 = new A();
和 B* pb2 = new B();
是个例外。 ()
初始化程序执行 value-initialization。 value-initialization 的效果是在这两种情况下整个对象将是 zero-initialized,因为 A
和 B
都有一个不是 user-provided 的默认构造函数. (第一次声明时的默认值不算作 user-provided。)
在 C
的情况下这不适用,因为 C
的默认构造函数是 user-provided 因此 value-initialization 只会导致 default-initialization, 调用不初始化 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
在上面的代码片段中,第一条语句使用 默认构造函数 A::A()
由编译器合成。这意味着数据成员 r
将 默认初始化 。由于 r
是 built-in 类型,它将具有 不确定值 。使用这个 uninitilized 变量是 undefined behavior.
案例二
这里我们考虑以下语句:
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
的函数,它不带参数并且return类型为A
.也就是说,第一条语句实际上是一个函数声明。现在,在第二个语句中,您试图访问名为 a2
的函数的数据成员 r
,这没有任何意义,因此您得到了提到的错误。
案例三
这里我们考虑以下语句:
A* pa1 = new A;
std::cout << pa1->r << std::endl; // output: -6.27744e+66
以上代码段中的第一条语句具有以下效果:
- 由于
new A
使用 默认构造函数 A::A()
[=140=,因此在堆上创建了类型 A
的未命名对象]由编译器合成。此外,我们还得到了指向这个未命名对象的指针。
- 接下来,我们在上面的第 1 步中获得的指针用作
pa1
的 初始化程序 。也就是说,创建了一个名为 pa1
的指向 A
的指针,并由指向我们在上面的步骤 1 中获得的未命名对象的指针初始化。
因为使用了默认构造函数(参见第 1 步),这意味着未命名对象的数据成员 r
是 default initilaized。由于数据成员 r
是内置类型,这意味着它具有 不确定值 。在上述代码片段的第二条语句中使用这个未初始化的数据成员 r
是 未定义行为 。这就是为什么你得到一些垃圾值作为输出。
案例4
这里我们考虑以下语句:
A* pa2 = new A();
std::cout << pa2->r << std::endl;
上述代码片段的第一条语句具有以下效果:
由于表达式 new A()
,创建了类型 A
的未命名对象。但是这次由于您使用了括号 ()
并且由于 class A
没有用户提供的默认构造函数,这意味着 值初始化 将会发生.这实质上意味着数据成员 r
将被 零初始化 。这是 why/how 您在上述代码段的第二条语句中得到的输出为 0
。此外,指向此未命名对象的指针作为结果 returned。
接下来,创建一个名为 pa2
的指向 A
的指针,并使用我们在上面的步骤 1 中获得的指向未命名对象的指针进行初始化。
接下来与 class B
相关的 4 个语句发生了完全相同的事情。所以我不讨论接下来与 class B
相关的 4 个陈述,因为我们不会从中学到任何新东西。同样的事情也会发生在他们身上,就像上面描述的前 4 个陈述一样。
现在考虑class C
相关的语句。我们不会跳过这 4 个语句,因为 class C
有一个 user-defined 默认构造函数。
声明 5
这里我们考虑以下语句:
C c1;
std::cout << c1.r << std::endl;
上述代码片段的第一条语句使用用户提供的默认构造函数A::A()
创建了一个名为c1
类型C
的变量。由于此用户提供的默认构造函数不执行任何操作,因此数据成员 r
未初始化,我们得到与我们讨论的 A a1;
相同的行为。也就是说,在第二条语句中使用这个未初始化的变量是 undefined behavior.
声明 6
这里我们考虑以下语句:
C c2();
std::cout << c2.r << std::endl;
上面片段中的第一条语句是函数声明。因此,您将获得与 class A
.
相同的 behavior/error
声明 7
这里我们考虑以下语句:
C* pc1 = new C;
std::cout << pc1->r << std::endl;
以上代码段中的第一条语句具有以下效果:
由于表达式 new A
,使用用户提供的默认构造函数 A::A()
在堆上创建类型 C
的未命名对象。并且由于用户提供的默认构造函数不执行任何操作,因此数据成员 r
未初始化。此外,我们得到一个指向这个未命名对象的指针作为结果。
接下来,创建一个名为pc1
的指向C
的指针ed 并由 pionter 初始化为我们在步骤 1 中获得的未命名对象。
现在上面代码片段中的第二个语句使用未初始化的数据成员 r
,这是未定义的行为,并解释了为什么你得到一些垃圾值作为输出。
声明 8
这里我们考虑以下语句:
C* pc2 = new C();
std::cout << pc2->r << std::endl;
上述代码片段的第一条语句具有以下效果:
由于 new C()
,在堆上创建了类型 C
的未命名对象。现在,由于您已经指定了括号 ()
,这将执行 value-initialization。但是因为这次我们有一个 user-provide 默认构造函数,所以 value-initialization 与 default-initialization 相同,后者将使用 user-provide 默认构造函数完成。由于用户提供的默认构造函数什么都不做,数据成员 r
将保持未初始化状态。此外,我们得到一个指向未命名对象的指针作为结果。
接下来,创建一个指向 C
的指针,名为 pc2
,并由 pionter 初始化为我们在上面的步骤 1 中获得的未命名对象。
上面代码片段中的第二条语句使用了未初始化的数据成员 r
,即 未定义的行为 并解释了为什么您得到一些垃圾值作为输出。
我习惯于在 class 构造函数中初始化成员变量,但我想我应该检查一下默认构造函数是否设置了默认值。我的测试是 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
且为空的函数声明参数列表。这种解释是首选,因此您声明的是函数,而不是变量。这也称为“最令人烦恼的解析”问题。
None 的默认构造函数(包括 A
的隐式构造函数)正在初始化 r
,因此它将具有不确定的值。读取该值将导致未定义的行为。
A* pa2 = new A();
和 B* pb2 = new B();
是个例外。 ()
初始化程序执行 value-initialization。 value-initialization 的效果是在这两种情况下整个对象将是 zero-initialized,因为 A
和 B
都有一个不是 user-provided 的默认构造函数. (第一次声明时的默认值不算作 user-provided。)
在 C
的情况下这不适用,因为 C
的默认构造函数是 user-provided 因此 value-initialization 只会导致 default-initialization, 调用不初始化 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
在上面的代码片段中,第一条语句使用 默认构造函数 A::A()
由编译器合成。这意味着数据成员 r
将 默认初始化 。由于 r
是 built-in 类型,它将具有 不确定值 。使用这个 uninitilized 变量是 undefined behavior.
案例二
这里我们考虑以下语句:
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
的函数,它不带参数并且return类型为A
.也就是说,第一条语句实际上是一个函数声明。现在,在第二个语句中,您试图访问名为 a2
的函数的数据成员 r
,这没有任何意义,因此您得到了提到的错误。
案例三
这里我们考虑以下语句:
A* pa1 = new A;
std::cout << pa1->r << std::endl; // output: -6.27744e+66
以上代码段中的第一条语句具有以下效果:
- 由于
new A
使用 默认构造函数A::A()
[=140=,因此在堆上创建了类型A
的未命名对象]由编译器合成。此外,我们还得到了指向这个未命名对象的指针。 - 接下来,我们在上面的第 1 步中获得的指针用作
pa1
的 初始化程序 。也就是说,创建了一个名为pa1
的指向A
的指针,并由指向我们在上面的步骤 1 中获得的未命名对象的指针初始化。
因为使用了默认构造函数(参见第 1 步),这意味着未命名对象的数据成员 r
是 default initilaized。由于数据成员 r
是内置类型,这意味着它具有 不确定值 。在上述代码片段的第二条语句中使用这个未初始化的数据成员 r
是 未定义行为 。这就是为什么你得到一些垃圾值作为输出。
案例4
这里我们考虑以下语句:
A* pa2 = new A();
std::cout << pa2->r << std::endl;
上述代码片段的第一条语句具有以下效果:
由于表达式
new A()
,创建了类型A
的未命名对象。但是这次由于您使用了括号()
并且由于 classA
没有用户提供的默认构造函数,这意味着 值初始化 将会发生.这实质上意味着数据成员r
将被 零初始化 。这是 why/how 您在上述代码段的第二条语句中得到的输出为0
。此外,指向此未命名对象的指针作为结果 returned。接下来,创建一个名为
pa2
的指向A
的指针,并使用我们在上面的步骤 1 中获得的指向未命名对象的指针进行初始化。
接下来与 class B
相关的 4 个语句发生了完全相同的事情。所以我不讨论接下来与 class B
相关的 4 个陈述,因为我们不会从中学到任何新东西。同样的事情也会发生在他们身上,就像上面描述的前 4 个陈述一样。
现在考虑class C
相关的语句。我们不会跳过这 4 个语句,因为 class C
有一个 user-defined 默认构造函数。
声明 5
这里我们考虑以下语句:
C c1;
std::cout << c1.r << std::endl;
上述代码片段的第一条语句使用用户提供的默认构造函数A::A()
创建了一个名为c1
类型C
的变量。由于此用户提供的默认构造函数不执行任何操作,因此数据成员 r
未初始化,我们得到与我们讨论的 A a1;
相同的行为。也就是说,在第二条语句中使用这个未初始化的变量是 undefined behavior.
声明 6
这里我们考虑以下语句:
C c2();
std::cout << c2.r << std::endl;
上面片段中的第一条语句是函数声明。因此,您将获得与 class A
.
声明 7
这里我们考虑以下语句:
C* pc1 = new C;
std::cout << pc1->r << std::endl;
以上代码段中的第一条语句具有以下效果:
由于表达式
new A
,使用用户提供的默认构造函数A::A()
在堆上创建类型C
的未命名对象。并且由于用户提供的默认构造函数不执行任何操作,因此数据成员r
未初始化。此外,我们得到一个指向这个未命名对象的指针作为结果。接下来,创建一个名为
pc1
的指向C
的指针ed 并由 pionter 初始化为我们在步骤 1 中获得的未命名对象。
现在上面代码片段中的第二个语句使用未初始化的数据成员 r
,这是未定义的行为,并解释了为什么你得到一些垃圾值作为输出。
声明 8
这里我们考虑以下语句:
C* pc2 = new C();
std::cout << pc2->r << std::endl;
上述代码片段的第一条语句具有以下效果:
由于
new C()
,在堆上创建了类型C
的未命名对象。现在,由于您已经指定了括号()
,这将执行 value-initialization。但是因为这次我们有一个 user-provide 默认构造函数,所以 value-initialization 与 default-initialization 相同,后者将使用 user-provide 默认构造函数完成。由于用户提供的默认构造函数什么都不做,数据成员r
将保持未初始化状态。此外,我们得到一个指向未命名对象的指针作为结果。接下来,创建一个指向
C
的指针,名为pc2
,并由 pionter 初始化为我们在上面的步骤 1 中获得的未命名对象。
上面代码片段中的第二条语句使用了未初始化的数据成员 r
,即 未定义的行为 并解释了为什么您得到一些垃圾值作为输出。