为什么C
在数组索引超出绑定
#include <stdio.h>
int main()
{
int a[10];
a[3]=4;
a[11]=3;//does not give segmentation fault
a[25]=4;//does not give segmentation fault
a[20000]=3; //gives segmentation fault
return 0;
}
据我所知,在a[11]
或a[25]
的情况下,它正在尝试访问分配给进程或线程的内存,并且在a[20000]
的情况下它会超出堆栈范围。
为什么编译器或链接器没有出错,他们不知道数组大小?如果没有,那么sizeof(a)
如何正常工作?
答案 0 :(得分:66)
问题是C / C ++实际上没有对数组进行任何边界检查。这取决于操作系统,以确保您访问有效的内存。
在这种特殊情况下,您将声明一个基于堆栈的数组。根据特定的实现,访问数组边界外部只会访问已分配的堆栈空间的另一部分(大多数操作系统和线程为堆栈保留一定的内存部分)。只要您恰好在预先分配的堆栈空间中玩游戏,一切都不会崩溃(注意我没说工作)。
最后一行发生的事情是,您现在已经访问了超出为堆栈分配的内存部分。因此,您将索引到未分配给进程或以只读方式分配的内存部分。操作系统看到这一点并向进程发送seg错误。
这就是C / C ++在边界检查方面如此危险的原因之一。
答案 1 :(得分:21)
段错误不是C程序的预期操作,它会告诉您索引超出范围。相反,它是未定义行为的意外后果。
在C和C ++中,如果声明一个类似
的数组type name[size];
您只能访问索引从0
到size-1
的元素。超出该范围的任何内容都会导致未定义的行为。如果索引接近范围,很可能您会读取自己程序的内存。如果索引大部分超出范围,很可能您的程序将被操作系统杀死。但你无法知道,任何事情都可能发生。
为什么C允许这样做?好吧,C和C ++的基本要点是如果性能成本不提供功能。 C和C ++已经用于高性能关键系统。 C已被用作内核和程序的实现语言,其中访问数组边界对于快速访问内存中相邻的对象非常有用。编译器禁止这样做是徒劳的。
为什么不警告这个?好吧,你可以把警告水平提高,并希望编译器的怜悯。这称为实施质量(QoI)。如果某些编译器使用开放行为(例如,未定义的行为)来做好事,那么它在这方面具有良好的实现质量。
[js@HOST2 cpp]$ gcc -Wall -O2 main.c
main.c: In function 'main':
main.c:3: warning: array subscript is above array bounds
[js@HOST2 cpp]$
如果它看到数据访问超出界限后会格式化你的硬盘 - 这对它来说是合法的 - 实现的质量会相当糟糕。我很高兴在ANSI C Rationale文档中阅读这些内容。
答案 2 :(得分:6)
如果您尝试访问您的进程不拥有的内存,通常只会出现分段错误。
在a[11]
(顺便提一下a[10]
)的情况下,您所看到的是您的进程 拥有但不属于{的内存{1}}数组。 a[]
离a[25000]
很远,它可能完全在你的记忆之外。
更改a[]
更加隐蔽,因为它会默默地影响另一个变量(或者当函数返回时可能导致不同分段错误的堆栈帧)。
答案 3 :(得分:3)
C没有这样做。操作系统的虚拟内存子系统是。
如果您只是略微超出范围,那么您正在解决为您的程序分配 的问题(在这种情况下在堆栈调用堆栈上)。如果您远远超出了界限,那么您正在解决未提供给您的程序的内存,并且操作系统正在抛出分段错误。
在某些系统上,还有一个操作系统强制执行的“可写”内存概念,您可能会尝试写入您拥有的内存,但标记为不可写。
答案 4 :(得分:2)
只是为了添加其他人所说的内容,在这些情况下你不能仅依赖程序崩溃,如果你试图访问超出“数组边界”的内存位置,就不会保证会发生什么。这就像你做了类似的事情一样:
int *p;
p = 135;
*p = 14;
这只是随机的;这可能会奏效。它可能不会。不要这样做。代码可以防止出现这类问题。
答案 5 :(得分:2)
如上所述,一些编译器可以在编译时检测到一些越界数组访问。但是在编译时检查边界将无法捕获所有内容:
int a[10];
int i = some_complicated_function();
printf("%d\n", a[i]);
要检测到这一点,必须使用运行时检查,并且由于它们对性能的影响而在C中避免使用它们。即使在编译时知道了数组的大小,即sizeof(a),如果不插入运行时检查,也无法防止这种情况。
答案 6 :(得分:2)
正如我理解这些问题和评论一样,你理解为什么当你访问内存超出范围时可以发生的坏事,但你想知道为什么你的特定编译器没有警告你。
允许编译器向您发出警告,许多人会在最高警告级别发出警告。然而,编写标准是为了让人们能够运行各种设备的编译器,以及具有各种功能的编译器,因此标准要求尽可能少,同时保证人们可以做有用的工作。
有一些标准要求某种编码风格会产生诊断。还有其他几次标准不需要诊断。即使需要诊断,我也不知道标准中应该指出准确措辞的任何地方。
但是你并没有彻底摆脱寒冷。如果您的编译器没有警告您,Lint可能会。此外,有许多工具可以检测堆上数组的这些问题(在运行时),其中一个更着名的是电围栏(或DUMA)。但即使是电围栏也无法保证它能够捕获所有超限误差。
答案 7 :(得分:1)
这不是C问题的操作系统问题。你的程序已被授予一定的内存空间,你在其中做的任何事都没问题。只有当您访问进程空间之外的内存时,才会发生分段错误。
并非所有操作系统都为每个进程分配了单独的地址空间,在这种情况下,您可以在没有警告的情况下破坏另一个进程或操作系统的状态。
答案 8 :(得分:1)
C哲学始终信任程序员。并且不检查边界也允许C程序更快地运行。
答案 9 :(得分:0)
正如JaredPar所说,C / C ++并不总是执行范围检查。如果您的程序访问分配的数组之外的内存位置,则您的程序可能会崩溃,也可能不会崩溃,因为它正在访问堆栈上的其他变量。
要回答有关C语言中sizeof运算符的问题: 您可以可靠地使用sizeof(array)/ size(array [0])来确定数组大小,但是使用它并不意味着编译器将执行任何范围检查。
我的研究表明,C / C ++开发人员认为您不应该为不使用的东西付费,并且他们信任程序员知道自己在做什么。 (请参见对此的公认答案:Accessing an array out of bounds gives no error, why?)
如果可以使用C ++代替C,也许可以使用矢量?您可以在需要性能时使用vector [](但不进行范围检查),或更优选地,使用vector.at()(以性能为代价进行范围检查)。请注意,向量一旦满就不会自动增加容量:为安全起见,请使用push_back(),它会在必要时自动增加容量。