《C语言点滴》读书笔记(3)第3章数据类型(关键词:C语言/数据类型)

来源:互联网 发布:淘宝客服售后技巧 编辑:程序博客网 时间:2024/06/06 11:37

第3章 数据类型

3.1 原码、反码和补码的解释

  • 原码:原码是一种计算机中对数字的二进制表示方法,数码序列中最高位符号位符号位0 表示正数符号位表示负数其余有效值部分二进制绝对值表示。

  • 反码:如果机器数是正数,则该机器数的反码原码一样;如果机器数是负数,则该机器数的反码它的原码符号位除外各位取反得到的。

  • 补码:如果机器数是正数,则该机器数的补码原码一样;如果机器数是负数,则该机器数补码是对它的原码除符号位外各位取反,并在末位1

(读者:以下描述中提到的图,请参照书上。)
顾名思义,无符号数不能表示负数。为了解决这个问题,我们把最高位定义为符号位。如果最高位1 就代表是一个负数,其他三位表示对应的十进制数。如图3-3所示,这就是有符号数的原码表示。

原码来表示一个有符号数会带来两个问题。第一个问题就是正负相加不等于零。如图3-3 所示,1+(-1),就是0001+1001=1010,按照原码表示等于-2。第二个问题就是有两个零存在,分别为0000 和1000。可见,原码不适合用来表示有符号数

为了保证正负相加等于零,我们采用了反码的表示方法,反码的表示如图3-4 所示。如果把二进制数1000 想象成12 点,把二进制数0000 想象成6 点,原码就是从12 点开始顺时针排列-1到-7,而反码就是从6 点开始逆时针排列-1到-7。这样做的好处就在于现在正负数相加等于零了。例如,1+(-1),就是0001+1110=1111,用反码表示的话就是(-0)了。

第一个问题解决了,但是第二个问题还是没有解决,在用反码表示有符号数的图3-4 中,依然有两个零存在,分别为00001111。聪明的读者一定已经猜到了,补码就是为了解决这个问题而发明的。

按照补码的定义,-1反码1110,不过现在必须在末位加1,那现在-1就是1111了。以此类推,补码的表示如图3-5。与反码表示不一样的是,补码在负数上从-1表示到-8,-0不再存在了。

虽然第二个问题解决了,但用补码表示时,正负相加还等于零吗?我们可以自己验证一下,如果丢弃最高位的进位,结果满足正负相加等于零。至此,我们找到了*终极的解决方案,那就是利用补码来表示一个有符号整数。注意一点,对于无符号数原码反码补码都是一致的。所以我们得到的最后结论是:整型数计算机中,使用补码表示。

3.2 整型数的溢出

如果3.1 节你已经完全明白了,那么下面我们开始讨论在实际编写程序中非常危险的一个bug,那就是C 语言的整型数溢出问题。大家不要害怕,有了3.1 节的基础,下面讨论的溢出问题会比较容易理解。

首先,我们知道,有符号数在计算机内部都是通过补码来表示的。其次,在图3-5中,如果加上x,就代表着顺时针走x 格;如果减去x,就代表着逆时针走x 格。有了这些知识,让我们用实例说话。在图3-5 中,如果3+1,那就是从3 顺时针走一格,等于4,没有任何问题。但是如果是7+1 呢?顺时针走1 格后,变成了-8了。如果7+7呢,顺时针走7 格,等于-2了。这就是整型数发生了溢出。所谓的溢出,就是因为4位二进制数,用补码表示一个整数的时候,所能表示最大正整数就是2(4-1)-1 = 7,如果在7 的基础上再加1,就会发生“轻微溢出”,变为了最小的那个负数,如果两个最大的正整数相加,就会发生“严重溢出”。结果等于-2。这里注意,所谓的“轻微溢出”和“严重溢出”都是从溢出的角度去定义的。事实上,它们都是非常严重的bug,在实际编程中并不是说“严重溢出”就比“轻微溢出”更严重。

3.3 溢出深入分析

3.3.1 溢出的定义

3.3.2 溢出的边界

3.3.3 溢出的危害

3.3.4 避免溢出的方法

一个比较简单的避免溢出的方法就是利用double 数据类型。一般情况下,让double 溢出还真是件比较困难的事。如果要求一定要用整型数来完成任务,那么你首先应该利用表3-1 中的宏定义,了解一下C 语言在你所用平台上的极限值,并且利用这些宏定义和程序3-4 中列出的判断表达式来避免溢出的发生。请注意,程序3-4 中的判断表达式有的时候代表溢出会发生,有的时候代表溢出已经发生。

