为什么这个循环在某些平台上退出,而不是在其他平台上退出?
我最近开始学习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永远不会终止。
你刚刚发现内存跺脚。 你可以在这里阅读更多关于它: 什么是“记忆跺脚”?
当你分配int array[10],i;
,那些variables进入内存(具体地说,它们被分配在堆栈上,这是与该函数关联的一块内存)。 array[]
和i
可能在内存中相邻。 看来,在Windows 8.1上, i
位于array[10]
。 在CentOS上, i
位于array[11]
。 而在Ubuntu上,它没有现货(也许是在array[-1]
?)。
尝试将这些debugging语句添加到您的代码。 你应该注意到在迭代10或11时, array[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; }
错误在于这些代码之间:
int array[10],i; for (i = 0; i <=10 ; i++) array[i]=0;
由于array
只有10个元素,所以在最后一次迭代中array[10] = 0;
是一个缓冲区溢出。 缓冲区溢出是UNDEFINED行为 ,这意味着他们可能会格式化您的硬盘驱动器或导致恶魔飞出你的鼻子。
所有的堆栈variables相邻布置是相当普遍的。 如果i
位于array[10]
写入的位置,则UB将把i
重置为0
,从而导致未终止的循环。
要修复,请将循环条件更改为i < 10
。
在循环的最后一次运行中,可以写入array[10]
,但array[10]
中只有10个元素,编号为0到9.C语言规范指出这是“未定义的行为”。 这在实践中意味着你的程序将尝试写入内存中紧跟在内存之后的内存大小。 然后会发生什么事情取决于实际上在哪里,这不仅取决于操作系统,还取决于编译器,编译器选项(如优化设置),处理器体系结构上的环境代码等等。甚至可能因执行而不同,例如由于地址空间随机化 (可能不在这个玩具的例子中,但它确实发生在现实生活中)。 一些可能性包括:
- 该位置没有使用。 循环正常结束。
- 该位置用于发生值为0的事情。循环正常结束。
- 该位置包含该函数的返回地址。 循环正常结束,但程序崩溃,因为它试图跳转到地址0。
- 该位置包含variables
i
。 循环永远不会终止,因为i
重新在0。 - 该位置包含一些其他variables。 循环正常结束,但然后“有趣”的事情发生。
- 该位置是无效的内存地址,例如,因为
array
正好在虚拟内存页的末尾,而下一页未映射。 - 恶魔从你的鼻子里飞出来 。 幸运的是,大多数电脑缺乏必要的硬件。
你在Windows上观察到的是,编译器决定将variablesi
立即放在内存之后,所以array[10] = 0
最终分配给了i
。 在Ubuntu和CentOS上,编译器没有把i
放在那里。 几乎所有的C实现都将内存中的局部variables组织在一个内存栈中 ,但有一个主要的例外:一些局部variables可以完全放在寄存器中 。 即使variables在堆栈中,variables的顺序也是由编译器决定的,它不仅取决于源文件中的顺序,还取决于它们的types(为了避免将内存浪费在会留下空洞的alignment约束上) ,他们的名字,编译器内部数据结构中使用的一些散列值等等。
如果你想知道你的编译器决定做什么,你可以告诉它显示汇编代码。 哦,并学会破译汇编(这比写它更容易)。 使用GCC(以及其他一些编译器,特别是在Unix世界中),通过选项-S
生成汇编代码而不是二进制文件。 例如,下面是在amd64上使用优化选项-O0
(无优化)编译GCC的循环的汇编代码片段,其中手动添加了注释:
.L3: movl -52(%rbp), %eax ; load i to register eax cltq movl $0, -48(%rbp,%rax,4) ; set array[i] to 0 movl $.LC0, %edi call puts ; printf of a constant string was optimized to puts addl $1, -52(%rbp) ; add 1 to i .L2: cmpl $10, -52(%rbp) ; compare i to 10 jle .L3
这里variablesi
是在堆栈顶部以下52字节,而数组从堆栈顶部以下48字节开始。 所以这个编译器碰巧把i
放在数组之前; 如果你碰巧写入array[-1]
你会覆盖i
。 如果将array[i]=0
更改为array[9-i]=0
,则在这个特定的平台上使用这些特定的编译器选项将会出现无限循环。
现在让我们用gcc -O1
编译你的程序。
movl $11, %ebx .L3: movl $.LC0, %edi call puts subl $1, %ebx jne .L3
这个更短 编译器不仅拒绝为i
分配一个堆栈位置 – 它只存储在寄存器ebx
– 但它并没有打算为array
分配任何内存,或者生成代码来设置它的元素,因为它注意到没有的元素被使用过。
为了让这个例子更具说服力,让我们确保数组赋值是通过向编译器提供一些无法优化的东西来执行的。 一个简单的方法是使用另一个文件中的数组 – 因为单独的编译,编译器不知道在另一个文件中发生了什么(除非它在链接时优化,哪个gcc -O0
或gcc -O1
不)。 创build一个包含的源文件use_array.c
void use_array(int *array) {}
并将您的源代码更改为
#include <stdio.h> void use_array(int *array); int main() { int array[10],i; for (i = 0; i <=10 ; i++) { array[i]=0; /*code should never terminate*/ printf("test \n"); } printf("%zd \n", sizeof(array)/sizeof(int)); use_array(array); return 0; }
编译
gcc -c use_array.c gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
这次汇编代码如下所示:
movq %rsp, %rbx leaq 44(%rsp), %rbp .L3: movl $0, (%rbx) movl $.LC0, %edi call puts addq $4, %rbx cmpq %rbp, %rbx jne .L3
现在数组在堆栈上,从顶部开始有44个字节。 那i
呢? 它不出现在任何地方! 但循环计数器保存在寄存器rbx
。 这不完全是i
,而是array[i]
的地址。 编译器已经决定,因为i
从来没有直接使用i
的值,所以在循环的每次运行期间,执行算术来计算存储0的位置是没有意义的。 地址是循环variables,确定边界的algorithm部分是在编译时执行的(每个数组元素乘以11次迭代,每个数组元素得到44次),部分是在运行时,但在循环开始之前一次性完成执行减法以获得初始值)。
即使在这个非常简单的例子中,我们已经看到了如何改变编译器选项(打开优化)或改变一些小的( array[i]
array[9-i]
),甚至改变明显不相关的东西(添加调用use_array
)可以使编译器生成的可执行程序具有显着的区别。 编译器优化可以执行很多在调用未定义行为的程序上可能看起来不直观的东西 。 这就是未定义的行为完全没有定义的原因。 当你在真实世界的程序中偏离轨道时,即使对于有经验的程序员,也很难理解代码和应该做什么之间的关系。
与Java不同的是,C没有进行数组边界检查,即没有ArrayIndexOutOfBoundsException
,确保数组索引有效的工作由程序员决定。 这样做的目的导致未定义的行为,任何事情都可能发生。
对于一个数组:
int array[10]
索引仅在0
到9
的范围内有效。 但是,您正试图:
for (i = 0; i <=10 ; i++)
在这里访问array[10]
,将条件改为i < 10
你有一个界限违规,并在非终止平台上,我相信你是无意中设置为零在循环结束,以便重新开始。
array[10]
无效; 它包含10个元素, array[0]
到array[9]
, array[10]
是11。 你的循环应该写在 10
之前停止,如下所示:
for (i = 0; i < 10; i++)
array[10]
土地是实现定义的,有趣的是,在你的两个平台上,它着陆于i
,这些平台显然是在array
之后直接布局的。 i
被设置为零,循环继续下去。 对于你的其他平台, i
可能位于array
之前,或者array
可能有一些填充。
你声明int array[10]
意味着array
索引为0
到9
(总共可以容纳10
整数元素)。 但是接下来的循环,
for (i = 0; i <=10 ; i++)
将循环0
到10
意味着11
次。 因此,当i = 10
它会溢出缓冲区,并导致未定义的行为 。
所以试试这个:
for (i = 0; i < 10 ; i++)
要么,
for (i = 0; i <= 9 ; i++)
它在array[10]
是未定义的,并且如前所述给出未定义的行为 。 想想这样:
我的杂货车上有10件物品。 他们是:
0:一盒麦片
1:面包
2:牛奶
3:派
4:鸡蛋
5:蛋糕
6:2升苏打水
7:沙拉
8:汉堡
9:冰淇淋
cart[10]
是未定义的,在某些编译器中可能会出现越界exception。 但是,很多显然不。 明显的第11项是一个实际上不在购物车中的物品。 第十一项指出我要称之为“极端主义的项目”。 它从来不存在,但它在那里。
为什么有些编译器给i
一个array[10]
或array[11]
或甚至array[10]
的索引是因为你的初始化/声明语句。 一些编译器将其解释为:
- “为
array[10]
和另一个int
块分配10个int
的块,使它们更容易,把它们放在一起。” - 和以前一样,但移动一两个空格,这样
array[10]
不会指向i
。 - 与之前一样,但是在
array[-1]
处分配i
(因为数组的索引不能也不应该是负数),或者在完全不同的地方分配它,因为操作系统可以处理它,而且更安全。
一些编译器希望事情更快,一些编译器更喜欢安全。 这是关于上下文的。 如果我正在为古老的BREW OS(基本电话的操作系统)开发应用程序,那么它就不会在乎安全性。 如果我正在开发iPhone 6,那么无论如何,它都可以运行得很快,所以我需要强调安全性。 (说真的,你读过苹果的App Store指南,或者阅读Swift和Swift 2.0的开发?)
既然你创build了一个大小为10的数组,for循环条件应该如下:
int array[10],i; for (i = 0; i <10 ; i++) {
目前,您正尝试使用array[10]
从内存中访问未分配的位置,并导致未定义的行为 。 未定义的行为意味着你的程序将performance出未确定的风格,因此它可以在每次执行时给出不同的输出。
那么,C编译器传统上不检查边界。 如果您引用不属于您的stream程的位置,则可能会出现分段错误。 但是,局部variables是分配在堆栈上的,取决于内存的分配方式,数组( array[10]
)之外的区域可能属于进程的内存段。 因此,不会出现分段故障陷阱,这就是您所看到的。 正如其他人所指出的那样,这在C中是未定义的行为,您的代码可能被认为是不稳定的。 既然你在学习C语言,那么你最好养成在代码中检查边界的习惯。
除了内存可能被布置的可能性,以致写入a[10]
的尝试实际上会覆盖i
,那么优化编译器也可能确定不能以大于十的值来达到循环testing没有代码先访问不存在的数组元素a[10]
。
由于试图访问该元素将是不确定的行为,编译器将没有任何义务的程序可能会在这之后做什么。 更具体地说,由于编译器在任何情况下都没有义务生成代码来检查循环索引,在任何情况下它都可能大于十,所以根本没有义务生成代码来检查它; 它可以相反地假定<=10
testing将总是产生真实的。 请注意,即使代码读取a[10]
而不是写入,情况也是如此。
当你遍历i==9
你将0赋给实际位于数组之外的“数组项”,所以你覆盖了其他一些数据。 很可能你会覆盖位于a[]
后的i
variables。 这样,你只需将i
variables重置为零 ,从而重新启动循环。
你可以发现你自己,如果你打印i
在循环中:
printf("test i=%d\n", i);
而不是仅仅
printf("test \n");
当然这个结果在很大程度上依赖于你的variables的内存分配,而这个variables又取决于编译器和它的设置,所以它通常是未定义的行为 – 这就是为什么在不同的机器或不同的操作系统或不同的编译器上的结果可能不同。
错误在部分数组[10] w / c也是我的地址(int数组[10],我;)。 当数组[10]设置为0,那么我将是0 w / c重置整个循环,并导致无限循环。 如果数组[10]介于0-10之间,将会出现无限循环。正确的循环应该是for(i = 0; i <10; i ++){…} int array [10],i; for(i = 0; i <= 10; i ++)array [i] = 0;
我会build议一些我在上面find的东西:
尝试赋值array [i] = 20;
我想这应该终止在任何地方的代码..(给你保持我<= 10或11)
如果这个运行,你可以坚定地决定,这里指定的答案已经是正确的[答案与记忆跺一个例子]
这里有两件事是错误的。 int i实际上是一个数组元素,数组[10],如堆栈中所见。 因为你已经允许索引实际上使数组[10] = 0,所以循环索引i将永远不会超过10.使其for(i=0; i<10; i+=1)
。
就像K&R所说的那样,“++”就是“坏风格”。 它增加了i的大小,而不是1. i ++是指针math,i + 1是代数。 虽然这取决于编译器,但对可移植性来说这不是一个好的惯例。