C
语
言缺陷与陷阱
(笔记)
C
语
言像一把雕刻刀,
锋
利,并且在技
师
手中非常有用。和任何
锋
利的工具一
样
,
C
会
伤
到那些不能掌握它的人。本文介
绍
C
语
言
伤
害粗心的人的方法,以及如何避免
伤
害。
第一部分
研究了当程序被划分
为记
号
时
会
发
生的
问题
。
第二部分
继续
研究了当程序的
记
号被
编译
器
组
合
为
声明、表达式和
语
句
时
会出
现
的
问题
。
第三部分
研究了由多个部分
组
成、分
别编译
并
绑
定到一起的
C
程序。
第四部分
处
理了概念上的
误
解:当一个程序具体
执
行
时
会
发
生的事情。
第五部分
研究了我
们
的程序和它
们
所使用的常用
库
之
间
的
关
系。在
第六部分
中,我
们
注意到了我
们
所写的程序也
许
并不是我
们
所运行的程序;
预处
理器将首先运行。最后,
第七部分
讨论
了可移植性
问题
:一个能在一个
实现
中运行的程序无法在另一个
实现
中运行的原因。
词
法分析器(
lexical analyzer
)
:
检查组
成程序的字符序列,并将它
们
划分
为记
号(
token
)一个
记
号是一个由一个或多
个字符构成的序列,它在
语
言被
编译时
具有一个(相
关
地)
统
一的意
义
。
C
程序被两次划分
为记
号
,
首先是
预处
理器
读
取程序
,
它必
须对
程序
进
行
记
号划分以
发现标识
宏的
标识
符。通
过对每
个宏
进
行求
值
来替
换
宏
调
用
,
最后,
经过
宏替
换
的程序又被
汇
集成字符流送
给编译
器。
编译
器再第二次将
这
个流划分
为记
号。
1.1=
不是
==
:
C
语
言
则
是用
=
表示
赋值
而用
==
表示比
较
。
这
是因
为赋值
的
频
率要高于比
较
,因此
为
其分配更短的符号。
C
还
将
赋值视为
一个运算符,因此可以很容易地写出多重
赋值
(如
a = b = c
),并且可以将
赋值
嵌入到一个大的表达式中。
C
语
言参考手册
说
明了如何决定:
“
如果
输
入流到一个
给
定的字符串
为
止已
经
被
识别为记
号,
则应该
包含
下一个字符以
组
成能
够
构成
记
号的最
长
的字符串
” “
最
长
子串原
则
”
组
合
赋值
运算符如
+=
实际
上是两个
记
号。因此,
a + /* strange */ = 1
和
a += 1
是一个意思。看起来像一个
单
独的
记
号而
实际
上是多个
记
号的只有
这
一个特例。特
别
地,
p - > a
是不合法的。它和
p -> a
不是同
义词
。
另一方面,有些老式
编译
器
还
是将
=+
视为
一个
单
独的
记
号并且和
+=
是同
义词
。
包
围
在
单
引号中的一个字符只是
编
写整数的另一
种
方法。
这
个整数是
给
定的字符在
实现
的
对
照序列中的一个
对应
的
值
。而一个包
围
在双引号中的字符串,只是
编
写一个有双引号之
间
的字符和一个附加的二
进
制
值为
零的字符所初始化的一个无名数
组
的指
针
的一
种简
短方法。
使用一个指
针
来代替一个整数通常会得到一个警告消息
(反之亦然),使用双引号来代替
单
引号也会得到一个警告消息(反之亦然)。但
对
于不
检查
参数
类
型的
编译
器却除外。
由于一个整数通常足
够
大,以至于能
够
放下多个字符,一些
C
编译
器允
许
在一个字符常量中存放多个字符。
这
意味着用
'yes'
代替
"yes"
将不会被
发现
。后者意味着
“
分
别
包含
y
、
e
、
s
和一个空字符的四个
连续
存
储
器区域中的第一个的地址
”
,而前者意味着
“
在一些
实现
定
义
的
样
式中表示由字符
y
、
e
、
s
联
合构成的一个整数
”
。
这
两者之
间
的任何一致性都
纯
属巧合。
理解
这
些
记
号是如何构成声明、表达式、
语
句和程序的。
每
个
C
变
量声明都具有两个部分:一个
类
型和一
组
具有特定格式的、期望用来
对该类
型求
值
的表达式。
float *g(), (*h)();
表示
*g()
和
(*h)()
都是
float
表达式。由于
()
比
*
绑
定得更
紧
密,
*g()
和
*(g())
表示同
样
的
东
西:
g
是一个返回指
float
指
针
的函数,而
h
是一个指向返回
float
的函数的指
针
。
当我
们
知道如何声明一个
给
定
类
型的
变
量以后,就能
够
很容易地写出一个
类
型的模型(
cast
):只要
删
除
变
量名和分号并将所有的
东
西包
围
在一
对圆
括号中即可。
float *g();
声明
g
是一个返回
float
指
针
的函数,所以
(float *())
就是它的模型。
(*(void(*)())0)();
硬件会
调
用地址
为
0
处
的子程序
(*0)();
但
这样
并不行,因
为
*
运算符要求必
须
有一个指
针
作
为
它的操作数
。另外,
这
个操作数必
须
是一个指向函数的指
针
,以保
证
*
的
结
果可以被
调
用。需要将
0
转换为
一个可以描述
“
指向一个返回
void
的函数的指
针
”
的
类
型。
(
Void(*)())0
在
这
里,我
们
解决
这
个
问题时
没有使用
typedef
声明。通
过
使用它,我
们
可以更清晰地解决
这
个
问题
:
typedef void (*funcptr)();
//
typedef funcptr void (*)()
;
指向返回
void
的函数的指针
(*(funcptr)0)(); // 调用地址为 0 处的子程序
(*(funcptr)0)(); // 调用地址为 0 处的子程序
绑
定得最
紧
密的运算符并不是真正的运算符:下
标
、函数
调
用和
结
构
选择
。
这
些都与左
边
相
关联
。
接下来是一元运算符。它
们
具有真正的运算符中的最高
优
先
级
。由于函数
调
用比一元运算符
绑
定得更
紧
密,你必
须
写
(*p)()
来
调
用
p
指向的函数;
*p()
表示
p
是一个返回一个指
针
的函数。
转换
是一元运算符,并且和其他一元运算符具有相同的
优
先
级
。一元运算符是右
结
合的,因此
*p++
表示
*(p++)
,而不是
(*p)++
。
在接下来是真正的二元运算符。其中数学运算符具有最高的
优
先
级
,然后是移位运算符、
关
系运算符、
逻辑
运算符、
赋值
运算符,最后是条件运算符。
需要
记
住的两个重要的
东
西是:
1.
所有的
逻辑
运算符具有比所有
关
系运算符都低的
优
先
级
。
2.
移位运算符比
关
系运算符
绑
定得更
紧
密,但又不如数学运算符。
乘法、除法和求余具有相同的
优
先
级
,加法和减法具有相同的
优
先
级
,以及移位运算符具有相同的
优
先
级
。
还
有就是六个
关
系运算符并不具有相同的
优
先
级
:
==
和
!=
的
优
先
级
比其他
关
系运算符要低。
在
逻辑
运算符中,没有任何两个具有相同的
优
先
级
。按位运算符比所有
顺
序运算符
绑
定得都
紧
密,
每种
与运算符都比相
应
的或运算符
绑
定得更
紧
密,并且按位异或(
^
)运算符介于按位与和按位或之
间
。
三元运算符的
优
先
级
比我
们
提到
过
的所有运算符的
优
先
级
都低。
这
个例子
还说
明了
赋值
运算符具有比条件运算符更低的
优
先
级
是有意
义
的。另外,
所有的
复
合
赋值
运算符具有相同的
优
先
级
并且是自右至左
结
合的
具有最低
优
先
级
的是逗号运算符。
赋值
是另一
种
运算符,通常具有混合的
优
先
级
。
或者是一个空
语
句,无任何效果;或者
编译
器可能提出一个
诊
断消息,可以方便除去掉它。一个重要的区
别
是在必
须
跟有一个
语
句的
if
和
while
语
句中。
另一个因分号引起巨大不同的地方是函数定
义
前面的
结
构声明的末尾
,
考
虑
下面的程序片段:
struct foo {
int x;
}
f() {
...
}
int x;
}
f() {
...
}
在
紧
挨着
f
的第一个
}
后面
丢
失了一个分号。它的效果是声明了一个函数
f
,返回
值类
型是
struct foo
,
这
个
结
构成了函数声明的一部分。如果
这
里出
现
了分号,
则
f
将被定
义为
具有默
认
的整型返回
值
[5]
。
C
中的
case
标签
是真正的
标签
:控制流程可以无限制地
进
入到一个
case
标签
中。
看看另一
种
形式,假
设
C
程序段看起来更像
Pascal
:
switch(color) {
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}
并且假
设
color
的
值
是
2
。
则该
程序将打印
yellowblue
,因
为
控制自然地
转
入到下一个
printf()
的
调
用。
这
既是
C
语
言
switch
语
句的
优
点又是它的弱点。
说
它是弱点,是因
为
很容易忘
记
一个
break
语
句,从而
导
致程序出
现隐
晦的异常行
为
。
说
它是
优
点,是因
为
通
过
故意去掉
break
语
句,可以很容易
实现
其他方法
难
以
实现
的控制
结
构。尤其是在一个大型的
switch
语
句中,我
们经
常
发现对
一个
case
的
处
理可以
简
化其他一些特殊的
处
理。
和其他程序
设计语
言不同,
C
要求一个函数
调
用必
须
有一个参数列表,但可以没有参数。因此,如果
f
是一个函数,
f();
就是
对该
函数
进
行
调
用的
语
句,而
f;
一个
else
总
是与其最近的
if
相
关联
。
一个
C
程序可能有很多部分
组
成,它
们
被分
别编译
,并由一个通常称
为连
接器、
连
接
编辑
器或加
载
器的程序
绑
定到一起。由于
编译
器一次通常只能看到一个文件,因此它无法
检测
到需要程序的多个源文件的内容才能
发现
的
错误
。
假
设
你有一个
C
程序,被划分
为
两个文件。其中一个包含如下声明:
int n;
而令一个包含如下声明:
long n;
这
不是一个有效的
C
程序,因
为
一些外部名称在两个文件中被声明
为
不同的
类
型。然而,很多
实现检测
不到
这
个
错误
,因
为编译
器在
编译
其中一个文件
时
并不知道另一个文件的内容。因此,
检查类
型的工作只能由
连
接器(或一些工具程序如
lint
)来完成;如果操作系
统
的
连
接器不能
识别
数据
类
型,
C
编译
器也没法
过
多地
强
制它。
那
么
,
这
个程序运行
时实际
会
发
生什
么
?
这
有很多可能性:
1.
实现
足
够聪
明,能
够检测
到
类
型冲突。
则
我
们
会得到一个
诊
断消息,
说
明
n
在两个文件中具有不同的
类
型。
2.
你所使用的
实现
将
int
和
long
视为
相同的
类
型。典型的情
况是机器可以自然地
进
行
32
位运算。在
这种
情况下你的程序或
许
能
够
工作,好象你两次都将
变
量声明
为
long
(或
int
)。
但
这种
程序的工作
纯
属偶然。
3.
n
的两个
实
例需要不同的存
储
,它
们
以某
种
方式共享存
储
区,即
对
其中一个的
赋值对
另一个也有效。
这
可能
发
生,例如,
编译
器可以将
int
安排在
long
的低位。不
论这
是基于系
统
的
还
是基于机器的,
这种
程序的运行同
样
是偶然。
4.
n
的两个
实
例以另一
种
方式共享存
储
区,即
对
其中一个
赋值
的效果是
对
另一个
赋
以不同的
值
。在
这种
情况下,程序可能失
败
。
这种
情况
发
生的
另
一个例子出奇地
频
繁。程
序的某一个文件包含下面的声明:
char filename[] = "etc/passwd";
而另一个文件包含
这样
的声明:
char *filename;
尽管在某些
环
境中数
组
和指
针
的行
为
非常相似,但它
们
是不同的。在第一个声明中,
filename
是一个字符数
组
的名字。尽管使用数
组
的名字可以
产
生数
组
第一个元素的指
针
,但
这
个指
针
只有在需要的
时
候才
产
生并且不会持
续
。在第二个声明中,
filename
是一个指
针
的名字。
这
个指
针
可以指向程序
员让
它指向的任何地方。如果程序
员
没有
给
它
赋
一个
值
,它将具有一个默
认
的
0
值
(
NULL
)(
[
译
注
]
实际
上,在
C
中一个
为
初始化的指
针
通常具有一个随机的
值
,
这
是很危
险
的!)。
这
两个声明以不同的方式使用存
储
区,它
们
不可能共存。
避免
这种类
型冲突的一个方法是使用像
lint
这样
的工具(如果可以的
话
)。
为
了在一个程序的不同
编译单
元之
间检查类
型冲突,一些程序需要一次看到其所有部分。典型的
编译
器无法完成,但
lint
可以。
一些
C
运算符以一
种
已知的、特定的
顺
序
对
其操作数
进
行求
值
。但另一些不能。例如,考
虑
下面的表达式:
a < b && c < d
C
语
言定
义规
定
a < b
首先被求
值
。如果
a
确
实
小于
b
,
c < d
必
须紧
接着被求
值
以
计
算整个表达式的
值
。但如果
a
大于或等于
b
,
则
c < d
根本不会被求
值
。
要
对
a < b
求
值
,
编译
器
对
a
和
b
的求
值
就会有一个先后。但在一些机器上,它
们
也
许
是并行
进
行的。
C
中只有四个运算符
&&
、
||
、
?:
和
,
指定了求
值顺
序。
&&
和
||
最先
对
左
边
的操作数
进
行求
值
,而右
边
的操作数只有在需要的
时
候才
进
行求
值
。而
?:
运算符中的三个操作数:
a
、
b
和
c
,最先
对
a
进
行求
值
,之后
仅对
b
或
c
中的一个
进
行求
值
,
这
取决于
a
的
值
。
,
运算符首先
对
左
边
的操作数
进
行求
值
,然后抛弃它的
值
,
对
右
边
的操作数
进
行求
值
[8]
。
C
中所有其它的运算符
对
操作数的求
值顺
序都是未定
义
的。事
实
上,
赋值
运算符不
对
求
值顺
序做出任何保
证
。
出于
这
个原因,下面
这种
将数
组
x
中的前
n
个元素
复
制到数
组
y
中的方法是不可行的:
i = 0;
while(i < n)
y[i] = x[i++];
while(i < n)
y[i] = x[i++];
其中的
问题
是
y[i]
的地址并不保
证
在
i
增
长
之前被求
值
。在某些
实现
中,
这
是可能的;但在另一些
实现
中却不可能。另一
种
情况出于同
样
的原因会失
败
:
i = 0;
while(i < n)
y[i++] = x[i];
while(i < n)
y[i++] = x[i];
而下面的代
码
是可以工作的:
i = 0;
while(i < n) {
y[i] = x[i];
i++;
}
while(i < n) {
y[i] = x[i];
i++;
}
当然,
这
可以
简
写
为
:
for(i = 0; i < n; i++)
y[i] = x[i];
y[i] = x[i];
在很多
语
言中,具有
n
个元素的数
组
其元素的号
码
和它的下
标
是从
1
到
n
严
格
对应
的。但在
C
中不是
这样
。
个具有
n
个元素的
C
数
组
中没有下
标为
n
的元素,其中的元素的下
标
是从
0
到
n - 1
。因此从其它
语
言
转
到
C
语
言的程序
员应该
特
别
小心地使用数
组
:
int i, a[10];
for(i = 1; i <= 10; i++)
a[i] = 0;
for(i = 1; i <= 10; i++)
a[i] = 0;
下面的程序段由于两个原因会失
败
:
double s;
s = sqrt(2);
printf("%g/n", s);
s = sqrt(2);
printf("%g/n", s);
第一个原因是
sqrt()
需要一个
double
值
作
为
它的参数,但没有得到。第二个原因是它返回一个
double
值
但没有
这样
声名。改正的方法只有一个:
double s, sqrt();
s = sqrt(2.0);
printf("%g/n", s);
s = sqrt(2.0);
printf("%g/n", s);
C
中有两个
简单
的
规则
控制着函数参数的
转换
:
(1)
比
int
短的整型被
转换为
int
;
(2)
比
double
短的浮点
类
型
被
转换为
double
。所有的其它
值
不被
转换
。
确保函数参数
类
型的正确性是程序
员
的
责
任。
因此,一个程序
员
如果想使用如
sqrt()
这样
接受一个
double
类
型参数的函数,就必
须仅传递给
它
float
或
double
类
型的参数。常数
2
是一个
int
,因此其
类
型是
错误
的。
当一个函数的
值
被用在表达式中
时
,其
值
会被自
动
地
转换为
适当的
类
型。然而,
为
了完成
这
个自
动转换
,
编译
器必
须
知道
该
函数
实际
返回的
类
型。没有更
进
一
步
声名的函数被假
设
返回
int
,因此声名
这样
的函数并不是必
须
的。然而,
sqrt()
返回
double
,因此在成功使用它之前必
须
要声名。
这
里有一个更加壮
观
的例子:
main() {
int i;
char c;
for(i = 0; i < 5; i++) {
scanf("%d", &c);
printf("%d", i);
}
printf("/n");
}
int i;
char c;
for(i = 0; i < 5; i++) {
scanf("%d", &c);
printf("%d", i);
}
printf("/n");
}
表面上看,
这
个程序从
标
准
输
入中
读
取五个整数并向
标
准
输
出写入
0 1 2 3 4
。
实际
上,它并不
总
是
这么
做。譬如在一些
编译
器中,它的
输
出
为
0 0 0 0 0 1 2 3 4
。
为
什
么
?因
为
c
的声名是
char
而不是
int
。当你令
scanf()
去
读
取一个整数
时
,它需要一个指向一个整数的指
针
。但
这
里它得到的是一个字符的指
针
。但
scanf()
并不知道它没有得到它所需要的:它将
输
入看作是一个指向整数的指
针
并将一个整数存
贮
到那里。由于整数占用比字符更多的内存,
这样
做会
影响到
c
附近的内存。
c
附近确切是什
么
是
编译
器的事;在
这种
情况下
这
有可能是
i
的低位。因此,
每
当向
c
中
读
入一个
值
,
i
就被置零。当程序最后到达文件
结
尾
时
,
scanf()
不再
尝试
向
c
中放入新
值
,
i
才可以正常地增
长
,直到循
环结
束。
==========================================================================
C
程序通常将一个字符串
转换为
一个以空字符
结
尾的字符数
组
。假
设
我
们
有两个
这样
的字符串
s
和
t
,并且我
们
想要将它
们连
接
为
一个
单
独的字符串
r
。我
们
通常使用
库
函数
strcpy()
和
strcat()
来完成。下面
这种
明
显
的方法并不会工作:
char *r;
strcpy(r, s);
strcat(r, t);
strcpy(r, s);
strcat(r, t);
这
是因
为
r
没有被初始化
为
指向任何地方。尽管
r
可能潜在地表示某一
块
内存,但
这
并不存在,直到你分配它。
让
我
们
再
试试
,
为
r
分配一些内存:
char r[100];
strcpy(r, s);
strcat(r, t);
strcpy(r, s);
strcat(r, t);
这
只有在
s
和
t
所指向的字符串不很大的
时
候才能
够
工作。不幸的是,
C
要求我
们为
数
组
指定的大小是一个常数,因此无法确定
r
是否足
够
大。然而,很多
C
实现带
有一个叫做
malloc()
的
库
函数,它接受一个数字并分配
这么
多的内存。通常
还
有一个函数称
为
strlen()
,可以告
诉
我
们
一个字符串中有多少个字符:因此,我
们
可以写:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
然而
这
个例子会因
为
两个原因而失
败
。首先,
malloc()
可能会耗尽内存,而
这
个事件
仅
通
过
静静地返回一个空指
针
来表示。
其次,更重要的是,
malloc()
并没有分配足
够
的内存。一个字符串是以一个空字符
结
束的。而
strlen()
函数返回其字符串参数中所包含字符的数量,但不包括
结
尾的空字符。因此,如果
strlen(s)
是
n
,
则
s
需要
n + 1
个字符来盛放它。因此我
们
需要
为
r
分配
额
外的一个字符。再加上
检查
malloc()
是否成功,我
们
得到:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1);
if(!r) {
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
r = malloc(strlen(s) + strlen(t) + 1);
if(!r) {
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
提
喻
法(
Synecdoche, sin-ECK-duh-key
)是一
种
文学手法,有点
类
似于明
喻
或暗
喻
,在牛津英文
词
典中解
释
如下:
“a more comprehensive term is used for a less comprehensive or vice versa; as whole for part or part for whole, genus for species or species for genus, etc.
(将全面的
单
位用作不全面的
单
位,或反之;如整体
对
局部或局部
对
整体、一般
对
特殊或特殊
对
一般,等等。)
”
要
记
住的是,
复
制一个指
针
并不能
复
制它所指向的
东
西
。
将一个整数
转换为
一个指
针
的
结
果是
实现
相
关
的(
implementation-dependent
),除了一个例外。
这
个例外是常数
0
,它可以保
证
被
转换为
一个与其它任何有效指
针
都不相等的指
针
。
这
个
值
通常
类
似
这样
定
义
:
#define NULL 0
但其效果是相同的。要
记
住的一个重要的事情是,当用
0
作
为
指
针时
它决不能被解除引用
。
换
句
话说
,当你将
0
赋给
一个指
针变
量后,你就不能
访问
它所指向的内存。不能
这样
写:
if(p == (char *)0) ...
也不能
这样
写:
if(strcmp(p, (char *)0) == 0) ...
因
为
strcmp()
总
是通
过
其参数来
查
看内存地
址的。
如果
p
是一个空指
针
,
这样
写也是无效的:
printf(p);
或
printf("%s", p);
C
语
言
关
于整数操作的上溢或下溢定
义
得非常明确。
只要有一个操作数是无符号的,
结
果就是无符号的,并且以
2n
为
模,其中
n
为
字
长
。如果两个操作数都是
带
符号的,
则结
果是未定
义
的。
例如,假
设
a
和
b
是两个非
负
整型
变
量,你希望
测试
a + b
是否溢出。一个明
显
的
办
法是
这样
的:
if(a + b < 0)
complain();
complain();
通常,
这
是不会工作的。
一旦
a + b
发
生了溢出,
对
于
结
果的任何
赌
注都是没有意
义
的。例如,在某些机器上,一个加法运算会将一个内部寄存器
设
置
为
四
种
状
态
:正、
负
、零或溢出。
在
这样
的机器上,
编译
器有
权
将上面的例子
实现为
首先将
a
和
b
加在一起,然后
检查
内部寄存器状
态
是否
为负
。如果
该
运算溢出,内部寄存器将
处
于溢出状
态
,
这
个
测试
会失
败
。
使
这
个特殊的
测试
能
够
成功的一个正确的方法是依
赖
于无符号算
术
的良好定
义
,即要在有符号和无符号之
间进
行
转换
:
if((int)((unsigned)a + (unsigned)b) < 0)
complain();
complain();
两个原因会令使用移位运算符的人感到
烦恼
:
1.
在右移运算中,空出的位是用
0
填充
还
是用符号位填充?
2.
移位的数量允
许
使用哪些数?
第一个
问题
的答案很
简单
,但有
时
是
实现
相
关
的。如果要
进
行移位的操作数是无符号的,会移入
0
。如果操作数是
带
符号的,
则实现
有
权
决定是移入
0
还
是移入符号位。如果在一个右移操作中你很
关
心空位,那
么
用
unsigned
来声明
变
量。
这样
你就有
权
假
设
空位被
设
置
为
0
。
第二个
问题
的答案同
样简单
:如果待移位的数
长
度
为
n
,
则
移位的数量必
须
大于等于
0
并且
严
格地小于
n
。因此,在一次
单
独的操作中不可能将所有的位从
变
量中移出
。
例如,如果一个
int
是
32
位,且
n
是一个
int
,写
n << 31
和
n << 0
是合法的,但
n << 32
和
n << -1
是不合法的。
注意,即使
实现
将符号
为
移入空位,
对
一个
带
符号整数的右移运算和除以
2
的某次
幂
也不是等价的。
为
了
证
明
这
一点,考
虑
(-1) >> 1
的
值
,
这
是不可能
为
0
的。
[
译
注:
(-1) / 2
的
结
果是
0
。
]
考
虑
下面的程序:
#include
main() {
char c; //int c ;
while((c = getchar()) != EOF)
putchar(c);
}
main() {
char c; //int c ;
while((c = getchar()) != EOF)
putchar(c);
}
这
段程序看起来好像要将
标
准
输
入
复
制到
标
准
输
出。
实际
上,它并不完全会做
这
些。
原因是
c
被声明
为
字符而不是整数。
这
意味着它将不能接收可能出
现
的所有字符包括
EOF
。
因此
这
里有两
种
可能性。有
时
一些合法的
输
入字符会
导
致
c
携
带
和
EOF
相同的
值
,有
时
又会使
c
无法存放
EOF
值
。在前一
种
情况下,程序会在文件的中
间
停止
复
制。在后一
种
情况下,程序会陷入一个无限循
环
。
实际
上,
还
存在着第三
种
可能:程序会偶然地正确工作。
C
语
言参考手册
严
格地定
义
了表达式
((c = getchar()) != EOF)
的
结
果。其
6.1
节
中声明:
当一个
较长
的整数被
转换为
一个
较
短的整数或一个
char
时
,它会被截去左
侧
;超出的位被
简单
地
丢
弃。
7.14
节
声明:
存在着很多
赋值
运算符,它
们
都是从右至左
结
合的。它
们
都需要一个左
值
作
为
左
侧
的操作数,而
赋值
表达式的
类
型就是其左
侧
的操作数的
类
型。其
值
就是已
经赋过值
的左操作数的
值
。
这
两个条款的
组
合效果就是必
须
通
过丢
弃
getchar()
的
结
果的高位,将其截短
为
字符,之后
这
个被截短的
值
再与
EOF
进
行比
较
。作
为这
个比
较
的一部分,
c
必
须
被
扩
展
为
一个整数,或者采取将左
侧
的位用
0
填充,或者适当地采取符号
扩
展。
然而,一些
编译
器并没有正确地
实现这
个表达式。它
们
确
实
将
getchar()
的
值
的低几位
赋给
c
。但在
c
和
EOF
的比
较
中,它
们
却使用了
getchar()
的
值
!
这样
做的
编译
器会使
这
个事例程序看起来能
够
“
正确地
”
工作。
立即安排
输
出的
显
示通常比将其
暂时
保存在一大
块
一起
输
出要昂
贵
得多。因此,
C
实现
通常允
许
程序
员
控制
产
生多少
输
出后在
实际
地写出它
们
。
这
个控制通常
约
定
为
一个称
为
setbuf()
的
库
函数。如果
buf
是一个具有适当大小的字符数
组
,
则
setbuf(stdout, buf);
将告
诉
I/O
库
写入到
stdout
中的
输
出要以
buf
作
为
一个
输
出
缓
冲,并且等到
buf
满
了或程序
员
直接
调
用
fflush()
再
实际
写出。
缓
冲区的合适的大小
在中定
义为
BUFSIZ
。
因此,下面的程序解
释
了通
过
使用
setbuf()
来
讲标
准
输
入
复
制到
标
准
输
出:
#include
main() {
int c;
char buf[BUFSIZ];
setbuf(stdout, buf);
while((c = getchar()) != EOF)
putchar(c);
}
main() {
int c;
char buf[BUFSIZ];
setbuf(stdout, buf);
while((c = getchar()) != EOF)
putchar(c);
}
不幸的是,
这
个程序是
错误
的,因
为
一个
细
微的原因。
要知道毛病出在哪,我
们
需要知道
缓
冲区最后一次刷新是在什
么时
候。答案;主程序完成之后,
库
将控制交回到操作系
统
之前所
执
行的清理的一部分。在
这
一
时
刻,
缓
冲区已
经
被
释
放了
!
有两
种
方法可以避免
这
一
问题
。
首先,使用静
态缓
冲区,或者将其
显
式地声明
为
静
态
:
static char buf[BUFSIZ];
或者将整个声明移到主函数之外。
另一
种
可能的方法是
动态
地分配
缓
冲区并且从不
释
放它:
char *malloc();
setbuf(stdout, malloc(BUFSIZ));
setbuf(stdout, malloc(BUFSIZ));
注意在后一
种
情况中,不必
检查
malloc()
的返回
值
,因
为
如果它失
败
了,会返回一个空指
针
。而
setbuf()
可以接受一个空指
针
作
为
其第二个参数,
这
将使得
stdout
变
成非
缓
冲的。
这
会运行得很慢,但它是可以运行的。
由于宏可以象函数那
样
出
现
,有些程序
员
有
时
就会将它
们视为
等价的。因此,看下面的定
义
:
#define max(a, b) ((a) > (b) ? (a) : (b))
注意宏体中所有的括号。它
们
是
为
了防止出
现
a
和
b
是
带
有比
>
优
先
级
低的表达式的情况。
一个重要的
问题
是,像
max()
这样
定
义
的宏
每
个操作数都会出
现
两次并且会被求
值
两次
。因此,在
这
个例子中,如果
a
比
b
大,
则
a
就会被求
值
两次:一次是在比
较
的
时
候,而另一次是在
计
算
max()
值
的
时
候。
这
不
仅
是低效的,
还
会
发
生
错误
:
bi
ggest = x[0];
i = 1;
while(i < n)
biggest = max(biggest, x[i++]);
i = 1;
while(i < n)
biggest = max(biggest, x[i++]);
当
max()
是一个真正的函数
时
,
这
会正常地工作,但当
max()
是一个宏的
时
候会失
败
。譬如,假
设
x[0]
是
2
、
x[1]
是
3
、
x[2]
是
1
。我
们
来看看在第一次循
环时
会
发
生什
么
。
赋值语
句会被
扩
展
为
:
biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]));
首先,
biggest
与
x[i++]
进
行比
较
。由于
i
是
1
而
x[1]
是
3
,
这
个
关
系是
“
假
”
。其副作用是,
i
增
长
到
2
。
由于
关
系是
“
假
”
,
x[i++]
的
值
要
赋给
biggest
。然而,
这时
的
i
变
成
2
了,因此
赋给
biggest
的
值
是
x[2]
的
值
,即
1
。
避免
这
些
问题
的方法是保
证
max()
宏的参数没有副作用:
biggest = x[0];
for(i = 1; i < n; i++)
biggest = max(biggest, x[i]);
for(i = 1; i < n; i++)
biggest = max(biggest, x[i]);
还
有一个危
险
的例子是混合宏及其副作用。
这
是来自
UNIX
第八版的中
putc()
宏的定
义
:
#define putc(x, p) (--(p)->_cnt >= 0 ? (*(p)->_ptr++ = (x)) : _flsbuf(x, p))
putc()
的第一个参数是一个要写入到文件中的字符,第二个参数是一个指向一个表示文件的内部数据
结
构的指
针
。注意第一个参数完全可以使用如
*z++
之
类
的
东
西,尽管它在宏中两次出
现
,但只会被求
值
一次。而第二个参数会被求
值
两次(在宏体中,
x
出
现
了两次,但由于它的两次出
现
分
别
在一个
:
的两
边
,因此在
putc()
的一个
实
例中它
们
之中有且
仅
有一个被求
值
)。由于
putc()
中的文件参数可能
带
有副作用,
这
偶
尔
会出
现问题
。不
过
,用
户
手册文档中提到:
“
由于
putc()
被
实现为
宏,其
对
待
stream
可能会具有副作用。特
别
是
putc(c, *f++)
不能正确地工作。
”
但是
putc(*c++, f)
在
这
个
实现
中是可以工作的。
有些
C
实现
很不小心。例如,没有人能正确
处
理
putc(*c++, f)
。另一个例子,考
虑
很多
C
库
中出
现
的
toupper()
函数。它将一个小写字母
转换为
相
应
的大写字母,而其它字符不
变
。如果我
们
假
设
所有的小写字母和所有的大写字母都是相
邻
的(大小写之
间
可能有所差距)
,我
们
可以得到
这样
的函数:
toupper(c) {
if(c >= 'a' && c <= 'z')
c += 'A' - 'a';
return c;
}
if(c >= 'a' && c <= 'z')
c += 'A' - 'a';
return c;
}
在很多
C
实现
中,
为
了减少比
实际计
算
还
要多的
调
用
开销
,通常将其
实现为
宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ?
(c) + ('A' - 'a') : (c))
很多
时
候
这
确
实
比函数要快。然而,当你
试
着写
toupper(*p++)
时
,会出
现
奇怪的
结
果。
另一个需要注意的地方是使用宏可能会
产
生巨大的表达式。例如,
继续
考
虑
max()
的定
义
:
#define max(a, b) ((a) > (b) ?
(a) : (b))
假
设
我
们这
个定
义
来
查
找
a
、
b
、
c
和
d
中的最大
值
。如果我
们
直接写:
max(a, max(b, max(c, d)))
它将被
扩
展
为
:
((a) > (((b) > (((c) > (d) ?
(c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))) ?
(a) : (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))))
(a) : (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))))
这
出奇的
庞
大。我
们
可以通
过
平衡操作数来使它短一些:
max(max(a, b), max(c, d))
这
会得到:
((((a) > (b) ? (a) : (b))) > (((c) > (d) ?
(c) : (d))) ?
(((a) > (b) ? (a) : (b))) : (((c) > (d) ? (c) : (d))))
(((a) > (b) ? (a) : (b))) : (((c) > (d) ? (c) : (d))))
这
看起来
还
是写:
biggest = a;
if(biggest < b) biggest = b;
if(biggest < c) biggest = c;
if(biggest < d) biggest = d;
if(biggest < b) biggest = b;
if(biggest < c) biggest = c;
if(biggest < d) biggest = d;
比
较
好一些。
==================================================================
宏的一个通常的用途是保
证
不同地方的多个事物具有相同的
类
型:
#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b, c;
FOOTYPE a;
FOOTYPE b, c;
这
允
许
程序
员
可以通
过
只改
变
程序中的一行就能改
变
a
、
b
和
c
的
类
型,尽管
a
、
b
和
c
可能声明在很
远
的不同地方。
使用
这样
的宏定
义还
有着可移植性的
优势
——
所有的
C
编译
器都支持它。很多
C
编译
器并不支持另一
种
方法:
typedef struct foo FOOTYPE;
这
将
FOOTYPE
定
义为
一个与
struct foo
等价的新
类
型。
这
两
种为类
型命名的方法可以是等价的,但
typedef
更灵活一些。例如,考
虑
下面的例子:
#define T1 struct foo *
typedef struct foo * T2;
typedef struct foo * T2;
这
两个定
义
使得
T1
和
T2
都等价于一个
struct foo
的指
针
。但看看当我
们试图
在一行中声明多于一个
变
量的
时
候会
发
生什
么
:
T1 a, b;
T2 c, d;
T2 c, d;
第一个声明被
扩
展
为
:
struct foo * a, b;
这
里
a
被定
义为
一个
结
构指
针
,但
b
被定
义为
一个
结
构(而不是指
针
)。相反,第二个声明中
c
和
d
都被定
义为
指向
结
构的指
针
,因
为
T2
的行
为
好像真正的
类
型一
样
。
今天,一个
C
程序
员
如果想写出
对
于不同
环
境中的用
户
都有用的程序就必
须
知道很多
这
些
细
微的差
别
。
一个
标识
符是一个字符和数字序列,第一
个字符必
须
是一个字母。下划
线
_
算作字母。大写字母和小写字母是不同的。只有前八个字符是
签
名,但可以使用更多的字符。可以被多
种汇编
器和加
载
器使用的外部
标识
符,有着更多的限制:
考
虑
下面
这
个
显
著的函数:
char *Malloc(unsigned n) {
char *p, *malloc();
p = malloc(n);
if(p == NULL)
panic("out of memory");
return p;
}
char *p, *malloc();
p = malloc(n);
if(p == NULL)
panic("out of memory");
return p;
}
这
个函数是保
证
耗尽内存而不会
导
致没有
检测
的一个
简单
的
办
法。程序
员
可以通
过调
用
Mallo()
来代替
malloc()
。如果
malloc()
不幸失
败
,将
调
用
panic()
来
显
示一个恰当的
错误
消息并
终
止程序。
然而,考
虑
当
该
函数用于一个忽略大小写区
别
的系
统
中
时
会
发
生什
么
。
这时
,名字
malloc
和
Malloc
是等价的。
换
句
话说
,
库
函数
malloc()
被上面的
Malloc()
函数完全取代了,当
调
用
malloc()
时
它
调
用的是它自己。
显
然,其
结
果就是第一次
尝试
分配内存就会陷入一个
递归
循
环
并随之
发
生混乱。但在一些能
够
区分大小写的
实现
中
这
个函数
还
是可以工作的。
C
为
程序
员
提供三
种
整数尺寸:普通、短和
长
,
还
有字符,其行
为
像一个很小的整数。
C
语
言定
义对
各
种
整数的大小不作任何保
证
:
1.
整数的四
种
尺寸是非
递
减的。
2.
普通整数的大小要足
够
存放任意的数
组
下
标
。
3.
字符的大小
应该
体
现
特定硬件的本
质
。
许
多
现
代机器具有
8
位字符,不
过还
有一些具有
7
位
获
9
位字符。因此字符通常是
7
、
8
或
9
位。
长
整数通常至少
32
位,因此一个
长
整数可以用于表示文件的大小。
普通整数通常至少
16
位,因
为
太小的整数会更多地限制一个数
组
的最大大小。
短整数
总
是恰好
16
位。
一
种
更可移植的做法是定
义
一个
“
新的
”
类
型:
typedef long tenmil;
现
在你就可以使用
这
个
类
型来声明一个
变
量并知道它的
宽
度了,最坏的情况下,你也只要改
变这
个
单
独的
类
型定
义
就可
以使所有
这
些
变
量具有正确的
类
型。
这
些
问题
在将一个
char
制
转换为
一个更大的整数
时变
得尤
为
重要。
对
于相反的
转换
,其
结
果却是定
义
良好的:多余
的位被
简单
地
丢
弃掉。但一个
编译
器将一个
char
转换为
一个
int
却需要作出
选择
:将
char
视为带
符号量
还
是无符号量?如果是前者,将
char
扩
展
为
int
时
要
复
制符号位;如果是后者,
则
要将多余的位用
0
填充。
这
个决定的
结
果
对
于那些在
处
理字符
时习惯
将高位置
1
的人来
说
非常重要。
这
决定着
8
位的字符范
围
是从
-128
到
127
还
是从
0
到
255
。
这
又影响着程序
员对
哈希表和
转换
表之
类
的
东
西的
设计
。
如果你
关
心一个字符
值
最高位置一
时
是否被
视为
一个
负
数,你
应该显
式地将它声明
为
unsigned char
。
这样
就能保
证
在
转换为
整数
时
是基
0
的,而不像普通
char
变
量那
样
在一些
实现
中是
带
符号的而在另一些
实现
中是无符号的。
另外,
还
有一
种误
解是
认为
当
c
是一个字符
变
量
时
,可以通
过
写
(unsigned)c
来得到与
c
等价的无符号整数。
这
是
错误
的,因
为
一个
char
值
在
进
行任何操作(包括
转换
)之前
转换为
int
。
这时
c
会首先
转换为
一个
带
符号整数再
转换为
一个无符号整数,
这
会
产
生奇怪的
结
果。
正确的方法是写
(unsigned char)c
。
这
里再一次重
复
:一个
关
心右移操作如何
进
行的程序最好将所有待移位的量声明
为
无符号
的。
假
设
我
们
用
b
除
a
得到商
为
q
余数
为
r
:
q = a / b;
r = a % b;
r = a % b;
我
们暂时
假
设
b > 0
。
1.
最重要的,我
们
期望
q * b + r == a
,因
为这
是
对
余数的定
义
。
2.
如果
a
的符号
发
生改
变
,我
们
期望
q
的符号也
发
生改
变
,但
绝对值
不
变
。
3.
我
们
希望保
证
r >= 0
且
r < b
。例如,如果余数将作
为
一个哈希表的索引,它必
须
要保
证总
是一个有效的索引。
这
三点清楚地描述了整数除法和求余操作。不幸的是,它
们
不能同
时为
真。
考
虑
3 / 2
,商
1
余
0
。
(1)
这满
足第一点。而
-3 / 2
的
值
呢?根据第二点,商
应该
是
-1
,但如果是
这样
的
话
,余数必
须
也是
-1
,
这违
反了第三点。或者,我
们
可以通
过
将余数
标记为
1
来
满
足第三点,但
这时
根据第一点商
应该
是
-2
。
这
又
违
反了第二点。
因此
C
和其他任何
实现
了整数除法舍入的
语
言必
须
放弃上述三个原
则
中的至少一个。
很多程序
设计语
言放弃了第三点,要求余数的符号必
须
和被除数相同。
这
可以保
证
第一点和第二点。很多
C
实现
也是
这样
做的。
尽管有些
时
候不需要灵活性,
C
语
言
还
是足
够
可以
让
我
们
令除法完成我
们
所要做的、提供我
们
所想知道的。例如,假
设
我
们
有一个数
n
表示一个
标识
符中的字符的一些函数,并且我
们
想通
过
除法得到一个哈希表入口
h
,其中
0 <= h <= HASHSIZE
。如果我
们
知道
n
是非
负
的,我
们
可以
简单
地写:
h = n % HASHSIZE;
然而,如果
n
有可能是
负
的,
这样
写就不好了,因
为
h
可能也是
负
的。然而,我
们
知道
h > -HASHSIZE
,因此我
们
可以写:
h = n % HASHSIZE;
if(n < 0)
h += HASHSIZE;
if(n < 0)
h += HASHSIZE;
同
样
,将
n
声明
为
unsigned
也可以。
这
个尺寸是模糊的,
还
受
库设计
的
影响。在
PDP-11
[10]
机器上运行的
仅
有的
C
实现
中,有一个称
为
rand()
的函数可以返回一个(
伪
)随机非
负
整数。
PDP-11
中整数
长
度包括符号位是
16
位,因此
rand()
返回一个
0
到
215-1
之
间
的整数。
当
C
在
VAX-11
上
实现时
,整数的
长
度
变为
32
位
长
。那
么
VAX-11
上的
rand()
函数返回
值
范
围
是什
么
呢?
对
于
这
个系
统
,加利福尼
亚
大学的
人
认为
rand()
的返回
值应该
涵盖所有可能的非
负
整数,因此它
们
的
rand()
版本返回一个
0
到
231-1
之
间
的整数。
而
AT&T
的人
则觉
得如果
rand()
函数仍然返回一个
0
到
215
之
间
的
值
则
可以很容易地将
PDP-11
中期望
rand()
能
够
返回一个小于
215
的
值
的程序移植到
VAX-11
上。
因此,
现
在
还
很
难
写出不依
赖实现
而
调
用
rand()
函数的程序。
toupper()
和
tolower()
函数有着
类
似的
历
史。他
们
最初都被
实现为
宏:
#define toupper(c) ((c) + 'A' - 'a')
#define tolower(c) ((c) + 'A' - 'a')
#define tolower(c) ((c) + 'A' - 'a')
这
些宏确
实
有一个缺陷,即:当
给
定的
东
西不是一个恰当的字符,它会返回垃圾。因此,下面
这
个通
过
使用
这
些宏来将一个文件
转为
小写的程序是无法工作的:
int c;
while((c = getchar()) != EOF)
putchar(tolower(c));
while((c = getchar()) != EOF)
putchar(tolower(c));
我
们
必
须
写:
int
c;
while((c = getchar()) != EOF)
putchar(isupper(c) ? tolower(c) : c);
while((c = getchar()) != EOF)
putchar(isupper(c) ? tolower(c) : c);
就
这
一点,
AT&T
中的
UNIX
开发组织
提醒我
们
,
toupper()
和
tolower()
都是事先
经过
一些适当的参数
进
行
测试
的。考
虑这样
重写
这
些宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ?
(c) + 'A' - 'a' : (c))
#define tolower(c) ((c) >= 'A' && (c) <= 'Z' ? (c) + 'a' - 'A' : (c))
#define tolower(c) ((c) >= 'A' && (c) <= 'Z' ? (c) + 'a' - 'A' : (c))
但要知道,
这
里
c
的三次出
现
都要被求
值
,
这
会破坏如
toupper(*p++)
这样
的表达式。因此,可以考
虑
将
toupper()
和
tolower()
重写
为
函数。
toupper()
看起来可能像
这样
:
int toupper(int c) {
if(c >= 'a' && c <= 'z')
return c + 'A' - 'a';
return c;
}
if(c >= 'a' && c <= 'z')
return c + 'A' - 'a';
return c;
}
tolower()
类
似。
这
个改
变带
来更多的
问题
,
每
次使用
这
些函数的
时
候都会引入函数
调
用
开销
。我
们
的英雄
认为
一些人可能不愿意支付
这
些
开销
,因此他
们
将
这
个宏重命名
为
:
#define _toupper(c) ((c) + 'A' - 'a')
#define _tolower(c) ((c) + 'a' - 'A')
#define _tolower(c) ((c) + 'a' - 'A')
这
就允
许
用
户选择
方便或速度。
这
里面其
实
只有一个
问题
:伯克利的人
们
和其他的
C
实现
者并没有跟着
这么
做。
这
意味着一个在
AT&T
系
统
上
编
写的使用了
toupper()
或
tolower()
的程序,如果没有
为
其
传递
正确大小写字母参数,在其他
C
实现
中可能不会正常工作。
如果不知道
这
些
历
史,可能很
难对这类错误进
行跟踪。
很多
C
实现为
用
户
提供了三个内存分配函数:
malloc()
、
realloc()
和
free()
。
调
用
malloc(n)
返回一个指向有
n
个字符的新分配的内存的指
针
,
这
个指
针
可以由
程序
员
使用。
给
free()
传递
一个指向由
malloc()
分配的内存的指
针
可以使
这块
内存得以再次使用。通
过
一个指向已分配区域的指
针
和一个新的大小
调
用
realloc()
可以将
这块
内存
扩
大或
缩
小到新尺寸,
这
个
过
程中可能要
复
制内存。
也
许
有人会想,真相真是有点微妙啊。下面是
System V
接口定
义
中出
现
的
对
realloc()
的描述:
realloc
改
变
一个由
ptr
指向的
size
个字
节
的
块
,并返回
该块
(可能被移
动
)的指
针
。
在新旧尺寸中比
较
小的一个尺寸之下的内容不会被改
变
。
此外,
还
包含了描述
realloc()
的另外一段:
如果在最后一次
调
用
malloc
、
realloc
或
calloc
后
释
放了
ptr
所指向的
块
,
realloc
依旧可以工作;因此,
free
、
malloc
和
realloc
的
顺
序可以利用
malloc
压缩
存
贮
的
查
找策略。
因此,下面的代
码
片段在
UNIX
第七版中是合法的:
free (p);
p = realloc(p, newsize);
p = realloc(p, newsize);
这
一特性保留在从
UNIX
第七版衍生出来的系
统
中:可以先
释
放一
块
存
储
区域,然后再重新分配它。
这
意味着,在
这
些系
统
中
释
放的内存中的内容在下一次内存分配之前可以保
证
不
变
。因此,在
这
些系
统
中,我
们
可以用下面
这种
奇特的思想来
释
放一个
链
表中的所有元素:
for(p = head; p != NULL; p = p->next)
free((char *)p);
free((char *)p);
而不用担心
调
用
free()
会
导
致
p->next
不可用。
不用
说
,
这种
技
术
是不推荐的,因
为
不是所有
C
实现
都能在内存被
释
放后将它的内容保留足
够长
的
时间
。然而,第七版的手册
遗
留了一个未声明的
问题
:
realloc()
的原始
实现实际
上是必
须
要先
释
放再重新分配的。出于
这
个原因,一些
C
程序都是先
释
放内存再重新分配的,而当
这
些程序移植到其他
实现
中
时
就会出
现问题
。
下面的程序
带
有两个参数:一个
长
整数和一个函数(的指
针
)。它将整数
转换
位十
进
制数,并用代表
其中
每
一个数字的字符来
调
用
给
定的函数。
void printnum(long n, void (*p)()) {
if(n < 0) {
(*p)('-');
n = -n;
}
if(n >= 10)
printnum(n / 10, p);
(*p)(n % 10 + '0');
}
if(n < 0) {
(*p)('-');
n = -n;
}
if(n >= 10)
printnum(n / 10, p);
(*p)(n % 10 + '0');
}
这
个程序非常
简单
。首先
检查
n
是否
为负
数;如果是,
则
打印一个符号并将
n
变为
正数。接下来,
测试
是否
n >= 10
。如果是,
则
它的十
进
制表示
中包含两个或更多个数字,因此我
们递归
地
调
用
printnum()
来打印除最后一个数字外的所有数字。最后,我
们
打印最后一个数字。
这
个程序
——
由于它的
简单
——
具有很多可移植性
问题
。首先是将
n
的低位数字
转换
成字符形式的方法。用
n % 10
来
获
取低位数字的
值
是好的,但
为
它加上
'0'
来
获
得相
应
的字符表示就不好了。
这
个加法假
设
机器中
顺
序的数字所
对应
的字符数
顺
序的,没有
间
隔,因此
'0' + 5
和
'5'
的
值
是相同的,等等。尽管
这
个假
设对
于
ASCII
和
EBCDIC
字符集是成立的,但
对
于其他一些机器可能不成立。避免
这
个
问题
的方法是使用一个表:
void printnum(long n, void (*p)()) {
if(n < 0) {
(*p)('-');
n = -n;
}
if(n >= 10)
printnum(n / 10, p);
(*p)("0123456789"[n % 10]);
}
if(n < 0) {
(*p)('-');
n = -n;
}
if(n >= 10)
printnum(n / 10, p);
(*p)("0123456789"[n % 10]);
}
另一个
问题发
生在当
n < 0
时
。
这时
程序会打印一个
负
号并将
n
设
置
为
-n
。
这
个
赋值
会
发
生溢出,因
为
在使用
2
的
补码
的机器上通常能
够
表示的
负
数比正数要多。例如,一个(
长
)整数有
k
位和一个附加位表示符号,
则
-2k
可以表示而
2k
却不能。
解决
这
一
问题
有很多方法。最直
观
的一
种
是将
n
赋给
一个
unsigned long
值
。然而,一些
C
便一起可能没有
实现
unsigned long
,因此我
们
来看看没有它怎
么办
。
在第一个
实现
和第二个
实现
的机器上,改
变
一个正整数的符号保
证
不会
发
生溢出。
问题仅
出在改
变
一个
负
数的符号
时
。因此,我
们
可以通
过
避免将
n
变为
正数来避免
这
个
问题
。
当然,一旦我
们
打印了
负
数的符号,我
们
就能
够
将
负
数和正数
视为
是一
样
的。
下面的方法就
强
制在打印符号之后
n
为负
数,并且用
负
数
值
完成我
们
所有的算法。如果我
们这么
做,我
们
就必
须
保
证
程序中打印符号的部分只
执
行一次;一个
简单
的方法是将
这
个程序划分
为
两个函数:
void printnum(long n, void (*p)()) {
if(n < 0) {
(*p)('-');
printneg(n, p);
}
else
printneg(-n, p);
}
void printneg(long n, void (*p)()) {
if(n <= -10)
printneg(n / 10, p);
(*p)("0123456789"[-(n % 10)]);
}
if(n < 0) {
(*p)('-');
printneg(n, p);
}
else
printneg(-n, p);
}
void printneg(long n, void (*p)()) {
if(n <= -10)
printneg(n / 10, p);
(*p)("0123456789"[-(n % 10)]);
}
printnum()
现
在只
检查
要打印的数是否
为负
数;如果是的
话则
打印一个符号。否
则
,它以
n
的
负绝对值
来
调
用
printneg()
。我
们
同
时
改
变
了
printneg()
的函数体来适
应
n
永
远
是
负
数或零
这
一事
实
。
我
们
得到什
么
?我
们
使用
n / 10
和
n % 10
来
获
取
n
的前
导
数字和
结
尾数字(
经过
适当的符号
变换
)。
调
用整数
除法的行
为
在其中一个操作数
为负
的
时
候是
实现
相
关
的。因此,
n % 10
有可能是正的!
这时
,
-(n % 10)
是
负
数,将会超出我
们
的数字字符数
组
的末尾。
为
了解决
这
一
问题
,我
们
建立两个
临时变
量来存放商和余数。作完除法后,我
们检查
余数是否在正确的范
围
内,如果不是的
话则调
整
这
两个
变
量。
printnum()
没有改
变
,因此我
们
只列出
printneg()
:
void printneg(long n, void (*p)()) {
long q;
int r;
if(r > 0) {
r -= 10;
q++;
}
if(n <= -10) {
printneg(q, p);
}
(*p)("0123456789"[-r]);
}
long q;
int r;
if(r > 0) {
r -= 10;
q++;
}
if(n <= -10) {
printneg(q, p);
}
(*p)("0123456789"[-r]);
}
《
The C Programming Language
》(
Kernighan and Ritchie, Prentice-Hall 1978
)是最具
权
威的
C
著作。它包含了一个
优
秀的教程,面向那些熟悉其他高
级语
言程序
设计
的人,和一个参考手册,
简洁
地描述了整个
语
言。尽管自
1978
年以来
这门语
言
发
生了不少
变
化,
这
本
书对
于很多主
题
来
说
仍然是个定
论
。
这
本
书
同
时还
包含了本文中多次提到的
“C
语
言参考手册
”
。
《
The C Puzzle Book
》(
Feuer, Prentice-Hall, 1982
)是一本少
见
的磨
炼
人
们
文法能力的
书
。
这
本
书
收集了很多
谜题
(和答案),它
们
的解决方法能
够测试读
者
对
于
C
语
言精妙之
处
的知
识
。
《
C: A Referenct Manual
》(
Harbison and Steele, Prentice Hall 1984
)是特意
为实现
者
编
写的一本参考
资
料。其他人也会
发现
它是特
别
有用的
——
因
为
他能从中参考
细节
。
1.这本书是基于图书《C Traps and Pitfalls》(Addison-Wesley, 1989, ISBN 0-201-17928-8)的一个扩充,有兴趣的读者可以读一读它。