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

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

本文关键字:C++ 存在 语法 delete 为什么      更新时间:2024-09-27

每当有人在这里问delete[]问题时,总会有一个非常笼统的"这就是C++的做法,使用delete[]"的回答。 来自普通的 C 背景,我不明白为什么需要不同的调用。

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

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

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

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

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

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

始终deletenew的,永远delete[]new[]的。但在现代C++中,通常不再使用newnew[]。使用std::make_uniquestd::make_sharedstd::vector或其他更具表现力和更安全的替代品。

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

为了详细说明François Andrieux的答案提到的未指定开销,您可以看到我对这个问题的回答,其中我研究了特定实现的作用(Visual C++ 2013,32位)。其他实现可能会也可能不会做类似的事情。

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

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

在其他(所有好的)答案中没有提到的是,造成这种情况的根本原因是数组 - 从 C 继承 - 在C++中从来都不是"一流"的东西。

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

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

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

和。。。因为有很多书籍、网站和教室在他们C++教学法的早期就教授数组,因为 a) 能够尽早编写有用/有趣的示例,这些示例实际上确实调用了操作系统 API,当然也因为 b) "这就是我们一直这样做的方式"的强大功能。

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

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

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

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

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

封面故事是,由于C++与C的关系,delete是必需的。

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

但是,由于 C 传统,指向对象类型的指针在两个抽象之间是不明确的:

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

deletedelete[]的情况正好由此而来。

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

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

为什么没有遵循这种设计?

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

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

一个例子可能会有所帮助。 分配 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 是一个运算符,用于销毁由新表达式生成的数组和非数组(指针)对象。

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