C陷阱与缺陷 —— 读书笔记-3、语义“陷阱”

来源:互联网 发布:第三方关键词优化排名 编辑:程序博客网 时间:2024/04/29 22:27
第三章    语义“陷阱”


 3.1    指针和数组

C语言中的数组值得注意的地方有以下两点:

1、C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来
  (注:C99标准允许变长数组(VLA)。GCC编译器中实现了变长数组,但细节与C99标准不完全一致。)

2、对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其他有关数组的操作,哪怕他们看上去是以数组下标进行运算的,实际上都是通过指针进行的。换句话说,任何一个数组下标运算都等同于一个对应的指针运算,因此我们完全可以依据指针行为定义数组下标的行为。很多程序设计语言中都内建有索引运算,在C语言中索引运算是以指针算术的形式来定义的。

如果一个指针指向的是数组中的一个元素,那么我们只要给这个指针加1,就能够得到指向该数组中下一个元素的指针。同样地,如果我们给这个指针减1,得到就是指向该数组中前一个元素的指针。


  int calendar[12][31];
  int *p;

则p = calendar; 是非法的。因为calendar是一个二维数组,即“数组的数组”,在此处的上下文中使用calendar名称会将其转换为一个指向数组的指针;而p是一个指向整型变量的指针,这个语句试图将一种类型的指针赋值给另一种类型的指针。

 要构造一个指向数组的指针的方法:
  int calendar[12][31];
  int (*monthp)[31]; 
  monthp = calendar;
这样,monthp将指向数组calendar的第一个元素,也就是数组calendar的12个有着31个元素的数组类型元素之一。



 3.2    非数组的指针


  在C语言中,字符串常量代表了一块包括字符串中所有字符以及一个空字符(' \0 ')的内存区域的地址。

  假定我们有两个字符串 s 和 t ,我们希望将这两个字符串连接成单个字符串 t。
  考虑:

  char *r,*malloc();
  r = mallor(strlen(s) + strlen(t));
  strcpy(r,s);
  strcat(r,t);

  这个例子的错误有3点:

  1、malloc函数有可能无法提供请求的内存。
  2、显式地分配了内存必须显式地释放内存。
  3、malloc函数并未分配足够的内存。

  正确:
   char *r,*malloc();
   r = malloc(strlen(s) + strlen(t) + 1);
   if(!r)
   {
     complain();
     exit(1);
    }
   strcpy(r,s);
   strcat(r,t);
 
   /*一段时间之后*/
   free(r);


 3.3    作为参数的数组声明


在C语言中,我们没有办法可以将一个数组作为函数参数直接传递。如果我们使用数组名作为参数,那么数组名会立刻被转换为指向该数组第1个元素的指针。

因此,将数组作为函数参数毫无意义。所以,C语言中会自动地将作为参数的数组声明转换为相应的指针声明。

 

 3.4    避免“举隅法”


容易混淆指针与指针所指向的数据。需要记住的是,复制指针并不同时复制指针所指向的数据。



 3.5    空指针并非空字符串


   #define Null 0

需要记住的重要一点是,当常数0被转换为指针使用时,这个指针绝对不能被解除引用(dereference)。 换句话说,当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。

比如:if ( strcmp ( p, (char *) 0) == 0 ) ...  就是非法的啦!!



 3.6    边界计算与不对称边界
  
在所有常见的程序设计错误中,最难于察觉的一类是“栏杆错误”,也常被称为“差一错误”(off-by-one error)。
  避免“栏杆错误”的两个通用原则:

  (1) 首先考虑最简单情况下的特例,然后将得到的结果外推。
  (2) 仔细计算边界,绝不掉以轻心。

用第一个入界点和第一个出界点来表示一个数值范围 能够降低这类错误发生的可能性。
比如整数x满足边界条件x>=16且x<=37我们可以说x>=16且x<38,这里下界是“入界点”,即包括在取值范围之中;而上界是“出界点”,即不包括在取值范围之中。

另一种考虑不对称边界的方式是,把上界视作某序列中第一个被占用的元素,而把下界视作序列中第一个被释放的元素。

 

 3.7    求值顺序


C语言中只有四个运算符(&&, ||, ?: 和 ,)存在规定的求值顺序。
运算符&&和运算符||首先对左侧操作数求值,只在需要时才对右侧操作数求值。
运算符?:有三个操作数: 在a?b:c中,操作数a首先被求值,根据a的值首先被求值,根据a的值再求操作数b或c的值。
而逗号运算符,首先对左侧操作数求值,然后该值被“丢弃”,再对右侧操作数求值。



 3.8    运算符&&, || 和 !


  按位运算符 &、 |、 ~ 对操作数的处理方式是将其视作一个二进制的位序列,分别对其每个位进行操作。

  逻辑运算符&&、 ||、 和 ! 对操作数的处理方式是将其视作要么是“真”,要么是“假”。



 3.9    整数溢出


 C语言中存在两类整数算术运算,有符号运算与无符号运算。在无符号算术运算中,没有所谓“溢出”一说:所有的无符号运算都是以2的n次方为模,这里n是结果中的位数。如果算术运算符的一个操作数是有符号整数,另一个是无符号整数,那么有符号整数会被转换为无符号整数,“溢出”也不可能发生。

当两个操作数都是有符号整数时,“溢出”有可能发生。当一个运算的结果发生“溢出”时,作出任何假设都是不安全的。



 3.10    为函数main提供返回值


一个返回值为整型的函数如果返回失败,实际上是隐含地返回了某个“垃圾”整数。只要该数值不被用到,就无关紧要。 

大多数C语言实现都通过main函数的返回值来告知操作系统该函数执行是成功还是失败。典型的处理方案是,返回值0代表成功,返回值非0则表示执行失败。





















































0 0
原创粉丝点击