从计算机系统角度去理解语言类型转换原理

原文链接: 《从计算机系统角度去理解语言类型转换原理》

一、前言

每个编程语言都存在变量类型和类型之间的转换问题,一般很多书籍都提供了类型之间怎样进行转换的知识,但是很少介绍这些类型转换的背后原理。有人会问,我只要知道怎样进行转换这些类型就可以了,有必要了解这些知识么?我的觉得还是很有必要了解,只有了解了这些类型转换的原理,我们在编程时才能避免一些坑。例如:

float sum_element(float a[], unsigned length){
    int i;
    float result = 0;
    for(i = 0; i <= length -1; i++){
        result += a[i];
    }
    return result;
}

以上代码看着好像是没问题,但是隐藏着一个很大的bug,这个问题就是因为类型转换导致的。在这里我们先买个关子,具体会出现什么问题?为什么会出现这个问题?我们先介绍本文主要内容后再回答这些问题。

二、整数的表示形式

首先介绍计算机中的整型是如何表示的,了解整数在计算机中的表示才能知道整数的转化原理。整数在计算的表示方式有:补码、反码还有原码。目前大部分计算机都采用补码来表示整数。整数分为有符号和无符号,这两种方式在计算机里的表示不同。

1.无符号整数

这种形式不包含负数,只表示0和正整数。采用补码表示时,其实就是将数字转化为对应的二进制表示。
例如:12345对应的补码表示是:00 00 30 39(16进制)。
无符号的补码编码形式:
$$B2U_w(x) = \sum_{i=0}{w-2}x_{i}2{i}$$

如果要将二进制转换为10进制数,就按照上面的式子计算。
例如$B2U_4([0001]) = 023+0*22 + 02^1 + 1*2^0$

2.有符号表示

补码使用最高位表示符号,1表示整数,0表示负数。因此正负数各占一半,因为0是非负数(正整数),因此负数的范围会比正整数的范围大。例如c语言中int的取值范围是$-2^7$ ~$2^7-1$。
有符号的补码编码形式如下:
$$B2T_w(x) = -x_{w-1}2{w-1}+\sum_{i=0}{w-2}x_{i}2^{i}$$

同样二进制数转换为10进制数则采用以上公式计算。
在c语言标准中并没有要求用补码形式来表示有符号的整数,但几乎所有的机器都这么做了。而java的标准非常明确要求使用补码表示

3. 有符号和无符号整数之间的转换内幕

在C语言中处理同样字长的有符号数和无符号数字之间相互转化的规则是:保持位值不变,只是改变了解释这些位的方式,因此可能得到不同的数值。例如:

short int sv = -12345;
unsigned short uv = (unsigned short) sv;
show_short(sv); //显示二进制
show_unshort(uv); //显示二进制

其输出结果如下:

[zjl@ ~/workplace/]$ ./show_bytes
 cf c7 
 cf c7

从结果可以看出sv 和uv表示的二进制是一样的,但是具体的值会发生变化。
C语言中有符号数字与无符号数值进行计算时,首先将有符号数值转化为无符号数值,然后运算。这种方式对于标准的运算方式来说并无差异,但是如果是逻辑关系运算符时,就会出现问题。

例如:

-1 < 0
-1 < 0u

以上两个例子中输出的结果并不是都为真。 -1 < 0 因为两个都是有符号数据,直接比较。而-1 < 0u, 因为后面的值是无符号类型,因此将1转换为无符号类型。此刻1变为了2147483647U, 故出现了-1 > 0U的现象。java中不存在无符号整型类型,因此也不存在以上问题。

4. 短字长类型与长字长类型之间的转换

1)短类型转化为长类型
对于无符号的转化为一个更大的数据类型,只要简单的在原来二进制值前添加0则可,这种运算叫做零扩散
例如:

unsigned short int v = 1; //二进制为  00 01
unsigned int  iv = vl; //二进制变为 00 00 00 01

对于有符号数值,将一补码转换为一个更大数值类型可以执行符号拓展,在补码之前补充最高位值得副本,即将$[x_{w-1},x_{w-2},...,x_0]$转换为$[x_{w-1},x_{w-1},...,x_{w-1},x_{w-2},...,x_0]$.
例如:

short sx = -12345; // cf c7
int x = sx; // ff ff c7 值为:53191

如果是负数,前面补充1,根据补码编码计算公式,最后得到的值不变。举个例子:比如将3位数变为4位数,位向量[101]的值为-4 + 1 = 3。对它进行符号拓展,得到位向量为[1101], 表示的值为-8 + 4 + 1 = -3。(具体证明公式感兴趣的可以参考《深入理解计算机系统》P49)同样如果是整数,补充0,最后的结果也是不变。

  1. 长类型转化为短类型
    长类型转换为短类型则采用截断方式,即根据短类型长度从长类型中截取最低位部分。$[x_{w-1},x_{w-2},...,x_0]$转为为k位的数字类型,变为$[x_{k-1},x_{k-2},...,x_0]$
    例如:
int i = -12345; // ff ff cf c7
short i = (short) i; // 值为-12345, 补码:cf c7

三.揭开谜底

知道了c语言的转换原理以后,我们再回头看看文章开头提出的问题。

float sum_element(float a[], unsigned int length){
    int i;
    float result = 0;
    for(i = 0; i <= length -1; i++){
        result += a[i];
    }
    return result;
}

当length = 0 作为输入时,这段代码存在两个问题:

  1. 如果a数组长度有限,则出现数组下标越界问题
    length 是一个unsigned类型,即无符号类型。此刻length -1 的值会变为UMax, 所以此刻会进入到循环中。因为最大值为UMax,则for循环会不断迭代。如果数组长度有限就会出现数组下标越界问题。
  2. 如果a数组长度非常大,则出现系统异常
    因为length是无符号数字,而int是符号数字,length的取值范围比i大。因此当i达到int类型的最大值时还是小于length-1。i 会继续加1,最大值加1后就变为了负数最小值-214783648。此刻访问未知地址,可能会导致系统异常。

四、参考

[1]《深入理解计算机系统》

posted @ 2017-03-14 19:51  along-JL  阅读(926)  评论(0编辑  收藏