繁体   English   中英

在Linux上混合`new []`和`delete`是否“安全”?

[英]Is it “safe” on Linux to mix `new[]` and `delete`?

有人在IRC上声称,尽管使用new[]和删除with delete不是 delete[] )分配是UB,但在Linux平台上(没有关于操作系统的更多细节)它是安全的。

这是真的? 有保证吗? 是否与POSIX中的某些内容有关,它指定动态分配的块在开始时不应该有元数据?

还是完全不真实?


是的,我知道我不应该这样做。 我永远不会。
我很好奇这个想法的真实性; 就是这样


“安全”,我的意思是:“不会导致除了new执行的原始分配之外的行为,或者是由delete[]执行的delete[]分配”。 这意味着我们可能会看到1 “元素”破坏 n ,但没有崩溃。

当然不是这样。 那个人正在混淆几个不同的问题:

  • 操作系统如何处理分配/解除分配
  • 正确调用构造函数和析构函数
  • UB表示UB

在第一点,我确信他是对的。 通常在该级别上以相同的方式处理它们:它只是对X字节的请求,或者是从地址X开始释放分配的请求。如果它是一个数组,它并不重要。

在第二点,一切都崩溃了。 new[]为分配的数组中的每个元素调用构造函数。 delete调用指定地址处的一个元素的析构函数。 因此,如果您分配一个对象数组,并使用delete释放它,则只有一个元素将调用其析构函数。 (这很容易被遗忘,因为人们总是使用int数组来测试它,在这种情况下,这种差异是不明显的)

然后是第三点,即全能。 这是UB,这意味着它是UB。 编译器可以基于您的代码没有表现出任何未定义行为的假设进行优化。 如果确实如此,它可能会打破其中的一些假设,看似无关的代码可能会破坏。

即使它在某些环境中恰好是安全的,也不要这样做。 没有理由想要这样做。

即使它确实将正确的内存返回给操作系统,也不会正确调用析构函数。

对于所有甚至大多数Linux来说,这绝对不是真的,你的IRC朋友正在谈论bollocks。

POSIX与C ++ 无关 一般来说,这是不安全的。 如果它可以在任何地方工作,那是因为编译器和库,而不是操作系统。

当在Visual C ++上完全混合new[]delete 看起来安全 (没有可观察到的问题)时, 这个问题会详细讨论。 我想通过“在Linux上”你实际上意味着“与gcc”,我观察到与ideone.com上的gcc非常相似的结果。

请注意,这需要

  1. 全局operator new()operator new[]()函数以相同的方式实现
  2. 编译器优化了“prepend with elements of elements”分配开销

并且仅适用于具有普通析构函数的类型。

即使满足这些要求,也无法保证它可以在特定版本的特定编译器上运行。 如果不这样做你会好得多 - 依赖未定义的行为是一个非常糟糕的主意。

这绝对不安全,因为您只需尝试使用以下代码:

#include<iostream>

class test {
public:
  test(){ std::cout << "Constructor" << std::endl; }
  ~test(){ std::cout << "Destructor" << std::endl; }
};

int main() {
  test * t = new test[ 10 ];
  delete t;
  return 1;
}

看看http://ideone.com/b8BiQ 它失败了。

它可以在你不使用类时使用,但只能使用基本类型,但即使这样也不能保证。

编辑 :对于那些想要知道崩溃原因的人的一些解释:

newdelete主要用作malloc()包装器,因此在newed指针上调用free()大多数时候是“安全的”(记得调用析构函数),但你不应该依赖它。 对于new[]delete[] ,情况更复杂。

当使用new[]构造类数组时,将依次调用每个默认构造函数。 当你delete[]每个析构函数都会被调用。 但是,每个析构函数也必须提供一个this指针,以将inside用作隐藏参数。 因此,调用析构函数之前的程序必须找到保留内存中的所有对象的位置,通过这些位置作为this指针的析构函数。 因此,稍后重建此信息所需的所有信息都需要存储在某处。

现在最简单的方法是在某处安装一个全局地图,它为所有new[] ed指针存储这些信息。 在这种情况下,如果调用delete而不是delete[] ,则只会调用其中一个析构函数,并且不会从映射中删除该条目。 但是,通常不使用此方法,因为映射很慢并且内存管理应该尽可能快。

因此,对于stdlibc ++,使用了不同的解决方案。 由于只需要几个字节作为附加信息,因此按这几个字节过度分配是最快的,将信息存储在存储器的开头并在寄存之后将指针返回到存储器。 因此,如果您分配一个包含10个字节的10个对象的数组,则程序将分配100+X个字节,其中X是重建此数据所需的数据大小。

所以在这种情况下它看起来像这样

| Bookkeeping | First Object | Second Object |....
^             ^
|             This is what is returned by new[]
|
this is what is returned by malloc()

因此,如果你传递了你从new[]接收到delete[]的指针,它将调用所有析构函数,然后从指针中减去X并将其赋予free() 但是,如果你调用delete ,它会调用第一个对象的析构函数,然后立即将该指针传递给free() ,这意味着free()刚刚传递了一个从未被malloced指针的指针,这意味着结果是UB。

看看http://ideone.com/tIiMw ,看看传递给deletedelete[] 如您所见, new[]返回的指针不是内部分配的指针,但在返回main()之前会添加4。 当正确调用delete[]时,相同的四个被减去,我们在delete[]得到正确的指针,但是当调用delete我们得到了错误的指针。

如果在基本类型上调用new[] ,编译器会立即知道它以后不必调用任何析构函数,它只是优化了簿记。 然而,即使对于基本类型,也绝对允许写簿记。 如果你打电话给new也可以增加簿记。

这个在真正指针前面的簿记实际上是一个非常好的技巧,万一你需要编写自己的内存分配例程来替换newdelete 您可以在那里存储的内容几乎没有任何限制,因此不应该假设从newnew[]返回的任何内容实际上是从malloc()返回的。

我希望new[]delete[]可以归结为Linux下的malloc()free() (gcc,glibc,libstdc ++),除了调用con(de)结构。 对于newdelete除了con(de)结构被不同地调用。 这意味着如果他的构造函数和析构函数无关紧要,那么他就可以逃脱它。 但为什么要试试?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM