Valgrind是否有问题,还是这确实是std map迭代器内存泄漏?

5

我对Valgrind和内存泄漏分析工具还很陌生。但我必须说,当你开始使用它们时,会感到有些害怕,因为你不停地想着自己有多少未解决的泄漏问题!

言归正传,由于我不是一名经验丰富的C++程序员,我想确认一下,这确实是内存泄漏,还是Valgrind出现了误报?

typedef std::vector<int> Vector;
typedef std::vector<Vector> VectorVector;
typedef std::map<std::string, Vector*> MapVector;
typedef std::pair<std::string, Vector*> PairVector;
typedef std::map<std::string, Vector*>::iterator IteratorVector;

VectorVector vv;
MapVector m1;
MapVector m2;

vv.push_back(Vector());
m1.insert(PairVector("one", &vv.back()));

vv.push_back(Vector());
m2.insert(PairVector("two", &vv.back()));

IteratorVector i = m1.find("one");
i->second->push_back(10);
m2.insert(PairVector("one", i->second));

m2.clear();
m1.clear();
vv.clear();

为什么会这样?难道清空命令不应该调用每个对象和向量的析构函数吗?
现在经过一些测试,我找到了不同的解决方案来解决泄漏问题:
1)删除:
i->second->push_back(10);

2) 添加:

delete i->second;

3)删除第二个

vv.push_back(Vector());
m2.insert(PairVector("two", &vv.back()));

使用解决方案2会使Valgrind打印:10个分配,11个释放。这样可以吗?
既然我没有使用new,为什么要删除呢?
感谢任何帮助!

不要使用块引用格式化代码,使用101010图标(或Ctrl+K)。 - Marcelo Cantos
1
您对typedef的使用使得代码对我来说变得难以理解。 - anon
@Marcelo:我认为代码现在很好了。 @Neil,我认为现在它相当易懂,唯一复杂的部分是我有一个向量的向量,和一个指向简单向量的映射... - Alberto Toglia
1
@Alberto Toglia:为什么用社区百科? - Gorpik
1
@Gorpik,因为问题已经被编辑超过5次。它会自动变成CW(社区维基)。 - Michael Kristofik
@Kristo:谢谢。在我提问后,我看到了大量的编辑。我想我们都试图同时进行相同的编辑,因为原始格式很糟糕。 - Gorpik
3个回答

2
基本上,这行代码导致了问题:
i->second->push_back(10);

这是因为当你执行以下操作时,i->second可能已经变得无效了:
vv.push_back(Vector());

第二次。

不需要调用clear。当vv对象超出范围时,它将正确销毁所有对象。而且所有映射都不拥有任何向量,因此它们的析构函数不会影响它们指向的向量。因此,您不需要使用clear。

如果您想保持相同的解决方案,请为您的vv对象创建一个向量列表。然后插入列表不会影响已存在的成员,您的映射将正常工作。

std::list<Vector> vv;  // insertion into this will not invalidate any other members.
                       // Thus any pointers to members you have will not become invalidated.

个人认为您在过度复杂化事情。
我认为您可以通过大幅简化来实现相同的结果。
如果向量没有被多个地图元素引用,则将该向量放入地图中即可。

std::map<std::string, std::vector<int> >    m1;

m1["one"].push_back(10);
m1["two"].push_back(20);

1

这里存在未定义的行为:

m1.insert(PairVector("one", &vv.back()));

vv.push_back(Vector());

插入无效的迭代器和指向向量的引用,这意味着您在映射中存储的指针基本上在插入后指向某个黑洞。

这会让Valgring打印:10个分配,11个释放。这样可以吗?

这很奇怪,它不会打印有关双重释放的内容吗?

对于解决方案,我建议使用与vector不同的容器(例如listdeque,其变异函数使迭代器无效,但不会使引用无效)。或者,您可以将指向向量中数据的指针(最好是智能指针,但也可以是普通指针)存储在映射中,以便实际数据的地址是稳定的。


它确实说了一些关于无效删除的事情,尽管我不知道如何摆脱调试调用,所以我只能看到错误。从现在开始,我将使用Valgrind,因此我希望在接下来的几天里变得更加熟悉它... - Alberto Toglia
2
在你引用的那些行中,它还不是未定义的行为。只要你不对其进行解引用,拥有一个无效指针是完全合法的。只有在 i->second->push_back(10) 这一行上,实际解引用了向量指针,才会变成未定义的行为。 - jalf

0

在这里,您正在使用一些危险的向量操作。您保存了可能在程序执行期间无效的向量指针。

std::vector<> ::push_back()如果已满,则可能使任何对std::vector<> 的迭代器或引用无效。由于std::vector<>保证其内容将被存储连续(因此可以将其用作数组),当它需要更多内存时,它必须将自身复制到不同的存储块中,原始块将无效。

这意味着您代码中所有对push_back()的调用(除第一个外)都会导致未定义的行为,因此可能发生任何事情。


@Gorpik,感谢你的建议。但是告诉我一些东西,我使用映射和向量的原因是,通过向量,我可以保证我的对象在内存中排列得很好,而映射则用于查找具有特定名称的对象。显然,这不是为int类型而设计的,而是为大型游戏对象...如果我小心处理停滞指针,这对你有意义吗? - Alberto Toglia
等一下,我现在明白了,向量可以在执行时将我的对象内存移动到不同的位置,而且还有其他的东西,这样会使我的地图变得无用。所以我觉得最好还是使用一个简单的数组来解决这个问题。 - Alberto Toglia
@Alberto Toglia:不完全是这样。如果你确定向量的大小,可以在构造时指定它,它就永远不会移动。如果你不确定,那么数组将会越界而不是增长,你将会遇到核心上的繁琐问题。 - Gorpik
只有在调整向量大小时(使用数组无法完成此操作),向量才会移动其对象。如果将向量视为数组(即保持相同的大小),则可以正常工作,不会移动任何内容。您可以调用 reserve() 函数来指定向量应预先分配多少空间。然后,您可以插入这么多对象,并且不会存在必须重新分配和移动对象的风险。但实际上,问题是向量和指针的组合。存储在向量中的对象的指针是危险的(至少如果调整了向量的大小)。 - jalf
@Gorpik,是的,我知道我的数组不会增长。但这似乎是一个常见的问题,我的对象内存对齐是必须的,并且拥有映射以获取这些对象指针也是必须的。如何始终更新它们?我知道有一种叫做句柄的东西,但我还没有看到类似于这个的好例子。 - Alberto Toglia
@Alberto Toglia:只需使用向量,但在构造函数中指定它们的大小(例如Vector(100),而不是仅使用Vector())。然后您可以确保您的向量不会移动。 - Gorpik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接