8.5 为什么会出现类型

    至此,我们学习了如何用开和关的组合(比特列)来表示整数和小数。在一般人看来,整数 7 和小数 7.0 是一样的,但在计算机看来,整数和浮点数是完全不同的(图 8.11)。

    空标题文档 - 图1

    图 8.11 浮点数 7.0 和整数 7 的比特列的差异

    对于计算机来说,如果仅仅给定一串比特列,它是不知道这应该解释为整数还是浮点数的。因此,需要有表示这个值为何种类型的额外的信息。这就是类型。我们来一起看一下没有类型会带来哪些麻烦。

    没有类型带来的麻烦

    浮点数运算中 3.0+7.0 的结果是 10.0。这里用到了浮点数相加运算的命令。然而,如果忘记了 3.0 和 7.0 是浮点数而将它们作为整数做相加运算的话,结果会怎样呢? 3.0 和 7.0 的比特列作为整数读取时将变成非常大的数值,因为左边第二个比特位在解释为整数时,已经是 1 073 741 824 这么大了 21。相加运算的结果就更大了(图 8.12 A)22。

    211 073 741 824 是由 2 自乘 30 次得到的。
    223.0 是由 1 073 741 824 + 4194304 得到 1 077 936 128,7.0 是由 1 073 741 824 + 8 388 608 + 4 194 304+2 097 152 得到 1 088 421 888,相加的结果是 2 166 358 016。考虑上符号的话就变得更复杂了,这里只解释不带符号的整数情况。

    空标题文档 - 图2

    图 8.12 7.0 和 3.0 作为整数做加法和作为浮点数做加法的结果差异

    重新把这个相加的结果作为浮点数来读的话,小数点以下是 37 个 0,变成十分接近 0 的一个负数 23。这与期待的结果大相径庭。看来,要不出现这样的错误必须多加注意。

    23正确地说是 -2.93874e-38。

    早期的 FORTRAN 语言中的类型

    在内存中记录的数值是整数还是浮点数,单靠人的记忆很难避免错误。有没有更为简易的方法呢?

    一种方法是用确定的规则来表示变量名所表达的内容。比如,早期的 FORTRAN 语言使用了一系列规则,指定以 I~N 开头的变量名表示整数,除此以外的表示浮点数。

    告诉处理器变量的类型

    另一种更好的方法是告诉处理器某某变量是整数,让计算机而不是人去记忆这一信息。

    这就是变量的类型的声明产生的原因。比如 C 语言中,声明 int x; 表示名字为 x 的变量指向的内存被解释为整数,声明 float y; 表示名字为 y 的变量指向的内存被解释为浮点数。这样通过提供关于类型的信息,处理器在进行运算时,就能自动判断该做整数相加运算还是浮点数相加运算,而不需要人们逐个去指定。

    隐性类型转换

    代码中 x+y 这样的语句,处理器是怎么执行的呢?人们可能会很直接地想到简单做个加法了事,但是对于计算机来说,整数的加法运算和浮点数的加法运算是完全不同的两件事情,不妥当地予与区分是不行的。

    整数之间、浮点数之间的运算

    计算机参照数据的类型来决定怎样执行。如果 x 和 y 同为整数,就做整数之间的加法运算。如果 x 和 y 同为浮点数,就做浮点数之间的加法运算。

    C语言中整数x和整数相加
    int x = 1, ret;
    ret = x + 1024; / ←这个加法运算 /
    return ret;

    在汇编语言中变成
    movl -12(%rbp), %eax
    addl $1024, %eax # ←整数的加法运算命令
    movl %eax, -16(%rbp)

    一边为整数一边为浮点数的运算

    那如果一边是整数另一边是浮点数,该如何处理呢 24 ?

    24因语言不同,有时需要考虑到更多情形,比如是字符串怎么办,是时间怎么办,等等。

    在早期的 FORTRAN 语言中这将导致错误发生,因此需要程序显式地使用转换函数来指示类型转换。而在 C 语言中是自动地隐性地将整数转换为浮点数再进行运算。比如 x + 1024 中,如果 x 是浮点数,程序会将 1024 先转换为浮点数再进行加法运算。

    C语言中浮点数x和整数相加
    float x = 1.0, ret;
    ret = x + 1024; / ←这个加法运算 /
    return ret;

    在汇编语言中变成
    movss -12(%rbp), %xmm0
    movabsq $1024, %rax
    cvtsi2ssq %rax, %xmm1 # ←整数到浮点数的转换命令
    addss %xmm1, %xmm0 # ←浮点数的加法运算命令
    movss %xmm0, -16(%rbp)

    这样一来就方便很多了。这是因为,不管是整数加法运算还是浮点数加法运算,结果都不会产生太大的差异。

    问题点

    我们考虑一下 x / 2 这样的代码是什么意思。如果 x 为整数,除法运算后,小数点以下部分会被舍去。也就是说,在 x 为 1 时,计算结果就是 0.

    如果 x 为浮点数,除法运算后变成小数点位数足够支持显示的数。也就是说,在 x 为 1 时,计算结果就是 0.5.

    程序员如果事先不知道 x 的类型,见到 x / 2 这样的代码也不能判断是否会发生小数部分舍去。而让人们非记住类型不可,这和类型产生的本意是相违背的 25。

    25从实用角度来看,写成 x / 2.0 的形式在 x 为整数时也将其转换为浮点数,这也是一种回避策略,但不习惯的话还是比较容易出错。
    C语言
    / 仅做除法的函数 /
    float divide_int(int x){
    return x / 2;
    }

    / 此函数和divide_int相类似,只是x的类型不同 /
    float divide_float(float x){
    return x / 2;
    }

    int main(){
    printf("%f\n", divide_int(1)); / → 输出0.000000 /
    printf("%f\n", divide_float(1)); / → 输出0.500000 /
    }

    用写法来区别的语言

    C 语言中采用的设计方法是由计算对象的类型来决定是否舍去小数部分。这一方法在很长时间内被很多语言使用,以至于很多程序员都非常习惯,认为理所当然。然而,这个不是恒久不变的物理法则,只不过是人们确立的设计方法而已。因此并不是所有的语言都采用这种设计。比如在 1973 年问世的 ML 语言中,整数的除法运算就表达为 x div y, 而浮点数的除法运算表达为 x / y26。另外 1991 年问世的 Python 语言起初使用的是混杂着 C 语言风格的除法运算方式。大家从 2001 年开始讨论 这种设计的恰当性,于 2008 年发布的 Python 3.0 中做了变更,把 x / y 作为与 x 和 y 类型无关不做舍去的除法运算,带舍去的除法运算用 x // y 来表示 27。

    26ML 语言使用了“类型推断”理论,即程序员不做类型声明时,仅根据变量使用方式来推论其类型,它是一门专注于类型的语言。其后继语言 OCaml 中也用 x / y 和 x /. y 来区分整数的除法运算和浮点数的除法运算。
    27PEP 238 — Changing the Division Operator, http://www.python.org/dev/peps/pep-0238/.
    Python 2.7
    >>> 1 / 2
    0
    >>> 1 // 2
    0

    Python 3.0
    >>> 1 / 2
    0.5
    >>> 1 // 2
    0