为什么在数组索引超出范围的情况下C会有所区别
#include
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)如何正常工作?
问题在于C / C ++实际上不对数组进行任何边界检查。取决于操作系统,以确保您正在访问有效内存。
在这种情况下,您要声明一个基于堆栈的数组。根据特定的实现,在数组范围之外进行访问将仅访问已分配的堆栈空间的另一部分(大多数OS和线程为堆栈保留内存的特定部分)。只要您恰好在预先分配的堆栈空间中玩耍,一切都不会崩溃(请注意,我没有说工作)。
最后一行发生的事情是,您现在已经访问了超出分配给堆栈的那部分内存。结果,您正在索引未分配给进程或以只读方式分配的一部分内存。操作系统会看到此情况,并向该过程发送段错误。
这是C / C ++在进行边界检查时如此危险的原因之一。
但是为什么编译器或链接器不给出错误,而又不知道数组大小呢?如果不是,那么sizeof(a)如何正常工作?
@ Kazoom,C可以知道数组访问的特定子集是否合法。但是这些远远超过了无法检测到的病例数。我的猜测是该功能尚未实现,因为这样做成本很高,并且仅在部分场景中有用
作为上述示例,请想象一个简单的情况" a [b] = 1;"。 -必须在运行时完成数组绑定检查,这将使每个(或大多数)数组操作花费额外的CPU周期。
sizeof(a)如何工作?
@Kazoom,编译器知道a的长度为10,而int的单位大小为4(例如),因此它仅使用值40。
@Kazoom:是的,在这种简单情况下,编译器可以检测到问题并向您抛出错误。但这不是标准所必需的(毕竟C应该很容易实现-没有花哨的功能)。
@Jared,对那些不知道如何正确使用它的人来说是唯一的危险。 Ive总是将语言比作强大的工具。如果您不知道如何使用电锯,那您就没事了。如果您断腿,那是您自己的错:-)
@Pax,绝对同意。但是C / C ++更像是一种缓慢起作用的毒药,而不是电锯。修剪腿部会立即产生明显效果。毒药虽然种类繁多,但起效慢,反应快,症状多样。
真正的问题是C和C ++实现通常不检查边界(无论是在编译时还是在运行时)。他们完全被允许这样做。不要为此责怪语言。
记住,即使我们使用malloc动态分配内存,也无法解决此问题。只有分配的内存将保持未初始化状态,其余的内存可以具有随机/不确定的值,并且可能不会如您所愿地给您带来分段错误
段错误不是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文档中读到这些内容。
香港专业教育学院删除了我自己的帖子,您很早就提供了更多扩展答案:
通常,如果尝试访问不属于您的进程的内存,则只会遇到分段错误。
在a[11](顺便说一下a[10])的情况下,您看到的是进程确实拥有但不属于a[]数组的内存。 a[25000]与a[]距离很远,它可能完全在内存之外。
更改a[11]更加隐蔽,因为它会默默地影响另一个变量(或堆栈帧,当函数返回时,堆栈帧可能会导致另一个分段错误)。
C没有这样做。操作系统的虚拟内存子系统是。
在您只是稍微超出界限的情况下,您要解决为程序分配的内存(在这种情况下,在堆栈调用堆栈中)。如果您的内存超出界限,那么您将寻址未分配给程序的内存,并且操作系统将引发分段错误。
在某些系统上,还有一个操作系统强制执行的"可写"内存的概念,您可能正在尝试写入自己拥有但被标记为不可写的内存。
就我理解的问题和评论而言,您了解了为什么超出范围访问内存时会发生不好的事情,但是您想知道为什么您的特定编译器没有警告您。
允许编译器向您发出警告,并且许多警告器会以最高警告级别发出警告。但是,编写该标准是为了允许人们为各种设备运行编译器,并且编译器具有各种功能,因此,该标准在确保人们可以做有用的工作的同时,对它的要求最少。
该标准几次要求某种编码样式将生成诊断。在其他几次情况下,该标准不需要诊断。即使需要诊断,我也不知道标准在哪里写出确切的措辞。
但是您并没有完全在这里冷落。如果您的编译器没有警告您,则可能是Lint。此外,有许多工具(在运行时)可检测堆上阵列的此类问题,其中最著名的一种是Electric Fence(或DUMA)。但是,甚至Electric Fence也无法保证会捕获所有超限错误。
如litb所述,某些编译器可以在编译时检测到某些超出范围的数组访问。但是在编译时进行边界检查并不能解决所有问题:
int a[10];
int i = some_complicated_function();
printf("%d
", a[i]);
为了检测到这一点,必须使用运行时检查,并且由于它们对性能的影响,因此在C语言中避免使用它们。即使知道编译时a的数组大小(即sizeof(a)),也无法在不插入运行时检查的情况下防止这种情况。
仅添加其他人的话,您就不能仅仅依赖于这些情况下崩溃的程序,就无法保证如果您尝试访问超出"数组界限"的内存位置,将会发生什么情况。就像执行以下操作一样:
int *p;
p = 135;
*p = 14;
那只是随机的;这可能有效。可能不会。不要这样防止出现此类问题的代码。
不一样。取消引用未初始化的指针应假定为随机指针。访问数组末尾的一项很有可能不会崩溃,因为系统通常一次分配一个完整的内存页(4KB或更多),在数组末尾留下一些空间。
是一样的。 C没有任何保证。如果一个系统以这种方式工作,那很好,那又如何呢?另外,我认为您应该完全重读我的观点,然后重新阅读我写的内容。我不知道你为什么回应这个,我很困惑。
p = 135是类型错误,不能将int分配给int*。
C哲学永远是程序员的信任。同样,不检查边界也可以使C程序运行得更快。
那不是C问题,而是操作系统问题。您的程序已被授予一定的内存空间,您在其中进行的所有操作都可以。仅当您在进程空间之外访问内存时,才会发生分段错误。
并非所有操作系统的每个进程都有单独的地址空间,在这种情况下,您可以在不发出警告的情况下破坏另一个进程或操作系统的状态。
正如JaredPar所说,C / C ++并不总是执行范围检查。如果程序访问分配的数组之外的内存位置,则程序可能会崩溃,也可能不会崩溃,因为它正在访问堆栈上的其他变量。
要回答有关C语言中的sizeof运算符的问题:
您可以可靠地使用sizeof(array)/ size(array [0])确定数组大小,但是使用它并不意味着编译器将执行任何范围检查。
我的研究表明,C / C ++开发人员认为您不应该为不使用的东西付费,并且他们信任程序员知道自己在做什么。 (请参见已接受的答案:超出范围访问数组不会出错,为什么?)
如果您可以使用C ++而不是C,也许可以使用vector?您可以在需要性能时使用vector [](但不进行范围检查),或更优选地,使用vector.at()(以性能为代价进行范围检查)。请注意,向量在充满时不会自动增加容量:为安全起见,请使用push_back(),必要时会自动增加容量。
有关矢量的更多信息:http://www.cplusplus.com/reference/vector/vector/