矩形 A = 矩形(3, 4);等效于矩形 A(3,4);

Is Rectangle A = Rectangle(3, 4); equivalent to Rectangle A(3,4);?

本文关键字:矩形 于矩形      更新时间:2023-10-16

下面是我的代码:

#include <iostream>
using namespace std;
class Rectangle {
int width, height;
public:
Rectangle(int, int);
int area() { return (width * height); }
};
Rectangle::Rectangle(int a, int b) {
width = a;
height = b;
}
int main() {
Rectangle A(3, 4);
Rectangle B = Rectange(3,4);
return 0;
}

我没有为类定义任何复制构造函数或赋值运算符Rectangle

Rectangle B = Rectangle(3, 4);真的连续做三件事是真的吗?

  1. 为 Rectangle 的临时变量(让我们使用tmp来表示它)分配内存空间,调用Rectangle::Rectangle(3, 4)来初始化它。

  2. 为变量B分配内存空间,使用默认构造函数初始化它

  3. (成员)使用赋值运算符Rectangle& operator = (const Rectangle &)tmp复制到B

这个解释有意义吗?我想我可能理解错了,因为与Rectangle A(3, 4);相比,这个过程看起来非常笨拙和低效。

有人对此有想法吗?Rectangle A(3,4)真的等同于Rectangle A = Rectangle(3, 4);吗?谢谢!

Rectangle B = Rectangle(3, 4);真的在序列中做三件事吗?

不,这不是真的,但这不是你的错:这令人困惑。

T obj2 = obj1不是赋值,而是初始化

所以你是对的,obj1将首先创建(在您的情况下,是临时的),但obj2将使用复制构造函数构造,而不是默认构造然后分配给。


对于Rectangle C = new Rectangle(3, 4);,唯一的区别是临时变量的内存空间被分配给堆而不是堆栈。

不,它不会编译,但Rectangle* C会编译。这里已经有很多关于动态分配的解释。

在这种情况下,实际发生的事情与理论上发生的事情之间存在显着差异。

第一个很简单:Rectangle A(3, 4);只是构造一个width矩形,height初始化为34。这一切都是使用您定义的Rectangle(int, int);构造函数在"一步"中完成的。简单明了 - 因此在可能的情况下,它通常是首选和推荐的。

然后让我们考虑一下:Rectangle B = Rectangle(3,4);.理论上,这会构造一个临时对象,然后从该临时对象复制构造B

实际上,发生的情况是编译器检查它是否能够创建临时对象,并且是否能够使用复制构造函数从该临时对象初始化B。在检查这是可能的之后,几乎任何有能力的编译器(至少在启用优化时,通常即使没有启用优化)都会生成与创建A基本相同的代码。

但是,如果删除复制构造函数,请通过添加:

Rectangle(Rectangle const &) = delete;

。然后编译器会发现它无法从临时复制构造B,并且会拒绝编译代码。即使最终生成的代码从未实际使用复制构造函数,它也必须可用才能正常工作。

最后,让我们看一下你的第三个示例(语法已更正):

Rectangle *C = new Rectangle(3, 4);

尽管看起来有点像上面创建B的行,但所涉及的结构实际上更像您用来创建A的前一个。只创建一个对象(甚至理论上)。它是从免费商店分配的,并使用Rectangle(int, int);构造函数直接初始化。

然后使用该对象的地址来初始化C(这只是一个指针)。就像A的初始化一样,即使删除Rectangle的复制构造函数,这也可以工作,因为即使在理论上也没有涉及Rectangle的复制。

这些都不涉及赋值运算符。如果要删除赋值运算符:

Rectangle &operator=(Rectangle const &) = delete;

。所有这些代码仍然很好,因为(尽管使用了=)代码中的任何地方都没有赋值。

要使用作业(如果你真的坚持这样做),你可以(例如)执行以下操作:

Rectangle A(3, 4);
Rectangle B = Rectangle(5, 6);
B = A;

这仍然只使用构造函数来创建和初始化AB,但随后使用赋值运算符将A的值分配给B。在这种情况下,如果如上所示删除赋值运算符,则代码将失败(无法编译)。

我们的一些误解似乎源于这样一个事实,即如果您不采取措施来阻止它这样做,编译器会自动为您创建"特殊成员函数"。您可以阻止它这样做的方法之一是上面显示的= delete;语法,但这不是唯一的语法。例如,如果您的类包含引用类型的成员变量,则编译器不会为您创建赋值运算符。如果你从这样简单的事情开始:

struct Rectangle { 
int width, height;
};

。编译器将完全自动生成默认构造函数、复制构造函数、移动构造函数、复制赋值和移动赋值运算符。

Rectangle A(3, 4);总是简单地调用Rectangle(int, int)构造函数,贯穿C++构造函数的所有历史。简单。无聊的。

现在是有趣的部分。

C++17

在标准的最新版本(截至撰写本文时),Rectangle B = Rectangle(3,4);立即折叠为Rectangle B(3,4);无论Rectangle移动或复制构造函数的性质如何,都会发生这种情况。此功能通常称为保证复制省略,但需要强调的是,此处没有副本,也没有移动。发生的情况是B(3,4)直接初始化。

C++17 之前

在 C++17 之前,构造了一个临时Rectangle,编译器可以对其进行优化(我的意思是它肯定会,除非你告诉它不要这样做)。但是您对事件的排序不正确。请务必注意,此处不会发生任何分配。我们不分配给B.我们正在建设B.表格代码:

T var = expr;

是复制初始化,不是复制赋值。因此,我们做两件事:

  1. 我们使用Rectangle(int, int)构造函数构造一个临时Rectangle
  2. 该临时直接绑定到隐式生成的移动(或复制,pre-C++11)构造函数中的引用,然后调用该构造函数 - 从临时构造函数执行成员级移动(或复制)。(或者,更准确地说,重载解析在给定类型为Rectangle的 prvalue 的情况下选择最佳Rectangle构造函数)
  3. 临时在声明中被销毁。

如果删除了移动构造函数(或者,在 C++11 之前,复制构造函数标记为private),则尝试以这种方式构造B格式不正确。如果保留特殊成员函数(如本例所示),则B的两个A声明肯定会编译为相同的代码。


如果实际删除类型,B中的初始化形式可能看起来更熟悉:

auto B = Rectangle(3,4);

这就是赫伯·萨特(Herb Sutter)喜欢的所谓AAA(几乎总是自动)风格的声明。这与Rectangle B = Rectangle(3, 4)完全相同,只是首先推导出B类型为Rectangle

  1. Rectangle* C = new Rectangle(3,4);,您的代码中存在语法错误。
  2. operator =(Rectangle& rho)是默认定义的返回引用。因此,Rectangle B = Rectangle(3, 4);不会像上面描述的那样创建tmp对象。