【问题标题】:Why do you have to call delete for local variables of a function that are stored in the heap?为什么必须为存储在堆中的函数的局部变量调用 delete?
【发布时间】:2015-05-04 04:37:18
【问题描述】:

假设你有以下函数:

 void doSomething(){
    int *data = new int[100];
 }

为什么会产生内存泄漏?既然不能在函数外访问这个变量,为什么每次调用这个函数结束时编译器都不会自己调用delete呢?

【问题讨论】:

  • 你说“变量”,编译器确实删除了这里的“变量”,但他没有删除它指向的内存,因为这就是堆的实际用途,至少部分是这样,否则您必须在取消分配之前验证没有指向该内存,这就是“垃圾收集器”的用途
  • 你是对的,但我还是不明白为什么内存没有自动释放。
  • 您正在寻找garbage collection。 Java 这样做(或多或少)。 C++ 只是做不同的事情,有它自己的优点和缺点。
  • @jsguy:理论上可以。 C++11 中的一些更改是为了至少可以创建一个包含垃圾收集器的符合要求的实现。事实上,这不会发生,因为大多数 C++ 程序员不愿意忍受添加垃圾收集器所涉及的权衡。但是,如果您非常想的话,您可以add on a garbage collector
  • 还请记住,作者可能不希望将其释放。不是因为他想重用内存,而是因为他知道该函数不会被足够多地调用,以至于它导致的内存泄漏实际上会导致问题,并且他不希望运行时间浪费在释放上。可以肯定的是,他可能是一个非常糟糕的程序员,但 C++ 的核心原则之一是你不用为不用的东西付费。

标签: c++ memory memory-leaks


【解决方案1】:

为什么会产生内存泄漏?

因为您有责任删除您使用new 创建的任何内容。

为什么每次调用这个函数结束时编译器不自己调用delete?

通常,编译器无法判断您是否还有指向已分配对象的指针。例如:

void doSomething(){
    int *data = new int[100];
    doSomethingElse(data);
}

doSomethingElse 是否只是在函数调用期间使用指针(在这种情况下我们仍要删除此处的数组)?它是否存储了指针的副本以供以后使用(在这种情况下,我们还不想删除它)?编译器无从知晓;由你来告诉它。与其制定一个复杂且容易出错的规则(例如“除非您能确定编译器必须知道没有其他对它的引用,否则您必须删除它”),而是保持简单:您必须删除它。

幸运的是,我们可以做得比处理原始指针并尝试在正确的时间删除它更好。 RAII 的原则允许对象获得分配的资源的所有权,并在它们的析构函数超出范围时自动释放它们。容器允许在单个范围内维护动态对象,并在需要时进行复制;智能指针允许在作用域之间移动或共享所有权。在这种情况下,一个简单的容器会给我们一个动态数组:

void doSomething(){
    std::vector<int> data(100);
} // automatically deallocated

当然,对于像这样一个固定大小的小数组,你不妨让它自动化:

void doSomething(){
    int data[100];
} // all automatic variables are deallocated

【讨论】:

  • shared_ptr 和 unique_ptr 如果分配的局部变量是 c-array 以外的一些更复杂的类型,比如结构体,也是不错的选择。
【解决方案2】:

像这样的动态分配的全部意义在于您手动控制已分配对象的生命周期。它比许多人想象的要少很多。事实上,我想不出new[] 的有效用法。

让编译器处理对象的生命周期确实是个好主意。这称为RAII。在这种特殊情况下,您应该使用std::vector&lt;int&gt; data(100)。现在,一旦data 以任何方式超出范围,内存就会自动释放。

