C++不会告诉您动态数组的大小。但是为什么?

C++ doesn't tell you the size of a dynamic array. But why?

本文关键字:为什么 数组 动态 C++      更新时间:2023-10-16

我知道C++中没有办法获得动态创建的数组的大小,例如:

int* a;
a = new int[n];

我想知道的是:为什么?人们是在C++规范中忘记了这一点,还是有技术原因?

这些信息不是存储在某个地方吗?毕竟,命令

delete[] a;

似乎知道它必须释放多少内存,所以在我看来,delete[]有某种方法可以知道a的大小。

它是对"不要为你不需要的东西买单"这一基本规则的继承。在您的示例中,delete[] a;不需要知道数组的大小,因为int没有析构函数。如果你写过:

std::string* a;
a = new std::string[n];
...
delete [] a;

然后delete必须调用析构函数(并且需要知道要调用多少)——在这种情况下,new必须保存该计数。然而,考虑到它不需要在所有情况下都保存,Bjarne决定不允许访问它。

(事后看来,我认为这是一个错误…)

当然,即使使用int也必须了解所分配内存的大小,但是:

  • 出于对齐和方便的原因,许多分配器将大小四舍五入到一些方便的倍数(比如64字节)。分配器知道一个块有64字节长,但它不知道这是否是因为n是1。。。或16。

  • C++运行时库可能无法访问所分配块的大小。例如,如果newdelete在后台使用mallocfree,则C++库无法知道malloc返回的块的大小。(当然,通常newmalloc都是同一个库的一部分,但并不总是如此。)

一个根本原因是指向动态分配的T数组的第一个元素的指针与指向任何其他T的指针之间没有区别。

考虑一个虚构的函数,它返回指针指向的元素数。
让我们称之为"尺寸"。

听起来真的很好,对吧?

如果不是因为所有指针都是相等的:

char* p = new char[10];
size_t ps = size(p+1);  // What?
char a[10] = {0};
size_t as = size(a);     // Hmm...
size_t bs = size(a + 1); // Wut?
char i = 0;
size_t is = size(&i);  // OK?

您可以争辩说,第一个应该是9,第二个10,第三个9和最后一个1,但要实现这一点,您需要在每个对象上添加一个"大小标签"
char将需要64位机器上的128位存储(因为对齐)。这是必需的十六倍
(上面,十个字符的数组a至少需要168个字节。)

这可能很方便,但也贵得令人无法接受。

当然,如果参数真的是默认operator new动态分配的第一个元素的指针,那么您可以设想一个定义良好的版本,但这并不像人们想象的那么有用。

您是对的,系统的某些部分必须了解一些关于大小的信息。但是获取这些信息可能不在内存管理系统的API中(想想malloc/free),并且您请求的确切大小可能不知道,因为它可能已经四舍五入。

您经常会发现内存管理器只会以特定的倍数分配空间,例如64字节。

因此,您可以请求新的int[4],即16个字节,但内存管理器将为您的请求分配64个字节。要释放这个内存,它不需要知道你需要多少内存,只需要知道它已经为你分配了一个64字节的块。

下一个问题可能是,它是否可以不存储请求的大小?这是一项额外的开销,并不是每个人都愿意为此付出代价。例如,Arduino Uno只有2k的RAM,在这种情况下,每次分配4个字节突然变得重要起来。

如果您需要该功能,那么您有std::vector(或等效的),或者您有更高级别的语言。C/C++的设计目的是使您能够尽可能少地使用开销,这就是一个例子。

有一个奇怪的operator delete过载的情况,我发现它的形式是:

void operator delete[](void *p, size_t size);

参数size似乎默认为void*p所指向的内存块的大小(以字节为单位)。如果这是真的,那么至少有理由希望它有一个由operator new调用传递的值,因此,只需要除以sizeof(type)就可以传递存储在数组中的元素数量。

至于你问题的"为什么"部分,马丁的"不要为你不需要的东西买单"规则似乎是最合乎逻辑的。

无法知道如何使用该数组。分配大小不一定与元素编号匹配,因此不能仅使用分配大小(即使它可用)。

这是C++以外的其他语言中的一个深层次缺陷。您使用std::vector实现了所需的功能,但仍然保留对数组的原始访问权限。保留原始访问对于任何实际需要做一些工作的代码来说都是至关重要的。

很多时候,您将对数组的子集执行操作,当语言中内置了额外的book-keeping时,您必须重新分配子数组,并将数据复制出来,以便使用API(需要托管数组)对其进行操作。

只需考虑一下对数据元素进行排序的老生常谈的情况。如果您有托管数组,那么在不复制数据的情况下就不能使用递归来创建新的子数组以递归传递。

另一个例子是FFT,它递归地处理从2x2"蝴蝶"开始的数据,并返回到整个数组。

要修复托管数组,您现在需要"其他东西"来修补此缺陷,而"其他事情"被称为"迭代器"。(您现在拥有托管数组,但几乎从不将它们传递给任何函数,因为90%的时间都需要迭代器。)

new[]分配的数组的大小没有明显地存储在任何地方,因此您无法访问它。new[]运算符不返回数组,只返回指向数组第一个元素的指针。如果您想知道动态数组的大小,您必须手动存储它,或者使用库中的类,如std::vector