《c陷阱与缺陷》读书笔记

刚刚读完一本书《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求值。

  现在假设存在两个数组xy。要求将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*, bint类型。

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’。因此,它使用了一个符号表来输出字符。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值