目录

一、变量

1.概念

2.C语言常见变量类型

二、浅析数值溢出问题

1.数值溢出问题

2.如何解决数值溢出问题

三、类型转换

1.隐式自动转换 

2.强制类型转换(显示转换)


一、变量

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));

     注意,强制转换实际上是依靠关闭编译器的某些类型检查来实现的,在实际编程中应尽量避免强制转换类型的使用,例如不同类型的枚举变量之间就不应该相互赋值。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