Chinaunix首页 | 论坛 | 博客
  • 博客访问: 652348
  • 博文数量: 128
  • 博客积分: 4385
  • 博客等级: 上校
  • 技术积分: 1546
  • 用 户 组: 普通用户
  • 注册时间: 2010-07-22 14:05
文章分类

全部博文(128)

文章存档

2012年(2)

2011年(51)

2010年(75)

分类: C/C++

2010-08-16 21:56:52

   c陷阱与缺陷学习笔记---------------by Andy

近日,宿舍停电,借此机会学习了下《C陷阱与缺陷》,果然是经典。也推荐大家有时间看看,内容不多,一个100多页,两天差不多!

“本书所揭示的知识,至少能帮助你减少C代码和初级C++代码中90%Bug!”
以下是我的学习笔记:

导读


   
程序中的单个字符孤立起来看并没有什么意义,只有结合上下文才有意义,如p->s = "->";两处的-意义
是不同的。

   
程序的基本单元是token ,相当于自然语言中的单词。 一个token的意义是不会变的。 而组成token 的字符序列则随上下文的不同而改变。

    token
之间的空格将被忽略。


                        Chapter 1
词法陷阱


1.1 =
不同于 ==

1.2 &
|不同于&&
||

1.3
词法分析中的贪心法


    token
分为单字符token和多字符token,如/ == ,当有岐义时,c语言的规则是:每一个token应包括
   
尽可能多的字符。

   
另外token的中间不能有空白(空格,制表符, 换行符)
    y = x /*p
应写为y = x / *p  或者y = x / (*p);

   
老编译器允许用=+来代表现在+=的含义。所以它们会将a=-1理解为a=- 1
a = (a-1);
它们还会将复合赋值语句看成两个token,于是可以处理 a>> =1, 而现代的编译器会报错。



1.4
整型常量

   
常量前加0代表是8进制。

1.5
字符与字符串

   
用双引号引起的字符串, 代表的是一个指向无名数组起始字符的指针

单引号引起的字符串,代表一个整数


    a+++++b
的含义是什么? a++ + ++b

    C
不允许嵌套注释。



                                      Chapter 2  
语法陷阱


2.1
构造函数声明

   
构造函数声明的规则:按照使用的方式来声明。

   
任何C声明都由两部分组成:类型及类似表达式的声明符(declarator)


    float *g(), (*h)();
    g
是一个函数,该函数的返回值类型为指向浮点数的指针。 h是一个函数指针, h所指向函数的返回值为
浮点类型。()的优先级高于*

   
因为float (*g)();表示g是一个指向返回值为浮点类型的函数的指针。所以(float (*)())表示一个指向
   
返回值为浮点类型的函数的指针的类型转换符。


   
一旦我们知道如何声明一个给定类型的变量, 那么该类型的类型转换符就很容易得到了:只需要把声明
中的参量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个封装起来即可。

    (*(void(*)())0)()
表示什么意思呢?

   
如果fp是一个函数指针, 那么(*fp)()就表示对其所指的函数的调用。简写为fp()。但这只是简写而已。
   
*((*fp)())可以简写为*fp()

   
根据上文(void(*)()) 表示一个指向返回值为void的函数的指针的类型。这里不过是对0作强制转换而

已。其实用typedef更好:

    typedef void (*funcptr)();
    (*(funcptr)0)();

    signal
的声明如下:
    void (*signal(int, void(*)(int)))(int);
   
或者用typedef
    typedef void (*HANDLER)(int);
    HANDLER signal(int, HANDLER);


2.2
运算符的优先级问题

   
注意条件运算符优先级比赋值运算符高,书上第22页是错的。
    & > ^ > |

A.判断flags的的最低位是否为1,定义FLAG=1

一般不这样用 if(flags & FLAG)

这样        if((flags & FLAG) != 0)       注意:!= 的优先级高于 &

B.hi的高4位和low的低4位时,

这样写 r = hi <<4 + low   是错误的,因为<<的优先级高于+

应该这样写:       r = hi << (4 + low)      r = hi << 4 | low;

 

运算符的优先级

单目运算符是自左至右结合。 单目>双目

双目运算符中 算术>移位>关系>逻辑>赋值>条件

赋值运算符自由向左结合

a = b = 0        b = 0;  a = b;  相同

2.3
分号

2.4 switch
语句

2.5
函数调用

    f();
   
是个函数调用。而f;则计算函数f的地址。
2.6 else

c语言的规则:else始终与同一个括号内最近的的未匹配的的if相结合。
    C
语言允许初始化列表中出现多余的逗号。为了列表对称


      
                                       Chapter 3
语义陷阱


3.1
指针与数组

    C
语言中只有一维数组, 而且数组的大小必须在编译期间就作为一个常数确定下来。多维数组是通过一维
数组仿真的,因为数组的元素可以是任何对象,当然也可以是数组。

   
对数组,我们只能做两件事,确定其大小,以及获得指向该数组下标为0的元素的指针。其它的有关数组的操作,实际上是通过指针进行的。

   
如果在应该出现指针的地方出现了数组名,则数组名就被当作指向该数组下标为0的元素的指针。
     int a;
     p = a;
     int *p;
是对的。但p = &aansi C中则是非法的。因为&a 是一个指向数组的指针,而p是一个指向整型变量的指针,
它们的类型不匹配。

数组a[3], sizeof(a) 得到的数值的大小,其他所有情况数组名a代表指向a[0]的指针。


    由于a[i] *(a+i);a+ii+a;所以a[i]i[a];但不推荐后者的写法

    int cal[12][31];
    int *p;
    int i;

    i = cal[4][7]
等于i = *(cal[4] + 7);也等于i = *(*(cal + 4) +7);


    p = cal;
是错误的,类型不匹配,后者是指向数组的指针。



   
我们来声明指向数组的指针:
    int (*ap)[31];
   
于是我们可以这样写:
    int cal[12][31];
    int (*monthp)[31];
    monthp = cal;

   
个指针不能相加。负数的移位运算不等于相应的乘或除运算。

3.2
非数组的指针

   
我们要将st连接成r.
 
  s = "abc";
    t = "efg";
    char *r;
    strcpy(r,s);
    strcat(r,t);
这并不能达到目的。

因为一是不能确定r指向何处, 二是不能保证r所指向的地址处还应该有内存空间可供容纳字符串。

较好的是把第一行改为char r[100];只是这样的话,大小固定了。

正确的应该是:

#include
#include

int main (void)
{
     char s[10];
     char t[10];
     
     char *r;
     char *malloc();
     r = malloc(strlen(s) + strlen(t) + 1);
     if(!r)
     {
          complain();
          exit(1);
     }
     
     scanf("%s",s);
     /*getchar();*/
     
     scanf("%s",t);
     
     strcpy(r,s);
     strcat(r,t);
     
     printf("%s\n",r);
     free(r);
     
}

