溢出、截断、类型提升:从易错代码入手分析整型数据存储与类型转换

简介: 这篇文章介绍了关于数据在内存中的存储以及可能出现的溢出问题,包括整型数据的存储方式、取值范围以及溢出的现象和原因。文章通过例子和罗盘图解释了整型数据溢出时的计算过程,并指出在进行运算时要注意数据类型的转换和可能的溢出情况。此外,文章还给出了几个练习题,帮助读者理解和应用这些知识。


前言

数据类型之间的转换包含强制类型转换与自动类型提升,而数据在内存中的存储方式也存在溢出、截断的问题。这样的问题比较隐蔽,在实际做题时往往稍不留心,就会犯错(而且常常错答与标答差十万八千里)。


本文整理了与“数据在内存中的存储”知识点相关的易错题,并在习题前附带了基础知识的简述。对该部分基础知识仍有疑问的同学可以移步至其它博主发表的该知识点基础知识详解博客。浮点数的存储与习题本文中不涉及。


本文以习题解析为主,供大家参考学习,希望有所帮助。


一、引例

有如下代码,运行后程序将输出怎样的结果?


int main()
{
  unsigned char a = 200;
  unsigned char b = 100;
  unsigned char c = 0;
  c = a + b;
  printf(“%d %d”, a+b,c);
  return 0;
}


解题方式在文末。


二、简述:整型在内存中的存储


1.整型的存储方式


尽管计算机打印让我们看到的是原码,但整型其实以二进制补码的方式存储在内存中,整形表达式计算使用的也是内存中存储的补码。


一个数的原码则是这个数直接转换成二进制,反码是原码的二进制符号位不变,其他位按位取反。


正数:补码=反码=原码


负数:补码=反码+1=原码按位取反+1


对于符号数(signed)而言,左边第一位二进制位为符号位,0表示正,1表示负,其余为数值位,存储实际的数值;


对于无符号数(unsigned)而言,所有的数位都是数值位,没有符号位。


特别注意:在进行加法减法等运算时,符号位也是参与运算的。



2.基本内置类型及其取值范围



3.取值范围

有符号数以其左边第一位是符号位,不计入数值计算。故 n 位有符号数,只有(n-1)位用来表示数值。因而它能表示的范围为:~ .正数方面之所以要减一,是因为其中还有个0在中间。


(注意:当二进制补码满位,而最左位是1,后面为全0时,该数表示 ,而不是负0。注意在补码表示法中只有一个0,如char类型存储二进制1000 0000,表示的不是十进制的-0,而是-128)。


如有符号的char类型,可表示的范围为到(-128到127)


无符号数的取值范围则是0到,因为它没有符号位,所有的数位(n)都用以表示数值。之所以还要减一,是因为范围从0开始。


如无符号的char类型,可表示的范围为0到(0到255)


二、详解:溢出


因为大小(即存储单元的位的数量)的限制,可以表达的整数范围是有限的。


1. 无符号数的溢出

在n位存储单元中,我们可以存储的无符号整数可表示的整数仅为0到-1之间。如果超出了上限-1,就会发生溢出。



无符号整数溢出 -- 罗盘图



下面的罗盘图显示了如果在仅为4位的内存中,存储大于15(即-1)的整数的情况。当已经存了整数11后,又再加上9,得到的结果21超出了可存储的最大值15,这时发生溢出。


表示十进制数20的最少需要5位,即20 = 10100b,而该计算机只能存4位,因此,计算机会丢掉最左边的位,并保留最右边的4位0100b。计算机会将0100b当成一个无符号整数解析成十进制的4.因此,当看到新的整数显示为4而不是20时,就不必惊讶了。


2.有符号数的溢出

以二进制补码表示法存储的整数也会溢出。下面的罗盘图显示了当使用4位存储单元存储一个带符号的整数时,出现的正负两种溢出情况。




正数溢出

当我们试图存储一个比7大的正整数时,出现正溢出。例如,我们先在存储单元中保存整数5,又再加上6。我们期望结果是11,但计算机响应的却是-5。这是因为在一个循环的表示中,从5开始顺时针走6个单位,就停在-5。一个正数溢出,将整数限制在了该范围中。



正数溢出 -- 罗盘图


负数溢出


当我们试图存储一个比-8小的负整数时,又出现负溢出。

