为什么 std::string 即使在删除后也会导致 class 中的内存泄漏
why is std::string causing a memory leak in a class even after deleting
我正在构建一个使用 void* 作为存储方法的 class。我知道使用 void* 不是一个好主意。但我不知道它在编译时会有什么价值,所以我认为这是最好的解决方案。它适用于所有东西,包括 C 字符串,但不适用于 std::string。它会导致非常严重的内存泄漏。我构建了一个具有相同问题的 class 的基本模型。
#include<iostream>
#include<string>
class CLASS {
public:
void* data;
CLASS(std::string str) {
std::string* s = new std::string;
*s = str;
data = s;
}
~CLASS() {
delete data;
}
};
int main() {
std::string str = "hi";
while (true)
{
CLASS val(str);
}
}
delete data
不起作用。 delete
将调用析构函数,然后释放一个对象的存储空间,但是 void
不能被销毁,也没有大小。
std::string
的析构函数会释放它分配的内存,但这里并没有调用它。您至少需要存储 data
中保存的值的析构函数。 std::any
会为您处理这一切。
但是,请考虑使用 std::variant<T1, T2, T3, ...>
将其限制为少数已知类型。
如果分配 string
,将指针指向 void *
,然后删除 , 您将尝试删除 [=16] =] 的东西,而不是 string
的东西。例如,任何分配辅助存储的 class(超出使用 new
创建的实际对象)很可能 运行 会遇到麻烦:
#include <iostream>
class Y {
public:
Y() { std::cout << "y constructor\n"; }
~Y() { std::cout << "y destructor\n"; }
};
class X {
public:
X() { std::cout << "x constructor\n"; y = new Y(); }
~X() { delete y; std::cout << "x destructor\n"; }
private:
Y *y;
};
int main() {
X *x = new X();
delete (void*)x;
}
此代码将“有效”,因为它是合法的,但它不会按照您的预期执行:
x constructor
y constructor
体面的编译器应该警告你:
program.cpp: In function ‘int main()’:
program.cpp:19:19: warning: deleting ‘void*’ is undefined [-Wdelete-incomplete]
19 | delete (void*)x;
| ^
您应该删除分配的类型,以确保使用正确的析构函数。换句话说,摆脱上面显示的代码中的转换(这样你 delete
正确的类型)会更好:
x constructor
y constructor
y destructor
x destructor
现代 C++ 具有类型安全类型,可以为您完成繁重的工作,例如 variant
或 any
(如果您的单个 class 需要存储多种类型在 运行 时间决定)或模板(如果它可以用于 一个 类型但任何种类)。您应该调查这些作为 void *
.
的替代方案
当你 delete
一个指针时,它必须与 new
返回的类型相同(或者,一个指向基数 class 的指针,如果它有一个 virtual
析构函数).
您不能删除一个 void*
指针并期望它知道要销毁的类型。将 void*
赋值给
时,所有类型信息都已丢失
因此,您必须将 void*
指针显式类型转换回其原始类型(就像您必须访问所指向数据的内容一样)。
此外,您还需要确保遵循 Rule of 3/5/0,以便在分配和 copy/move 操作期间管理指针 属性。
试试这个:
#include <iostream>
#include <string>
class MyClass {
public:
void* data;
MyClass() {
data = nullptr;
}
MyClass(const std::string &str) {
data = new std::string(str);
}
MyClass(const MyClass &src) {
data = new std::string(*static_cast<std::string*>(src.data));
}
MyClass(MyClass &&src) {
data = src.data; src.data = nullptr;
}
~MyClass() {
delete static_cast<std::string*>(data);
}
MyClass& operator=(MyClass rhs) {
MyClass tmp(std::move(rhs));
std::swap(data, tmp.data);
return *this;
}
};
int main() {
std::string str = "hi";
while (true)
{
MyClass val1(str);
MyClass val2(val1);
MyClass val3(std::move(val2));
MyClass val4;
val4 = val3;
val4 = std::move(val3);
}
}
当您开始引入更多类型供 void*
指向时,这会变得有点复杂。那么你需要一种方法来识别实际指向的是什么类型,例如:
#include <iostream>
#include <string>
enum MyClassType { mctNull, mctString, mctInteger, ... };
class MyClass {
public:
void* data;
MyClassType dataType;
MyClass() {
dataType = mctNull;
data = nullptr;
}
MyClass(const std::string &value) {
dataType = mctString;
data = new std::string(value);
}
MyClass(int value) {
dataType = mctInteger;
data = new int(value);
}
...
MyClass(const MyClass &src) {
dataType = src.dataType;
switch (src.dataType) {
case mctNull:
data = nullptr;
break;
case mctString:
data = new std::string(*static_cast<std::string*>(src.data));
break;
case mctInteger:
data = new int(*static_cast<int*>(src.data));
break;
...
}
}
MyClass(MyClass &&src) {
dataType = src.dataType; src.dataType = mctNull;
data = src.data; src.data = nullptr;
}
~MyClass() {
switch (dataType) {
case mctString:
delete static_cast<std::string*>(data);
break;
case mctInteger:
delete static_cast<int*>(data);
break;
...
}
}
MyClass& operator=(MyClass rhs) {
MyClass tmp(std::move(rhs));
std::swap(data, tmp.data);
std::swap(dataType, tmp.dataType);
return *this;
}
};
int main() {
std::string str = "hi";
int i = 12345;
int counter;
while (true)
{
MyClass val1 = (counter++ % 2 == 0) ? MyClass(str) : MyClass(i);
MyClass val2(val1);
MyClass val3(std::move(val2));
MyClass val4;
val4 = val3;
val4 = std::move(val3);
}
}
幸运的是,现代 C++ 提供了 std::variant
和 std::any
,因此您根本不必手动管理这些东西,例如:
#include <iostream>
#include <string>
#include <variant>
class MyClass {
public:
std::variant<std::string, int> data;
MyClass() = default;
MyClass(const std::string &value) { data = value; }
MyClass(int value) { data = value; }
...
};
int main() {
std::string str = "hi";
int i = 12345;
int counter;
while (true)
{
MyClass val1 = (counter++ % 2 == 0) ? MyClass(str) : MyClass(i);
MyClass val2(val1);
MyClass val3(std::move(val2));
MyClass val4;
val4 = val3;
val4 = std::move(val3);
}
}
您可以免费获得所有复制、移动和销毁逻辑,它实际上为您做了正确的事情。
我正在构建一个使用 void* 作为存储方法的 class。我知道使用 void* 不是一个好主意。但我不知道它在编译时会有什么价值,所以我认为这是最好的解决方案。它适用于所有东西,包括 C 字符串,但不适用于 std::string。它会导致非常严重的内存泄漏。我构建了一个具有相同问题的 class 的基本模型。
#include<iostream>
#include<string>
class CLASS {
public:
void* data;
CLASS(std::string str) {
std::string* s = new std::string;
*s = str;
data = s;
}
~CLASS() {
delete data;
}
};
int main() {
std::string str = "hi";
while (true)
{
CLASS val(str);
}
}
delete data
不起作用。 delete
将调用析构函数,然后释放一个对象的存储空间,但是 void
不能被销毁,也没有大小。
std::string
的析构函数会释放它分配的内存,但这里并没有调用它。您至少需要存储 data
中保存的值的析构函数。 std::any
会为您处理这一切。
但是,请考虑使用 std::variant<T1, T2, T3, ...>
将其限制为少数已知类型。
如果分配 string
,将指针指向 void *
,然后删除 , 您将尝试删除 [=16] =] 的东西,而不是 string
的东西。例如,任何分配辅助存储的 class(超出使用 new
创建的实际对象)很可能 运行 会遇到麻烦:
#include <iostream>
class Y {
public:
Y() { std::cout << "y constructor\n"; }
~Y() { std::cout << "y destructor\n"; }
};
class X {
public:
X() { std::cout << "x constructor\n"; y = new Y(); }
~X() { delete y; std::cout << "x destructor\n"; }
private:
Y *y;
};
int main() {
X *x = new X();
delete (void*)x;
}
此代码将“有效”,因为它是合法的,但它不会按照您的预期执行:
x constructor
y constructor
体面的编译器应该警告你:
program.cpp: In function ‘int main()’:
program.cpp:19:19: warning: deleting ‘void*’ is undefined [-Wdelete-incomplete]
19 | delete (void*)x;
| ^
您应该删除分配的类型,以确保使用正确的析构函数。换句话说,摆脱上面显示的代码中的转换(这样你 delete
正确的类型)会更好:
x constructor
y constructor
y destructor
x destructor
现代 C++ 具有类型安全类型,可以为您完成繁重的工作,例如 variant
或 any
(如果您的单个 class 需要存储多种类型在 运行 时间决定)或模板(如果它可以用于 一个 类型但任何种类)。您应该调查这些作为 void *
.
当你 delete
一个指针时,它必须与 new
返回的类型相同(或者,一个指向基数 class 的指针,如果它有一个 virtual
析构函数).
您不能删除一个 void*
指针并期望它知道要销毁的类型。将 void*
赋值给
因此,您必须将 void*
指针显式类型转换回其原始类型(就像您必须访问所指向数据的内容一样)。
此外,您还需要确保遵循 Rule of 3/5/0,以便在分配和 copy/move 操作期间管理指针 属性。
试试这个:
#include <iostream>
#include <string>
class MyClass {
public:
void* data;
MyClass() {
data = nullptr;
}
MyClass(const std::string &str) {
data = new std::string(str);
}
MyClass(const MyClass &src) {
data = new std::string(*static_cast<std::string*>(src.data));
}
MyClass(MyClass &&src) {
data = src.data; src.data = nullptr;
}
~MyClass() {
delete static_cast<std::string*>(data);
}
MyClass& operator=(MyClass rhs) {
MyClass tmp(std::move(rhs));
std::swap(data, tmp.data);
return *this;
}
};
int main() {
std::string str = "hi";
while (true)
{
MyClass val1(str);
MyClass val2(val1);
MyClass val3(std::move(val2));
MyClass val4;
val4 = val3;
val4 = std::move(val3);
}
}
当您开始引入更多类型供 void*
指向时,这会变得有点复杂。那么你需要一种方法来识别实际指向的是什么类型,例如:
#include <iostream>
#include <string>
enum MyClassType { mctNull, mctString, mctInteger, ... };
class MyClass {
public:
void* data;
MyClassType dataType;
MyClass() {
dataType = mctNull;
data = nullptr;
}
MyClass(const std::string &value) {
dataType = mctString;
data = new std::string(value);
}
MyClass(int value) {
dataType = mctInteger;
data = new int(value);
}
...
MyClass(const MyClass &src) {
dataType = src.dataType;
switch (src.dataType) {
case mctNull:
data = nullptr;
break;
case mctString:
data = new std::string(*static_cast<std::string*>(src.data));
break;
case mctInteger:
data = new int(*static_cast<int*>(src.data));
break;
...
}
}
MyClass(MyClass &&src) {
dataType = src.dataType; src.dataType = mctNull;
data = src.data; src.data = nullptr;
}
~MyClass() {
switch (dataType) {
case mctString:
delete static_cast<std::string*>(data);
break;
case mctInteger:
delete static_cast<int*>(data);
break;
...
}
}
MyClass& operator=(MyClass rhs) {
MyClass tmp(std::move(rhs));
std::swap(data, tmp.data);
std::swap(dataType, tmp.dataType);
return *this;
}
};
int main() {
std::string str = "hi";
int i = 12345;
int counter;
while (true)
{
MyClass val1 = (counter++ % 2 == 0) ? MyClass(str) : MyClass(i);
MyClass val2(val1);
MyClass val3(std::move(val2));
MyClass val4;
val4 = val3;
val4 = std::move(val3);
}
}
幸运的是,现代 C++ 提供了 std::variant
和 std::any
,因此您根本不必手动管理这些东西,例如:
#include <iostream>
#include <string>
#include <variant>
class MyClass {
public:
std::variant<std::string, int> data;
MyClass() = default;
MyClass(const std::string &value) { data = value; }
MyClass(int value) { data = value; }
...
};
int main() {
std::string str = "hi";
int i = 12345;
int counter;
while (true)
{
MyClass val1 = (counter++ % 2 == 0) ? MyClass(str) : MyClass(i);
MyClass val2(val1);
MyClass val3(std::move(val2));
MyClass val4;
val4 = val3;
val4 = std::move(val3);
}
}
您可以免费获得所有复制、移动和销毁逻辑,它实际上为您做了正确的事情。