3.3
作为参数的数组声明

   
我们没有办法将一个数组作为函数参数直接传递。数组名会被转为指向该数组第一个元素的指针。

    int strlen(char s[]){}
   
与下面的写法完全相同:
    int strlen(char* s){}


   
但其它地方就未必相同了。
   
下面两 个语句是完全不同的。
    extern char *hello;
    extern char hello[];

   
下面则是一样的
    main(int argc, char* argv[]){}
    main(int argc, char** argv){}

3.4
避免举隅法

   
复制指针并不同时复制指针所指向的数据。而是让两个指针指向同一个地址。


3.5
空指针并非空字符串

   
把常数0转为指针,则指针不等于任何有效的指针,即 void 指针。其它将整数转为指针得到的结果未定义。当常数0被转为指针时,这个指针绝对不能被解除引用(dereferenc)。换句话说,当我们将0赋给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。

   
下面的是合法的:
    if (p == (char *) 0)
   
但下面是非法的
    if (strcmp(p, (char *) 0) == 0)

   
如果p是一个空指针,即使printf(p);printf("%s",p);的行为也是未定义的。

3.6
边界计算与不对称边界

   
数组的下标如果用入界口加出界口来表达(即10个元素,其下标为0 <= n < 10 ),则元素个数即为上界与下界
之差,即下界。若为空,则上界等于下界。任何情况下上界也永远不可能小于下界。

   
尽量采用非对称边界法。
   
一个有N个元素的数组 ,我们可以使用a[N]进行比较和赋值,但不能引用其内容。
while(--n >= 0) 而不用 while(n-- > 0)

3.7
求值顺序

    C
语言只有四个运算符(&&, ||, ?: , ,)存在规定的求值顺序。另外,分隔函数参数的逗号并非逗号运算符。例如,在xy在函数fxy)中的求值顺序是未定义的,而在函数g((x,y))是先算x,再算yy的值为参数。特别是赋值运算符没有规定求值顺序。


