19
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. 但是,这只是取ptrin的值b并将其放入a。最初分配的内存a似乎丢失了。我可以a.~Foo();在分配之前打个电话,但我听说你应该很少需要显式调用析构函数。因此,假设我编写了一个赋值运算符,用于在将 r 值分配给 l 值之前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克隆指向的int那个,并设置它自己的指针。b也许在这种情况下,deleteandnew不是必需的,因为它只涉及ints,但如果数据成员不是 anint*而是 aBar*或诸如此类,则可能需要重新分配。

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

4

3 回答 3

12

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

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

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


解决您的困境的方法是:

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

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

你需要遵守三法则!


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

法则规定:

如果您的班级需要

复制构造函数、赋值
运算
构函数

那么很可能需要他们三个

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

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

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

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

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

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


建议:

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

于 2012-05-26T05:01:50.367 回答
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.

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

于 2012-05-26T05:01:43.130 回答
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
}

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

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

于 2012-05-26T08:24:39.417 回答