为什么C++不能告诉你动态数组的大小?

63

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

int* a;
a = new int[n];
我想知道的是为什么?人们是忘了在C++的规范中加入还是由于技术原因而不包含该功能?
信息不是被存储在某个地方吗?毕竟,命令
delete[] a;

似乎知道需要释放多少内存,所以我认为delete[]有一些方法可以知道a的大小。


24
动态数组是 C++ 的一个设计失误,这样的功能可以由库解决方案更好地提供(例如,std::vector)。当然,模板是晚于 new 添加到 C++ 中的,所以这只是从后见之明来看的。 - Kerrek SB
12
delete[] a 不需要比 free(p) 更多地知道大小。只有在需要调用析构函数时才需要知道大小,但对于 int 类型来说并没有这样的需求。 - Kerrek SB
6
实际原因是许多分配器会将分配的大小向上舍入。 - MSalters
9
需要知道需要释放多少内存,但不需要知道这些内存表示多少个“对象”,因为C语言中没有析构函数可供调用。 - Lightness Races in Orbit
5
目前在comp.std.c上有关此问题的讨论。malloc/free可能只是调用系统分配器,而这些分配器不允许用户代码查找已分配块的大小。free() 可能知道malloc()给了你64字节,但它不知道是因为你请求了1个int还是16个。 - Martin Bonner supports Monica
显示剩余18条评论
7个回答

41

这是“不要为不需要的东西付费”基本法则的衍生规则。在你的例子中,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都是同一库的一部分,但并非总是如此。)


4
即使使用int,内存大小也必须知道吗?在用于记录已分配/空闲内存的任何结构中标记内存释放? - bolov
4
即使这是个错误,至少也是一个向前兼容的错误。如果委员会决定纠正它,他们可以这么做而不会破坏任何东西。相反则不成立。 - Benjamin Lindley
18
不一定。分配的块大小可能已知于分配器,但这可能不是您所要求的大小;由于避免碎片化或某些平台上的对齐问题,可能会分配更大的块。 - TartanLlama
4
当数组元素类型不是平凡析构类型时,C++14要求使用数组删除语句调用大小释放函数。演示 - Kerrek SB
@KerrekSB 刚刚注意到你的评论。1. 在所有版本的C++中,使用array-delete删除数组的要求都存在。2. 这个要求是绝对的 - 即使元素类型可以平凡销毁的(不使用数组删除是未定义行为)。 - Martin Bonner supports Monica
1
@MartinBonner:我指的不是那个细节。我特别指的是作为delete表达式结果调用的数组释放函数,而从C++14开始,有多个候选项(大小与无大小)。截至C++17,甚至还有更多(即对齐扩展)。 - Kerrek SB

13

一个基本的原因是,指向动态分配的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,但是要实现这一点,你需要在每个对象上添加“大小标签”。
在64位机器上,char将需要128位的存储空间(因为对齐)。这比必要的空间多16倍。
(上面的十个字符数组a至少需要168字节。)

这可能很方便,但代价也太高了。

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


3
我理解了。我想知道的是:当数组被删除时,编译器突然似乎记住了数组的大小。从上面的评论中我学到的是,编译器只记住了为数组预留的内存量,这只是数组大小下限的一个指示。 - jarauh
3
更简单的解决方案是将所有这些用法定义为未定义行为。size()可以被定义为仅适用于使用new[]动态分配的数组。 - John Kugelman
2
@jarauh 尝试删除 a[1]。你期望会发生什么?实际上会发生什么? :) 另外,你认为 编译器 是如何知道数组的大小的?难道 C++ 编译器在我不注意的时候解决了停机问题吗? :D - Luaan
2
你可能还想提出子数组的问题。我可能会使用 new 生成一个包含 10 个 char 的数组,但是将其用作两个包含 5 个 char 的单独数组。编译器无法知道我已经为数组的不同部分赋予了不同的语义含义,因此 size() 函数将给出错误的值。 - Eldritch Cheese
2
删除 &a[1] 可以在支持从其分配范围内的任何位置使用指针进行释放的平凡对象的后备内存分配器的情况下工作。C++ 不要求这样做,并要求您从 new 获得的地址进行删除。 - Yakk - Adam Nevraumont
在那个例子中,对除了 p 以外的任何东西调用 delete[] 都将导致未定义行为。我们为什么期望 size 表现得与此不同呢?只需声明当针对可能被合法删除的内容调用时才定义它。即使它只返回实际内存大小而不是请求的大小,我也可以看到某些情况下这将非常有用。例如,如果您有一个向量类想要在重新分配之前知道其实际可用存储空间量。 - Joel Croteau

4

您说得对,系统的某些部分需要知道一些关于大小的信息。但是获取这些信息可能不在内存管理系统的API(例如malloc/free)中,而且您请求的确切大小可能未知,因为它可能已经被舍入。


2
这是一个关于过载 operator delete 的有趣案例,我在其中发现了一个形式为found的问题。
void operator delete[](void *p, size_t size);

参数 size 似乎默认为void *p指向的内存块的大小(以字节为单位)。如果这是真的,那么有望通过调用operator new传递一个值,并且仅需要将其除以sizeof(type)即可得到存储在数组中的元素数量。
至于你问题中的“为什么”部分,Martin的“不为所不需要的东西付费”规则似乎是最合理的。

2
你假设分配的块的大小等于数组的大小,但我认为这并不一定是真的。当然,它不能更小,但我不认为有什么能阻止它变得更大(例如,如果分配只能按固定块大小进行)。 - Iker
2
C++14 要求在数组元素类型不是平凡析构时,该释放函数必须由数组删除表达式调用。 - Kerrek SB
@KerrekSB 我(虚弱的,只有模糊的记忆)认为它只会在构造期间失败时才被调用?你能找到引用吗? - Yakk - Adam Nevraumont
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Kerrek SB

2

通常情况下,内存管理器只会按照一定的倍数进行内存分配,例如64字节。

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

接下来的问题可能是,它不能存储所请求的大小吗?这是一种额外的开销,并非每个人都愿意承担。例如,Arduino Uno只有2k的RAM,在这种情况下,每个分配的4个字节突然变得相当重要。

如果你需要该功能,则可以使用std::vector(或等效物),或者使用更高级别的编程语言。C / C ++旨在使你能够尽可能地使用少量开销工作,这是其中之一。


1

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

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

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

只需考虑排序数据元素的平凡情况。 如果您有托管数组,则无法使用递归,因为需要复制数据以创建新的子数组以递归传递。

另一个例子是FFT,它从2x2“蝴蝶”开始递归地操作数据,并向后处理整个数组。

要修复托管数组,现在需要“其他东西”来弥补这个缺陷,这个“其他东西”称为'迭代器'。(现在您有托管数组,但几乎从不将它们传递给任何函数,因为您90%以上的时间都需要迭代器。)


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

4
我认为问题是为什么它被设计成这样? - juanchopanza
10
在某些情况下,数组的大小必须被存储在某个地方(以便delete[]可以调用正确数量的析构函数)。 - Martin Bonner supports Monica
2
并非所有指针都指向数组的开头。因此,即使一个数组知道它的大小,如果你将&array[1]传递给一个函数,它就不再指向该数组,并且无法找到其大小。 - Barmar

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