为什么 C++ 中存在 delete[] 语法?

Why does the delete[] syntax exist in C++?

每次有人在这里问有关 delete[] 的问题时,总会有一种非常笼统的回答“C++ 就是这样做的,使用 delete[]”。来自香草 C 背景,我不明白的是为什么需要完全不同的调用。

对于malloc()/free(),您的选择是获取指向连续内存块的指针并释放连续内存块。实现领域的一些东西出现了,并且知道你分配的块的大小是基于基地址的,当你必须释放它时。

没有函数free_array()。我已经看到一些与此无关的其他问题的疯狂理论,例如调用 delete ptr 只会释放数组的顶部,而不是整个数组。或者更正确的说法是,它不是由实现定义的。当然……如果这是 C++ 的第一个版本,并且您做出了一个有意义的奇怪设计选择。但是为什么$PRESENT_YEAR的C++标准没有被重载???

C++ 添加的唯一额外部分似乎是遍历数组和调用析构函数,我认为这可能是它的症结所在,它实际上是使用一个单独的函数来为我们节省一个运行时长度查找,或列表末尾的 nullptr 以换取折磨每一个新的 C++ 程序员或那些度过了模糊的一天并忘记了不同的保留字的程序员。

有人可以一劳永逸地澄清是否除了“标准就是这么说的,没有人质疑”之外还有其他原因吗?

C++ 中的对象通常具有需要在其生命周期结束时 运行 的析构函数。 delete[] 确保调用数组的每个元素的析构函数。但是这样做 has unspecified overhead,而 delete 则不会。这就是为什么有两种形式的删除表达式。一种用于支付开销的数组,另一种用于支付开销的单个对象。

为了只有一个版本,实现需要一种机制来跟踪有关每个指针的额外信息。但 C++ 的基本原则之一是,不应强迫用户支付他们并非绝对必须支付的费用。

总是deletenew,总是delete[]new[]。但在现代 C++ 中,newnew[] 一般不再使用。使用 std::make_unique, std::make_shared, std::vector 或其他更具表现力和更安全的替代方法。

基本上,mallocfree分配内存,newdelete创建和销毁对象。所以你必须知道对象是什么。

要详细说明 François Andrieux 的回答中提到的未指定开销,您可以查看 my answer on this question,其中我检查了特定实现的作用(Visual C++ 2013,32 位)。其他实现可能会也可能不会做类似的事情。

如果 new[] 与具有非平凡析构函数的对象数组一起使用,它所做的就是多分配 4 个字节,并返回向前移动 4 个字节的指针,所以当 delete[] 想知道那里有多少个对象,它获取指针,将其前移 4 个字节,然后获取该地址处的数字并将其视为存储在那里的对象数。然后它在每个对象上调用一个析构函数(对象的大小从传递的指针类型中得知)。然后,为了释放确切的地址,它传递了传递地址之前 4 个字节的地址。

在此实现中,将使用 new[] 分配的数组传递给常规 delete 会导致调用第一个元素的单个析构函数,然后将错误的地址传递给释放函数,破坏堆。不要这样做!

其他(所有好的)答案中提到的 not 的根本原因是数组 - 从 C 继承 - 从来都不是“第一-class" C++ 中的东西。

它们具有原始的 C 语义但不具有 C++ 语义,因此具有 C++ 编译器和运行时支持,这将允许您或编译器运行时系统使用指向它们的指针执行有用的操作。

事实上,C++ 不支持它们,以至于指向事物数组的指针看起来就像指向单个事物的指针。特别是,如果数组是该语言的适当部分,即使作为库的一部分,如字符串或向量,也不会发生这种情况。

C++ 语言的这种缺点是由于 C 的这种继承而发生的。它仍然是语言的一部分 - 即使我们现在有 std::array 用于定长数组并且(一直有)std::vector 用于可变长度数组——主要是出于兼容性目的:能够使用 C 语言互操作从 C++ 调用操作系统 API 和以其他语言编写的库。

而且...因为有大量的书籍和网站以及class教室在他们的 C++ 教学法中很早就教授数组,因为 a)能够尽早编写 useful/interesting 个示例,实际上调用 OS API,当然是因为 b) 的强大功能“这就是我们一直这样做的方式”。

通常,C++ 编译器及其关联的 运行 时间构建在平台的 C 运行 时间之上。特别是在这种情况下,C 内存管理器。

