Char类型数值超出范围,导致程序陷入死循环深入分析

一、问题

        本段代码有什么问题?如何修改?

#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”


    此时[ebp-104h]处存储的是“0xFF”继续执行下一轮for循环。

       执行“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的深入思考。




  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值