这个 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
并将其放入a
。 a
分配的内存原本似乎丢失了。我可以在分配之前拨打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 a
和Foo 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
,并设置了自己的指针指向它。也许在这种情况下,delete
和new
不是必需的,因为它只涉及int
s,但是如果数据成员不是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 著名的答案提供了足够的细节来解释优势它提供。
建议:
此外,您最好使用 智能指针 作为类成员,而不是使用显式内存管理负担的原始指针。智能指针将为您隐式管理内存。使用什么样的智能指针取决于为您的成员设计的 lifetime 和 ownership 语义,您需要根据您的要求 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++ 代码会泄漏内存吗?的主要内容,如果未能解决你的问题,请参考以下文章