【问题标题】:Why can freed dynamically allocated memory still be accessed after a delete operation in C++? [duplicate]为什么在 C++ 中的删除操作后仍然可以访问释放的动态分配的内存? [复制]
【发布时间】:2021-08-11 13:27:35
【问题描述】:

假设我已经分配了一些内存来存储这样的 int 值:

int *p=new int; 

在这里,我使用new 运算符创建了所需的内存并分配了该内存块的地址,以便我可以访问该内存块。

现在,我可以控制我在该内存块中存储的内容。

但是当我写这样的语句时:

delete p;

我们说我删除了动态分配的内存。

但是如果我真的delete'd 或释放了那个内存,我应该在delete 操作之后不能再访问那个内存了吗?但是我能够使用相同的指针变量访问该内存块。那么如果删除内存块后可以访问,那么删除内存块的目的是什么?

这里有一些示例代码:

#include <iostream>
using namespace std;

int main(void)
{
    int *p;
    p=new int;
    *p=10;
    cout << *p << endl;
    delete p;

    //here look we can still access that block of memory using the same pointer variable
    //But we did not created the  memory block after deletetion
    cout << *p << endl;
    *p=20;
    cout << *p << endl;

    return 0;
}

在这种情况下,“删除/释放动态分配的内存”是什么意思?

【问题讨论】:

  • 未定义的行为就是未定义的行为。
  • 查看常见问题解答:stackoverflow.com/q/2397984
  • 声明了int类型的指针,然后这个指针指向内存中新分配的整数位置。这个包含整数的内存位置是作为“新”关键字动态分配的。 'delete p' 语句不会删除指针本身,但会释放 'new' 分配的内存。现在指针 p 仍然存在,但现在不能保证它仍然指向相同的内存位置。它可能指向相同的地址或者不是未定义的行为。指针 p 现在是一个悬空指针。删除指针后的最佳实践将其分配给 nullptr。
  • 为什么我们说你不能在篮球比赛中抢球并带球?我做到了,它奏效了
  • @StoryTeller 不确定这个类比是否有效——因为你没有机会打破其他玩家的鼻子;或者在你认为非常重要的一个小时后给球一个小刺,你会意识到这会引起问题

标签: c++ memory-management dynamic-memory-allocation undefined-behavior delete-operator


【解决方案1】:

其他答案说在delete 之后取消引用指针是未定义的行为是正确的。但我认为了解正在发生的事情而不是简单地说“任何事情都可能发生”是有用的。

当你delete一个指针时,程序会采取几个步骤:

  1. 调用指针所指对象的析构函数
  2. 调用内存管理器的函数,该函数将执行释放内存所需的任何簿记工作。

第二步,取决于实现,可能实际上将内存块返回给操作系统,或者它可能只是标记该块可用于后续分配,或者做其他事情。在您的情况下,显然,该块被简单地标记为免费但没有返回给操作系统。进一步的分配可能会返回此块内的地址,然后未定义的行为可能会从“正常工作”变为“工作奇怪”。

但是即使您仍然可以通过已删除的指针访问该对象,编译器仍然可以假定该对象不再存在并进行一些依赖于此的优化。这可能会以非常意想不到且看似不合逻辑的方式破坏您的程序,因此您最好远离未定义的行为。

【讨论】:

  • 进一步的分配可能会返回此块内的地址。这是要记住的最重要的一点。 :)
【解决方案2】:

当您为指针分配内存时,内存中的该位置成为您的,也就是说,其他应用程序无法覆盖它。现在,当您释放该位置时,它就可以用于其他应用程序,因此您当前的代码不再具有排他性,您的有意义的数据变成了从其他应用程序的角度来看的随机数据。实际值没有改变是非常合乎逻辑的,因为从您的应用程序的角度来看,这将涉及一些额外的工作,而不是它不关心的内存块。至于地址是否可达,基本上你已经释放了那块内存,所以其他应用程序,包括这个应用程序都会到达它,所以你能到达它并不奇怪。

【讨论】:

  • 这种解释只有在没有内存保护的操作系统中才有意义——例如DOS,您的应用程序所在的位置,例如一个 TSR,“其他应用程序”是任何后续运行的应用程序。
  • @Ruslan 我们正在谈论未定义的行为。该地址仍然可用,即使该应用不再对其拥有专有权利。
  • 是的,我的意思是你关于“其他应用程序可以使用这个在delete 之后你仍然可以访问的地址”的解释不适用于现代操作系统,1)通常不共享堆,以及 2) 使相同的虚拟地址引用不同的物理地址。
  • 当我释放内存时,如果同时另一个程序分配了该内存,我仍然可以从我的程序访问内存块。然后它会抛出错误还是未定义的行为
  • @S.M.TusharIbneSalam 我不能 100% 确定这一点,但根据我的观点,这应该意味着您当前的应用无法访问该位置。
