试图理解默认构造函数和成员初始化

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,因为 AB 都有一个不是 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

以上代码段中的第一条语句具有以下效果:

  1. 由于 new A 使用 默认构造函数 A::A() [=140=,因此在堆上创建了类型 A 的未命名对象]由编译器合成。此外,我们还得到了指向这个未命名对象的指针。
  2. 接下来,我们在上面的第 1 步中获得的指针用作 pa1 初始化程序 。也就是说,创建了一个名为 pa1 的指向 A 的指针,并由指向我们在上面的步骤 1 中获得的未命名对象的指针初始化。

因为使用了默认构造函数(参见第 1 步),这意味着未命名对象的数据成员 rdefault initilaized。由于数据成员 r 是内置类型,这意味着它具有 不确定值 。在上述代码片段的第二条语句中使用这个未初始化的数据成员 r 未定义行为 。这就是为什么你得到一些垃圾值作为输出。

案例4

这里我们考虑以下语句:

A* pa2 = new A();
std::cout << pa2->r << std::endl;

上述代码片段的第一条语句具有以下效果:

  1. 由于表达式 new A(),创建了类型 A 的未命名对象。但是这次由于您使用了括号 () 并且由于 class A 没有用户提供的默认构造函数,这意味着 值初始化 将会发生.这实质上意味着数据成员 r 将被 零初始化 。这是 why/how 您在上述代码段的第二条语句中得到的输出为 0。此外,指向此未命名对象的指针作为结果 returned。

  2. 接下来,创建一个名为 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;

以上代码段中的第一条语句具有以下效果:

  1. 由于表达式 new A,使用用户提供的默认构造函数 A::A() 在堆上创建类型 C 的未命名对象。并且由于用户提供的默认构造函数不执行任何操作,因此数据成员 r 未初始化。此外,我们得到一个指向这个未命名对象的指针作为结果。

  2. 接下来,创建一个名为pc1的指向C的指针ed 并由 pionter 初始化为我们在步骤 1 中获得的未命名对象。

现在上面代码片段中的第二个语句使用未初始化的数据成员 r,这是未定义的行为,并解释了为什么你得到一些垃圾值作为输出。

声明 8

这里我们考虑以下语句:

C* pc2 = new C();
std::cout << pc2->r << std::endl;

上述代码片段的第一条语句具有以下效果:

  1. 由于 new C(),在堆上创建了类型 C 的未命名对象。现在,由于您已经指定了括号 (),这将执行 value-initialization。但是因为这次我们有一个 user-provide 默认构造函数,所以 value-initialization 与 default-initialization 相同,后者将使用 user-provide 默认构造函数完成。由于用户提供的默认构造函数什么都不做,数据成员 r 将保持未初始化状态。此外,我们得到一个指向未命名对象的指针作为结果。

  2. 接下来,创建一个指向 C 的指针,名为 pc2,并由 pionter 初始化为我们在上面的步骤 1 中获得的未命名对象。

上面代码片段中的第二条语句使用了未初始化的数据成员 r,即 未定义的行为 并解释了为什么您得到一些垃圾值作为输出。