例如,我们先在存储单元中保存整数-3,又再减去7。我们期望结果是-10,但计算机响应为+6。这是因为在一一个循环的表示中,从-3开始逆时针走7个单位,就停在+6了。



负数溢出 -- 罗盘图


2.小结

以上,我们通过罗盘图非常详细地了解了溢出的原理。溢出并不复杂,说白了就是,当超出某一数据类型的存储范围时,因为存不下,多余的部分会被处理,剩下的数值部分则按照原来的存储方式解析。


例如,signed char类型的数据最多只能存放127,这时如果令 char a = 500,a必然存不下,则会发生如下状况:


(注:常量500为int类型,为32位整数,在赋值给char的时候会先发生截断,将从右往左超过8位的数位全部丢弃。下面为方便清晰起见,没有把int原有的32位全部都写出来。这个点后面还会提。)




如果令a为200,则会发生如下情况:




但若将代码语句写成 unsigned char,再赋值200或500,输出又会有所不同。因为它是以无符号数存储的,虽然二进制列不变,但解析方式发生了变化,以无符号数解析出的二进制序列自然与有符号数的解析不同。


大家需要了解这个。


简而言之,判断溢出结果的关键就是思路清晰地一步一步模拟计算机的操作步骤:


1. 看左右数据类型是否匹配。若不匹配,则发生自动类型转换。


如char a = 500; 左为char右为int,先将int转变成char,丢弃数位。


2.转换为二进制序列,判断以何种方式存储,进而判断是否能存得下。若存不下,则说明发生了溢出。


如char类型存不下500


3.保留与要存储的数据类型相同的位数,并按该类型的存储方式解析数据。多余的数位丢弃,并判断最高位是否为符号位。最后将该转换后的二进制序列输出为十进制数即可。


三、详解:整型的数据类型转换

数据类型转换就是将数据(变量、数值、表达式的结果等)从一种类型转换为另一种类型。顺带说,它往往也是学校C语言专业课期末考试的重点。


1.自动类型转换

自动类型转换是编译器偷偷地、隐式地进行的数据类型转换,这种转换不需要程序员干预,会自动发生。


赋值时发生的自动类型转换

将一种类型的数据赋值给另外一种类型的变量时,就会发生自动类型转换。


float f = 100;

100 是 int 类型的数据,需要先转换为 float 类型才能赋值给变量 f。再如:


int n = f;


int n = f;


f 是 float 类型的数据,需要先转换为 int 类型才能赋值给变量 n。


在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型。由于不同类型的数据的存储模式与解析方式不同,自动类型转换可能会导致数据失真或者精度降低。上面提到的char a = 500,就发生了将int类型的500自动类型转换为char类型的情况。


运算时发生的自动类型转换

在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。


转换的规则如下:


转换按数据长度增加的方向进行,以保证数值不失真或精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。

所有的浮点运算都是以双精度(double)进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。

char 和 short 参与运算时,必须先转换成 int 类型。

示意图如下(unsigned即unsigned int)

由短数据向长数据转换


//示例
 
 
    float PI = 3.14159;
    int s1, r = 5;
    double s2;
    s1 = r * r * PI;    
    //r与PI运算时,先都转换成double类型,右边的表达式为double类型
    //s1为int型,最终赋值时将右边的double转换为左边的int
    //s1存的是 78,小数部分被舍弃
    s2 = r * r * PI;    
    //s2本身为double类型,左右数据类型相同
    //s2存的是 78.539749


2.强制类型转换

强制类型转换是程序员明确提出的、需要通过特定格式的代码来指明的一种类型转换。换句话说,自动类型转换不需要程序员干预,强制类型转换必须有程序员干预。


强制类型转换的格式为:(type_name) expression


下面的代码就发生了强制类型转换。它的具体规则与自动类型转换相同,只不过它是由我们程序员明确指定要求的转换罢了。当然,使用强制类型转换时,程序员自己要意识到潜在的风险。


#include <stdio.h>
int main(){
    int sum = 100; 
    int count = 50; 
    double average; 
    average = (double) sum / count;
    printf("Average = %lf\n", average);
 
    return 0;
}


另外,类型转换都只是临时性的,无论是自动类型转换还是强制类型转换,都只是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的内存空间,并不会改变数据本来的类型或者值。


四、练习题:陷阱何处来