【解决方案3】:

但是如果我们真的删除或释放了那个内存,我们不应该做不到吗? 在删除操作后访问该内存。

不,那是错误的。

删除指针会释放它指向的内存。这就是删除的目的。不访问已删除的内存是您的责任,而不是编译器。如果您的程序确实访问了已删除的内存,那么根据 C++ 标准,它的行为是未定义的,这正是它所说的。

【讨论】:

    【解决方案4】:

    在您执行delete p 之后,您访问*p。 delete p 结束对象的生命周期,并且您在对象的生命周期结束后访问对象。这是未定义的行为。当你的程序包含未定义的行为时,所有的赌注都没有了——不能保证程序会做什么。 “工作正常”是“不保证”的一个例子。鼻恶魔是另一个例子。事实上,当遇到某些类型的未定义行为时,一个版本的 GCC (1.17) 尝试启动 NetHack、Rogue 和 Towers of Hanoi 游戏。 [1] 所以你不能指望它。

    引用FAQ:

    未定义的行为是 C 和 C++ 语言的这些方面之一 这可能会让来自其他语言的程序员感到惊讶 (其他语言试图更好地隐藏它)。基本上是可以的 编写行为无法预测的 C++ 程序,甚至 虽然很多 C++ 编译器不会在程序中报任何错误!

    [...]

    这正是未定义行为的问题。基本上, 一旦您调用未定义的行为,标准允许任何事情发生 (甚至鼻恶魔)。如果有一个“正确”的行为根据 你对语言的心理模型,那个模型是完全错误的; C++ 标准有唯一的投票,句号。

    【讨论】:

      【解决方案5】:

      在调用 delete 之后,您的程序会在标准输出中写入几个字符。可能,只是可能,为了执行这个 write() 操作,C++ I/O 系统必须分配几个字节,所以它向 C++ 内存管理系统请求几个字节。运气不好,内存系统给了 I/O 系统正是这个小区域,由于您的删除操作,它刚刚变得可用。

      I/O 系统完全在其权限范围内操作,将指向某个辅助结构的有用指针存储到那个小地方。然后,通过将 20 存储到它刚刚“删除”的位置,您的程序会破坏该有用的指针。在那之后,I/O 系统数据结构就会损坏。

      请注意,硬件或操作系统无法保护内存位置不被滥用。这是因为内存写入权限是每个进程而不是每个函数,并且 C++ I/O 系统与您的 main() 函数属于同一进程。

      如果稍后 I/O 系统开始删除或更改文件而没有通知,您可能不会向您的 C++ 编译器供应商投诉,因为您写入的内存不再属于您。

      如果你的编程人员容易犯这种错误,你必须坚持让他们写出这样的东西:“delete p ; p = nullptr;”。这样一来,由随后滥用指针引起的崩溃会立即发生并且非常容易调试,这与由 I/O 系统数据结构的潜在损坏导致的(可能更远的)崩溃不同。

      但是现代 C++ 的精神是用称为“智能指针”的对象替换“原始指针”,也就是您在这里使用的那种指针。因此,您可能必须熟悉 std::shared_ptr 和 std::unique_ptr 类。这是一个小示例,您可以在其中看到指针的数值已重置为 NULL:

      #include  <memory>
      #include  <iostream>
      
      
      int main(void)
      {
          std::unique_ptr<int> uPtr = std::make_unique<int>(0);
          *uPtr = 10;
      
          std::cout << *uPtr << std::endl;
          uPtr.reset();
      
          auto ptrValue = reinterpret_cast<unsigned long>(uPtr.get());
      
          std::cout << "uPtr is: " << ptrValue << std::endl;
      
          std::cout << "So far so good ... " << std::endl;
      
          // here, the program will crash :
      
          *uPtr = 20;
          std::cout << *uPtr << std::endl;
      
          return EXIT_SUCCESS;
      }
      

      如果你允许我对程序员的幽默进行一次蹩脚的尝试:在 main 函数写了 20 之后,你的程序的状态可以描述为“到目前为止还不错”。不知道你是否熟悉金融服务行业。

      有一个经典笑话,关于一位传奇的华尔街交易员,他用次级金融工具做了许多非常糟糕的交易。因此,交易员决定从大楼的 94 层跳入下面的街道。到了五楼,他看到了一个秘书,秘书问他:“怎么样?”。交易员回复:“到目前为止一切顺利。”。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-05-14
        • 1970-01-01
        • 2013-04-12
        • 2020-12-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多