为什么数组的最大大小会“太大”?

60

我和这个答案持有相同的看法,即标准始终保证size_t足够大,可以容纳给定系统中最大的类型。

然而,在gcc/Mingw上,这段代码无法编译:

#include <stdint.h>
#include <stddef.h>

typedef uint8_t array_t [SIZE_MAX];

错误:数组 'array_t' 的大小太大

我是否对标准有什么误解?在给定的实现中,size_t 是否允许过大?还是这是Mingw的另一个错误?


编辑:进一步研究表明

typedef uint8_t array_t [SIZE_MAX/2];   // does compile
typedef uint8_t array_t [SIZE_MAX/2+1]; // does not compile

恰好与之相同

#include <limits.h>

typedef uint8_t array_t [LLONG_MAX];           // does compile
typedef uint8_t array_t [LLONG_MAX+(size_t)1]; // does not compile

因此,我现在倾向于认为这是Mingw中的一个漏洞,因为基于有符号整数类型设置最大允许大小没有任何意义。


14
SIZE_MAX 大小的数组很可能会占用所有内存空间。 - Paul Ogilvie
1
@PaulOgilvie 那为什么他们选择了一个对于给定的实现来说太大的数字呢? - Lundin
1
根据GCC 源代码,该限制由sizetype的有符号对应项(注释中的INT_MAX是误导性的)强制执行。在第5933行,index被赋值为c_common_signed_type(sizetype);。这可能解释了“半范围”问题。 - Grzegorz Szpetkowski
1
@Lundin:我没有找到任何注释,为什么他们使用了有符号类型,所以这可能是一个错误。编辑:我认为2501是正确的,这是由于ptrdiff_t类型是有符号的。 - Grzegorz Szpetkowski
5
请注意,标准中并没有暗示编译器必须允许任何大小不超过SIZE_MAX的对象存在。它只是暗示编译器不能允许大于SIZE_MAX的对象存在。即使您并未实际创建该对象,由于sizeof也可以应用于类型,因此这仍然成立。 - Brian Bi
显示剩余8条评论
4个回答

69
根据您的实现,size_t和ptrdiff_t类型具有相同的宽度,因此限制SIZE_MAX / 2的大小。C标准规定size_t类型为无符号整数,而ptrdiff_t类型为带符号整数。两个指针之间的差异的结果将始终为ptrdiff_t类型。这意味着,在您的实现中,对象的大小必须限制为PTRDIFF_MAX,否则无法用ptrdiff_t类型表示两个指针之间的有效差异,导致未定义的行为。因此,值SIZE_MAX / 2等于值PTRDIFF_MAX。如果实现选择最大对象大小为SIZE_MAX,则必须增加ptrdiff_t类型的宽度。但是,将对象的最大大小限制为SIZE_MAX / 2比使ptrdiff_t类型具有大于或等于size_t类型正数范围更容易。标准在该主题上提供了以下评论。

例如,当将负数转换为无符号类型(如size_t)时,会出现负数的绝对值非常大的情况。此外,一些实现不支持像size_t类型所能表示的最大值那么大的对象。

4(K.3.4 整数类型 4)
因此,限制对象大小的范围有时是有益的,以便检测编程错误。对于针对具有大地址空间的机器的实现,建议将RSIZE_MAX定义为支持的最大对象的大小或(SIZE_MAX >> 1)的较小值,即使此限制小于某些合法但非常大的对象的大小。针对具有小地址空间的机器的实现可能希望将RSIZE_MAX定义为SIZE_MAX,这意味着没有被认为是运行时约束违规的对象大小。


1
这很有道理。那么这更像是C标准的缺陷,意味着SIZE_MAX永远不能以有意义的方式使用,而应该使用PTRDIFF_MAX来代替size_t - Lundin
4
@Lundin 这并不是一个缺陷,因为SIZE_MAX并不代表对象最大允许大小的值。即使使用PTRDIFF_MAX作为限制也不正确,因为它理论上可能比SIZE_MAX还要大。我认为正确的值应该是min(SIZE_MAX,PTRDIFF_MAX) - 2501
gcc 可以选择使用 #define SIZE_MAX PTRDIFF_MAX。这样做可以使其与 C 标准保持一致,而无需发明一些巨大的 ptrdiff_t。 - Lundin
3
@Lundin说:“我不认为这是被允许的,因为C标准将SIZE_MAX定义为size_t的限制”(C99第7.20.3节)。 - mtvec
8
您好,@Lundin似乎误解了SIZE_MAX的用途。它并不是用来表示"对象的最大可能大小"的,而是用来表示"type size_t的整数的最大可能值"。虽然我们使用size_t来测量对象的大小,但并不要求实现实际上允许创建如此巨大的对象。需要明确的是,在64位系统上,SIZE_MAX / 2仍然是一个非常巨大的数字。即使作为静态全局变量,也没有理智的程序员会想要创建如此庞大的数组。 - Kevin
2
标准允许实现提供大于PTRDIFF_MAX的对象;唯一的缺点是,如果两个指针相差太远,则会导致未定义的行为。(这是一个相当大的缺点,并解释了为什么他们选择不这样做)。 - M.M

23

