这个C ++代码是否会泄漏内存?

时间:2012-05-26 04:56:30

标签: c++ memory-leaks assignment-operator

struct Foo
{
    Foo(int i)
    {
        ptr = new int(i);
    }
    ~Foo()
    {
        delete ptr;
    }
    int* ptr;
};

int main()
{
    {
        Foo a(8);
        Foo b(7);
        a = b;
    }
    //Do other stuff
}

如果我理解正确,编译器将自动为Foo创建赋值运算符成员函数。但是,只需ptr中的b值即可将其放入a。由a分配的内存最初似乎丢失了。我可以在进行赋值之前调用a.~Foo();,但我听说你应该很少需要显式调用析构函数。所以我要说的是我为Foo编写一个赋值运算符,它在将r值赋给l值之前删除左操作数的int指针。像这样:

Foo& operator=(const Foo& other) 
{
    //To handle self-assignment:
    if (this != &other) {
        delete this->ptr;
        this->ptr = other.ptr;
    }
    return *this;
}

但是如果我这样做,那么当Foo aFoo b超出范围时,不要同时运行它们的析构函数,删除相同的指针两次(因为它们现在都指向同一个东西) )?

修改

如果我正确理解Anders K,这是正确的方法:

Foo& operator=(const Foo& other) 
{
    //To handle self-assignment:
    if (this != &other) {
        delete this->ptr;
        //Clones the int
        this->ptr = new int(*other.ptr);
    }
    return *this;
}

现在,a克隆了int指向的b,并设置了自己的指针。也许在这种情况下,deletenew不是必需的,因为它只涉及int,但如果数据成员不是int*而是{{1}或者诸如此类,可能需要重新分配。

编辑2: 最佳解决方案似乎是copy-and-swap idiom

3 个答案:

答案 0 :(得分:12)

这是否会泄漏内存? 不,不。

似乎大多数人都忽略了这一点。所以这里有一点澄清。

此答案中“No it not leak”的初始响应不正确,但此处建议的解决方案是唯一且最合适的解决问题。


你的困境的解决方案是:

不使用指向整数成员(int *)的指针,而只使用整数(int),这里不需要动态分配指针成员。您可以使用int作为成员来实现相同的功能 请注意,在C ++ You should use new as little as possible.

如果由于某种原因(我在代码示例中看不到)你不能没有动态分配的指针成员读取:

您需要按照 Rule of Three!

进行操作

为什么你需要遵循三法则?

三条规则陈述:

  

如果你的班级需要

     

复制构造函数
   分配运算符
  或 析构函数

     

然后可能需要 所有这三个

您的类需要一个自己的显式析构函数,因此它还需要一个显式的复制构造函数和复制赋值运算符 由于您的类的复制构造函数和复制赋值运算符是隐式,因此它们也隐式 public ,这意味着类设计允许复制或分配此类的对象。隐式生成的这些函数版本只会生成动态分配的指针成员的 浅层副本 ,这会将您的类暴露给:

  • 内存泄漏&
  • 悬空指针&
  • 双重释放的潜在未定义行为

这基本上意味着你无法使用隐式生成的版本,你需要提供自己的重载版本,这就是三级规则所说的。

显式提供的重载应该对已分配的成员进行 深层复制 ,从而防止所有问题。

如何正确实施复制赋值运算符?

在这种情况下,提供复制赋值运算符的最有效和最优化的方法是使用:
copy-and-swap Idiom
@GManNickG's着名的答案提供了足够的细节来解释它所提供的优势。


建议:

另外,使用智能指针作为类成员而不是使用显式内存管理负担的原始指针会更好。智能指针将隐式管理内存。要使用哪种智能指针取决于您的成员的生命周期所有权语义,您需要 choose an appropriate smart pointer 。要求。

答案 1 :(得分:10)

处理此问题的常规方法是创建指针所指向的对象的克隆,这就是为什么拥有赋值运算符很重要的原因。当没有定义分配运算符时,默认行为是一个memcpy,当两个析构函数都试图删除同一个对象并且内存泄漏时会导致崩溃,因为前一个值ptr指向b中的值不会被删除。

Foo a

         +-----+
a->ptr-> |     |
         +-----+

Foo b

         +-----+
b->ptr-> |     |
         +-----+

a = b

         +-----+
         |     |
         +-----+
a->ptr            
       \ +-----+
b->ptr   |     |
         +-----+

when a and b go out of scope delete will be called twice on the same object.

编辑:正如本杰明/阿尔斯正确指出上述内容只是指这个特定的例子,见下面的评论

答案 2 :(得分:1)

显示的代码具有未定义的行为。因此,如果它泄漏内存(如预期的那样)那么这只是UB的一种可能表现形式。它还可以向巴拉克奥巴马发送愤怒的威胁信,或者喷出红色(或橙色)鼻涕守护进程,或者什么都不做,或者表现得好像没有记忆泄漏,奇迹般地回收记忆,或其他什么。

解决方案:使用int*代替int,即

struct Foo
{
    Foo(int i): blah( i ) {}
    int blah;
};

int main()
{
    {
        Foo a(8);
        Foo b(7);
        a = b;
    }
    //Do other stuff
}

这更安全,更短,更有效,更清晰。

没有针对这个问题的其他解决方案,在任何客观衡量标准上胜过上述内容。