现在,我们来看习题。


1.练习1:引例


//输出结果为多少?
int main()
{
  unsigned char a = 200;
  unsigned char b = 100;
  unsigned char c = 0;
  c = a + b;
  printf(“%d %d”, a+b,c);//输出300   44
  return 0;
}


求解如下:


1. printf在传入参数时,如果是整形,会默认传入四字节。所以a+b的结果实际上是用一个四字节的整数接收的,不会越界。


2. 而c已经在c = a + b这一步中,丢弃了最高位的1,剩下的转换为了44。


3. 注意:printf不管传入什么类型的参数,printf只会根据类型的不同,用两种不同的长度存储。其中用8字节接收的只有long long、float和double(注意float会处理成double再传入),其他类型都用4字节接收。所以虽然a + b的类型是char,实际接收时还是用一个四字节整数接收的。


另外,scanf读取时,%lld、%llx等整型方式和%f、%lf等浮点型方式读为8字节,其他都读为4字节。



2.练习2


//在32位大端模式处理器上变量b等于?
unsigned int a= 0x1234; 
unsigned char b=*(unsigned char *)&a;


字节序的知识在这篇文章中,这里不赘述:

http://t.csdn.cn/eKS96


该题常见的错解是0x12。但要注意,0x1234是int类型的十六进制数,依然以32bit存储。因此,实际存储时,事实上在0x1234的左边还存着16个0,只是题干中没有表示出来:


0x 00 00 12 34


这时以unsigned char*取出变量存储单元内的值,取到是第一个存储单元中的0x00,也即0



3.练习3


//下面代码的结果是:
 
int main()
{
  char a[1000] = {0};
  int i=0;
  for(i=0; i<1000; i++)
  {
    a[i] = -1-i;
  }
  printf("%d",strlen(a));
  return 0;
}


注意:a是字符型数组,而strlen的原理是找第一次出现'\0'('\0'ASCII值为0)的位置。这时可以有两种思路:


思路一:找规律。经过分析后我们得知,题目实际上是要求a[i]等于0之前共遍历了多少个数。我们考虑char可以存储的边界-128,当-128之后,char内就存不下了,这时发生溢出,-128-1结果不是-129,而是127(注意计算是以二进制补码计算的,而不是直接用原码),127-1为126,a[i]再从上退下来,退到0时,正好128+127个数。故strlen结果为255



思路二:由于a[i]是char型,char型总共只有8位,要a[i]的值为0,只需要 -1-i 的低八位全0即可。


这时问题简化成了“寻找当-1-i的结果第一次出现低八位全部为0的情况时,i的值”


只看低八位的话,此时int类型的常量-1相当于255。所以当 i==255 的时候,-1-i 的低八位全部都是0。也即当i为255的时候,a[i]第一次为0,所以a[i]的长度就是255了。



4.补充练习-1


//输出什么?
#include <stdio.h>
int main()
{
    char a= -1;    //-1补码1:1111 1111 ... 1111 1111    char截取后八位,按有符号数解析,仍为-1
    signed char b=-1;    //同上    
    unsigned char c=-1;    //unsigned char截取后八位,按无符号解析,1111 1111为无符号数255
    printf("a=%d,b=%d,c=%d",a,b,c);    //类型提升,8位补回32位,有符号数高位补符号数后换为原码,无符号数高位补0,补码=原码
    return 0; 
}
 
//*也可以按经验,有符号char的范围在-128到127,-1在范围内,肯定没问题
//而无符号char范围在0到255,-1不再范围内,肯定要特殊处理


答案:-1  -1    255


5.补充练习-2


//输出什么?
#include <stdio.h>
int main()
{
    char a = -128;
    printf("%u\n",a);    //%u 打印无符号int类型
    return 0; 
}
 
//-128原码:1000 0000 ... 00 1000 0000
//-128补码:1111 1111 ... 11 1000 0000
//a:1000 0000
//printf类型提升:1111 1111 ... 1000 0000 (补码)
//%u 看作无符号整数,那么上面提升后的二进制序列为正数的原码,换成十进制打印4294967168


答案:4294967168


6.补充练习-3


//输出什么?
#include <stdio.h>
int main()
{
    char a = 128;
    printf("%u\n",a);
    return 0; 
}
 
//结果同上一题,128换成32位二进制序列后,由于char只取后8位,所以最终结果与上一题的-128相同


