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

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

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

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

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

信息不是存储在某处吗?毕竟,命令

delete[] a;

似乎知道它必须释放多少内存,所以在我看来 delete[] 有某种方式知道 a.

的大小

它是 "don't pay for what you don't need" 基本规则的延续。在您的示例中,delete[] a; 不需要 知道数组的大小,因为 int 没有析构函数。如果你写过:

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

然后 delete 必须调用析构函数(并且需要知道要调用多少)——在这种情况下 new 必须保存该计数。但是,鉴于它 不需要 在所有情况下都保存,Bjarne 决定不授予访问权限。

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

当然,即使 intsomething 也必须知道分配内存的大小,但是:

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

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

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

等库中的 类

found 以以下形式重载 operator delete 有一个奇怪的案例:

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

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

至于你问题的 "why" 部分,Martin 的 "don't pay for what you don't need" 规则似乎是最合乎逻辑的。

一个根本原因是指​​向 T 的动态分配数组的第一个元素的指针与指向任何其他 T.

的指针之间没有区别

考虑一个虚构函数,returns 指针指向的元素数。
我们称它为 "size".

听起来很不错,对吧?

如果不是所有的指针都是平等的:

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,但要实现这一点,您需要在 每个对象 .
上添加 "size tag" char 在 64 位机器上需要 128 位存储空间(因为对齐)。这比需要的多十六倍。
(以上,十字符数组 a 至少需要 168 个字节。)

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

您当然可以设想一个只有当参数确实是默认情况下指向动态分配的第一个元素的指针时才明确定义的版本 operator new,但这几乎没有用正如人们可能认为的那样。

你是对的,系统的某些部分必须知道一些关于大小的信息。但是获取该信息可能不在内存管理系统的 API 范围内(想想 malloc/free),并且您请求的确切大小可能不知道,因为它可能有被围捕了。

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

这是其他语言的严重缺陷,而不是 C++。 您可以使用 std::vector 实现所需的功能,但仍保留对数组的原始访问权限。保留原始访问权限对于任何实际上必须做一些工作的代码都是至关重要的。

很多时候你会对数组的子集执行操作,当你有额外的簿记内置到语言中时,你必须重新分配子数组并复制数据用需要托管数组的 API 来操纵它们。

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

另一个示例是 FFT,它递归地处理以 2x2 "butterflies" 开头的数据,然后返回整个数组。

要修复托管阵列,您现在需要 "something else" 来修补此缺陷,并且 "something else" 称为 'iterators'。 (您现在拥有托管数组,但几乎从不将它们传递给任何函数,因为 +90% 的时间您需要迭代器。)

你会经常发现内存管理器只会分配space一定倍数,例如64字节。

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

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

如果您需要该功能,那么您有 std::vector(或同等功能),或者您有高级语言。 C/C++ 旨在使您能够以尽可能少的开销工作,这只是一个例子。