赞
踩
一、问题
本段代码有什么问题?如何修改?
- #include <iostream>
- using namespace std;
-
- #define MAX 255
-
- main()
- {
- char p[MAX+1];
- char ch;
-
- for (ch=0;ch<=MAX;ch++)
- {
- p[ch]=ch;
- cout<<ch<<" ";
- }
- cout<< ch<<" ";
- }
解析:
首先这段程序在运行时会陷入死循环,归其原因是代码中“char数值问题”。具体分析如下。
1、char取值范围
char类型占一字节(8位),取值范围为-128到127。如何得到的取值范围呢?具体如下:
由于数字在计算机中是以补码形式表示,并且char类型为有符号数,则一字节(8位)中的最高位为符号位。能表示的最大正数二进制形式为“01111111”,由于正数补码与原码相同,其原码同样为“01111111”,对应十进制为127;同理,能表示的最小负数二进制形式为“10000000”,由于这是补码形式,其原码对应为“110000000”,对应十进制为-128。
2、知道了char的取值范围,回过头看题目中代码的for循环语句,ch的取值从0到127这阶段的循环语句都很正常。当ch=128时,由于超出char取值范围,编译器会将ch值变为-128,仍然满足<=MAX条件,继续执行循环,同时ch++。由于每次for循环ch都自增(ch++),ch值将会逐渐从-128递增到0,这时又回到了for循环的起始条件处(ch=0),当再次递增到127后,ch值又会变为-128,以后不断循环。
3、从以上分析中可以看出,“ch++”语句并没有使ch值一直增加,相反ch值不断在“-128到127”中循环变化,进而是“ch<=MAX”(MAX=255)永远成立,程序陷入死循环。
下面,为了探究其根本原因,对程序进行调试,从编译器角度分析程序陷入死循环原因。
深入分析:
主要看程序中“for (ch=0;ch<=MAX;ch++)”和“p[ch]=ch;”语句的汇编代码:
for (ch=0;ch<=MAX;ch++)
0040116E mov byte ptr [ebp-104h],0
00401175 jmp main+35h (00401185)
00401177 mov al,byte ptr [ebp-104h]
0040117D add al,1
0040117F mov byte ptr [ebp-104h],al
00401185 movsx ecx,byte ptr [ebp-104h]
0040118C cmp ecx,0FFh
00401192 jg main+7Ch (004011cc)
{
p[ch]=ch;
00401194 movsx edx,byte ptr [ebp-104h]
0040119B mov al,byte ptr [ebp-104h]
004011A1 mov byte ptr [ebp+edx-100h],al
从汇编代码中可知,edx寄存器中存放的是ch值,[ebp+edx-100h]对应“char型数组p”下标。通过调试程序可知p[0]内存地址为0x0013fe80,p[1]内存地址为0x0013fe81,p[2]内存地址为0x0013fe82,……p[127]内存地址为0x0013feff。p[0]至p[127]内存布局如下图所示(其中127的十六进制表示为“0x7F”):
从p[0]到p[127],一切都很正常。当执行ch++后ch=128 时,就会出现问题,继续看汇编代码。
for (ch=0;ch<=MAX;ch++)
00401177 mov al,byte ptr [ebp-104h] //此时[ebp-104h]地址为0x13fe7c,对应数据为“0x7F”。
0040117D add al,1
0040117F mov byte ptr [ebp-104h],al //执行自增操作后,结果写入内存[ebp-104h]地址处,也就是
说0x13fe7c处内容将变为“0x80”。
00401185 movsx ecx,byte ptr [ebp-104h] //将内存[ebp-104h]地址处内容符号扩展后传入ecx寄存器。
看下执行这条语句后ecx寄存器中内容:
对“0x80”进行符号扩展后变为“0xFFFFFF80”。这里有必要对movsx进行下说明,
movsx:先符号位扩展,再传送。以“0x80”为例,二进制形式为“10000000”,符号扩展成32位后为“11111111111111111111111110000000”,十六进制为“0xFFFFFF80”。与movsx相对的有movzx,movzx:先零扩展再传送。
这时ecx寄存器中的数据其值的十进制已变为-128。继续往下看汇编代码。
{
p[ch]=ch;
0040118C cmp ecx,0FFh //-128确实小于255,执行循环体
00401194 movsx edx,byte ptr [ebp-104h] //同样将将内存[ebp-104h]地址处内容符号扩展后传入edx寄
存器。
0040119B mov al,byte ptr [ebp-104h]
004011A1 mov byte ptr [ebp+edx-100h],al //由于edx的内容为“-128”,此时[ebp+edx-100h]地址为
0x0013fe00。
因此,al寄存器中的数据“0x80”将被写入内存0x0013fe00地址处。
写入数据之后,程序再继续往下执行,由于数据写入的位置超出了程序之前给p[]申请的栈空间,而这部分内存空间之前的内容是cout函数地址。并且,在程序执行循环体中的“cout<<ch<<" "”代码时,会被用来存储临时数据,所以之前写入的p[ch]值,在执行一次for循环后会被清除掉,且其地址处的内容会恢复到程序开始时存入的cout函数地址。
此时,ch=-128,继续执行for循环,同时ch自增(ch++),ch的值会从-128变到0,之后程序又会正常地将p[ch]值从内存地址0x0013fe80处开始依次写入,当ch增到127后,由于编译器的movsx操作又会将ch值变为-128,陷入死循环。
总结:
从编译器角度分析,出现这种循环的根本原因是,编译器使用“movsx”操作传值,由于ch是char型,当其值在0至127之间时,对应二进制数最高位为0,也就是符号位为正,使用movsx符号扩展后仍然为正,并且高位用都用0填充。当ch值大于等于128时,对应二进制最高位为1,也就是符号位为负,符号位扩展后仍然为负,并且高位都用1填充,这就是为什么ch自增到128时,编译器将将其值变为-128的根源。
二、问题进阶
如前面所述,char范围为-128至127,若将程序改成unsigned char型,还会陷入死循环吗?修改后的代码:
#include <iostream>
using namespace std;
#define MAX 255
main()
{
char p[MAX+1];
unsigned char ch; //修改成无符号char型
for (ch=0;ch<=MAX;ch++)
{
p[ch]=ch;
cout<<ch<<" ";
}
cout<< ch<<" ";
}
运行此程序,同样陷入死循环。Why?先来看下for循环部分的汇编代码:
for (ch=0;ch<=MAX;ch++)
0040116E mov byte ptr [ebp-104h],0
00401175 jmp main+35h (00401185)
00401177 mov al,byte ptr [ebp-104h]
0040117D add al,1
0040117F mov byte ptr [ebp-104h],al
00401185 mov ecx,dword ptr [ebp-104h]
0040118B and ecx,0FFh
00401191 cmp ecx,0FFh
00401197 jg main+86h (004011d6)
{
p[ch]=ch;
00401199 mov edx,dword ptr [ebp-104h]
0040119F and edx,0FFh
004011A5 mov al,byte ptr [ebp-104h]
004011AB mov byte ptr [ebp+edx-100h],al
可以看出将ch设置成unsigned char后的汇编代码与设置成char的汇编代码有少量不同。在unsigned char版本中编译器没有使用movsx指令。在ch与MAX作比较时,char版本使用“movsx ecx,byte ptr [ebp-104h]”代码对[ebp-104h]地址取一字节放入ecx寄存器中,而unsigned char版本使用“mov ecx,dword ptr [ebp-104h]”代码对[ebp-104h]地址取一双字放入ecx寄存器中。
由于设置成unsigned char类型,此时ch取值范围为0至255。在char版本中编译器使用“movsx”进行符号扩展来限制char的取值范围,而在unsigned char版本中编译器只是确保unsigned char类型占一字节。
当ch=255时,
执行“00401185 mov ecx,dword ptr [ebp-104h]” 后ecx寄存器存储的是“0xCCCCCCFF”,
执行“0040118B and ecx,0FFh” 后 ecx寄存器存储的是“0xFF”,判断等于MAX 执行循环体内赋值代码,
执行到“00401199 mov edx,dword ptr [ebp-104h]” 后edx寄存器存储的是“0xCCCCCCFF”,
执行“0040119F and edx,0FFh” 后edx寄存器存储的是“0xFF”
执行到“004011AB mov byte ptr [ebp+edx-100h],al” 后[ebp+edx-100h]的地址为“0x0013ff7f”
执行“00401177 mov al,byte ptr [ebp-104h]
0040117D add al,1
0040117F mov byte ptr [ebp-104h],al”代码后,[ebp-104h]处存储数据将变为“0x00”,之后又会从ch=0开始执行for循环,程序再一次陷入死循环。
三、正确版本
其实,把char改为unsigned char后,还需要把for循环中的判断语句改成ch<MAX即可。正确代码如下:
#include <iostream>
using namespace std;
#define MAX 255
main()
{
char p[MAX+1];
unsigned char ch; //修改成无符号char型
for (ch=0;ch<MAX;ch++)
{
p[ch]=ch;
cout<<ch<<" ";
}
p[ch] = ch;
cout<< ch<<" ";
}
【注】本文目的在于通过一个题目的深入分析,了解编译器在处理char与unsigned char的不同之处。就题目本身而言,修改方法很多,但通过本文的思维方式,可以深入到编译器层面了解陷入死循环的根本原因。此篇内容是对《程序员面试宝典(第二版)》P.208 面试例题3的深入思考。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。