size_t的取值范围保证足以存储实现所支持的最大对象的大小,但反过来不一定成立:不能保证可以创建一个大小填满整个size_t取值范围的对象。

在这种情况下,问题是:SIZE_MAX代表什么?最大支持的对象大小还是size_t所能表示的最大值?答案是:后者,即SIZE_MAX(size_t) -1。不能保证创建SIZE_MAX字节大小的对象。

原因是除了提供size_t之外,实现还必须提供ptrdiff_t,用于存储指向同一数组对象的两个指针之间的差异(虽然不能保证)。由于ptrdiff_t类型是带符号的,实现面临以下选择:

  1. 允许大小为SIZE_MAX的数组对象,并使ptrdiff_tsize_t“宽”。它必须至少比size_t多一位。这样的ptrdiff_t可以容纳指向大小为SIZE_MAX或更小的数组的两个指针之间的任何差异。

  2. 允许大小为SIZE_MAX的数组对象,并使用与size_t相同宽度的ptrdiff_t。接受指针减法可能会发生溢出并导致未定义行为的事实,如果指针之间距离超过SIZE_MAX / 2个元素。语言规范不禁止这种方法。

  • 使用与size_t相同宽度的ptrdiff_t,并通过SIZE_MAX / 2限制最大数组对象大小。 这样的ptrdiff_t可以容纳任何两个指向大小为SIZE_MAX / 2或更小的数组中的指针之间的差异。

  • 你只需处理遵循第三种方法的实现。


    2
    问题中的代码并没有尝试创建任何数组对象,它只是进行了一个typedef。因此,如果实现拒绝typedef,那么它是否不符合规范? - M.M
    1
    我真的认为这是一个非常基准的答案(针对所有这些ptrdiff/size/intptr的东西),我敦促你将其他答案合并到此答案中,概述每种类型的_MAX和_MIN常量,在解释它们之前(因为参考文献会使其更容易理解),并将其他答案的细节纳入此答案中。干得好! - Evan Carroll
    实际上,我已经得到了你的答案和 https://dev59.com/ZGox5IYBdhLWcg3wKBR9#9387041 上的答案,我几乎觉得让另一个问题保持开放是不公正的。也许你可以将你的答案迁移到那个问题,并完全关闭这个问题。 - Evan Carroll
    如果您想为您非常棒的分解图更新更对称,那么可以免费获取这个链接,它会使处理更加轻松。(在我看来)https://gist.github.com/EvanCarroll/121dfb870fd3da0dc52ba4e7edefe3ee - Evan Carroll
    做得好,现在只需要指出任何编程语言的实现在技术上是有限的,但旨在在某种程度上具有功能性...我们可能可以通过黑客手段让操作系统给我们提供4GB的堆栈,但那将是大部分浪费,因此决定在1-4MB的调用堆栈空间中达成妥协(这就是这个分配最终可能会结束的地方,除非分配),而且仅仅因为它编译通过并不意味着它有用... - autistic
    此外,我觉得回到经典的“对象”定义(它是一个必要的状态变化存储区域,并不一定需要可寻址或可迭代使用 C 风格指针算术运算符)......我们可以将一个数组映射到整个磁盘上,假设 32 位 size_t,我们就无法访问超过 2-4GB。然而,如果我们坚持在特殊块设备上使用 readwrite 调用,我们就可以访问设备上的任何位置,理论上我们可以拥有一个无限长的磁带。实际上,这会更慢,因此我们的程序应该在两者之间找到平衡点。 - autistic

    5

    看起来非常像是实现特定的行为。

    我在这里运行Mac OS,并且使用gcc 6.3.0,我能够编译您的定义的最大大小为SIZE_MAX/2;使用SIZE_MAX/2 + 1无法编译。

    另一方面,使用clang 4.0.0时,最大的一个是SIZE_MAX/8,而SIZE_MAX/8 + 1会出错。


    SIZE_MAX/8+1 很有趣。那里是否有错误信息?您能成功地分配 SIZE_MAX/8+1 的内存吗? - 2501
    错误信息非常相似:错误:数组太大(2305843009213693952个元素) - avysk
    clang 4.0.0是什么意思?RSIZE_MAX的值是多少?PTRDIFF_MAX的值是多少? - 2501
    SIZE_MAX: 18446744073709551615 RSIZE_MAX: 9223372036854775807 PTRDIFF_MAX: 9223372036854775807 - avysk
    确实。不,我甚至不能malloc SIZE_MAX/128/1024:咕咕声,设置一个断点在malloc_error_break来调试SIZE_MAX/256/1024可以正常分配内存。+1/-1对于那些不改变行为的人。 - avysk

    0

    从头开始推理,size_t是一种可以容纳任何对象大小的类型。任何对象的大小都受地址总线宽度的限制(忽略复用和能够处理32位和64位代码的系统,称之为“代码宽度”)。类似于最大整数值MAX_INTSIZE_MAXsize_t的最大值。因此,大小为SIZE_MAX的对象是所有可寻址的内存。实现标记它为错误是合理的,但我同意只有在分配了实际对象的情况下,无论是在堆栈上还是在全局内存中,才会出现错误。(对于那么多的内存,调用malloc也将失败)


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