delete[] 如何知道它操作的是一个数组?

148

好的,我认为我们都同意以下代码发生的情况是未定义的,具体取决于传递了什么参数。

void deleteForMe(int* pointer)
{
     delete[] pointer;
}

指针可能是各种不同的东西,因此在其上执行无条件的delete[]是未定义的。但是,让我们假设我们确实传递了一个数组指针,

int main()
{
     int* arr = new int[5];
     deleteForMe(arr);
     return 0;
}

我的问题是,在指针 数组的情况下,谁知道这一点呢?我的意思是,从语言/编译器的角度来看,它不知道 arr 是数组指针还是指向单个 int 的指针。甚至它也不知道 arr 是否是动态创建的。然而,如果我做以下操作,

int main()
{
     int* num = new int(1);
     deleteForMe(num);
     return 0;
}

操作系统足够聪明,只会删除一个 int 而不会像删除非 '\0' 结尾的字符串时那样继续删除内存中该点后面的所有内容(这种情况下,程序会一直执行到碰到 0 字符)。

那么是谁来记住这些事情?操作系统在后台保留某种记录吗?(我知道,我之前说过这种情况是未定义的,但实际上,“疯狂删除”这种情况并没有发生,因此在实践中,某个人在记住这些事情。)


8
它从删除操作后的方括号中知道。 - JoelFan
2
指针不是数组。它们通常指向数组的第一个元素,但这是不同的东西。 - Aaron McDaid
16个回答

114
到目前为止,答案似乎没有解决一个问题:如果运行时库(实际上不是操作系统)可以跟踪数组中的元素数量,那么为什么我们还需要delete[]语法呢?为什么不能使用单个delete形式来处理所有删除操作?
这个问题的答案追溯到C++作为与C兼容的语言(它不再真正追求这一点)的根源。 Stroustrup的理念是程序员不应该为他们没有使用的任何特性付费。如果他们没有使用数组,则他们不应该为每个分配的内存块承担对象数组的成本。
也就是说,如果你的代码只是简单地执行以下操作:
Foo* foo = new Foo;

如果为foo分配的内存空间不需要包含支持Foo数组所需的任何额外开销。

由于只有数组分配被设置为带有额外的数组大小信息,因此在删除对象时需要告诉运行时库查找该信息。这就是为什么我们需要使用

delete[] bar;

而不是仅仅

delete bar;

如果bar是一个指向数组的指针。

对于我们大多数人(包括我自己),现在这种对于一些额外字节内存的吹毛求疵似乎有点陈旧。但是,在某些情况下,从可能非常高数量的内存块中节省几个字节仍然很重要。


22
现代的程序员已经不再介意多占用几个字节的内存了。幸运的是,对于这些人来说,使用裸数组(bare arrays)也开始变得陈旧了,他们可以使用向量(vector)或 boost::array,而且永远不必再考虑 delete[] 了 :-) - Steve Jessop

105

编译器并不知道它是一个数组,而是信任程序员。使用delete[]删除指向单个int的指针将导致未定义行为。即使第二个main()例子不会立即崩溃,它也是不安全的。

编译器确实需要以某种方式跟踪需要删除的对象数量。可能通过过度分配来存储数组大小。有关更多详细信息,请参见C++ Super FAQ


16
实际上,使用delete[]来删除使用new创建的内容存在漏洞。http://taossa.com/index.php/2007/01/03/attacking-delete-and-delete-in-c/#comment-63884 - Rodrigo
25
@Rodrigo,你评论中的链接已经失效,但幸运的是wayback machine在http://replay.web.archive.org/20080703153358/http://taossa.com/index.php/2007/01/03/attacking-delete-and-delete-in-c 上保存了一份副本。该文章讲述了C语言中的“delete”和“free”函数以及如何避免它们造成的错误。 - David Gardner

31

是的,操作系统会把一些东西放在“后台”中。例如,如果你运行

int* num = new int[5];

操作系统可以分配额外的4个字节,在所分配的内存的前4个字节中存储分配的大小,并返回一个偏移指针(即,它分配内存空间1000到1024,但返回的指针指向1004,而位置1000-1003存储了分配的大小)。然后,当调用删除操作时,它可以查看传递给它的指针之前的4个字节来找到分配的大小。

我确定还有其他跟踪分配大小的方法,但那是其中一种选择。


