为什么内存泄漏只发生在赋值运算符重载的情况下而不发生在复制构造函数中以及复制和交换习语如何解决它

Why memory leak only happens in case of assignment operator overloading but not in copy constructor and how copy and swap idiom resolves it

P.S:我是编程新手,所以请用简单的语言回答我的疑问。我找到了几个答案,但无法理解。 下面是复制构造函数和赋值运算符重载。

template <class T>
Mystack<T>::Mystack(const Mystack<T> &source)            // copy constructor
{
    input = new T[source.capacity];
    top = source.top;
    capacity = source.capacity;
    for (int i = 0; i <= source.top; i++)
    {
        input[i] = source.input[i];
    }
}


template <class T>
Mystack<T> & Mystack<T>::operator=(const Mystack<T> &source)       // assignment operator overload 
{
    input = new T[source.capacity];
    top = source.top;
    capacity = source.capacity;

    for (int i = 0; i <= source.top; i++)
    {
        input[i] = source.input[i];
    }
    return *this;
}

主要函数片段

   Mystack <int> intstack = tempstack; (copy constructor)
    Mystack <int> floatstack, temp_1;
    floatstack = temp_1;  (assignment operator)

理解:我明白我们需要复制和赋值运算符,这样我们就可以在使用堆内存的情况下进行深度复制,从而不会出现悬空指针问题当我们删除其中一个对象时。

有人可以回答下面的问题吗?

1. : 我的理解对吗?

2. :开发人员建议我在赋值运算符中存在内存泄漏。如果是,请告诉我如何做?

3. 复制构造函数与赋值运算符的代码或多或少相同,那么为什么我只有在赋值运算符的情况下才会出现内存泄漏,而在复制构造函数中却没有。

4. : 万一我真的有内存泄漏。什么神奇的复制和交换习语解决了内存泄漏问题。

P.S:它不是完整的 运行 代码。在实际代码中对象确实包含一些数据。请多多包涵!

复制构造函数的目的是使 class 的两个实例的 input 指针不会最终指向堆中的同一个缓冲区。如果他们这样做,修改一个堆栈将影响另一个堆栈,并且一个堆栈的析构函数将释放另一个堆栈的内存,从而导致释放后使用错误。

您的赋值运算符确实会导致内存泄漏,因为它不会释放之前分配给该堆栈实例的内存。因此,input 指向的缓冲区在调用析构函数时并没有最终被取消分配。这不是复制构造函数的问题,因为它仅在 class 的新实例上调用,而之前没有为其分配任何内存。为了解决这个问题,在赋值运算符的开头添加下面一行:

delete [] input;

"Is my understanding correct?"

是的,你似乎明白了。完整的原因最好用 Rule of Three 概念来描述。如果您发现自己必须实现这三个(复制构造函数、赋值操作或析构函数)中的任何一个来管理动态内存,那么您非常可能需要所有这三个(也许更多,请参阅文章)。


"I have been suggested by developers that I have memory leak in assignment operator. If yes, can some please explain me how?"

您还没有发布您的默认构造函数,但我认为它看起来像这样:

Mystack<T>::Mystack(size_t size = N)
{
    input = new T[size];
    top = 0;
    capacity = size;
}

或类似的东西。现在,让我们看看赋值运算符中发生了什么:

input = new T[source.capacity]; 

嗯,这个对象在 input 中的 old 值刚刚发生了什么?它不再是可达的,并且其中的内存不再是可回收的。泄露了。


"Copy constructor has more or less same code as assignment operator then how come I have memory leak only in case of assignment operator but not in the copy constructor function."

在您的复制构造函数的 target 中没有为 input 分配 previous 值。 IE。 input 还没有指向任何东西(怎么可能?你刚刚在创建目标对象)。因此,没有泄漏。


"In case I really have memory leak. What magic copy and swap idiom does that mem leak gets resolved."

复制交换习语使用复制构造函数创建一个临时持有的值复制,然后使用赋值运算符交换对象"guts"复制。这样做时,传出的临时对象将在其析构函数触发时销毁目标对象的原始内容,而目标对象已将临时对象的传入内容的所有权据为己有。

这提供了 多项 好处(是的,一个缺点),and is brilliantly described here。您的代码中的一个简单示例是:

template <class T>
void Mystack<T>::swap(Mystack<T>& src)
{
    std::swap(input, src.input);
    std::swap(top, src.top);
    std::swap(capacity, src.capacity);
}

你的赋值运算符变为:

template <class T>
Mystack<T> & Mystack<T>::operator=(Mystack<T> src) // NOTE by-value intentional,
                                                   // invokes copy-ctor.
{
    this->swap(src);
    return *this;
}

现在您有 一个 的复制实现(在复制构造器中)要管理。此外,如果发生任何异常,它们将在构建值副本期间发生,而不是在这里发生。这个对象被污染到不确定状态的机会减少了(好事)

如果您对我之前提到的缺点感到好奇,请考虑自赋值 (x = x;) 如何在这样的范例中发挥作用。老实说,我个人并不认为自我分配效率低下是一个缺点。如果您的代码经常出现 x = x; 之类的代码,您的设计可能一开始就有一股腐臭味。


强烈建议您阅读the article以获取有关该概念的其他信息。这是您将在余下的职业生涯中记住的那些可能会改变您的想法的事情之一。