【问题标题】:C++ class destructor not being called automatically causes memory leak?未自动调用 C++ 类析构函数会导致内存泄漏?
【发布时间】:2021-06-13 13:08:04
【问题描述】:

(在有人问之前:,我没有忘记 delete[] 语句)

我在摆弄动态分配的内存时遇到了这个问题。我认为解释它的最好方法是向您展示我编写的这两段代码。它们非常相似,但其中一个没有调用我的类析构函数。

// memleak.cpp
#include <vector>

using namespace std;

class Leak {
    vector<int*> list;

  public:
    void init() {
        for (int i = 0; i < 10; i++) {
            list.push_back(new int[2] {i, i});
        }
    }

    Leak() = default;
    ~Leak() {
        for (auto &i : list) {
            delete[] i;     
        }
    }
};

int main() {
    Leak leak;
    while (true) {
        // I tried explicitly calling the destructor as well,
        // but this somehow causes the same memory to be deleted twice
        // and segfaults
        // leak.~Leak();
        leak = Leak();
        leak.init();
    }
}

// noleak.cpp
#include <vector>

using namespace std;

class Leak {
    vector<int*> list;

  public:
    Leak() {
        for (int i = 0; i < 10; i++) {
            list.push_back(new int[2] {i, i});
        }
    };
    ~Leak() {
        for (auto &i : list) {
            delete[] i;     
        }
    }
};

int main() {
    Leak leak;
    while (true) {
        leak = Leak();
    } 
}

我使用g++ filename.cpp --std=c++14 &amp;&amp; ./a.out 编译它们并使用top 检查内存使用情况。

如您所见,唯一的区别是,在memleak.cpp 中,类构造函数不做任何事情,并且有一个 init() 函数来完成它的工作。但是,如果您尝试一下,您会发现这会以某种方式干扰被调用的析构函数并导致内存泄漏。

我是否遗漏了一些明显的东西?提前致谢。

另外,在有人建议不要使用动态分配的内存之前:我知道这并不总是一个好的做法等等,但我现在感兴趣的主要事情是了解为什么我的代码不能正常工作预计。

【问题讨论】:

  • 你怎么知道析构函数被调用/没有被调用?
  • @largest_prime_is_463035818 我只是在代码中添加了一些 printf 语句
  • 为什么在这里发帖时删除它们?您的问题实际上是关于您的代码的一些输出,但是您在此处发布的代码中没有输出
  • @largest_prime_is_463035818 我删除它们只是为了使代码更易于阅读。正如我所说,您只需运行top 并查看内存使用情况即可看到存在问题

标签: c++ pointers c++14


【解决方案1】:

这是构造一个临时对象,然后分配它。由于您没有编写赋值运算符,因此您会得到一个默认值。默认的只是复制向量list

在您拥有的第一个代码中:

  1. 创建一个临时的Leak 对象。它的向量中没有指针。
  2. 将临时对象分配给leak 对象。这会复制向量(覆盖旧向量)
  3. 删除临时对象,这会删除 0 个指针,因为它的向量是空的。
  4. 分配一堆内存并将指针存储在向量中。
  5. 重复。

在第二个代码中:

  1. 创建一个临时的Leak 对象。分配一些内存并将指针存储在其向量中。
  2. 将临时对象分配给leak 对象。这会复制向量(覆盖旧向量)
  3. 删除临时对象,这将删除临时对象向量中的 10 个指针。
  4. 重复。

请注意,在leak = Leak(); 之后,在临时对象的向量中的相同指针也在leak 的向量中。即使它们被删除。

要解决此问题,您应该为您的班级写一个operator =rule of 3 是一种记住,如果你有一个析构函数,你通常还需要编写一个复制构造函数和复制赋值运算符。 (从 C++11 开始,您还可以选择编写移动构造函数和移动赋值运算符,使其成为规则 5)

您的赋值运算符将删除向量中的指针,清除向量,分配新内存以保存正在分配的对象的 int 值,将这些指针放入向量中,然后复制 int 值。这样旧的指针就被清理掉了,被分配to的对象成为被分配from的对象的副本,而不共享相同的指针。

【讨论】:

  • 这是我的第一次分析,但我认为该错误还有更多,因为分配执行的是移动,而不是副本,我认为应该将原始向量留空。另请注意,您描述的 operator = 实现不是异常安全的。
  • @Quentin move 不会将原始向量留空。原始向量之后处于有效但未指定的状态。
  • @Zereges 这让我有点惊讶,但它确实解释了观察到的行为。
  • @Zereges 其实是guaranteed to be empty
  • @super 似乎 move-constructor 和 move-assignment operator 有不同的保证......什么?
【解决方案2】:

你的班级不尊重rule of 3/5/0leak = Leak(); 中默认生成的 move-assignment 复制赋值运算符使 leak 引用临时 Leak 对象的内容,它在其生命周期结束时立即删除,留下 @ 987654328@ 带有悬空指针,稍后它将再次尝试删除。

注意:如果您的 std::vector 实现在移动时系统地清空了原始向量,但 that is not guaranteed 时,这可能会被忽视。

注 2:我在上面写的被删除的部分没有意识到,正如StoryTeller 向我指出的那样,您的类确实生成移动赋值运算符because it has a user-declared destructor。生成并使用复制赋值运算符。

使用智能指针和容器为您的类建模(std::vector&lt;std::array&lt;int, 2&gt;&gt;std::vector&lt;std::vector&lt;int&gt;&gt;std::vector&lt;std::unique_ptr&lt;int[]&gt;&gt;)。不要使用newdelete 和原始拥有指针。在您可能需要它们的极少数情况下,请务必将它们紧密封装并仔细应用上述 3/5/0 规则(包括异常处理)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-28
    • 1970-01-01
    • 2021-05-26
    • 2015-08-28
    • 2016-04-02
    • 1970-01-01
    相关资源
    最近更新 更多