3.4 无符号数

前面已经介绍了整数的二进制表示方法,对于n 位二进制数,也就只能表示2n个整数。一个顺其自然的想法就是用这2n个整数的一半来表示正数,另外一半来表示负数。这种表示方法就是有符号数了。

但是在实际的应用中,有很多种情况是不会出现负数的。比如说我们的年龄,一个班级的课程数,一个国家的人数等。如果用有符号数来保存这些值,那么永远不会用到表示负数的那一半范围,这样就被白白浪费了,而且还使得正数的表示范围被占用了一半。

针对这个问题,C 语言中引入了无符号数的概念。无符号数的源码、反码、补码都一致,具体的表示方法请参考3.1 节中的介绍。对于n 位二进制数,表示0 到2n-1-1范围内的整数。

注意,无符号这个特性,只适用于整型数,而不适用于浮点数。同时我们一定要注意在表达式中混用有符号和无符号数的情况。这是因为C 语言的表达式中会发生一种比较神奇的隐式数据类型转换,这种隐式的数据转换会带来一些隐含错误,如程序3-5 中所示。

程序3-5 无符号整型数溢出实例

1 int Sum(int a[], unsigned length){2 int i = 0;3 int sum = 0;4 for(i = 0;i<=length-1 ;i++){5 sum+=a[i];6 }7 return sum;8 }9 if(-10U){10 printf("???");11 }

程序3-5 中的Sum 函数是一段非常中规中矩的程序,每个细节都考虑得很好。数组长度 length 可能是的,所以声明为unsigned。在程序3-5 第4 行的表达式i<=length-1 中,由于length 是一个无符号整数,整个表达式length-1最后的结果也被隐式转换为无符号类型。这样,
当length=0 的时候,整个表达式变为了 0U-1U,在3.3.2 节中的图3-7 中,下溢出正好对应这种情况。最后造成的结果就是,当length=0 的时候,表达式length-1 的值是最大的无符号数UINT_MAX,这样for 循环就会执行UINT_MAX 次。不过你不用担心,for 循环不会真的执行
UINT_MAX 次,因为它会因为执行非法的内存访问而被操作系统一脚踢出。

与此类似的一个情况如程序3-5 中的第9~11 行所示,这段程序会打印出“???”。因为在表达式中会发生隐式类型转换,所以-1 被转换成了无符号类型。别忘了,-1的二进制表示(全部二进制位都是1)在无符号整数中被定义为UINT_MAX。

无符号数据类型另外的一个主要隐含错误来源于sizeof,我们在3.9 节会给出另外一个实例。本来无符号数是为了能扩大其表示的范围,但是却带来了很多的隐含错误,有点得不偿失了。所以,尽量不要在你的程序中使用无符号类型,以免增加不必要的复杂性,尤其不要仅仅因为无符号类型不存在负数而使用它来表示一个数值。随着计算机机器字长从32 位过渡到64位,一个有符号数据类型int 所表示的范围已经很大了,基本的问题都可以hold 住,别忘了,实在不行还有long long类型。用int 数据类型的好处在于,当涉及混合类型操作(比如比较有符号数和无符号数)的时候,我们不必担心上面描述的边界溢出情况(比如-1 会被提升为一个非常大的正数)。我的一个建议就是:避免一个表达式混合使用无符号数有符号数

如果你一定要用,最好在表达式中使用强制类型转换,使它们同时为有符号数或无符号数,精确地告诉编译器你想干什么,别让编译器隐式地替你拿主意。

目前,无符号数多用在位段以及位操作上。在位操作中,有的时候需要逻辑移位,而不是数学移位,这个时候我们就必须用无符号数了。关于什么是逻辑移位,我们在4.7 节中再详细讨论。

3.5 int 和char 的关系

3.5.1 char 就是short short

3.5.2 char 的符号

3.6 浮点数的有效位

3.7 判断两个浮点数相等

3.8 常量与常量后缀

3.9 sizeof 运算符

3.9.1 sizeof 返回值

3.9.2 sizeof 的用处

3.9.3 sizeof(指针)和sizeof(数组)的区别

3.10 本章小结

参考文献:
1.《C语言点滴》

阅读全文
0 0
原创粉丝点击