C语言常见问题(四)——变量、数值溢出与类型转换
一、变量1.概念在程序执行期间值可变的数据对象称为变量。变量的值之所以可变,是因为编译器在编译时给每个变量分配了一定大小的存储空间,这个存储空间用来保存变量的值(如图)。2.C语言常见变量类型C语言每个变量都有特定的类型,类型决定了变量存储空间的大小和布局。值得注意的是,对于浮点数而言,取值范围不代表精度。float的有效数字最多为7位,精度为6~7位有效...
目录
一、变量
1.概念
在程序执行期间值可变的数据对象称为变量。变量的值之所以可变,是因为编译器在编译时给每个变量分配了一定大小的存储空间,这个存储空间用来保存变量的值(如图)。
2.C语言常见变量类型
C语言每个变量都有特定的类型,类型决定了变量存储空间的大小和布局。
值得注意的是,对于浮点数而言,取值范围不代表精度。float的有效数字最多为7位,精度为6~7位有效数字;double的有效数字最多为16位,精度为15~16位有效数字。
这是因为在计算机的存储中,实数由符号位、尾数和指数组成(即1.尾数*2^指数)。比如float变量由32位二进制数组成,其中1位为符号位,尾数占23位,指数占8位(如下图)。实数中有效位数由尾数决定,所以float的尾数最大值为2^23=8388608,一共7位,能绝对保证精度的有6位(尾数为8388609时溢出),所以float的精度为6~7位。double也是同理。
3.其他变量类型
C语言变量除了上述基本类型,还有指针类型、数组类型以及自定义的结构体类型等。
二、浅析数值溢出问题
上面我们说了,变量存储于一定的内存空间中,这个内存空间不是无限大的,其大小一般由变量的类型来决定。因此,变量有取值范围和精度的限制,我们把超出变量取值范围和精度限制的情况称为数值溢出。
1.数值溢出问题
1)先举个小例子,感受一下数值溢出
//例1
#include <stdio.h>
#define X 32767 //2^15-1
int main(){
short v1=0,v2=0;
v1=X;
v2=X+1;
printf("v1=%d,v2=%d\n",v1,v2);
return 0;
}
解析:因为short类型变量所占内存大小为2字节,共16位进制数,其中一位为符号位。因此取值范围为-2^15~(2^15-1),即-32768~32767。程序中常量X值为32767,X+1的值为32768,超出了short类型取值范围。
值得注意的是,对于unsigned整型溢出,C的规范是有定义的——溢出后的数会以2^(8*sizeof(type))作模运算。
而对于signed整型的溢出,C的规范定义是“undefined behavior”,也就是说,不同编译器可能有不同的处理方式,其运算结果有可能不同。
2)下面是一个浮点数溢出的例子
//例2
#include <stdio.h>
#define X 1.2345678
int main(){
float vf=X;
double vd=X;
printf("vf=%.16f \nvd=%.16f\n",vf,vd);
/*注意printf输出float和double类型变量,格式都是%f。而scanf输入double类型变量格式为%lf*/
return 0;
}
我们通过调试看一下vf和vd在存储空间的值 :
解析:浮点数占4字节,双精度浮点数占8字节,因此float精度为6~7位有效数字,double精度为15~16位有效数字。当我们把一个8位有效数字的数字赋值给float时,就会发生精度丢失。
3)为了让读者进一步感受浮点数溢出的问题,我们这里举了一个例子,有的初学者会误以为是浮点数溢出问题,其实并不是。
a. 计算0.0001的平方
//例3
#include <stdio.h>
#define X 0.0001
int main(){
float vf=0,v=0;
v=X;
vf=v*v;
printf("%f",vf);
return 0;
}
b. 有同学认为结果不对的原因是float变量溢出了,于是用了double类型来接受v*v的结果。发现结果仍不对。
c. 其实这里运算的结果没有错,问题出在printf上。因为printf输出浮点数时,如果程序员不控制输出的精度,printf只会打印小数部分的前6位。这个程序中的float变量并没有溢出。
d. 这个程序并不需要用到double类型,因为只涉及里一位有效数字的运算,修改一下printf的格式就可以了。(注意float的乘积是double类型,语句vd=v*v使用了隐式转换,把double类型的积转换成了float类型)
#include <stdio.h>
#define X 0.0001
int main(){
float vf=0,v=0;
v=X;
vf=v*v;
printf("%.8f",vf);
return 0;
}
总结一下,事实上造成float变量溢出的原因,可能不是我们想的数过于大或者过于小(比如对于float类型,一般最大正数可取到2^128,最小负数可取到-2^128;而最小正数可取到2^-128,最大负数可取到2^-128 ) 。编程中我们更常见的是由于有效位数过多,造成精度丢失的问题,就如例2。简单来说,就是我们要重点关注数的有效位数,而不能单纯看数的大小。
2.如何解决数值溢出问题
首先,编译器是不容易检查出数值溢出的,因此我们在写代码时,如果可能涉及比较大的数值,一定要做好数值检测(在输入时或参与运算前)。输入时可以考虑用字符串类型读入,方便检查。
如果真的需要存储或运算一个很大的数值时,我们可以考虑使用数组类型,分段存储。并自己实现其运算。
具体如何解决可以参照我的另一篇文章:超长整数运算——从斐波那契数列开始
三、类型转换
C语言的类型转换包括隐式自动转换和强制类型转换。
1.隐式自动转换
1)引例
#include <stdio.h>
#define X 0.0001
int main(){
float vf=0,v=0;
v=X;
vf=v*v;
printf("%.8f",vf);
return 0;
}
解析:变量v类型为float,v*v的结果为double类型。我们知道double类型在内存中占8byte,float占4byte。当我们把double类型的数赋值给一个float类型变量时,就发生了隐式转换(double类型自动转换成float类型)。注意,这里double类型转float类型的方式是直接截断(不存在四舍五入的情况)。
2)隐式转换规则
在双目运算(2个操作数的运算)和三目运算(3个操作数的运算)中,当参与运算的操作数类型不同时 :
运算分量及结果向类型(优先级)高的运算分量转换,
短类型向长类型转换,
整数类型向浮点类型转换,
有符号类型向无符号类型转换。
其中char、short类型只要参与双目运算,不论参加运算的两个操作数的类型是否相同,一律转换为int类型;unsigned char、unsigned short类型只要参与双目运算,不论参加运算的两个操作数的类型是否相同,一律转换为unsigned int类型。
隐式转化规则( ->的左侧类型(优先级)比右侧低 ):
int -> unsigned int -> long -> unsigned long -> long long -> unsigned long long -> float -> double -> long double
例如,一个int类型变量和一个float类型变量相加,int类型比float类型低,会先把int类型自动转换为float类型,再与float变量相加,运算结果为float类型;两个char类型变量相加,都先转换为int类型,相当于两个int类型相加,结果为int类型。
3)赋值语句中的隐式转换
例如:
int a;
float b=3.14;
a=b;
其中赋值语句“a=b;”发生了隐式转换,相当于“a=(int)b;”。运行后a的值为3。
赋值语句中的隐式转换规则是,赋值号右边表达式的值向左边变量的类型转换,如果左边变量的存储空间较小则直接截取高位。
2.强制类型转换(显示转换)
强制类型转换的格式 :
(类型名)表达式
指程序员想要将表达式转换为()中的类型,()中的类型名可以是关键字,也可以是用户自定义的类型。例如,(float)a将a转变为了float类型。
什么时候使用强制类型转换 :
(1)不同类型间数据的赋值(尤其是不同枚举类型间的赋值),例如:
#include <stdio.h>
enum e1{A,B,C} v1;
enum e2{D=5,E,F} v2;
int main() {
v1=A;
v2=(enum e2)B;
printf("%d,%d",v1,v2);
return 0;
}
如果你不使用强制类型转换,编译器就会报错 :
(2)使用malloc分配动态内存时,例如
p=(ptype)malloc(sizeof(celltype));
注意,强制转换实际上是依靠关闭编译器的某些类型检查来实现的,在实际编程中应尽量避免强制转换类型的使用,例如不同类型的枚举变量之间就不应该相互赋值。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)