在 Linux 上混合使用"new[]"和"delete"是否"safe"?

Is it "safe" on Linux to mix `new[]` and `delete`?

本文关键字:delete 是否 safe new Linux 混合      更新时间:2023-10-16

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

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

还是完全不真实?


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


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

当然这不是真的。这个人混淆了几个不同的问题:

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

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

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

然后是第三点,接球。它是UB,这意味着它是UB。编译器可能会根据您的代码没有表现出任何未定义行为的假设进行优化。如果它真的这样做了,它可能会打破其中的一些假设,而看似无关的代码可能会打破。

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

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

对于所有甚至大多数Linux来说,这绝对不是真的,你的IRC朋友在胡说八道。

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

这个问题详细讨论了在Visual C++上完全混合new[]delete何时看起来安全(没有可观察到的问题)。我想"在Linux上"实际上是指"使用gcc",我在ideone.com上观察到了与gcc非常相似的结果

请注意,此需要:

  1. 全局CCD_ 14和CCD_
  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[] 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()刚刚被传递了一个从未被mallocated的指针,也就是说结果是UB。

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

在对基本类型调用new[]的情况下,编译器立即知道以后不必调用任何析构函数,只需优化记账。然而,即使对于基本类型,也绝对允许进行记账。如果您调用new,也可以添加记账。

如果您需要编写自己的内存分配例程来代替newdelete,那么在实际指针前进行这种记账实际上是一个非常好的技巧。您可以在那里存储的内容几乎没有任何限制,因此永远不要假设从newnew[]返回的任何内容实际上都是从malloc()返回的。

我预计new[]delete[]在Linux(gcc、glibc、libstdc++)下可以归结为malloc()free(),只是调用了con(de)结构。对于newdelete是相同的,只是con(de)structor的调用不同。这意味着,如果他的构造函数和析构函数无关紧要,那么他可能会逃脱惩罚。但为什么要尝试呢?