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

Posted

技术标签:

【中文标题】这个 C++ 代码会泄漏内存吗?【英文标题】:Does this C++ code leak memory? 【发布时间】:2012-06-01 14:00:47 【问题描述】:
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 并将其放入aa 分配的内存原本似乎丢失了。我可以在分配之前拨打a.~Foo();,但我听说你应该很少需要显式调用析构函数。因此,假设我为Foo 编写了一个赋值运算符,它在将右值分配给左值之前删除了左操作数的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 克隆了b 指向的int,并设置了自己的指针指向它。也许在这种情况下,deletenew 不是必需的,因为它只涉及ints,但是如果数据成员不是int* 而是Bar* 或诸如此类,则可能需要重新分配.

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

【问题讨论】:

我不知道答案。您是否考虑过使用 valgrind 或其他工具对其进行测试? 这不会编译,因为您尝试使用默认构造函数,但没有。 @BenjaminLindley 对此感到抱歉,已编辑。 您已经正确回答了自己的问题。在第一种情况下,您对内存泄漏是正确的。在第二种情况下,您对双重删除是正确的(这是未定义的行为)。 赋值运算符可以而且应该使用更安全、更高效的复制和交换习语来实现。 【参考方案1】:

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

似乎大多数人都错过了这里的重点。所以这里有一点澄清。

“不,它不会泄漏” 在此答案中的初始响应是不正确的,但此处建议的解决方案 过去和现在 是唯一且最合适的解决方案解决问题。


解决问题的方法是:

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

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

您需要关注Rule of Three!


为什么需要遵循三法则?

三法则规定:

如果你的班级需要任何一个

一个复制构造函数赋值运算符, 或析构函数

那么它很可能需要他们三个

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

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

这基本上意味着您无法使用隐式生成的版本,您需要提供自己的重载版本,这就是三法则的开头。

显式提供的重载应该对分配的成员进行深拷贝,从而避免所有问题。

如何正确实现 Copy 赋值运算符?

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


建议:

此外,您最好使用 智能指针 作为类成员,而不是使用显式内存管理负担的原始指针。智能指针将为您隐式管理内存。使用什么样的智能指针取决于为您的成员设计的 lifetimeownership 语义,您需要根据您的要求 choose an appropriate smart pointer

【讨论】:

这是错误的。链接的示例确实确实泄漏了内存(您可以使用 valgrind 进行检查,正如其他人所建议的那样)并在剩余的指针上执行 double-free 以启动。 @AndyRoss:好的,是的,但它仍然没有改变答案建议的解决方案。答案是错误的(疏忽)说 No memory Leak ,但是它比任何答案都更正确。我没有过多注意它泄漏内存的事实,因为我注意到的第一件事是程序不遵循 三规则 并且从那里开始,它几乎打开了一罐蠕虫。 我使用int* 而不是int 并忽略智能指针/三规则只是为了更好地理解这些概念。我接受这个答案主要是因为链接的复制和交换习语。 任何关于为什么这仍然被否决的推理都会很有启发性。你觉得哪里不对? @newprogrammer:我没有在霍格沃茨学习,我也不是巫师,也没有魔杖可以猜测和读懂你的想法,知道里面有什么,而不是在 Q 中表达出来。所以要么你Q 应该清楚,或者准备好听到合乎逻辑且正确的答案(也许)你可能知道但你没有告诉我们你知道。话虽如此,我们鼓励这里的答案完整,以便他们不仅可以帮助 OP,还可以帮助将来偶然发现的任何其他人,因为必须编写包含必要细节的答案。【参考方案2】:

处理这个问题的正常方法是创建指针指向的对象的克隆,这就是为什么有一个赋值运算符很重要的原因。当没有定义赋值运算符时,默认行为是 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.

编辑:正如 Benjamin/Als 正确指出的那样,上面只是指这个特定的例子,见下面的 cmets

【讨论】:

"当没有定义赋值运算符时,默认行为是 memcpy" -- 不,默认行为是递归调用所有子对象的赋值运算符。对于 POD,这相当于 memcpy,是的。也许您不是泛泛而谈,仅指的是这个具体案例,但我认为从您的措辞方式来看并不明显。 @BenjaminLindley 感谢您的澄清。 为了更清楚,行为在 C++03,12.8 中定义:“- 如果子对象是类类型,则使用该类的复制赋值运算符(如如果通过显式限定;也就是说,忽略更多派生类中的任何可能的虚拟覆盖函数); - 如果子对象是数组,则以适合元素类型的方式分配每个元素; - 如果子对象是标量类型, 使用内置的赋值运算符。" 这个答案不完整。OP还需要有自己的复制构造函数。他需要遵循三法则,只需提供重载的复制赋值运算符即可用于此示例,但由于复制构造函数隐式为public,该类允许复制其对象,一旦发生这种情况,OP 就会出现同样的问题。在此示例中不会发生复制,但类设计允许这样做(否则复制构造函数必须是明确标记private)。唯一正确的解决方案是遵循三法则【参考方案3】:

呈现的代码具有未定义的行为。因此,如果它泄漏内存(如预期的那样),那么这只是 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

这样更安全、更短、更高效、更清晰。

没有其他解决方案针对这个问题提出,在任何客观衡量标准上都优于上述解决方案。

【讨论】:

+1 双倍免费发送愤怒的信件。但是,-1 错过了 OP 问题的重点,即(OP 认为)需要一些手动清理的资源。如果您明确介绍了 RAII 的概念,以及我们为什么要实现它,那么您肯定会赚到钱。 +1 以补偿愚蠢的downvote。OP 应该清楚他们试图在 OP 中做什么,而不是在发布答案后表达他们的想法。这不是霍格沃茨,也没有拿着魔杖猜测和读心的巫师。

以上是关于这个 C++ 代码会泄漏内存吗?的主要内容,如果未能解决你的问题,请参考以下文章

这个 Qt 代码会泄漏内存吗?

为啥使用“新”会导致内存泄漏?

C++ 中的 new/delete 导致奇怪的内存泄漏

为啥.NET 没有内存泄漏?

c++内存泄漏实战

C++:内存泄漏