C 内存管理器允许您在不知道其大小的情况下释放内存块,但是没有标准的方法从 运行time 获取块的大小并且不能保证实际分配的块正是您请求的大小。它可能会更大。

因此 C 内存管理器存储的块大小不能有效地用于启用更高级别的功能。如果更高级别的功能需要有关分配大小的信息,那么它必须自己存储它。 (对于具有析构函数的类型,C++ delete[] 确实需要这个,对于每个元素 运行 它们。)

C++ 也有一种“你只为你使用的东西付费”的态度,为每个分配存储一个额外的长度字段(与底层分配器的簿记分开)不适合这种态度。

由于在 C 和 C++ 中表示未知(在编译时)大小的数组的正常方法是使用指向其第一个元素的指针,因此编译器无法区分单个对象分配和数组基于类型系统的分配。所以就留给程序员去区分了。

举个例子可能会有帮助。当您分配一个 C 风格的对象数组时,这些对象可能有自己的需要调用的析构函数。 delete 运算符不会那样做。它适用于容器对象,但不适用于 C 风格的数组。他们需要 delete[]

这是一个例子:

#include <iostream>
#include <stdlib.h>
#include <string>

using std::cerr;
using std::cout;
using std::endl;

class silly_string : private std::string {
  public:
    silly_string(const char* const s) :
      std::string(s) {}
    ~silly_string() {
      cout.flush();
      cerr << "Deleting \"" << *this << "\"."
           << endl;
      // The destructor of the base class is now implicitly invoked.
    }

  friend std::ostream& operator<< ( std::ostream&, const silly_string& );
};

std::ostream& operator<< ( std::ostream& out, const silly_string& s )
{
  return out << static_cast<const std::string>(s);
}

int main()
{
  constexpr size_t nwords = 2;
  silly_string *const words = new silly_string[nwords]{
    "hello,",
    "world!" };

  cout << words[0] << ' '
       << words[1] << '\n';

  delete[] words;

  return EXIT_SUCCESS;
}

该测试程序明确检测了析构函数调用。这显然是一个人为的例子。一方面,程序不需要在终止并释放所有资源之前立即释放内存。但它确实展示了发生了什么以及发生了什么顺序。

有些编译器,例如 clang++,足够聪明,如果您在 delete[] words; 中遗漏了 [],它会警告您,但如果您强制它编译有错误的代码,你会得到堆损坏。

封面故事是 delete 是必需的 因为 C++ 与 C 的关系

new 运算符可以动态分配几乎任何 object 类型的 object。

但是,由于 C 的继承,指向 object 类型的指针在两个抽象之间是不明确的:

  • 是单个 object 的位置,并且
  • 作为动态数组的基础。

deletedelete[] 的情况就是由此而来。

然而,事实并非如此,因为尽管上述观察结果是正确的,但可以使用单个 delete 运算符。从逻辑上讲,不需要两个运算符。

这是非正式的证明。 new T 运算符调用(单个 object 情况)可以隐式地表现得好像它是 new T[1]。也就是说,每个 new 总是可以分配一个数组。当没有提到数组语法时,可能会暗示将分配一个 [1] 的数组。然后,只需要存在一个 delete 的行为就像今天的 delete[].

为什么不遵循该设计?

我认为这可以归结为通常的情况:这是一只献给效率之神的山羊。当你用new []分配一个数组时,会为meta-data分配额外的存储空间来跟踪元素的数量,这样delete []就可以知道有多少元素需要迭代销毁。当您用 new 分配单个 object 时,不需要这样的 meta-data。 object 可以直接在来自底层分配器的内存中构造,无需任何额外的 header.

就 run-time 成本而言,这是“不为未使用的东西付费”的一部分。如果您正在分配单个 objects,您不必为那些 objects 中的任何表示开销“支付”来处理指针引用的任何动态 object 的可能性可能是一个数组。但是,您有责任按照使用数组 new 分配 object 并随后将其删除的方式对该信息进行编码。

delete是一个操作符,用于销毁由new表达式生成的数组和非数组(指针)对象。

可以通过删除运算符或删除[]运算符来使用 一个新的运算符用于动态内存分配,它将变量放在堆内存上。 这意味着 Delete 运算符从堆中释放内存。 指向对象的指针未被销毁,指针指向的值或内存块被销毁。 删除运算符有一个没有 return 值的 void return 类型。