c语言数组范围内,关于C ++:C中的数组索引超出范围

为什么在数组索引超出范围的情况下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/