C++构造:"MyClass c"不好,"MyClass c = MyClass()"慢,我要"MyClass c()"

C++ construction: "MyClass c" is bad, "MyClass c = MyClass()" is slow, I want "MyClass c()"

本文关键字:MyClass 我要 不好 构造 C++      更新时间:2023-10-16

下面是一些代码:

class MyClass
{
public:
    int y;
};
int main()
{
    MyClass item1;
    MyClass item2 = MyClass();
}

当我运行它时,我收到以下值:

item1.y == [garbage]
item2.y == 0

这让我感到惊讶。

我预计 item1 是默认构造的,item2 是从 MyClass 的匿名默认构造实例复制构造的,导致两者都等于 0(因为默认构造函数将成员初始化为默认值)。检查程序集:

//MyClass item1;
//MyClass item2 = MyClass();
xor         eax,eax  
mov         dword ptr [ebp-128h],eax  
mov         ecx,dword ptr [ebp-128h]  
mov         dword ptr [item2],ecx  

显示通过在临时某处写入"0"值,然后将其复制到 item2 中来构造的 item2,如预期的那样。但是,item1 没有程序集。

因此,程序在堆栈中具有 item1 的内存,但它从不构造 item1。

我可以理解出于速度目的想要这种行为,但我想要两全其美!我想知道 item1.y == 0(它被构造了),但我不想像 item2 那样在默认构造匿名实例然后复制构造上浪费时间。

令人沮丧的是,我不能通过说MyClass item1();来强制默认构造,因为它被解释为函数原型。

所以......如果我想在 item1 上使用默认构造函数而不进行复制构造,我到底该怎么做?

旁注:看起来如果我为 MyClass 声明一个构造函数,item1 会像往常一样构造。因此,此行为仅适用于编译器生成的构造函数。

让你的类看起来像这样:

class MyClass 
{
public:
    MyClass() : y(0) 
    {
    }
public:
    int y;
};

这两个示例都可以正常工作。您的问题是,如果没有提供构造函数,则不会初始化基本类型成员。因此,y表示item1所在的位置的堆栈上碰巧的任何随机数据。

显式实现构造函数可解决此问题。

这种行为的存在是因为C++的"你只为你使用的东西付费"的心态。基本类型没有"默认值",因为这会使分配某些内容然后稍后填充成本不必要地(略微)增加,因为值实际上被设置了两次。一次用于"默认值",一次用于您实际想要的值。

在C++中,您只需为要求完成的工作付费。 具有不初始化其数据的类型的能力可能是一个重要的性能提升:令人讨厌的是,这也是默认行为,因为它是从 C 继承的。

您可以通过创建一个构造函数来解决此问题,该构造函数零初始化int,使用统一初始化语法{}或在类型声明中使用新的默认语法int y = 0;(最后一个需要 C++11,第二个需要 C++11,如果类型是非 POD)。

通过检查调试生成程序集,会误导您对Foo f = Foo();的性能问题。 在这种微不足道的情况下,复制省略得到了市场上每个非脑死亡编译器的支持,即使 ctor 有副作用,也是合法的。

问题是你对两件事的误解。

  1. 编译器生成的默认构造函数的作用。
  2. 什么是声明(因此当使用赋值时)。

编译器生成的默认构造函数的作用。

如果未定义构造函数,编译器将为您生成默认构造函数。但是有两个不同的版本。一个"值初始化"默认构造函数(对于内置 POD 类型,它不执行任何操作,并且使它们保持未初始化状态)。"零初始化"默认构造函数(对于内置 POD 类型,将其设置为零)。

什么是声明(因此当使用赋值时)。

仅当=左侧的对象已完全构造时,赋值运算符才适用。由于声明中的对象在";"之前不是完全构造的,因此这不是赋值。

Bar x = Bar(); // There is no assignment here (this is a declaration using the default constructor
Bar y = Bar(2);// There is no assignment here (this is a declaration using a constructor).

这是使用复制构造函数从临时构造对象。但这并不重要,因为编译器只是省略了实际的副本并就地构建,所以如果发生任何副本,我会感到非常惊讶。

代码中发生了什么

int  x;          // default-Inititalized.      [ Value = Garbage]
int  z = int();  // Zero-Inititalized.         [ Value = 0]

相同的规则适用于具有编译器生成的默认构造函数的类:

LokiClass  xL;               // Value-Initialized -> Default Initialized
                             //                     This is an explicit call to 
                             //                     the default constructor but  
                             //                     will only Value-Initialize
                             //                     class types and not initialize
                             //                     built-in POD types. 
LokiClass  yL = LokiClass(); // Zero-Initialized    This is an explicit call to 
                             //                     the default constructor but  
                             //                     makes sure to use the 
                             //                     Zero-Initialization version if
                             //                     it is the compiler generated 
                             //                     version.
LokiClass  y1L {};           // C++11 version of Zero-Initialization constructor used.
LokiClass  zL((LokiClass()));// This is copy construction
                             // Which will probably lead to copy elision by the compiler.

那么值/零初始化有什么区别?
值初始化不会对内置 POD 类型进行初始化。它将在任何基类和成员上调用值初始化编译器生成的默认构造函数(请注意,如果您定义默认构造函数,那么它将使用该构造函数,因为没有编译器生成的

构造函数)。

零初始化将对 POD 类型执行零初始化。它将在任何基类和成员上调用零初始化编译器生成的默认构造函数(请注意,如果您定义默认构造函数,那么它将使用该构造函数,因为没有编译器生成的构造函数)。

你可以简单地做:

MyClass item1;

根据变量,它可能会也可能不会将 POD 类型初始化为零。

或者,如果您使用 C++11,您可以执行以下操作:

MyClass item1 {};

显式调用默认构造函数。

item1 没有汇编代码的原因是编译器认为没有必要为所示类生成代码,因为它没有您编写的显式默认构造函数。