问题描述
我有一个包含字符串的结构,并且该结构用于向量中。当向量增长时,所有元素都被移动到新的分配中。不幸的是,这一举动也会导致 std::string 内存泄漏。
以下是一些可重现的最低限度的案例。第一个示例将说明内存泄漏发生的位置,但可以理解它。第二个示例将涵盖困扰我的内存泄漏。第三个将更进一步。最后,我将展示实际用例来演示我什至在做什么以及我为什么要问这个问题。
int main(void)
{
char* allocation = new char[sizeof(std::string)];
std::string start("start");
std::string* move = (std::string*)allocation;
new (move) std::string(std::move(start));
delete[] allocation;
}
不出所料,这会导致内存泄漏。构造 start
字符串并应为数据分配内存。然后将该字符串移入 move
并在程序的其余部分保留在那里。因为它已被移动,move
的析构函数将不会被调用,这将导致如下所示的内存泄漏。我创建字符数组的原因是为了避免对 move
字符串的默认构造函数和析构函数调用。
Detected memory leaks!
Dumping objects ->
{207} normal block at 0x00000278D16848C0,16 bytes long.
Data: < g x > D0 D7 67 D1 78 02 00 00 00 00 00 00 00 00 00 00
Object dump complete.
这里需要注意的一个有趣的事情是数据 < g x >
不包含用于初始化 start
的字符串,这可能表明此泄漏不是来自字符串的缓冲区。>
现在我们将在前面的示例中添加一行以尝试消除内存泄漏。
int main(void)
{
char* allocation = new char[sizeof(std::string)];
std::string start("start");
std::string* move = (std::string*)allocation;
new (move) std::string(std::move(start));
start = std::move(*move); // The new line.
delete[] allocation;
}
现在,该字符串不再保留在 move
中直到程序结束,而是移回 start
并且由于应该调用 start's
析构函数,因此字符串的分配应该是获释。但是,当我运行它时,我仍然遇到类似的内存泄漏。
Detected memory leaks!
Dumping objects ->
{207} normal block at 0x000001AE813FECC0,16 bytes long.
Data: < @ > 80 15 40 81 AE 01 00 00 00 00 00 00 00 00 00 00
Object dump complete.
我还尝试使用单个字符串分配而不是分配字符数组进行此测试,但使用下面的代码仍然遇到类似的内存泄漏。
int main(void)
{
std::string start("start");
std::string* move = new std::string;
new (move) std::string(std::move(start));
start = std::move(*move);
delete move;
}
我做这一切的原因是因为我正在编写自己的矢量版本。当我的向量增长,并且向量中包含的类型包含一个字符串时,我在上面突出显示的内存泄漏就会发生。这就是我的增长功能的样子。 Util::Move
是我自己的 std::move
版本。我这样做是为了好奇心、学习和娱乐目的。
template<typename T>
void Vector<T>::Grow(int newCapacity)
{
LogAbortIf(
newCapacity <= mCapacity,"The new capacity must be greater than the current capacity.");
T* oldData = mData;
mData = Createallocation(newCapacity);
for (int i = 0; i < mSize; ++i)
{
new (mData + i) T(Util::Move(oldData[i]));
}
mCapacity = newCapacity;
Deleteallocation(oldData);
}
// Using this struct in the vector would cause a leak.
struct Example
{
std::string mString;
};
为什么会发生这种内存泄漏?是否有一些关于 std::string 的东西我在这里遗漏了,或者我使用了新的放置方式是罪魁祸首?
附加说明: Memory leak in placement new of standard library string - 这个问题只涉及我的第一个例子。它没有解释或涵盖最后两个。
解决方法
因为已经被移动了,所以不会调用move的析构函数
不,这是错误的。将不会调用名为 *move
的对象的析构函数,因为 move
是一个原始指针。
如果它被自动调用,它会在你删除底层分配之后发生,所以无论如何它都会导致 UB。这段代码完全没有意义。
我创建字符数组的原因是为了避免对移动字符串的构造函数和析构函数调用
你明确地调用了placement new,所以这也是无稽之谈。故意调用移动构造函数如何避免构造函数?
您唯一避免的是析构函数,这是一个错误而不是优化。
...这意味着应该调用 start
的析构函数并且应该释放字符串的分配...
start
的析构函数将无论你做什么都会被调用,因为它是一个自动的局部变量。它也在第一个示例中被调用。但是,您假设移动 from *move
意味着可以不调用其析构函数。这不是真的,它仍然是一个错误(即使它没有泄漏也仍然是一个错误)。
您的最终测试动态分配(并默认构造)一个字符串,然后在其存储上放置新的另一个字符串,而不首先销毁它。这也是废话。
您的问题依次是:
-
您尚未展示泄漏的真实代码,因此我们无法真正解释该泄漏。你所有的例子都是错误的,所以我们只能推测你的真实代码可能以同样的方式出错。
-
您正在尝试优化可能无法从中受益的内容。许多库实现将对五个字符的字符串使用短字符串优化 - 找出您的是否是其中之一。
就此而言,请不要忘记在开始优化代码之前对其进行概要分析。
-
您正在尝试使用一种您不了解的技术来优化可能不需要它的东西。
您需要了解对象生命周期规则,并且当构造函数和析构函数被调用时,之前您开始尝试使用placement new。手动控制对象和存储生命周期是相当先进的,除非您首先了解默认行为,否则您无法正确执行此操作。
通常的学习实验是编写一个玩具类,其中实现了所有构造函数、赋值运算符和析构函数,并且都打印了一些东西。然后,您可以轻松地看到在这些示例中将
std::string
替换为您的类会发生什么。
从对象移动并不意味着不需要调用析构函数。 任何构造的对象都需要销毁:
{
std::string s;
std::move(s);
} // s’ destructor is run here!
对于您通过placement-new(地址move
)显式构造的代码中的对象也是如此。问题是,由于它驻留在动态分配的内存中,它的析构函数不会自动运行,并且当您删除作为内存底层的 char
数组 时,您告诉 C++ 不要考虑std::string
对象要占用的该存储位置的字节数。
简而言之,解决方案是在释放存储之前手动运行 std::string
析构函数:
move->std::string::~string();
delete[] allocation;
为每个新展示位置执行此操作。