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

Does this C++ code leak memory?

本文关键字:内存 泄漏 C++ 代码      更新时间:2023-10-16
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 编写了一个赋值运算符,该运算符在将 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克隆了b指向的int,并设置了自己的指针。也许在这种情况下,deletenew不是必需的,因为它只涉及int,但如果数据成员不是int*而是Bar*或其他什么,则可能需要重新分配。

编辑 2:最好的解决方案似乎是复制和交换成语。

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

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

此答案中"不,它不会泄漏"的最初响应是不正确的,但此处建议的解决方案是解决问题的唯一和最合适的解决方案。


解决你的困境的办法是:

不使用指向整数成员(int *)的指针,而是只使用整数(int),这里实际上并不需要动态分配的指针成员。您可以使用int作为成员来实现相同的功能。
请注意,在C++中,您应该尽可能少地使用new

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

你需要遵循三法则!


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

三法则指出:

如果您的班级需要

复制构造函数
赋值运算符
析构函数

那么很可能需要这三个。

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

  • Memory Leaks &
  • 悬空指针和
  • 双重释放的潜在未定义行为

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

显式提供的重载应该创建已分配成员的深层副本,从而防止所有问题。

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

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


建议:

此外,最好使用智能指针作为类成员,而不是原始指针,这会增加显式内存管理的负担。智能指针将隐式为您管理内存。使用哪种智能指针取决于适用于成员的生存期所有权语义,您需要根据需要选择合适的智能指针

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

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

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

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

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