我正在学习 valgrind 并决定在我的测试用例中尝试它(删除数组中的额外元素)。这是一个示例程序(AMD64/LINUX)
#include <iostream>
using namespace std;
struct Foo
{
Foo(){ cout << "Creation Foo" << endl;}
~Foo(){ cout << "Deletion Foo" << endl;}
};
int main()
{
Foo* ar = new Foo[3];
*(reinterpret_cast<int*>(ar)-2) = 4;
delete[] ar;
return 0;
}
但是 valgrind 的输出让我吃惊:
$ valgrind --leak-check=full ./a.out -v
==17649== Memcheck,内存错误检测器
==17649== 版权所有 (C) 2002-2017 和 GNU GPL,由 Julian Seward 等人提供。
==17649== 使用 Valgrind-3.13.0 和 LibVEX;使用 -h 重新运行以获取版权信息
==17649== 命令:./a.out -v
==17649==
创造符
创造符
创造符
删除 Foo
删除 Foo
删除 Foo
删除 Foo
==17649==
==17649== 堆摘要:
==17649== 在退出时使用:72,704 字节在 1 个块中
==17649== 总堆使用量:3 次分配,2 次释放,73,739 字节分配
==17649==
==17649== 泄漏摘要:
==17649== 肯定丢失:0 个块中的 0 个字节
==17649== 间接丢失:0 个块中的 0 个字节
==17649== 可能丢失:0 个块中的 0 个字节
==17649== 仍然可以访问:1 个块中的 72,704 个字节
==17649== 抑制:0 个块中的 0 个字节
==17649== 未显示可到达块(找到指针的块)。
==17649== 要查看它们,请重新运行: --leak-check=full --show-leak-kinds=all
==17649==
==17649== 对于检测到和抑制的错误计数,重新运行:-v
==17649== 错误摘要:来自 0 个上下文的 0 个错误(抑制:来自 0 的 0 个)
看起来 valgrind 无法检测到这种危险的内存释放。据我了解,这是一个错误?还是 valgrind 仍然能够检测到这种错误?
UPD:main.cpp通过命令编译g++ -g main.cpp
Valgrind 不会检测到数组“前缀”的变化,因为它是一块有效的内存。即使它不应该被用户代码修改,它仍然可以被初始化数组的代码访问,并且 valgrind 不提供这种细粒度的访问控制。还需要注意的是,在这样的操作过程中,堆没有损坏,内存的释放是正常完成的。
Valgrid 没有检测到带有无效对象的析构函数调用,因为该调用不访问内存。如果在类中添加一个字段,那么情况就会发生变化:
分配有
new[]具有非平凡析构函数的对象数组的内存块在“传统”实现中具有以下结构您准确地更正了此字符串中的第二个值。它只影响调用正确数量的析构函数的
new[]/机制delete[]。但是完全不需要通过机制malloc/free(或其类似物)正确释放内存。您未触及的第一个值用于正确释放内存。因此,内存释放是正常发生的,不会被 valgrind 捕获。由于析构函数无法访问数组元素的内存,正如@VTT 正确指出的那样,valgrind 也不会注意到对第二个字段的损坏。Valgrind 仅“知道”分配的内存块的内部组织,有条件地说,“在 C 级别”,即 他只看到了“俄罗斯套娃”的第一层,并且知道这对
malloc/创造的第一个服务领域的特殊作用free。事实上,C++ 在这个内存中创建了自己的嵌套娃娃级别,即new[]/对delete[]还存储数组元素的数量(在非平凡析构函数的情况下),valgrind 不知道。它认为从条件返回的点开始的所有内容malloc都是用户内存,用户可以使用它做任何他们想做的事情。出于这个原因,valgrind 不会将第二个字段的损坏视为非法访问,即 只有在这种损害以后以诱发错误的形式出现时,他才能抓住他。考虑使用分配的内存的可能结构
new-expression您重写了包含已创建对象数量的尾部。这意味着
delete-expression将调用更多的析构函数。因此,我们已经有了 UB(不是我们的内存 + 为不存在的对象调用析构函数)。但是operator delete[]返回的相同地址会被转移到operator new[],内存会被正确释放,当然,如果在此之前程序没有崩溃,或者整个复合体的自毁程序没有启动。