C++ 类可以有多个隐式默认构造函数吗?

0kc*_*ats 4 c++

我在 C++ 行为中发现了一个令我困惑的有趣案例。预先警告,我不会在自己的代码中执行此操作。这只是为了了解标准。

这里我有一个没有显式默认构造函数的类 M。它继承自具有显式默认构造函数的类 Base。当我像 M() 或 M{} 那样构造 M 类时,它将把成员 r 归零。如果我在没有任何括号的情况下构造它,则不会初始化成员 r,但会调用基类构造函数(如预期)。对我来说奇怪的是,这种行为似乎 M 类现在有两个隐式默认构造函数 - 一个使成员 r 未初始化,但调用所有继承的构造函数,另一个将其清零。

如果 M 是纯 POD,那么我理解这种行为(默认初始化与统一化),但这里甚至添加 M() = default; 仍然会导致它与 new M() 和 new M 的行为不同。

因为两个编译器执行相同的操作,所以看起来不像是错误,但是这个功能是什么?这有点令人困惑。有没有一个简化的方案来理解它。

这是代码的 godbolt 链接https://godbolt.org/z/9srh48Y5Y 我在这里做一些技巧来强制将类分配在一些垃圾填充的内存上,以便在 godbolt 上看到该行为。

#include <iostream>
#include <cstring>

//Base class with default constructor
//explicitly defined
class Base
{
public:
  int k;
  Base() : k(0xF){}
};

//A class with default constructor
//explicitly defined
class C
{
public:
  int z;
  C() : z(13){}
};

//Class M does not have explicitly defined
//default constructor
class M : public Base
{
public:
  int r;
  C c;
  //M() = default;
  //M() {}
};

union Buffer
{
    char bbb[sizeof(M)];
    double ddd;
    long long llll;
};

int main()
{
  //to demonstrate which members are initialized
  //I need to fill the memory with some garbage
  //before using it to construct a class
  Buffer buf1;
  Buffer buf2;
  Buffer buf3;
  std::memset(buf1.bbb, 0xCD, sizeof(Buffer));
  std::memset(buf2.bbb, 0xCD, sizeof(Buffer));
  std::memset(buf3.bbb, 0xCD, sizeof(Buffer));

  M* m1 = new (buf1.bbb) M{};
  M* m2 = new (buf2.bbb) M();
  M* m3 = new (buf3.bbb) M;
  std::cout << m1->k << " "  << m1->c.z << " " << m1->r << std::endl;
  std::cout << m2->k << " "  << m2->c.z << " " << m2->r << std::endl;
  std::cout << m3->k << " "  << m3->c.z << " " << m3->r << std::endl;
}i
Run Code Online (Sandbox Code Playgroud)

clang 16 (x86-64) 的输出:

15 13 0
15 13 0
15 13 -842150451
Run Code Online (Sandbox Code Playgroud)

海湾合作委员会也这样做。

编辑

这里是评论中讨论的第二个更有趣的代码变体(https://godbolt.org/z/aso8Gz4sh)。

#include <iostream>
#include <cstring>

//Base class with default constructor
//explicitly defined
class Base
{
public:
  int k;
  Base() : k(0xF){}
};

//A class with default constructor
//explicitly defined
class C
{
public:
  int z1;
  int z2;
  C() : z1(13) {}
};

//Class M does not have explicitly defined
//default constructor
class M : public Base
{
public:
  int r;
  C c;
  //M() = default;
  //M() {}
};

union Buffer
{
    char bbb[sizeof(M)];
    double ddd;
    long long llll;
};

int main()
{
  //to demonstrate which members are initialized
  //I need to fill the memory with some garbage
  //before using it to construct a class
  Buffer buf1;
  Buffer buf2;
  Buffer buf3;
  std::memset(buf1.bbb, 0xCD, sizeof(Buffer));
  std::memset(buf2.bbb, 0xCD, sizeof(Buffer));
  std::memset(buf3.bbb, 0xCD, sizeof(Buffer));

  M* m1 = new (buf1.bbb) M{};
  M* m2 = new (buf2.bbb) M();
  M* m3 = new (buf3.bbb) M;
  std::cout << m1->k << " " << m1->c.z1 << " " << m1->c.z2 << " " << m1->r << std::endl;
  std::cout << m2->k << " " << m2->c.z1 << " " << m2->c.z2 << " " << m2->r << std::endl;
  std::cout << m3->k << " " << m3->c.z1 << " " << m3->c.z2 << " " << m3->r << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

铿锵 16.0 输出

15 13 -842150451 0
15 13 0 0
15 13 -842150451 -842150451
Run Code Online (Sandbox Code Playgroud)

海湾合作委员会 13.2 输出

15 13 0 0
15 13 0 0
15 13 -842150451 -842150451
Run Code Online (Sandbox Code Playgroud)

Mil*_*nek 6

这是预期的行为。 M是一个聚合,因为它没有:

  • 用户声明的构造函数
  • virtualprivateprotected基类
  • 直接privateprotected数据成员
  • virtual成员函数
  • 默认成员初始值设定项

这意味着它遵循聚合初始化规则。

聚合初始化的规则基本上如下:

  • 如果对象是默认初始化的
    • 所有基础和成员子对象也得到默认初始化。这就是当您构造M不带任何括号的 时会发生的情况。
  • 如果对象是用braced-init-list初始化的:
    • 该对象的前 N ​​个基本对象和成员子对象使用在braced-init-list中传递的参数进行初始化。
    • 所有剩余的基础和成员子对象都获得初始化值M这就是当您构造带有空括号的 时会发生的情况M{}
  • 如果对象已初始化值
    • 所有基类和成员子对象也都进行了值初始化M这就是当您构造using 空括号时会发生的情况M()

所以:

  • new M:所有子对象都是默认初始化的(对于POD对象来说是未初始化的)
  • new M{}或者new M():所有子对象都进行值初始化(这意味着 POD 对象的零初始化)
  • new M{{}, 10}M对象的Base子对象使用 初始化{},其r子对象使用 初始化10,其子c对象使用值初始化