刚刚读完一本书《c陷阱与缺陷》,这本书的作者是Andrew Koening。作者编写此书时,ansi c标准还未完成。因此,此书中的很多问题我们应该不会遇到。但这本书中还是有些启发性的例子、思想。我在此简单总结一下给大家分享。
1.else悬挂问题。如下例子
if (x == 0)
if (y == 0) error();
else{
z = x + y;
f(&z);
}
从缩进来看,上述源码等价于:
If (x == 0) {
If (y == 0) error();
}else{
z = x + y;
f(&z);
}
但实际情况,源码等价于:
If (x == 0){
If (y == 0) {
error();
}else{
z = x + y;
f(&z);
}
}
原因在于,为了防止if-else的二义性,c语言规定了一个“靠近原则”。即,else语句与最靠近它的if语句匹配。“靠近原则”并非所有人都清楚。因此,这里要提倡“防御性编程”。即,或者强制所有的if语句都有else语句,或者强制if语句、else语句加大括号。这也是我们编码规范中要求的。
2. 求值顺序
C语言中只有 “&&”, “||”, “?:”, “,”四个运算符存在规定的求值顺序。如下,
If (y != 0 && x/y > tolerance) complain(); //可以保证进行x/y运算时,y肯定不为零。因为&&保证先对左操作数求值,只有当y!=0为真时,才会对x/y>tolerance求值。
现在假设存在两个数组x,y。要求将x数组的前n个元素赋值给y数组。看如下代码,
i = 0;
while (i < n){
y[i] = x[i++]; //如何保证y[i]先被求值呢?标准中定义赋值运算符”=”的求值顺序了吗?这行代码具有良好的移植性吗?
}
改为如下的代码较好,
i = 0;
while (i < n){
y[i] = x[i];
i++;
}
3. 预定义宏
1) 加括号。如下的例子,
#define MAX(a, b) ((a)>(b)?(a):(b)) //最外层的括号有必要加吗?为什么要添加呢?
我们在定义这个宏时并不知道其他人如何使用这个宏。出现以下情况是完全可能的,
a = MAX(i,j); //不加最外层的括号也可以。
b = MAX(i,j)+5; //不加最外层的括号就会出错。假如不加最外层括号,那么编译器将这行代码转换为b = (i)>(j)?(i):(j)+5;
2) 求值次数。如下的例子,
#define toupper(c) \
((c)>=’a’ && (c)<=’z’?(c)+(‘A’-‘a’):(c)); //c会被求值3次,因此不能与++、--运算符连用。
如果某程序为 toupper(*p++);那么就会出现及其怪异的结果。
3) 宏不是一个语句,而是一个表达式。如下例子,
#define assert(e) if(!(e)) _assert_error(__FILE__, __LINE__)
如果程序员使用宏的方法如下,
If (x > 0 && y > 0) assert(x > y); //期望x <=y时,程序终止运行。
else assert(y > x);//期望x>=y时,程序终止运行。
那么就会出现错误。展开之后,代码变为
If(x>0 && y>0) if (!(x>y)) _assert_error(__FILE__,__LINE__);
else if(!(y>x)) _assert_error(__FILE__, __LINE__);
if(!(x>y) 与else匹配了。
一般情况下会将assert宏设计为
#define assert(e) ((e) || _assert_error(__FILE,__LINE))
4)不要用宏来定义类型,尤其不要用宏来定义指针类型。如下例子,
#define T1 int*
typedef int *T2;
T1 a, b; //只有a被声明为了int*, b是int类型。
T2 a, b; //a, b都是int*
4.仔细考虑移植性。直接举例如下,
//p是一个函数指针,用于输出一个字符
//本方法是将n转换为字符串输出
void printnum(long n, void (*p)(char))
{
If (n < 0) {
(*p)(‘-‘);
n = -n;
}
If (n >= 10) {
printnum(n/10, p);
}
(*p)((int)(n%10) + ‘0’);
}
充分考虑移植性之后,程序变为
void printneg(long n, void (*p)(char));//新增函数
void printnum(long n, void (*p)(char))
{
If (n < 0){
(*p)(‘-‘);
printneg(n, p);
}else{
printneg(-n, p);
}
}
void printneg(long n, void (*p)(char))
{
long q;
int r;
q = n / 10;
r = n % 10;
if (r > 0) {
r -= 10;
q++;
}
If (n <= -10){
printneg(q, p);
}
(*p)(“0123456789”[-r]);
}
谁能说出为什么要改为下面的样子?究竟带来什么好处了?
作者修改源码的原因如下,
1.一个采用补码的系统,负数的范围比正数的范围大1。举例如下,
8位带符号数的取值范围是[-2^7, 2^7 – 1].
如果n=-2^7,那么-n = ?因此,为了防止出现负值转换为正值时出现的溢出,应该不将负值转换为正值。
因此,将所有正数转换为负数。同时,增加了一个printneg函数。这个函数打印负数。
2. 负数求余的结果是正,还是负?举例如下,
-5 % 10 = -5可以,只要-5 / 10 = 0就行。
-5 % 10 = 5也可以,只要-5 / 10 = -1 就行。
不同的芯片得出的结果也是不一样的。
因此,为了保证在任何芯片上求余结果相同,增加了一段处理代码。
3.作者写书时,ansi c标准还未出现。作者担心在其他国家新增的字符编码格式中 ‘A’ – ‘a’ != ‘B’ – ‘b’。因此,它使用了一个符号表来输出字符。