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

时间:2012-01-20 10:38:59

标签: c++ linux

IRC上有人声称,虽然使用new[]进行分配并使用delete delete[]进行删除)是UB,但在Linux平台上(无更多详细信息)关于操作系统)这将是安全的。

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

还是完全不真实?


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


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

5 个答案:

答案 0 :(得分:13)

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

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

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

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

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

答案 1 :(得分:7)

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

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

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

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

答案 2 :(得分:3)

This question详细讨论了在Visual C ++上混合new[]delete 看起来是否安全(没有可观察到的问题)的详细信息。我想通过“在Linux上”你实际上意味着“使用gcc”,我观察到与ideone.com上的gcc非常相似的结果。

请注意, 需要

  1. 全局operator new()operator new[]()函数以相同方式实现和
  2. 编译器优化了“prepend with elements of elements”分配开销
  3. 并且仅适用于具有普通析构函数的类型。

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

答案 3 :(得分:2)

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

#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()的包装器,因此在新指针上调用free()大部分时间都是“安全”(记得调用析构函数) ),但你不应该依赖它。对于new[]delete[],情况会更复杂。

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

现在最简单的方法是在某处附近有一张全局地图,它可以存储所有new[]指针的信息。在这种情况下,如果调用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()返回的。

答案 4 :(得分:0)

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