C++ Valgrind未检测到释放内存的危险

C++ Valgrind未检测到释放内存的危险,c++,debugging,memory,memory-management,valgrind,C++,Debugging,Memory,Memory Management,Valgrind,我正在学习valgrind框架,我决定在自己的小测试用例上运行它。下面是强制从堆中删除额外对象的程序(我在AMD64/LINUX上运行它): #包括 使用名称空间std; 结构Foo { Foo(){coutValgrind未检测到数组“prefix”更改可能是因为它是内存的有效部分。即使用户代码不应该直接更改它,但它仍然由数组构造函数代码访问和修改,并且valgrind没有提供如此精细的访问检查分离。还要注意,此损坏似乎不会损坏堆,因此释放成功 Valgrid没有检测到对无效对象的析构函数调用

我正在学习valgrind框架,我决定在自己的小测试用例上运行它。下面是强制从堆中删除额外对象的程序(我在AMD64/LINUX上运行它):

#包括
使用名称空间std;
结构Foo
{

Foo(){coutValgrind未检测到数组“prefix”更改可能是因为它是内存的有效部分。即使用户代码不应该直接更改它,但它仍然由数组构造函数代码访问和修改,并且valgrind没有提供如此精细的访问检查分离。还要注意,此损坏似乎不会损坏堆,因此释放成功

Valgrid没有检测到对无效对象的析构函数调用,可能是因为此调用实际上没有访问无效存储。添加一些类字段将改变这种情况:

struct Foo
{
    int i;
    Foo(): i(0) { cout << i << "Creation Foo" << endl;}
   ~Foo(){ cout << i << "Deletion Foo" << endl;}
};
structfoo
{
int i;

Foo():i(0){coutValgrind没有检测到内存问题,因为没有内存问题

让我们一步一步地介绍一下您的程序(这取决于实现,但它基本上适用于gcc和其他主要编译器):

调用
新Foo[3]

  • 分配了
    8+3*sizeof(Foo)
    字节的内存,我们称之为指针
    p
    。需要8个字节来存储数组中的元素数。当调用
    delete
    时,我们需要这个数字
  • 数组中的对象数保存为
    p[0]=3
  • 为内存地址
    p+8
    p+8+sizeof(Foo)
    p+8+2*sizeof(Foo)
    调用新操作符
    Foo()
    ,即创建3个对象
  • ar
    具有地址
    p+8
    ,并指向第一个
    Foo
    -对象
  • 操纵对象数量
    *(重新解释投射(ar)-2)=4

  • 好的,
    p[0]
    现在是
    4
    。每个人都认为数组中有
    4
    对象(但实际上只有
    3
  • 注意:如果
    Foo
    将有一个微不足道的析构函数(比如
    int
    has),那么情况将有所不同,访问
    ar-8
    将是无效的访问

    在这种情况下,编译器优化了析构函数的调用,因为无需执行任何操作。但是无需记住元素的数量-因此
    p
    实际上是
    ar
    ,并且在开头没有偏移量/额外的8个字节

    这就是为什么大多数编译器的代码实际上是错误的:

    int *array=new int[10];
    delete array;//should be delete [] array;
    
    工作没有问题:内存管理器不需要知道指针后面有多少内存,不管它是一个int还是多个int,它只是释放内存

    呼叫
    删除[]ar

  • 析构函数的调用次数为
    p[0]=4次,也适用于
    arr[0]、arr[1]、arr[2]
    arr[3]
    。为
    arr[3]
    调用析构函数是一种未定义的行为,但不会发生任何不好的事情:调用析构函数不会释放内存(在您的情况下甚至不会触及内存)。它只会打印出一些东西,没有任何错误
  • 释放数组内存。实际上
    p
    -指针被释放,而不是
    ar
    ,因为内存管理器“只知道”p
  • -我们可以从
    ar
    计算
    p
    。在地下某个地方调用
    free(p)
    -没有人关心它有多少内存-使用的
    操作符delete(*void)
    没有提供它 没什么,从瓦尔格兰德的观点来看,这是个问题


    为了使我的观点更清楚(请参见结果汇编程序):

    将导致仅调用析构函数(无内存访问),但不会释放内存-这就是程序中对象
    arr[0]
    arr[1]
    arr[2]
    arr[3]

    call    Foo::~Foo()
    
    但是

    将导致调用析构函数和运算符delete,这将删除堆上的内存:

        call    Foo::~Foo()
        movq    %rbp, %rdi
        call    operator delete(void*) ; deletes memory, which was used for f
    
    然而,在您的案例中,并没有为每个对象调用操作符
    delete
    ,因为内存也不是按位分配的,而是作为整个内存块分配的,即
    p


    如果您要调用
    delete ar;
    而不是
    delete[]ar;
    您可以看到发生了什么:

  • 仅为第一个
    Foo
    -对象调用析构函数
  • 程序将尝试释放指针
    arr
    ,而不是指针
    p
    。然而,内存管理器不知道指针
    ar
    (它只知道
    p
    ),这是有问题的

  • 正如VTT所指出的,如果析构函数触及对象中的某些内存,您将看到对数组之外内存的无效内存访问


    如果析构函数必须释放一些内存(例如,有一个向量作为成员),从而将随机内存内容解释为地址,并调用运算符
    delete
    ,以删除这些随机地址,则会出现错误。

    *(reinterpret_cast(ar)-2)=4;
    是一种潜在的严格别名冲突,会导致未定义的行为,并且是发布模式下优化的潜在目标,会抛出整行代码。您是如何编译此代码的?@VTT,我更新了问题。我知道这是一种肮脏的黑客行为-我只是想隐式更改数组中分配对象的计数,而要编译的命令是什么您使用?我添加了
    inti;
    但没有添加任何内容occured@LmTinyToon您需要在析构函数
    cout处实际访问它,但据我所知,运算符delete[]释放了错误长度的块(长度应小于1foo大小).我想这应该已经导致了错误的结果results@LmTinyToon我们处于一个行为未定义的领域,因此一切都依赖于实现。然而,在delete之后,有一个简单的无大小的C-free
    free(void*)
    。可能有一个内存管理器,它利用指向内存的大小来加快查找速度,
    call    Foo::~Foo()
    
    Foo *f=new Foo();
    delete f;
    
        call    Foo::~Foo()
        movq    %rbp, %rdi
        call    operator delete(void*) ; deletes memory, which was used for f