我最近开始学习C语言,我正在上一门以C为主题的课程。我目前正在玩循环,我遇到了一些奇怪的行为,我不知道如何解释。

#include <stdio.h>

int main()
{
  int array[10],i;

  for (i = 0; i <=10 ; i++)
  {
    array[i]=0; /*code should never terminate*/
    printf("test \n");

  }
  printf("%d \n", sizeof(array)/sizeof(int));
  return 0;
}

在我运行Ubuntu 14.04的笔记本电脑上,这段代码没有崩溃。它运行到完成。在我学校运行CentOS 6.6的电脑上,它也运行得很好。在Windows 8.1上,循环永远不会终止。

更奇怪的是,当我将for循环的条件编辑为:I <= 11时,代码只在运行Ubuntu的笔记本电脑上终止。它永远不会在CentOS和Windows中终止。

有人能解释一下内存中发生了什么吗?为什么运行相同代码的不同操作系统会产生不同的结果?

编辑:我知道for循环越界了。我是故意这么做的。我只是不明白在不同的操作系统和计算机上,这种行为是如何不同的。


当前回答

在我运行Ubuntu 14.04的笔记本电脑上,这段代码不会中断它的运行直至完成。在我学校运行CentOS 6.6的电脑上,它也运行得很好。在Windows 8.1上,循环永远不会终止。 更奇怪的是,当我将for循环的条件编辑为:I <= 11时,代码只在运行Ubuntu的笔记本电脑上终止。CentOS和Windows永远不会终止。

您刚刚发现了内存踩踏。你可以在这里读到更多关于它的信息:什么是“memory stomp”?

当你分配int数组[10],i;,这些变量进入内存(具体地说,它们分配在堆栈上,这是一个与函数相关的内存块)。数组[]和I在内存中可能是相邻的。似乎在Windows 8.1中,i位于数组[10]中。在CentOS上,i位于数组[11]中。在Ubuntu上,它不在这两个位置(也许它在数组[-1]?)

尝试将这些调试语句添加到代码中。你应该注意到在迭代10或11时,数组[i]指向i。

#include <stdio.h>
 
int main() 
{ 
  int array[10],i; 
 
  printf ("array: %p, &i: %p\n", array, &i); 
  printf ("i is offset %d from array\n", &i - array);

  for (i = 0; i <=11 ; i++) 
  { 
    printf ("%d: Writing 0 to address %p\n", i, &array[i]); 
    array[i]=0; /*code should never terminate*/ 
  } 
  return 0; 
} 

其他回答

它在数组[10]上是未定义的,并给出了之前描述的未定义行为。你可以这样想:

我的购物车里有10样东西。它们是:

0:一盒麦片 1:面包 2:牛奶 3:派 4:鸡蛋 5:蛋糕 6:一杯2升的苏打水 7:沙拉 8:汉堡 9:冰淇淋

购物车[10]未定义,在某些编译器中可能会出现越界异常。但是,很多人显然没有。显而易见的第11件商品实际上并不在购物车中。第11项是指,我称之为“恶作剧项目”。它从未存在过,但它就在那里。

为什么一些编译器给i一个数组[10]或数组[11]甚至数组[-1]的索引是因为你的初始化/声明语句。一些编译器将其解释为:

“为数组[10]分配10个int块和另一个int块。为了方便起见,把它们放在一起。” 和之前一样,但是移动一两个空格,这样数组[10]就不指向i了。 执行与前面相同的操作,但是在数组[-1]处分配i(因为数组的索引不能或不应该为负),或者在完全不同的位置分配i,因为操作系统可以处理它,这样更安全。

一些编译器希望运行得更快,而一些编译器更喜欢安全。这一切都与环境有关。例如,如果我正在为古老的BREW操作系统(基本手机的操作系统)开发一款应用程序,它就不会关心安全性。如果我开发的是iPhone 6,那么它无论如何都能运行得很快,所以我需要强调安全性。(说真的,你读过苹果的应用商店指南吗,或者读过Swift和Swift 2.0的开发吗?)

与Java不同,C不做数组边界检查,也就是说,没有ArrayIndexOutOfBoundsException异常,确保数组索引有效的工作留给了程序员。故意这样做会导致未定义的行为,任何事情都可能发生。


对于数组:

int array[10]

索引只在0到9的范围内有效。然而,你正试图:

for (i = 0; i <=10 ; i++)

访问数组[10],将条件更改为I < 10

错误是在部分数组[10]w/c也是I的地址(int数组[10],I;)。 当数组[10]被设置为0时,I将为0 w/c重置整个循环和 导致无限循环。 如果数组[10]在0-10之间,就会出现无限循环。正确的循环应该是(i = 0;I <10;我+ +){…} int数组[10],我; For (i = 0;I <=10;我+ +) array[我]= 0;

除了内存布局的可能性之外,写入[10]的尝试实际上会覆盖i,优化编译器也可能会确定,如果代码没有首先访问不存在的数组元素[10],则i值大于10时无法达到循环测试。

Since an attempt to access that element would be undefined behavior, the compiler would have no obligations with regard to what the program might do after that point. More specifically, since the compiler would have no obligation to generate code to check the loop index in any case where it might be greater than ten, it would have no obligation to generate code to check it at all; it could instead assume that the <=10 test will always yield true. Note that this would be true even if the code would read a[10] rather than writing it.

漏洞存在于以下代码段之间:

int array[10],i;

for (i = 0; i <=10 ; i++)

array[i]=0;

由于数组只有10个元素,在最后一次迭代中数组[10]= 0;是缓冲区溢出。缓冲区溢出是未定义的行为,这意味着它们可能格式化您的硬盘驱动器或导致恶魔从您的鼻子里飞出来。

所有的堆栈变量都是相邻排列的,这是很常见的。如果i位于数组[10]写入的位置,则UB将i重置为0,从而导致未终止循环。

要修复,将循环条件更改为i < 10。