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

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

本文关键字:复制 习惯 惯用法 交换 何解决 解决 构造函数 赋值运算符 重载 泄漏 情况下      更新时间:2023-10-16

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. :万一我真的有内存泄漏。什么魔术复制和交换成语解决了内存泄漏。

PS:它不是完整的运行代码。在实际代码中,对象确实包含一些数据。请熊!

"我的理解正确吗?"

是的,你似乎明白。全部原因最好用三法则概念来描述。如果您发现自己必须实现这三者中的任何一个(copy-ctor、assignment op 或析构函数)来管理动态内存,那么您很可能需要这三者(也许更多,请参阅文章)。


"开发人员建议我在赋值运算符中存在内存泄漏。如果是,有人可以解释一下我怎么做吗?

您尚未发布默认构造函数,但我认为它看起来像这样:

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

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

input = new T[source.capacity]; 

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


"复制构造函数或

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

复制 ctor 的复制构造目标中没有先前分配的input值。 即 input还没有指向任何东西(怎么可能?你现在正在创建目标对象)。因此,没有泄漏。


"万一我真的有记忆泄漏。什么神奇的复制和交换成语解决了内存泄漏。

复制交换习惯用法使用 copy-constructor 创建临时持有的值副本,然后使用赋值运算符将对象"guts"与该副本交换。 这样做时,传出的临时对象将在其析构函数触发时销毁目标对象的原始内容,而目标对象已将临时对象的传入内容的所有权作为自己的内容。

这提供了多种好处(是的,一个缺点),这里对此进行了精彩的描述。代码中的一个简单的示例是:

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;
}

现在,您有一个复制实现(在复制ctor中)要管理。此外,如果发生任何异常,他们将在构建价值副本期间这样做,而不是在这里。该物体被污染到不确定状态的机会减少(一件好事)

如果你对我之前提到的缺点感到好奇,请考虑一下自我分配(x = x;)在这样的范式中会如何发挥作用。老实说,我个人并不认为自我分配效率低下是一个缺点。如果你的代码经常出现在x = x;这样的代码中,那么你的设计一开始就有一种腐烂的气味。


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

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

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

delete [] input;