C++ class 未自动调用析构函数导致内存泄漏?
C++ class destructor not being called automatically causes memory leak?
(之前有人问:没有,我没忘记delete[]语句)
我正在摆弄动态分配的内存,我 运行 陷入了这个问题。我认为解释它的最好方法是向您展示我编写的这两段代码。它们非常相似,但其中之一没有调用我的 class 析构函数。
// 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 && ./a.out
编译了它们,并用 top
检查内存使用情况。
如您所见,唯一的区别是,在 memleak.cpp
中,class 构造函数不执行任何操作,并且有一个 init() 函数执行其工作。但是,如果您尝试这样做,您会发现这会以某种方式干扰正在调用的析构函数并导致内存泄漏。
我是不是遗漏了什么明显的东西?提前致谢。
此外,在有人建议不使用动态分配的内存之前:我知道这并不总是一个好的做法等等,但我现在感兴趣的主要事情是理解为什么我的代码不按预期工作。
您的 class 不尊重 rule of 3/5/0。 leak = Leak();
中默认生成的 移动赋值 复制赋值运算符使 leak
引用临时 Leak
对象的内容,它会立即删除在其生命周期结束时,留下 leak
悬空指针,稍后将尝试再次删除。
注意:如果您的 std::vector
的实现在移动时系统地清空了原始向量,但是 that is not guaranteed.
注2:上面删掉的部分是我自己写的,没注意,如 pointed out to me, your class does not generate a move-assignment operator because it has a user-declared destructor。生成并使用复制赋值运算符。
使用智能指针和容器为您的 classes(std::vector<std::array<int, 2>>
、std::vector<std::vector<int>>
或 std::vector<std::unique_ptr<int[]>>
)建模。不要使用 new
、delete
和原始拥有指针。在您可能需要它们的极少数情况下,请务必将它们紧密封装并仔细应用上述 3/5/0 规则(包括异常处理)。
这是构造一个临时对象,然后赋值给它。由于您没有编写赋值运算符,因此您得到了一个默认值。默认的只是复制向量 list
.
在您的第一个代码中:
- 创建一个临时
Leak
对象。它的向量中没有指针。
- 将临时对象分配给
leak
对象。这会复制矢量(覆盖旧的)
- 删除临时对象,这会删除 0 个指针,因为它的向量是空的。
- 分配一堆内存并将指针存储在向量中。
- 重复。
在第二个代码中你有:
- 创建一个临时
Leak
对象。分配一些内存并将指针存储在其向量中。
- 将临时对象分配给
leak
对象。这会复制矢量(覆盖旧的)
- 删除临时对象,这将删除临时对象向量中的 10 个指针。
- 重复。
请注意,在 leak = Leak();
之后,临时对象向量中的相同指针也在 leak
的向量中。即使他们被删除了。
要解决这个问题,您应该为您的 class 写一个 operator =
。 rule of 3 是一种记住的方法,如果你有一个析构函数,你 通常 还需要编写一个复制构造函数和复制赋值运算符。 (从 C++11 开始,您还可以选择编写移动构造函数和移动赋值运算符,使其成为 5 的规则)
您的赋值运算符将删除向量中的指针、清除向量、分配新内存以保存正在分配的对象中的 int 值、将这些指针放入向量中并复制 int 值。这样旧的指针就被清理掉了,被赋值给to的对象变成了被赋值对象from的副本,而不共享相同的指针.
(之前有人问:没有,我没忘记delete[]语句)
我正在摆弄动态分配的内存,我 运行 陷入了这个问题。我认为解释它的最好方法是向您展示我编写的这两段代码。它们非常相似,但其中之一没有调用我的 class 析构函数。
// 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 && ./a.out
编译了它们,并用 top
检查内存使用情况。
如您所见,唯一的区别是,在 memleak.cpp
中,class 构造函数不执行任何操作,并且有一个 init() 函数执行其工作。但是,如果您尝试这样做,您会发现这会以某种方式干扰正在调用的析构函数并导致内存泄漏。
我是不是遗漏了什么明显的东西?提前致谢。
此外,在有人建议不使用动态分配的内存之前:我知道这并不总是一个好的做法等等,但我现在感兴趣的主要事情是理解为什么我的代码不按预期工作。
您的 class 不尊重 rule of 3/5/0。 leak = Leak();
中默认生成的 移动赋值 复制赋值运算符使 leak
引用临时 Leak
对象的内容,它会立即删除在其生命周期结束时,留下 leak
悬空指针,稍后将尝试再次删除。
注意:如果您的 std::vector
的实现在移动时系统地清空了原始向量,但是 that is not guaranteed.
注2:上面删掉的部分是我自己写的,没注意,如
使用智能指针和容器为您的 classes(std::vector<std::array<int, 2>>
、std::vector<std::vector<int>>
或 std::vector<std::unique_ptr<int[]>>
)建模。不要使用 new
、delete
和原始拥有指针。在您可能需要它们的极少数情况下,请务必将它们紧密封装并仔细应用上述 3/5/0 规则(包括异常处理)。
这是构造一个临时对象,然后赋值给它。由于您没有编写赋值运算符,因此您得到了一个默认值。默认的只是复制向量 list
.
在您的第一个代码中:
- 创建一个临时
Leak
对象。它的向量中没有指针。 - 将临时对象分配给
leak
对象。这会复制矢量(覆盖旧的) - 删除临时对象,这会删除 0 个指针,因为它的向量是空的。
- 分配一堆内存并将指针存储在向量中。
- 重复。
在第二个代码中你有:
- 创建一个临时
Leak
对象。分配一些内存并将指针存储在其向量中。 - 将临时对象分配给
leak
对象。这会复制矢量(覆盖旧的) - 删除临时对象,这将删除临时对象向量中的 10 个指针。
- 重复。
请注意,在 leak = Leak();
之后,临时对象向量中的相同指针也在 leak
的向量中。即使他们被删除了。
要解决这个问题,您应该为您的 class 写一个 operator =
。 rule of 3 是一种记住的方法,如果你有一个析构函数,你 通常 还需要编写一个复制构造函数和复制赋值运算符。 (从 C++11 开始,您还可以选择编写移动构造函数和移动赋值运算符,使其成为 5 的规则)
您的赋值运算符将删除向量中的指针、清除向量、分配新内存以保存正在分配的对象中的 int 值、将这些指针放入向量中并复制 int 值。这样旧的指针就被清理掉了,被赋值给to的对象变成了被赋值对象from的副本,而不共享相同的指针.