【问题标题】:Why does the delete[] syntax exist in C++?为什么 C++ 中存在 delete[] 语法?
【发布时间】:2021-08-07 05:12:31
【问题描述】:

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

使用malloc()/free(),您的选择是获取指向连续内存块的指针并释放连续内存块。实现中的某些东西会根据基地址知道您分配的块大小,以便您必须释放它。

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

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

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

【问题讨论】:

  • 如果你想测试你的内存分配和释放看看那些疯狂的理论是否正确,你可以使用 Valgrind 来看看实际发生了什么。我怀疑重载删除问题比目前答案中描述的要多,但我没有专业知识。
  • 相关问题:How does delete[] know it's an array?,特别注意this answer

标签: c++ memory-management syntax language-lawyer standards


【解决方案1】:

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

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

总是delete 你是什么new 并且总是delete[] 你是什么new[]。但在现代 C++ 中,newnew[] 通常不再使用。使用std::make_uniquestd::make_sharedstd::vector 或其他更具表现力和更安全的替代方案。

【讨论】:

  • 哇,这是一个快速的反应,感谢分配功能的提示。令人惊讶的是,C++ 中的答案经常是“不要使用那个关键字”,使用 std::someWeirdFunctionIntroducedInC++>=11()
  • @awiebe C++ 为您提供了尽可能接近硬件工作的工具。但这些工具通常功能强大且生硬,使其危险且难以有效使用。因此,它还通过标准库提供了与硬件稍微远离但非常安全和简单的工具。这就是为什么您了解了这么多功能,但被告知不要使用它们的原因。因为除非您正在做一些非常独特或奇怪的事情,否则那些低级工具是没有用的。更方便的功能通常就可以了。
  • @awiebe 大多数时候你是对的,如果有一个整洁的标准库功能来替换内置机制,它来自 C++11 或更高版本。 C++11 基本上彻底改变了语言,允许以前无法实现的标准库功能。 C++11 和以前的版本之间的差异是如此之大,以至于它们基本上可以被认为是两种不同的语言。在学习 C++ 时,请注意区分针对 C++03 及更早版本的教育材料与针对 C++11 及更高版本的材料。
  • @awiebe,还请注意,像new 这样的低级机制的存在允许大多数标准库(和其他库)用纯 C++ 编写(某些部分可能需要编译器支持)。所以建议也可以是“只使用这些来构建更高级别的抽象”。
  • @FrançoisAndrieux:Choice-of-words-nitpick “...那些工具通常强大而生硬...”:我实际上将它们视为超级锋利的外科工具:您可以得到什么你想要的,你想要的。但是缝合或清理外科手术需要同等的技能和材料,创可贴做不到。
【解决方案2】:

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

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

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

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

【讨论】:

    【解决方案3】:

    其他(所有好的)答案中没有提到的事情是,其根本原因是数组 - 从 C 继承而来 - 在 C++ 中从来都不是“一流”的东西。

    它们具有原始的 C 语义,但没有 C++ 语义,因此支持 C++ 编译器和运行时,这将让您或编译器运行时系统使用指向它们的指针来做有用的事情。

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

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

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

    【讨论】:

    • 这个答案提出了许多完全不正确的主张,显然是基于不知道 C 和 C++ 都支持“指向数组的指针”类型。不是缺乏表达指向数组的指针的能力,而是在实践中没有使用这种能力。
    • pointer-to-array 会立即衰减为pointer-to-element,不过,这就是它的使用方式。不是吗?有多少 C++(或 C)函数/方法签名采用指向数组的类型?没有人,但没有人,教它,也不是它是如何使用的。你不同意吗?例如,告诉我在 Unix/Linux API 中,指向数组的指针用于函数签名中的位置,而不是文档假定为数组的裸指针? @BenVoigt
    • Effective C++ - 第 3 版(Meyers,2008 年)和 More Effective C++(Meyers,1996 年)均未提及指向数组的类型。我可以继续阅读我图书馆里的书,但是……我真的不在乎。关键不是在某个时候——甚至最初——语言在技术上是否具有这种能力。关键是没有人使用过它。曾经。我在回答中没有提到它并不意味着我不知道它。只是我知道这是编译器编写者知识库的无用痕迹。它从未使用过,从未教过。
    • 这里的核心问题是指向数组和引用数组的类型真的很难阅读,所以人们养成了不使用它们的习惯,这导致知识落到了一边。使用它们的最简单方法是使用模板或decltype,使用它们通常很快就会演变成a nigh-unreadable messcreate() 这里已经够糟糕了(以多种方式),想象一下一个函数,它接受两个数组的指针并返回一个指向不同类型数组的指针。
    • 由于 new[] 的一个常见用途是在编译时分配一个大小未知的数组,因此 C 的指向数组的指针并没有多大帮助。
    【解决方案4】:

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

    C 内存管理器允许您在不知道其大小的情况下释放一块内存,但是没有标准的方法可以从运行时获取块的大小,也不能保证实际分配的块是完全正确的您要求的尺寸。它可能会更大。

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

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

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

    【讨论】:

      【解决方案5】:

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

      new 运算符可以生成几乎任何对象类型的动态分配对象。

      但是,由于 C 的传统,指向对象类型的指针在两个抽象之间是模棱两可的:

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

      deletedelete[] 的情况就是如此。

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

      这是非正式的证明。 new T 运算符调用(单对象情况)可以隐含地表现得好像它是 new T[1]。也就是说,每个new 总是可以分配一个数组。如果未提及数组语法,则可能暗示将分配 [1] 的数组。然后,只需要存在一个 delete,其行为类似于今天的 delete[]

      为什么不遵循这个设计?

      我认为这归结为通常:这是一只被献给效率之神的山羊。当您使用new [] 分配数组时,会为元数据分配额外的存储空间以跟踪元素的数量,以便delete [] 可以知道需要迭代多少元素才能销毁。当您使用new 分配单个对象时,不需要这样的元数据。该对象可以直接在来自底层分配器的内存中构造,无需任何额外的标头。

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

      【讨论】:

        【解决方案6】:

        一个例子可能会有所帮助。当您分配一个 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; 中遗漏了[],会很聪明地警告你,但如果你强制它编译有错误的代码,你会得到堆损坏。

        【讨论】:

          【解决方案7】:

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

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2020-05-05
            • 1970-01-01
            • 1970-01-01
            • 2012-06-15
            • 1970-01-01
            • 2021-09-09
            • 2010-12-29
            相关资源
            最近更新 更多