【讨论】:

  • 那么,如何在一个注定要在外部使用的函数中分配内存?
  • @Guiroux 你按值返回向量。
  • @Guiroux 不,我们有 RVO 和移动任务。如果您是 C++11 之前的版本,则可以使用 std::vector&lt;int&gt;&amp; 输入参数可能,具体取决于具体情况。
  • @Guiroux 不,那么您将返回对本地对象的引用,该对象将在函数返回后立即超出范围。所以返回的引用将指向已经被删除的东西。
  • @Guiroux 即使编译器不执行 RVO(我希望它这样做),因为 C++11 向量是可移动的,所以我们可以肯定它最多只是一个动作。跨度>
【解决方案3】:

实际上你可以在你的doSomething()函数之外访问这个变量,你只需要知道这个指针持有的地址。

在下图中,矩形为一个存储单元,矩形上方为存储单元地址,矩形内部为存储单元值。

例如,在上图中,如果您知道分配的数组从地址 0x200 开始,那么您可以在函数之外执行下一步:

int *data = (int *)0x200;
std::cout << data[0];

所以编译器不能帮你删除这块内存,但是请注意编译器警告:

main.cpp: In function ‘void doSomething()’:
main.cpp:3:10: warning: unused variable ‘data’ [-Wunused-variable]
     int *data = new int[100];

当你做new时,操作系统会为你在RAM中分配内存,所以你需要让操作系统知道,当你不再需要这块内存时,做delete,所以只有你知道什么时候执行delete,不是编译器。

【讨论】:

  • 实际上,像这样暴露地址是不可能的,或者至少是极其难看的,你可以通过返回一个指针或将它传递给其他一些函数来让地址逃脱分配范围指针的副本。除此之外,好点......动态分配的整个目的是让用户可以决定生命周期,而不是范围。但是,许多人可能会出于后一种意图而滥用它。我只想说我不使用动态分配,除非我必须这样做,并且目前正在处理一个具有 400 kB 主对象的项目......我仍然不需要。
  • @underscore_d 当然,这只是一个说明事情如何运作的例子。在现实生活中,不应该做那样的事情。顺便说一句,有 MISRA C 标准(用于关键任务系统)完全禁止动态内存分配。但是以 Linux 内核为例,不应该广泛使用堆栈,因为内核堆栈只有 4 KB 左右。所以所有“大”变量和结构都应该动态分配。所以这都是关于上下文的,我猜。
  • 真的!实际上,我应该将我的“在现实中”限定为 在主流平台上 - 就像在嵌入式系统上一样,可能需要通过文字数字和强制类型直接访问地址,而 IMO 是 C/ 之一C++ 的主要优势(结合不使其成为 UB 的实现!)正在实现这一点。我猜这通常是读取硬件寄存器或使用 placement new 到一个明确定义的地址。
【解决方案4】:

这是在堆上分配的内存,因此 在函数外部可用于任何有其地址的东西。不能期望编译器知道您打算如何处理堆分配的内存,并且它不会在编译时通过(例如)跟踪是否有任何东西对内存有活动引用来推断释放内存的需要。也没有像 Java 中那样的自动运行时垃圾收集。在 C++ 中,程序员有责任释放在堆上分配的所有内存。

另请参阅:Why doesn't C++ have a garbage collector?

【讨论】:

    【解决方案5】:

    这里有一个在堆上动态分配的int 数组,其指针数据指向数组的第一个元素。

    由于您使用new 在堆上显式分配了您的数组,因此当您超出范围时,该数组不会从内存中删除。当您超出范围时,您的指针将被删除,然后您将无法访问数组 -> 内存泄漏

    【讨论】:

      【解决方案6】:

      既然我不能在函数外访问这个变量,为什么不 每次调用此函数时,编译器都会自行调用 delete 结束了吗?

      因为您不应该在堆上分配一开始就不会使用的内存。 这就是 C++ 的工作原理。

      编辑: 另外,如果编译器会在函数返回后删除指针,那么就会有从函数返回指针的方法。

      【讨论】:

        猜你喜欢
        • 2015-06-23
        • 1970-01-01
        • 2013-07-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-06-01
        相关资源
        最近更新 更多