C ++没有告诉你动态数组的大小。

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

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

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

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

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

1
delete[] a;

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


这是"不要为你不需要的东西付钱"这一基本原则的后续行动。在您的示例中,delete[] a;不需要知道数组的大小,因为in t没有析构函数。如果你写过:

1
2
3
4
std::string* a;
a = new std::string[n];
...
delete [] a;

然后,delete必须调用析构函数(并且需要知道要调用多少个析构函数),在这种情况下,new必须保存该计数。然而,考虑到它不需要在任何情况下保存,比亚恩决定不给它访问权。

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

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

  • 为了对齐和方便起见,许多分配器将大小归整为一些方便的倍数(比如64字节)。分配器知道一个块有64个字节长,但不知道是不是因为n是1…或16。

  • C++运行时库可能无法访问所分配的块的大小。例如,如果EDCOX1,2,EDCX1,1,则使用EDOCX1,7,EDCX1,8,ED,则C++库没有办法知道EDCOX1,7,7所返回的块的大小。(通常情况下,newmalloc都是同一个库的一部分,但并不总是如此。)


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

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

听起来不错,对吧?

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

1
2
3
4
5
6
7
8
9
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,但要做到这一点,你需要在每个对象上添加一个"尺寸标签"。在64位机器上,char需要128位存储(因为对齐)。这是必要的16倍。(上面,十字符数组a至少需要168字节。)

这可能很方便,但也很贵。

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


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


例如,您经常会发现内存管理器只在特定的多个64字节中分配空间。

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

下一个问题可能是,它不能存储请求的大小吗?这是一个额外的管理费用,不是每个人都准备支付。例如,一个arduino uno只有2K的RAM,在这种情况下,每个分配的4个字节突然变得非常重要。

如果你需要这个功能,那么你有STD::向量(或等效),或者你有高级语言。设计C/C++是为了让你能在你选择使用的时候尽可能少的开销,这是一个例子。


我发现有一个奇怪的情况,即operator delete过载,其形式如下:

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

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

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


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

这是其他语言中的一个深层缺陷,而不是C++。通过STD::vector实现了您希望的功能,但仍然保留对数组的原始访问。保留原始访问对于任何实际需要做一些工作的代码都是至关重要的。

很多时候,您将对数组的子集执行操作,当您在语言中内置了额外的簿记时,您必须重新分配子数组,并将数据复制出来,以便使用期望托管数组的API来操作它们。

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

另一个例子是一个快速傅立叶变换,它递归地处理从2x2"蝴蝶"开始的数据,并返回整个数组。

要修复托管数组,现在需要"其他东西"来修补这个缺陷,"其他东西"称为"迭代器"。(您现在已经有了托管数组,但几乎从未将它们传递给任何函数,因为您需要90%以上的迭代器。)


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