为什么运行环境不能决定是应用delete还是delete[]而需要程序员来决定?(这是一个关于IT技术的问题)

18

我读到过delete[]运算符是必需的,因为运行时环境不能保留分配块是否为需要调用析构函数的对象的信息,但它确实可以保留分配块存储在内存中的位置和块的大小。
只需要一个额外的元数据位来记住是否需要在delete时调用析构函数,那么为什么不这样做呢?

我很确定有一个很好的解释,我不怀疑它,我只想知道它。


2
除非标准明确说明分配的大小存储在某个地方(我怀疑不是这样),否则不总是需要存储分配的大小。以以下示例为例:int * p = new int; 定义删除此内容的唯一方法是调用 int * 上的 delete。因此,从您知道它是 int 的事实中隐含地知道了大小。(请注意,在 void * 上调用 delete 是 UB:https://dev59.com/questions/gHNA5IYBdhLWcg3wfN6O#941959)。 - Thomas Eding
3
@trinithis -- deletedelete[] 两个运算符都接受 void* 类型的参数,它们并不知道具体的类型。 - Gene Bushuyev
2
@GeneBushuyev,您误解了(可能由用户提供的)delete操作符实现的签名和操作符的调用。标准允许实现使用可选的大小参数,该参数设置为已删除对象的大小,这个大小可以从静态类型信息中推断出来(因此对于 void *UB),或者是通过多态对象的vtable在析构函数中传递。 - Simon Richter
@Simon Richter -- 标准的签名中没有可选的大小参数,请参阅标准的18.6节。UB与void*参数无关,标准解释了如果删除的指针不是由适当的newnew[]创建的有效对象,则编译器和运行时不需要验证或执行任何特殊操作。 - Gene Bushuyev
整个线程都被不清晰的问题和不精确的答案搞糊涂了。人们混淆了delete-expression和operator delete。而且我仍然不知道OP想要知道什么。 - Gene Bushuyev
显示剩余6条评论
4个回答

10

我认为原因在于C++不会强制你做任何你不想做的事情。如果加入额外的元数据,但有人没有使用它,这将强制施加额外的开销,与C ++语言设计目标相矛盾。

当你需要描述的功能时,C++确实提供了一种方法。它被称为std::vector,你应该几乎总是优先选择它、另一种类型的容器或智能指针,而不是裸指针的newdelete


可能吧,虽然有人会争论C中的数组语义已经出现了问题,所以这只是顺理成章的事情。我担心忽略大小是一种过早的微观优化。 - Matthieu M.
我认为这不是问题所在。 - Gene Bushuyev
关于 std::vector……并不完全是因为它可以动态增长。一个更简单的容器也可以工作。尽管如此,已经足够接近了。 - Thomas Eding

4
C++可以让你尽可能高效,所以如果他们必须要跟踪块中的元素数量,那么每个块将会额外使用4个字节。这对许多人来说可能很有用,但对于那些不介意使用[]的人来说,它也会阻碍总体效率。
这类似于C++和Java之间的差异。Java可以更快速地进行编程,因为您无需担心垃圾回收,但是如果正确编写程序,C++可以更加高效,并且使用较少的内存,因为它不需要存储任何变量,并且您可以决定何时删除内存块。

2
现在去查看任何通用的堆内存分配器,并查看每个分配的开销有多少。我不会对每个分配的16-64字节这样的数字感到惊讶,因此大小为4-8字节将不会被注意到。而且,可能大多数分配器都会跟踪分配情况。 - Gene Bushuyev

4
基本上,这取决于语言设计不想对实现者施加太多限制。许多C++运行时使用malloc()作为::operator new(),并且使用free()作为::operator delete()(或多或少)。标准的malloc/free不提供记录元素数量所需的簿记,并且无法在free时间确定malloc的大小。在每个单独的对象之间添加另一层内存操作,从C/C++的角度来看,这是一个相当大的复杂性/抽象度的跃升。此外,将这种开销添加到每个对象中会破坏一些内存管理方法,因为它们是根据对象的大小进行设计的。

3
这里有两件需要澄清的事情。
第一:假设malloc保留了你要求的精确大小是错误的。实际上,malloc只关心提供足够大的内存块。尽管出于效率原因它可能不会过度分配,但仍然可能给您一个“标准”大小的内存块,例如一个2^n字节的块。因此,实际分配的对象数量(也就是实际大小)实际上是未知的。
第二:所需的“额外位”
确实,用于确定给定对象是否属于数组的信息只需要一个额外的位。从逻辑上讲没错。
但从实现的角度来看:在哪里放置这个位呢?
为对象本身分配的内存应该不会被触及,毕竟对象正在使用它。那么呢?
在某些平台上,这可以保存在指针本身中(某些平台忽略了一部分位),但这并不是可移植的;
所以它需要额外的存储空间,至少一个字节,除非由于对齐问题,它可能会达到8个字节。 演示:(如下所述,不太令人信服)
// A plain array of doubles:
+-------+-------+-------
|   0   |   1   |   2   
+-------+-------+-------

// A tentative to stash our extra bit
+-------++-------++-------++
|   0   ||   1   ||   2   ||
+-------++-------++-------++

// A correction since we introduced alignment issues
// Note: double's aligment is most probably its own size
+-------+-------+-------+-------+-------+-------
|   0   |  bit  |   1   |  bit  |   2   |  bit
+-------+-------+-------+-------+-------+-------

嗯!

编辑

因此,在大多数平台上(地址很重要的地方),您需要“扩展”每个指针,并实际上将它们的大小加倍(对齐问题)。

所有指针都增加一倍大小以便您可以存储额外的位,这样做是否可行?对于大多数人来说,我想是可以的。但是,C++不是为大多数人设计的,它主要是为那些关心性能的人设计的,无论是速度还是内存,因此这是不可接受的。

编辑结束

那么正确的答案是什么?正确的答案是恢复类型系统丢失的信息是昂贵的。不幸的是。


1
你不需要为每个数组元素存储一个位,因为删除指向“内部”数组元素的指针是无效的。只有对于第一个元素,您需要确定是否跟随整个数组。 - sth
@sth: 是的,这个演示有缺陷...我对我的ASCII图形付出了很多爱 :( 我已经纠正了,感谢您的注意。 - Matthieu M.
你有检查过通用堆分配器的开销吗? - Gene Bushuyev
@GeneBushuyev:是哪种分配器?如果我们以jemalloc为例(图4和相关文本),浪费的空间是以位为单位衡量的。特殊处理小型分配以提高效率相对容易。 - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接