按值传递对象时调用的析构函数

Destructor called when objects are passed by value

本文关键字:析构函数 调用 对象 按值传递      更新时间:2023-10-16

即使对象通过法线传递给函数按值调用参数传递机制,在理论上保护并且隔离了主叫论点,对于一方来说仍然是可能的可能影响甚至损坏用作论点例如,如果用作参数的对象分配内存,并在内存被销毁时释放该内存,然后释放其本地副本当函数的析构函数为呼叫。这将使原始对象受到损坏无用的

这是用C++编写的:完整参考

在此程序中

#include<iostream>
using namespace std;
class Sample
{         
 public:
         int *ptr;
         Sample(int i)
         {
         ptr = new int(i);
         }
         ~Sample()
         {
         cout<<"destroyed";
         delete ptr;
         }
         void PrintVal()
         {
         cout << "The value is " << *ptr;
         }
 };
 void SomeFunc(Sample x)
{
 cout << "Say i am in someFunc " << endl;
}
 int main()
{
 Sample s1= 10;
SomeFunc(s1);
 s1.PrintVal();
}

当对象s1从该对象返回时被销毁,它会生成运行时错误。我不明白为什么会发生这种事,因为应该复印一份。我想可能是因为类定义中没有复制构造函数。但我惊讶地发现,如果使用这个函数声明

 void SomeFunc(Sample &x)
{
 cout << "Say i am in someFunc " << endl;
}

在这个声明中没有出现错误。错误是否也应该出现在这里,因为它被引用了?有人能解释一下这两种情况下发生了什么吗。

这确实是因为您没有提供复制构造函数。因此,编译器将为您生成一个,用于执行琐碎的复制。这是指针的琐碎副本,在这里有问题。

以下申报

void SomeFunc(Sample x);

当您将s1传递给函数时,确实会有一个副本,但这个副本将有指针的副本,也就是说,两个对象将指向同一个int

然后,当退出函数时,副本将被销毁并删除该指针,在调用代码中留下一个刚刚删除的指针的原始对象(记住,它们指向相同的东西)。

然后,对于以下声明

void SomeFunc(Sample &x);

你没有任何副本,所以问题没有出现。事实上,通过引用传递意味着在函数内部,您正在操作的Sample对象与您传递给函数的对象完全相同,并且在函数退出时不会被销毁。

我将从现代C++的角度给出更多的答案,即"如果可以的话,避免原始指针"。但我也要指出一个你应该注意的重要区别:

C++构造函数语法

但首先,让我们考虑一下你的意图。如果我写:

Sample x = 1;
Sample y = x;

语义应该是什么?

Sample"副本"是否应该每个都有自己独立的"ptr",其指向对象的生存期只与它们所在的类一样长?

如果这是你想要的,通常情况下你根本不需要指针。

大多数情况下,类的总大小将足够合理,如果您在没有new的情况下声明它们(就像您在这里一样),则堆栈分配不会成为问题。那么,为什么要涉及指针呢?只需使用int i(或任何非POD类)。

如果您确实有这样的情况,确实需要动态分配大块数据来管理自己(相对于延迟到C++库集合或类似的集合),那么这些数据可能会超出您的堆栈。如果您需要动态分配,您将需要以某种方式复制构造。这意味着Sample将需要显式管理复制构造,或者使用一个智能指针类来对其进行精细处理,这样它就不必了。

首先假设您保留原始指针,这意味着:

Sample(const Sample & other)
{
   ptr = new int(*other.ptr);
}

但是在这种情况下,您可以使用unique_ptr来减少出错的可能性。unique_ptr将在其析构函数运行时自动销毁它所持有的原始指针所指向的数据。因此,您不必担心呼叫delete

同样,unique_ptr默认情况下将拒绝复制。因此,如果你只是写:

class Sample
{         
public:
     unique_ptr<int> ptr;
     Sample(int i)
     {
         ptr = std::unique_ptr<int>(new int(i));
     }
     ~Sample()
     {
        cout << "destroyed";
     }
     void PrintVal()
     {
         cout << "The value is " << *ptr;
     }
};

类本身可以构建,但在调用站点上会出现错误。他们会指出,你正在为一些没有正确定义复制结构的东西进行复制。不仅如此。。。你不只是在你的程序中制作一个副本,而是两个:

In function ‘int main()’:
error: use of deleted function ‘Sample::Sample(const Sample&)’
Sample s1 = 10;
            ^
note: ‘Sample::Sample(const Sample&)’ is implicitly deleted
       because the default definition would be ill-formed:
error: use of deleted function ‘Sample::Sample(const Sample&)’
SomeFunc(s1);
           ^

这让你有机会添加一个复制构造函数,相当于:

     Sample(const Sample & other)
     {
         ptr = std::unique_ptr<int>(new int(*other.ptr));
     }

另外,您可能希望将Sample s1 = 10;更改为Sample s1 (10);,以避免复制。就这一点而言,您可能希望SomeFunc也通过引用获取其值。我还将提到查看初始值设定项列表与赋值。

(注意:实际上,复制名为clone_ptr的智能指针类的模式有一个名称,所以您甚至不必编写复制构造函数。它不在标准C++库中,但您可以在周围找到实现。)

Sample"副本"是否应该共享一个只有在最后一个引用消失后才删除的公共动态ptr

使用智能指针更容易,而且在Sample上根本不需要复制构造函数。使用shared_ptr。shared_ptr的默认行为是能够通过简单的赋值进行复制。

class Sample
{         
public:
     shared_ptr<int> ptr;
     Sample(int i)
     {
         ptr = make_shared<int>(i);
     }
     ~Sample()
     {
        cout << "destroyed";
     }
     void PrintVal()
     {
         cout << "The value is " << *ptr;
     }
};

这个故事的寓意是,你越能让默认行为为你做正确的工作。。。你写的代码越少。。。你产生bug的可能性就越小。因此,虽然知道复制构造函数的作用以及何时调用它们很好,但知道如何而不是编写它们也同样重要!

请注意,unique_ptr和shared_ptr来自C++11,需要#include <memory>

您的对象是逐字段复制的,因此在SomeFunc中有两个实例Sample-s1x(但只有x可访问),x.ptr的值等于s1.ptr。然后,当SomeFunc结束时,将调用析构函数,并从那时起s1.ptr指向未分配的内存。它被称为"悬空指针"。