首先看一下以下这个程序:
void printnum (long n)
{
if (n < 0)
{
putchar('-');
n = -n;
}
if (n >= 10)
printnum(n / 10);
putchar ((int)(n % 10) + '0');
}
该程序的流程:
首先检查n是否为负,如果是负数,就打印一个负号,然后让n反号,即-n。接着检查n是否大于等于10,假如条件满足,说明n的十进制包含两个或两个以上的数字,然后递归调用printnum函数打印出n的十进制表示中的末尾数字。
程序虽然简单,但是这段代码存在几个可移植性的问题。
第一个问题在于
putchar ((int)(n % 10) + '0');
通过n%10来得到末尾数字的值,这没问题,但是与字符’0’相加得到对应的数字字符表示却不一定正确。该加法操作实际上假定了在机器的字符集中数字是顺序排列、没有间隔的,这样才会得到正确的字符。这种假定,对ASCII字符集和EBCDIC字符集是正确的,对符合ANSI的C实现也是正确的,但对某些机器却有可能出错。
要避免这个问题,可以使用一张代表数字的字符表,改写成如下:
putchar ("0123456789"[n % 10]);
程序就改写成
void printnum (long n)
{
if (n < 0)
{
putchar('-');
n = -n;
}
if (n >= 10)
printnum(n / 10);
putchar ("0123456789"[n % 10]);
}
但是这段程序还存在一个很隐蔽的错误,就是
n = -n;
这里可能会发生溢出,因为基于2的补码的计算机一般允许表示的负数取值范围要大于正数的取值范围,所以要避免对-n求值。如果能够保证不将n转换为对应的正数,那么就可以避免这个问题。
要解决这个问题,首先反过来思考,改变一个正整数的符号都可以确保不会发生溢出。所以具体做法如下:
当n为负数时,打印一个负号。程序打印完负号之后强制n为负数,并且让所有的算术运算都是针对负数进行的。但必须保证打印负号的操作所对应的程序只被执行一次,最简单的方法就是把程序分解为两个函数。
改写的程序如下:
void printneg (long n)
{
if (n <= -10)
{
printneg(n / 10);
}
putchar ("0123456789"[-(n % 10)]);
}
void printnum (long n)
{
if (n < 0)
{
putchar('-');
printneg(n);
}
else
printneg(-n);
}
但是这段程序还是存在一个可移植的问题,原因在于以下这句:
putchar ("0123456789"[-(n % 10)]);
这里先要普及一个知识,
假定a除以b,商为q,余数为r:
q = a / b;
r = a % b;
假定b > 0,则希望a、b、q、r之间维持怎么样的关系?
- 最重要的一点是,q * b + r = a,这个是定义余数的关系
- 如果改变a的正负号,则希望同时可以改变q的负号,但这不会改变q的绝对值
- 当b > 0时,希望保证r >= 0 且 r < b
但是,这三个性质不可能同时成立。
考虑一个简单的例子:3/2,商为1,余数为1。此时性质1满足。(-3)/2的值应该为多少?如果先满足性质2,则商应该是-1,余数则为-1,这样性质3就无法满足了。而如果要先满足性质3,余数为1,然后根据性质1得出商为-2,但这样性质2就无法满足了。
所以必须放弃上述3个性质中的其中之一。大多数程序设计语言选择了放弃性质3。而对于C语言的定义,它只保证了性质1,以及当a >= 0 且 b > 0时,保证 |r| < |b|以及r >= 0。所以对于语句:
putchar ("0123456789"[-(n % 10)]);
当n为负数时,n%10的行为表现与具体实现有关,它有可能为正,也有可能为负。当为正数时,这句话就会出错。
要解决这个问题,可以创建两个临时变量来分别保存商和余数。在除法运算完成之后,检查余数是否在合理的范围内,如果不是,则适当调整两个变量。
改写的最后程序如下:
void printneg (long n)
{
long q;
int r;
q = n / 10;
r = n % 10;
if (r > 0)
{
r -= 10;
q++;
}
if (n <= -10)
{
printneg(q);
}
putchar ("0123456789"[-r]);
}
void printnum (long n)
{
if (n < 0)
{
putchar('-');
printneg(n);
}
else
printneg(-n);
}
至此也算告一段落,为了满足可移植性修改了如此之多的内容。但是努力提高软件的可移植性,实际上是延长了软件的生命期。
可移植性的软件比较不容易出错。该函数的代码改动看上去是提高软件的可移植性,实际上大多数工作是确保边界条件的正确。即保证当printnum函数的参数是可能取到的最小负数时,它仍然能够正常工作。