返回C++对象时的复制和销毁

Copying and destruction when returning a C++ object

本文关键字:复制 C++ 对象 返回      更新时间:2023-10-16

我有一段相当简单的测试代码:

#include <stdio.h>
class PG
{
public:
PG(){
    m_ptr = new int;
    printf("Created PG %in", (int)m_ptr);
} 
~PG(){
    printf("Deleted PG %in", (int)m_ptr);
    delete (m_ptr);
}
PG& operator =(const PG& src)
{
    printf("Copied PG %i %in", (int)m_ptr, (int)src.m_ptr);
    return(*this);
}
private:
    int * m_ptr;
};
PG CreatePG()
{
    PG ret;
    return ret;
}
int main(int argc, char* argv[])
{
    PG test;
    test = CreatePG();
    printf("Endingn");
    return 0;
}

如果我使用 GCC、VS2008 或 VS2012 进行全面优化并运行它,我会得到我所期望的:
创建 PG 7837600 - 创建的测试
创建 PG 7689464 创建 ret
复制 PG 7837600 768946 - 复制的 ret 进行测试
已删除的PG 7689464 - 已删除的ret
结束
已删除 PG 7837600 - 已删除

测试

但是,当我在没有优化的情况下在VS2008或VS2012上编译时,我得到这个:
创建的 PG 3888456 创建的测试
创建 PG 4036144 创建 ret
删除了PG 4036144 - 删除了。 等等,我们还没有复制它!
复制的PG 3888456 4036144 - 我们现在正在尝试复制已删除的数据
已删除的PG 4036144 - 这已被删除。应用程序崩溃

我不敢相信这是VS中从未修复的错误,但我也看不出我做错了什么。在我的应用程序中,我有一个实例,在编译针对速度优化的更复杂的类时,也会发生这种行为。我知道使用会更有效:

 PG test = CreatePG();

但我仍然遇到类似的问题,
尽管在这种情况下它显然使用了复制省略:创建 PG 11228488
删除的PG 11228488
结束
删除的PG 11228488
我仍然得到双重删除。

如果有人能对此有所了解,我将不胜感激。

这是因为您的代码违反了三法则:由于您没有复制构造函数,因此幕后发生了一些重要的事情,您看不到打印输出。

当您没有复制构造函数时,C++很乐意为您定义一个。这通常是您想要的确切构造函数,但一种情况除外:当您的类显式管理资源时。在这种情况下,逐字节复制内容会创建一个假别名,当同一个指针可以多次删除时。启用优化时,编译器会跳过复制构造函数的调用(返回值优化)。但是,关闭优化后,将调用复制构造函数,然后删除m_ptr的副本,从而保留指向已删除内存的实际指针。

以下是解决此问题的方法:

PG& operator =(const PG& src) {
    *m_ptr = *(other->m_ptr);
    printf("Assigned PG %x %xn", (void*)m_ptr, (void*)src.m_ptr);
    return(*this);
}
PG(const PG& other) {
    m_ptr = new int;
    *m_ptr = *(other->m_ptr);
    printf("Copied PG %xn", (void*)m_ptr);
}

注: 未定义将指针转换为int;应将指针转换为void*,并使用%x格式说明符进行打印。

你的复制构造函数在哪里? 您需要一个复制构造函数命令返回一个值,你得到的只是默认副本构造函数,这将导致多次删除相同的对象(如果使用)(因为您最终会得到多个包含相同指针的对象)。 至于为什么它做一个优化时的东西,不优化时的另一个,大概是 NRVO(称为返回值优化)仅在代码已优化。

这就是您遇到问题的原因。

让我们逐步完成您的代码

PG test;  

调用 PG::P G(),这会创建一个带有 ptr 的新 PG,以在地址 X 处的堆上分配的新 int,打印"创建的 PG X"

test = CreatePG();

首次调用 CreatePG()

PG ret;

调用 PG::P G(),这会创建一个带有 ptr 的新 PG,以在地址 Y 处的堆上分配的新 int,打印"创建的 PG Y"

return ret;

由于该函数声明为按值返回,因此使用执行按位复制的默认复制构造函数。 所以未命名副本的m_ptr返回的是 Y。

当我们离开 CreatePG() 的范围时,调用本地对象的析构函数,它打印"删除了 PG Y",然后删除了 Y。 然后我们回到 main() 中的赋值。

test = CreatePG();

我们现在使用 operator= 分配临时未命名的 PG 进行测试。 这将打印"复制的 PG X Y",然后简单地返回对测试的引用,而无需实际执行任何操作。

在表达式结束时,临时超出范围并调用析构函数。 析构函数打印"已删除的 PG Y"并尝试删除 Y,但 Y 已被删除,因此这是一个很大的问题。

编写处理指向堆的指针的复制构造函数的建议很好,可以帮助您解决问题。