答案:4294967168


7.补充练习-4


//输出什么?
int i= -20;
unsigned  int  j = 10;
printf("%d\n", i+j); 
//按照补码的形式进行运算,最后格式化成为有符号整数
 
//两个数都在合理范围内,因此不用考虑是否溢出或截断。直接计算即可


答案:-10


8.补充练习-5


//输出什么?
unsigned int i;
for(i = 9; i >= 0; i--) {
    printf("%u\n",i);
}


答案:死循环


9.补充练习-6


//输出什么?
int main()
{
    char a[1000];
    int i;
    for(i=0; i<1000; i++)
   {
        a[i] = -1-i;
   }
    printf("%d",strlen(a));
    return 0; 
}


答案:255


10.补充练习-7


//输出什么?
#include <stdio.h>
unsigned char i = 0;
int main()
{
    for(i = 0;i<=255;i++)
   {
        printf("hello world\n");
   }
    return 0; 
}


答案:死循环


五、总结与回顾




相关文章
|
SQL 存储 数据挖掘
【虚拟机数据恢复】VMware虚拟机文件被误删除的数据恢复案例
虚拟机数据恢复环境: 某品牌R710服务器+MD3200存储,上层是ESXI虚拟机和虚拟机文件,虚拟机中存放有SQL Server数据库。 虚拟机故障: 机房非正常断电导致虚拟机无法启动。服务器管理员检查后发现虚拟机配置文件丢失,所幸xxx-flat.vmdk磁盘文件和xxx-000001-delta.vmdk快照文件还在。服务器管理员在尝试恢复虚拟机的过程中,将原虚拟机内的xxx-flat.vmdk删除后新建了一个虚拟机,并分配了精简模式的虚拟机磁盘和快照数据盘,但原虚拟机内的数据并没有恢复。
【虚拟机数据恢复】VMware虚拟机文件被误删除的数据恢复案例
|
测试技术 持续交付 开发工具
一文掌握:Gitlab的完整使用手册
一文掌握:Gitlab的完整使用手册
|
Ubuntu 安全 网络协议
|
数据采集 前端开发 开发者
Selenium中如何实现翻页功能
在使用Python的Selenium库进行网页爬虫开发时,翻页操作是常见需求。本文详细介绍如何通过Selenium实现翻页,包括定位翻页控件、执行翻页动作以及等待页面加载等关键步骤,并提供了基于“下一页”按钮和输入页码两种方式的具体示例代码。此外,还特别提醒开发者注意页面加载完全、动态内容加载及反爬机制等问题,确保爬虫稳定高效运行。
1606 3
|
存储 关系型数据库 MySQL
使用Docker快速部署Mysql服务器
本文介绍了如何使用Docker快速部署MySQL服务器,包括下载官方MySQL镜像、启动容器、设置密码、连接MySQL服务器以及注意事项。
1317 18
|
Java
浅谈Java中的NAN与INFINITY:数值迷失与无限可能
浅谈Java中的NAN与INFINITY:数值迷失与无限可能
1104 0
|
Ubuntu Linux Windows
如何在WSL中的ubuntu编译Linux内核并且安装使用ebpf?
请注意,在WSL1中可能会由于内核架构限制而无法成功进行以上过程,WSL2对于Linux内核的完整支持更为合适。此外,部分步骤可能因不同的Linux发行版或内核版本而异。
1060 4
|
JavaScript API
Vue学习之--------列表排序(ffilter、sort、indexOf方法的使用)、Vue检测数据变化的原理(2022/7/15)
这篇博客文章讲解了Vue中列表排序的方法,使用`filter`、`sort`和`indexOf`等数组方法进行数据的过滤和排序,并探讨了Vue检测数据变化的原理,包括Vue如何通过setter和数组方法来实现数据的响应式更新。
Vue学习之--------列表排序(ffilter、sort、indexOf方法的使用)、Vue检测数据变化的原理(2022/7/15)
|
安全 算法 Shell
PWN练习---Heap_1
PWN练习---Heap_1
|
消息中间件 API
【FreeRTOS(二)】FreeRTOS新手入门——计数型信号量和二进制信号量的基本使用并附代码解析
【FreeRTOS(二)】FreeRTOS新手入门——计数型信号量和二进制信号量的基本使用并附代码解析