#include <iostream>
using namespace std;
class Car
{
public:
~Car() { cout << "Car is destructed." << endl; }
};
class Taxi :public Car
{
public:
~Taxi() {cout << "Taxi is destructed." << endl; }
};
void test(Car c) {}
int main()
{
Taxi taxi;
test(taxi);
return 0;
}
这是输出:
Car is destructed.
Car is destructed.
Taxi is destructed.
Car is destructed.
我使用MS Visual Studio Community 2017(对不起,我不知道如何查看Visual C ++版本)。
当我使用调试模式时。我发现按预期离开void test(Car c){ }
函数体时执行了一个析构函数。当test(taxi);
结束时,出现了一个额外的析构函数。
test(Car c)
函数使用值作为形式参数。
转到该功能时会复制汽车。
因此,我认为离开该功能时只会有一辆“汽车被毁”。
但是实际上在离开函数时有两个“汽车被破坏”。(输出中显示的第一行和第二行)
为什么会有两个“汽车被毁”?谢谢。
===============
当我在class Car
中添加虚拟函数时
例如:virtual void drive() {}
然后我得到了预期的输出。
Car is destructed.
Taxi is destructed.
Car is destructed.
答案 0 :(得分:7)
在将taxi
切片为函数调用时,Visual Studio编译器似乎有点捷径,具有讽刺意味的是,它导致它执行的工作比预期的多。
首先,它要使用您的taxi
并从中复制构建Car
,以便参数匹配。
然后,它再次复制Car
以传递值。
当您添加用户定义的副本构造函数时,此行为消失了,因此编译器似乎是出于自身原因(也许在内部,这是一条更简单的代码路径)使用“允许”这一事实因为副本本身是微不足道的。您仍然可以使用非平凡的析构函数观察到这种行为,这有点像反常。
我不知道在什么程度上合法(尤其是自C ++ 17以来),或者为什么编译器会采用这种方法,但是我同意这不是输出会凭直觉期望的。 GCC和Clang都没有这样做,尽管它们可能以相同的方式进行操作,但是在复制副本方面做得更好。我已经注意到,即使VS 2019仍然不能保证淘汰率。
答案 1 :(得分:3)
创建Taxi
时,还将创建一个Car
子对象。当出租车被摧毁时,两个物体都被破坏了。调用test()
时,您将按值传递Car
。因此,第二个Car
被复制构造,并且在离开test()
时将被破坏。因此,我们对3个析构函数进行了解释:序列中的第一个和最后两个。
第四个析构函数(即序列中的第二个析构函数)是意外的,我无法使用其他编译器进行重现。
它只能是临时Car
,它是Car
参数的源。由于直接提供Car
值作为参数时不会发生,因此我怀疑这是将Taxi
转换为Car
的结果。这是意外的,因为每个Car
中已经有一个Taxi
子对象。因此,我认为编译器确实将不必要的转换成临时文件,并且没有执行避免该临时文件的复制省略。
注释中的说明:
以下是关于语言律师标准的说明,以验证我的主张:
[class.conv.ctor]
进行的转换,即根据另一种类型的参数(此处为Taxi)构造一个类(此处为Car)的对象。 Car
值。允许编译器根据[class.copy.elision]/1.1
进行复制省略,因为它可以构造要直接返回到参数中的值,而不是构造一个临时值。我现在可以使用相同的编译器来重现您的情况,并进行实验以确认正在发生的情况。
我上面的假设是,编译器使用构造函数转换Car(const &Taxi)
选择了次优参数传递过程,而不是直接从Car
的{{1}}子对象复制构造。
因此,我尝试调用Taxi
,但将test()
强制转换为Taxi
。
我的第一次尝试未能成功改善局势。编译器仍使用次佳的构造函数转换:
Car
第二次尝试成功。它也进行强制转换,但是使用指针强制转换以强烈建议编译器使用test(static_cast<Car>(taxi)); // produces the same result with 4 destructor messages
的{{1}}子对象,而不创建这个愚蠢的临时对象:
Car
而且令人惊讶:它按预期工作,仅产生3条销毁消息:-)
结论实验:
在最后的实验中,我通过转换提供了一个自定义构造函数:
Taxi
并使用test(*static_cast<Car*>(&taxi)); // :-)
来实现。听起来很傻,但是这也会生成仅显示3个析构函数消息的代码,从而避免了不必要的临时对象。
这导致认为编译器中可能存在导致此行为的错误。这是在某些情况下可能会丢失从基类进行直接复制构造的可能性。