3.9
整数溢出

   
无符号算术运算中,没有所谓的溢出一说。有符号运算中发生溢出,则结果未定义。

   
下面检测溢出的方法不可靠:(a,b是非负整型变量)
    if(a + b <0)
    complain();

   
应该这样:

    if((unsigned) a + (unsigned) b >INT_MAX)
    complain();

或者这样
        if(a > INT_MAX - b)
        complain();

3.10
为函数main提供返回值

     
如果没 有为函数声明返回类型,则默认为int.


                                         Chapter 4
连接


4.1
什么是连接器
   
连接器通常把目标模块看成是由一组外部对象组成的。 第个外部对象都代表着机器内存中的某个部分,并通达一个外部名称来识别。因此, 程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部对象。 某些编译器会对静态函数和静态变量的名称做一定改变,将它们也作为外部对象。

   
除了外部对象,目标模块还可能包括了对其它模块中的外部对象的引用。

4.2
声明与定义

   
每个外部变量只能定义一次。

int a

其出现在函数体外,称为外部对象a的定义。说明a是个外部整型变量,同时为a分配了空间,初值默认为0

同一个外部变量被多次定义(在同一个源文件或多个源文件),出现命名冲突。

4.3
命名冲突与static修饰符
     
避免命名冲突: static int a;

a的作用域只限于一个源文件内,对于其他源文件不可见。因此,当多个函数共享一个外部变量,可将这些函数放于同一源文件中,并将用到的外部变量也在同一个源文件中用static声明。
4.4 形参、实参与返回值----------------------------本节的程序不懂
      任何一个c函数都有返回类型,要么是void 要么是生成结果的类型。


   
每个函数都要在调用之前进行声明定义,不然返回类型为int

        int i;

        char c;

        for(i=0;i<5;i++)

        {

                scanf("%d",&c); // scanf ()类型不一致,要求输入整型%d,而输入为char&c。容易出错!

                printf("%d ",i);

        }
   
如果一个函数没有floatshort或者char类型的参数,在函数声明中完全可以省略类型声明(定义不能省略)


4.5
检查外部类型

   
同一个外部变量在不同的地方被声明为不同的类型,这种错误大部分编译器是检不出来的。
    char file[]= "/etc/password";
   

    extern char* file;
是不一样的。
因此,声明外部变量时类型要和定义的类型一致。

char a[] = "abe";

extern char a[];

char *a ="abe"

extern char *a


4.6
头文件
避免上节问题:每个外部对象只在头文件中声明。


                                        Chapter 5 库函数

    C
标准没有定义执行底层I/O操作的readwrite函数。
5.1
返回整数的getchar函数
     
原型:int getchar(void);
5.2
更新顺序文件


   
为了与以前的程序保持兼容,一个输入操作不能随后紧跟一个输出操作,反之亦然。如果要同时进行输入
   
和输出操作,必须在其中插入fseek函数的调用。

    FILE *fp;
    struct record rec;
   
    while (fread((char *)&rec, sizeof(rec),1,fp) = 1)
    {
        /*    */
        if(/* */)
        {
          fseek(fp, -(long)sizeof(rec), 1);
          fwrite((char *)&rec, sizeof(rec), 1,fp);
          fseek(fp, 0l,1);
         }
}

5.3
缓冲输出与内存分配------------------本节不太懂,以后再看

    #include

void main(void)
{
     int c;
     char buf[BUFSIZ];
     setbuf(stdout,buf);
     
     while((c = getchar()) != EOF)
          putchar(c);
}
这个是不对的。buf最后一次被清空是在什么时候?答案是在main函数结束之后,作为程序交回控制给操作系
统之前C运行时库所必须进行的清理工作的一部分。但是在此之前buf已经被释放。

   
解决方法一是加上static 声明。也可以把buf声明完全移到main函数之外。第二种办法是动态分配缓冲区,
在程序中并不主动释放分配的缓冲区


5.4
使用erron检测错误

   
很多的库函数,特别是那些与操作系统有关的,当执行失败时会通过一个名称为errno的外部变量,通知
程序该函数调用失败。

   
下面的是错误的:
    /*
调用库函数*/
    if(errno)
        /*
处理错误
*/
   
   
因为,在库函数调用没有失败的情况下,并没有强制要求库函数一定要设置errno0,这样errno的值可能

    就是前一个执行失败的库函数设置的值。
   
