在C语言中,数组的最大大小是多少?

62
我理解硬件会限制程序执行期间分配的存储器数量。但是,我的问题是不考虑硬件条件的情况下。假设没有限制存储器的数量,那么数组是否就没有限制?

2
实际上,软件(操作系统)通常是导致您的C程序看到内存限制的原因。 - TJD
2
没有内存限制,指针大小就没有限制。没有指针大小限制,一切都不确定。 - Sergey Kalinichenko
4
它将受指针大小的限制(32位与64位)所影响。 - Mitch Wheat
7个回答

60
在 C 语言中,数组的大小没有固定的限制。任何单个对象的大小,包括任何数组对象的大小,都受到类型 size_t 的最大值 SIZE_MAX 的限制,它是 sizeof 运算符的结果。虽然C标准并未明确允许超过 SIZE_MAX 字节的对象,但实际上不支持这样的对象(请参见脚注)。因为 SIZE_MAX 是由实现确定的,并且不能被任何程序修改,所以对于任何单个对象,都会有一个大小的上限 SIZE_MAX 字节。(这是一个上限,不是最小上限;实现可能会并且通常会强加更小的限制。)
类型 void* 的宽度是所有执行程序中所有对象总大小的上限(可能比单个对象的最大大小要大)。
C 标准对这些固定大小施加了下限,但没有施加上限。任何符合 C 实现都不能支持无限大小的对象,但它可以原则上支持任何有限大小的对象。上限由各自的 C 实现、它们运行的环境和物理因素而强加,而不是由语言本身所决定的。
例如,一种符合标准的实现可能使 SIZE_MAX 等于 2^1024-1,这意味着它原则上可以有对象大小达到179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215 字节。
祝你好运,找到实际支持这样的对象的硬件。

:并没有明确规定任何对象的大小不能超过SIZE_MAX字节。你无法对这样的对象使用有效的sizeof运算符,但就像任何其他运算符一样,sizeof也可能溢出;这并不意味着你不能对这样的对象执行操作。但实际上,任何理智的实现都会将size_t设得足够大,以表示其支持的任何对象的大小。


1
@J.C.Leitão:原则上,我认为编译器可以使用任意精度库来实现非常宽的整数类型。但作为程序员,你不能这样做;实际的整数类型(包括字面量、运算符等)受限于编译器提供的内容。 - Keith Thompson
1
存在远指针和相应的内存模型是否会以任何方式改变您的答案? - jfs
1
@J.F.Sebastian:我自己从未使用过far指针,但我的理解是它们包括段号和段内偏移量。如果16位指针可以访问20位地址空间,那么必须至少有4位隐式信息在其他地方,而据我所知,这正是far指针做的事情。在符合C实现的情况下,void*可以唯一地寻址当前存在的每个对象的每个字节。 - Keith Thompson
2
@caot:你的数组在函数内部定义时没有使用static关键字,因此它具有自动存储期。在大多数实现中,这些对象都分配在堆栈上。很可能你的系统限制了堆栈的大小。使用static,或者全局定义,或者使用malloc() - Keith Thompson
1
@M.M:标准中并没有禁止超过SIZE_MAX字节大小的对象,但实际上,任何能够支持大于2**32字节的对象的合理实现都会使size_t比32位更宽。一个允许你定义这样大对象但不费心扩展size_t以容纳它们大小的实现可能是符合标准的,但这是反常的。 - Keith Thompson
显示剩余10条评论

12

C99 5.2.4.1 "翻译限制" 最小尺寸

实现必须能够翻译和执行至少包含以下每个限制中的至少一个实例的程序:13)

  • 对象中的65535字节(仅在托管环境中)
  1. 尽可能避免强制实施固定的翻译限制。

这表明符合规范的实现可以拒绝编译具有超过short字节的对象(包括数组)。

PTRDIFF_MAX也对数组施加了一些限制

C99标准6.5.6加法运算符说:

当两个指针相减时,它们都必须指向同一数组对象的元素或该数组对象的最后一个元素之后;结果是两个数组元素下标的差。结果的大小是实现定义的,并且其类型(有符号整数类型)在<stddef.h>头文件中定义为ptrdiff_t。如果结果不能用该类型的对象表示,则行为未定义。
这意味着理论上允许比ptrdiff_t更大的数组,但是您无法以可移植的方式获取它们的地址差异。
因此,也许出于这个原因,GCC似乎只限制您使用ptrdiff_t。这也在Why is the maximum size of an array "too large"?中提到。
也许最终重要的是您的编译器接受什么,所以我们来试试吧:

main.c

#include <stdint.h>

TYPE a[(NELEMS)];

int main(void) {
    return 0;
}

sizes.c

#include <stdint.h>
#include <stdio.h>

int main(void) {
    printf("PTRDIFF_MAX 0x%jx\n", (uintmax_t)PTRDIFF_MAX);
    printf("SIZE_MAX    0x%jx\n", (uintmax_t)SIZE_MAX);
    return 0;
}

然后我们尝试使用以下命令进行编译:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o sizes.out sizes.c
./sizes.out
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out \
  -DNELEMS='((2lu << 62) - 1)' -DTYPE=uint8_t main.c 

结果:

  • PTRDIFF_MAX: 0x7fffffffffffffff = 2^63 - 1

  • SIZE_MAX: 0xffffffffffffffff = 2^64 - 1

  • -DNELEMS='((2lu << 62) - 1)' -DTYPE=uint8_t: 编译成功(等于2^63-1)。在我的仅有32GB RAM的系统上运行它会立即崩溃 :-)

  • -DNELEMS='(2lu << 62)' -DTYPE=uint8_t: 编译失败,报错:

    error: size of array ‘a’ is too large
    
  • -DNELEMS='(2lu << 62 - 1)' -DTYPE=uint16_t: 编译失败,报错:

    error: size ‘18446744073709551614’ of array ‘a’ exceeds maximum object size ‘9223372036854775807’
    

    其中 9223372036854775807 == 0x7fffffffffffffff

