delete[]运算符的时间复杂性

Time complexity of delete[] operator

本文关键字:时间复杂性 运算符 delete      更新时间:2023-10-16

delete[]运算符的时间复杂性是什么?

我的意思是它是如何实现的——它是否迭代数组中的所有元素,并为每个元素调用析构函数?

该运算符对基元类型(int等)和用户定义的类型执行相同操作吗?

::operator delete[]在cplusplus.com上被记录为:

operator delete[]可以作为正则函数显式调用,但在C++中,delete[]是一个具有非常特定行为的运算符:一个具有delete[]运算符的表达式,首先为数组中的每个元素调用适当的析构函数(如果这些元素属于类类型),然后调用函数operator delete[](即此函数)来释放存储。

因此析构函数被调用n次(每个元素一次),然后释放内存的"函数"被调用一次。

请注意,每次销毁可能需要与其他销毁不同的时间(甚至复杂程度)。一般来说,大多数破坏都很快,并且具有相同的复杂性。。。。但如果每个被破坏的元素都是一个复杂的树、节点或图,情况就不是这样了

对于像int这样的基元类型,int的假想析构函数是no-op。编译器可能会对此进行优化(如果需要的话)。

您应该查看真正的C++11标准,或者至少是其最新的n3337工作草案,该草案在n3337:第110页§5.3.5.6中写道(感谢Matteo Italia在评论中指出)

如果删除表达式的操作数值不是空指针值,则delete表达式

如果您使用并足够信任GCC 4.8或更好的版本,您可以使用带有-fdump-tree-phiopt-fdump-tree-all选项的g++编译器(注意,它们正在转储大量文件!)或MELT插件来查询某些示例的中间Gimple表示。或者使用-S -fverbose-asm来获得汇编代码。您还想添加优化标志,如-O1-O2。。。

NB:IMHO,cppreference.com也是一个关于C++的有趣网站,请参阅delete(如Cubbi所评论)

deletedelete[]的实现由两个阶段组成:

  1. 对析构函数的递归调用(如果有的话)
  2. 已删除对象的内存释放

更不用说对析构函数的调用链了,它的复杂性基本上由您决定,我们只剩下如何释放内存来考虑。

C++规范没有涉及第二点。因此,任何编译器套件/OS都可以自由地采用自己的策略。

常见的内存分配/释放策略是在需要时从OS分配整个内存页,然后在每个new/new[]返回适当大小的块,然后将其长度和属性作为页眉/页脚存储在页面内。对应的delete/delete[]可以简单到将同一块标记为"空闲",这显然是O(1)。

如果内存释放的复杂性是O(1),那么delete的复杂性本质上由对析构函数的调用控制。默认实现(几乎)什么都不做,对于单个调用是O(1),因此是一个整体O(n),其中n是调用的总数(例如,如果被析构函数的对象有两个字段调用了析构函数,则为n = 1 (object) + 2 (o. fields) = 3)。

把所有的部分放在一起:你可以通过在析构函数(可以由你编写)中执行操作来任意增加复杂性,但你不能比O(n)(上一段中定义的n)"执行得更好">正式正确的表述方式是:"delete的复杂性是Omega(n)">


请允许我在这一点上有点非正式

对于类类型,理论复杂度为O(n)。为每个元素调用析构函数。当然,这取决于实现是否遵守可观察的行为,因此,如果析构函数是无操作的,或者行为与仅将整个块标记为已释放的行为相同,则复杂性可能仅为O(1)

对于基元类型,编译器可能会一次释放整个内存块,因此复杂性为O(1)

delete[]运算符的时间复杂度是多少?

所需的时间量当然是由实现定义的然而,运算符仅适用于指向1D数组的指针,因此它是O(1)

我的意思是它是如何实现的——它是否迭代所有元素并为每个元素调用析构函数?


前提是它只在精确指针上调用,该指针被分配了一个使用new[]创建的内存。对于基元类型,没有用户定义的析构函数。

这个运算符对基元类型(int等)和用户定义的类型?

这种比较是不公平的,因为基元类型没有用户定义的析构函数,它们不能是polymorhpic
例如,多态类上的delete[]是未定义的行为。即

Base* p1 = new Derived, *p2 = new Derived[2];
delete p1; // ok
delete[] p2;  // bad