31
一般来说这是一个有价值的观点,但通常情况下,存储这些元数据的责任落在语言运行时上,而非操作系统。+1 - sharptooth
数组的大小或者定义了数组的对象大小会发生什么?如果你在这个对象上执行一个sizeof操作,它是否会显示额外的4个字节? - Shree
3
不,sizeof只显示数组的大小。如果运行时选择使用我描述的方法来实现,那只是一种实现细节,从用户的角度来看,应该被掩盖起来。指针前面的内存不属于用户,并且不会被计算在内。 - bsdfish
2
更重要的是,sizeof在任何情况下都不会返回动态分配数组的真实大小。它只能返回在编译时已知的大小。 - bdonlan
能否使用这个元数据在for循环中准确地遍历数组?例如:`for(int i = 0; i < *(arrayPointer - 1); i++){}` - sion
1
@Sam 虽然在某些环境中可能有效,但这只是一种实现细节,不能依赖它。我怀疑实际的数组长度不会被存储,最有可能的是为支持数组分配的内存块的大小。 - Aidiakapi

13
这与这个问题非常相似,其中包含了您在寻找的许多细节。
但是可以说,跟踪数组大小并不是操作系统的工作。实际上,运行时库或底层内存管理器将跟踪数组的大小。这通常是通过预先分配额外的内存并在该位置(大多数使用头结点)存储数组大小来完成的。
您可以通过执行以下代码在某些实现上查看此内容:
int* pArray = new int[5];
int size = *(pArray-1);

这个会有效吗?在Windows和Linux上我们没能让它工作。 - buddy
1
尝试使用 size_t size = *(reinterpret_cast<size_t *>(pArray) - 1) 替代。 - user6377043

9

deletedelete[]都可以释放分配的内存(指向的内存),但区别在于,对数组使用delete不会调用每个元素的析构函数。

无论如何,混合使用new/new[]delete/delete[]可能导致未定义行为。


1
清晰、简短且最有用的答案! - GntS

6

它不知道它是一个数组,所以你必须提供delete[]而不是普通的delete


6

我有一个类似的问题。在C语言中,您可以使用malloc()(或另一个类似的函数)分配内存,并使用free()释放内存。只有一个malloc()函数,它仅分配一定数量的字节。只有一个free()函数,它仅以指针作为参数。

那么为什么在C语言中,您可以直接将指针交给free()函数,但在C++语言中,您必须告诉它是数组还是单个变量呢?

答案与类析构函数有关。

如果您分配了一个MyClass类的实例...

classes = new MyClass[3];

使用delete删除对象时,只会调用第一个MyClass实例的析构函数。如果使用delete[],则可以确保所有数组中的实例都会调用析构函数。

这是一个重要的区别。如果你只是使用标准类型(例如int),你不会真正遇到这个问题。此外,你应该记住,在new[]上使用delete和在new上使用delete[]的行为是未定义的--在每个编译器/系统上可能不会以相同的方式工作。


我认为这对于标准类型来说不是无关紧要的问题,因为它不仅涉及解构,还涉及释放内存。你最终会得到元素1+的内存泄漏。 - Andrew

3

运行时负责内存分配,就像在标准C中使用free删除使用malloc创建的数组一样。我认为每个编译器都有不同的实现方式。一种常见的方法是为数组大小分配额外的单元。

然而,运行时并不能聪明地检测到它是一个数组还是指针,你必须告诉它,如果你犯了错误,你要么不能正确地删除(例如,使用ptr而不是array),要么会取一个无关的值作为大小并造成严重的损害。


3

编译器的一种方法是分配更多的内存,并在头元素中存储元素数量。

以下是一个演示如何实现的示例:

int* i = new int[4];

编译器将分配 sizeof(int)*5 个字节。
int *temp = malloc(sizeof(int)*5)

4存储在第一个sizeof(int)字节中

*temp = 4;

并设置i

i = temp + 1;

所以i指向的是由4个元素组成的数组,而不是5个。

而且

delete[] i;

将按照以下方式进行处理。
int *temp = i - 1;
int numbers_of_element = *temp; // = 4
... call destructor for numbers_of_element elements if needed
... that are stored in temp + 1, temp + 2, ... temp + 4
free (temp)

1

同意编译器不知道它是否为数组。这取决于程序员。

编译器有时会通过过度分配足够的存储空间来跟踪需要删除多少个对象,但并非总是必要的。

有关在分配额外存储空间时的完整规范,请参阅C++ ABI(编译器的实现方式):Itanium C++ ABI: Array Operator new Cookies


我只希望每个编译器都遵循一些C++的文档化ABI。感谢链接加一,我之前访问过。谢谢。 - Don Wakefield

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