这样我们就可以理解GCC施加了两个不同错误信息的限制:

  • 元素数量不能超过2^63(等于PTRDIFF_MAX)
  • 数组大小不能超过2^63(也等于PTRDIFF_MAX)

在Ubuntu 20.04 amd64,GCC 9.3.0上测试通过。

另请参阅


gcc也不支持动态数组大于指针宽度的一半。请注意,指针减法结果会按对象大小进行缩放,因此理论上,在ILP32目标上,一个3GiB的int数组应该可以工作,并且end-start应该给出3 * 1024 ** 3/4,计算时没有溢出,因为C标准规定如果最终结果是可表示的,则不是UB。但是gcc发出的代码使用指针宽度进行减法,然后进行算术右移,从减法中丢失了进位。https://godbolt.org/g/NG6zZ6。 - Peter Cordes
我使用-m32进行测试,得到了-536870896(指针减法)与536870928(手动计算)的结果。在x86上的32位用户空间中(在64位内核下),Glibc的malloc成功分配了2GiB + 16的内存,但对于3GiB的分配则失败了。无论如何,显然这不是一个“错误”,因为gcc认为PTRDIFF_MAX是最大对象大小:https://developers.redhat.com/blog/2017/02/22/memory-error-detection-using-gcc/。`-Wall`启用了`-Walloc-size-larger-than=PTRDIFF_MAX`,在编译我的测试程序时我收到了该警告。 - Peter Cordes
事实上,已经有一个(关闭 - 无效)的 GCC 错误确认这确实是 GCC 的工作方式:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=45779。 - Peter Cordes
1
这个限制(PTRDIFF_MAX)是指元素数量还是字节数量的限制?据我所见,这是一个元素数量的限制,但如果元素足够大,你很容易就会先达到字节的硬性限制(SIZE_MAX)。然而,这是非常危险的,因为通过char *访问该数组的函数可能会出现问题。 - alx - recommends codidact
1
@CacahueteFrito 我的想法是,你可以将较大尺寸的指针强制转换为uint8_t类型,这样你就能够达到比PTRDIFF_MAX更大的差异。这只是我认为GCC做出这种决定的一种启发式方法。我现在对实验进行了一些改进。 - Ciro Santilli OurBigBook.com
1
@alx:它是以字节为单位的。原因是当指针减法不会溢出最终值时,需要准确计算,即使字节距离不适合(如果在减法之前将指针转换为char *)。设置实现限制意味着GCC可以将更宽的类型的end-start实现为指针的原始整数减法,并通过sizeof(T)进行除法运算(当然要使用移位或乘法逆元)。如果即使在32位系统上,指针相隔2.1 GiB,也需要精确的结果,那么它将需要扩展精度或将进位标志旋转以除以2。 - Peter Cordes

5

在考虑内存的情况下,数组的最大大小受到用于索引数组的整数类型的限制。


当将整数类型的表达式添加到或从指针中减去时,结果具有指针操作数的类型(C11 n1570,第6.5.6节加法运算符)。因此,使用比指针更宽的整数类型不允许您访问比您可以访问的更多的内存。 (回想一下,arr [idx]*((arr)+(idx))完全等效,这实际上是C标准定义 []数组下标运算符的方式。)无论如何,这是所有C实现共享的限制,但在实践中,实现具有较小的限制,例如gcc的PTRDIFF_MAX。 - Peter Cordes

5
一台64位计算机理论上最多可寻址2^64字节的内存。

1
我可以想像它在我的有生之年发生。 - Prof. Falken
1
这个答案是完全错误的。我已经在一个16位的机器上访问了22位地址空间。一个“64位机器”,它没有明确的定义,没有理由限制64位地址空间。 - Eric Postpischil
@EricPostpischil 理论上,您可以组合任意数量的单元信息类型以访问更高数量级的地址。但这不是重点。指针的定义是64位,因此可以寻址2^64个内存地址。您无法使用16位进行足够的组合,并在22位机器中寻址每个内存单元。 - wafL
这并不意味着机器不能使用各种寻址技术,例如使用两个寄存器(如段寄存器和偏移量寄存器)来寻址内存。“64位机器”并不意味着机器上的指针是64位。 - Eric Postpischil
根据这个定义,没有任何单一的数据类型结构可以在64位机器上被定义,这是不切实际的。正如我在我的回答中所提到的,你可以将任何信息单元连接起来,从而能够寻址任意数量的信息。但我们生活在现实世界中,幻想通常不是一个选项。例如,在C语言中,指针的最大大小为8字节,由于我们要实际操作,这是你在未来100-1000年内所需要的极限,但如果你需要寻址超过16 exabytes的内存,那就是你的问题了;)。 - wafL
显示剩余4条评论

2

我猜最大的理论数组应该是"unsigned long"的最大值(或者是最新标准/编译器支持的最大整数)。


不要使用比指针更宽的类型(例如,在具有32位指针的实现中使用long long)。 - Peter Cordes

2
指针的大小将限制您能够访问的内存。即使硬件支持无限内存,如果您能使用的最大数据类型为64位,则只能访问2^64字节的内存。

0

我正在寻找一种确定数组最大大小的方法。这个问题似乎是在问同样的事情,所以我想分享我的发现。

起初,C语言没有提供任何函数来确定编译时可分配的数组元素的最大数量。这是因为它将取决于将要执行的机器中可用的内存。

另一方面,我发现内存分配函数(calloc()malloc())可以允许分配更大的数组。此外,这些函数允许您处理运行时内存分配错误。

希望这有所帮助。


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