下面更正了,可还是错误的:
    errno = 0;   
    /*
调用库函数*/
    if(errno)
     /*
处理错误
*/

     
库函数在调用成功时,既没有强制要求对errno清零,但同时也没有禁止设置errno


     
下面才是对的:

     /*
调用库函数 */
     if(
返回的错误值
)
        
检查
errno

5.5
库函数
signal

   
从理论上说,一个信号可能在C程序执行期间的任何时刻上发生,甚至可能出现在某些复杂的库函数(如malloc)的执行过程中。因此从安全的角度讲,信号的处理函数不应该调用上述类型的库函数。基于同样的原因,从signal处理函数中使用longjump退出,通常情况下也是不安全的:因为信号可能发生在malloc 或者其它库函数开始更新某个数据结构,却又没有最后完成的过程中。因此signal处理函数能够做的安全的事情,似乎就只有设置一个标志然后返回,期待以后主程序能够检查到这个标志,发现一个信号已经发生。


   
然而,就算这样做也并不总是安全的。当一个算术运算错误引发一个信号时,某些机器在signal处理函数返回后还将重新执行失败的操作。因此对于算术运算错误,signal处理函数的惟一安全、可移植的操作就是打印一条出错消息,然后使用longjumpexit立即退出程序。
    当一个程序异常终止时,程序输出的最后几行常常会丢失,原因是缓冲。


                                       Chapter 6
预处理器

6.1
不能忽视空格
     
#define SUB(a,b) a-b
6.2
宏并不是函数

6.3
宏并不是语句

    #define assert(e) ((void)((e)||_assert_error(_FILE_,_LINE_)))
6.4
宏并不是类型定义

我们没有办法在一个C表达式的内部声明一个临时变量。
避免副作用的一个办法就是再引入一个变量。

在某个上下文中本应需要函数而实际上却用了函数指针,那么该指针所指向的函数将会自动地被取得并替换这个函数指针。


                                     Chapter 7
可移植性缺陷

7.1
应对C语言标准变更
7.2
标识符名称的限制

    c
标准所能保证的只是,c实现必须能够区别出前6个字符不同的外部名称,且并没有要求区分大小写。
7.3
整数的大小

   
一个普通(int)整数足够大以容纳任何数组下标。

   
字符长度由硬件决定
7.4
字符是有符号整数还是无符号整数

   
若为有符号,则将其转为int时,应该同时复制符号位,而无符号,则填 0即可。
   
一个常见的错误是:如果c是一个字符变量,使用(unsigned)c就可得到与c等价的无符号整数。这是错误的,因为在将字符c转换为无符号整数之前,c将先被转为int型,而此时可能得到非预期的结果。
   
正确的是使用语句(unsigned char)c,这样就直接转换。
7.5
移位运算符

   
如果被移位的对象长度是n位,那么移位计数必须大于或等于0,而严格小于n

   
即使某些c实现将符号位复制 到空出的位中,有符号整数的向右移位运算也并不等于除以2的某次幂。
    (-1)>>1
这一般不可能为0,(-1)/2一般为0.

7.5
内存位置
0

     NULL
指针并不指向任何对象,只能用于赋值或比较运算。


7.7
除法运算的截断

    q = a / b;
    r = a % b;
    C
语言的定义只保证q*b+r==a,以及当a>=0b>0时,保证|r|<|b|以及r>=0.最好避免a为负值。

7.8
随机数的大小

    RAND_MAX

7.9
大小写转换

   
7.10
首先释放,然后重新分配

     
注意早期的C实现可以realloc一个已经free了的指示针。

7.11
一个例子

     
因为字符串常量可以用来表示一个字符数组,所以在数组名出现的地方都可以用字符串常量末端替换。
     
如:
     "0123456789"[n%10]

     -n
可能溢出,因为最小负数的绝对值大于最大正数的绝对值。所以改亦正数的符号不会有问题,而改变
      
负数的符号则可能有问题。

void printnum(long n, void (*p)())
{
        if(n<0)
        {
        (*) ('-');
        n=-n;
        }
        if(n>=10)
        printnum(n/10,p);
        (*p)((int)(n%10) + '0');
}
上面的是有问题的。下面的才是对的:
void printneg(long n, void (*p)())
{
        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]);
}

void printnum (long n, void (*p)())
{
        if(n < 0)
        {
        (*p)('-');
        printneg(n,p);
        }
        else
        printneg(-n,p);
}

阅读(1397) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~