这里通过分析一个练习题来总结:
考虑下列代码,这段代码试图计算数组a中所有元素的和,其中元素的数量由参数length给出。
/* WARNING: This is buggy code */
float sum_elements(float a[], unsigned length) {
int i;
float result = 0;
for (i = 0; i <= length - 1; i++)
result += a[j];
return result;
}
当参数length等于0时,运行这段代码应该返回0.0。但实际上,运行时会遇到一个内存错误。请解释为什么会发生这样的情况,并且说明如何修改代码。
解:
首先我们发现参数length的形式参数的类型为unsigned,是一个无符号数,而无符号数是非负的,比如一个字节可以表示的无符号数范围是0~255,在代码中如果参数length等于0,则在循环中会首先对length减1来判断,而这个结果并不是一个负数,而是一个很大的正数。举个例子,对于一个字节,我们这里用十六进制来表示方便一点,0的十六进制为0x00,而0 - 1会得到0xFF,这个数表示为无符号数的大小为255,也就是一个字节所能表示的最大无符号数,这就产生了错误,但是如果我们用有符号数来解读0xFF,此时这个数的大小为-1,就正确了,所以我们的改正方法是将length声明为int类型。
在C语言中,我们通过unsigned来声明一个变量为无符号数,使用int来声明有符号数,在计算机中使用补码来表示有符号数。
在计算的时候,无符号数很容易,比如对于0101,它的无符号数大小为:0 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 = 5,很符合直觉,按照2的幂次计算然后都加起来即可;
而对于有符号数,使用补码来表示,对于一个w位数的补码,它的最高有效位称为符号位,1代表负数,0代表正数,而且计算方法也不是将2的幂次都加起来,举个例子,对于0101,它的补码大小为:-0 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 = 5,很明显这是一个正数,因为最高位是0,而对于1011,它的补码大小为:-1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 1 * 2^0 = -5,我们可以发现在计算补码的时候,要算上符号位,如果是0,那么结果和无符号是一样的,如果是1,则还要加上一个负的2的幂次,希望上面两个例子能很清楚说明有符号和无符号数之间的区别和相似之处,不要混淆两者,它们只是对一个二进制01序列的两种不同的解读方法,比如上面的1011,在无符号中,它的大小就是:1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 1 * 2^0 = 11。
但就是因为两种不同的解读方法,有时候我们在使用的时候就会出现一些反直觉的错误,这就是由于我们对计算机中的表示不了解的后果,特别是一些隐式转换,比如一个有符号数和一个无符号数运算,则有符号数会自动转换为无符号数,那么如果这个有符号数刚好是一个负数,当它转换为无符号数时,是不是就变成了一个较大的无符号正数,比如:-1 < 0U,这里U表示0是一个无符号数,这个表达式的结果是0,这很违反我们平时学习的数学,为什么-1小于0是假的呢?这就是因为-1自动转换为无符号数了,我们这里还是取一个字节,-1的补码表示为1111,0的无符号数表示为0000,但是当-1转换为无符号数后,1111就表示15,是一个较大的正数,所以15 < 0是假的。这里涉及到补码和无符号数之间的转换,有便捷的计算公式,我这里就不说了。
来源:oschina
链接:https://my.oschina.net/u/4395489/blog/3219166