C陷阱与缺陷

1.词法分析中的贪心法
编译器读取多字符符号(如==,/*等符号)的原则是:每一个富豪应该包含尽可能多的字符——贪心法
符号的中间不能嵌有空白(空格,制表符,换行符)
       ==单个符号,= =是两个符号
    a---b(a--,a-b)与a-- -b含义相同,与a- --b(--b,a-b)含义不同
    /*是一段注释的开始, y=x/ *p是x/指针p指向的变量值,等同于y = x/(*p) 
    老版本的编译器允许使用=+表示现在的+=,所以会将a=-1理解为a=- 1即a=a-1,而不是a= -1;将复合赋值视为两个符号,可处理a>>=1
2.数字0开头被认为是八进制数,010是8,10是10
3.对于采用ASCII 字符集的编译器而言,'a'的含义与0141和97严格一致
  打印换行应该是printf("\n")而不是printf('\n')
  int型数存储空间可容纳多个字符,所以有的编译器用'yes'代替"yes"无警告
    但是"yes"的含义是:依次包含'y','e','s','\0'的4个连续内存单元的首地址
            'yes'含义无准确定义
4.声明
    float ((f));含义:当对其求值时,((f))的类型为浮点类型,由此推知f为float类型
    float (*h)();含义:h是一个函数指针,h所指向的函数的返回值时float类型
    (float(*)())表示一个指向返回值为浮点类型的函数的指针
     【函数指针定义方式:函数返回值类型 (* 指针变量名) (函数参数列表);int(*p)(int,int)
        用typedef更加清晰Typedef void (*funcptr)();
                (*(funcptr)0){};】
    【函数指针的调用:int func(int,int);
             p=func;//将func函数的首地址赋给指针变量p
             c=(*p)(2,3)//通过函数指针调用函数
             d=p(2,3)//通过函数指针调用函数的简写形式】
    (*(void(*)()0)()//void(*)()是一个返回值为void的函数指针
            //void(*)()0是一个指向首地址为0的返回值为void的函数指针,相当于对0的强制类型转换
        //(*(void(*)()0)()是通过一个指向首地址为0的返回值为void的函数指针调用该函数
5.运算符优先级[多加括号]
    !=的优先级高于&,所以if(flags & FLAG != 0)的意思是if(flags & (FLAG != 0))//而不是判断flags的某位是否与FLAG的某位同为1
    加法运算的优先级高于移位运算,所以r=hi<<4+low?的意思是r=hi<<(4+low)
    单目运算符高于双目运算符,双目运算符中算术运算符>移位运算符>逻辑运算符>赋值运算符>条件运算符[三目]
*p++意思是*(p++)//自右至左
while(c = getc(in) != EOF)//意思是将getc(in) != EOF的结果存入c(c取值0或1),而不是一直读取c直到EOF为止
6.语句末尾分号
    strct{} main(){}//结构体定义完没加分号,导致意思变为main返回值类型为定义的结构体类型
7.switch语句
    用法:
        switch(xxx){        
       case 1:yyy1;break;//如果不加break,假设输入2,在执行完yyy2后会把yyy3也执行了
       case 2:yyy2;break;//如果特意去掉break语句实现某些功能,可以/*此处没有break*/提示一下
       case 3:yyy3;break;
        }
比如: case '\n'://在查找符号时跳过程序中的空白字符
          linecount++;
       /*此处没有 break语句*/
      case '\t':
      case ' ':
8.函数调用
 如果f是一个无参函数,f();//表示调用f
             f;//这个语句计算函数f的地址,却并不调用该函数 
9.else与最近的第一个if结合,注意不要写错
  c语言允许初始化列表中出现多余的逗号如 int days[]={1,2,3,4,},方便自动化生成代码,程序员可能会制作代码生成工具来生成代码,这样可以不用特殊处理最后一行
10.数组
c语言中的数组大小必须在【编译期间】就作为一个常数确定下来,数组的元素可以是任何类型的对象,也可以是另外一个数组
11.记得分配空间
    char *r;
    strcpy(r,s);//不行,因为不能确定r指向何处,将上一行换成char r[100]就可以,当然前提是s的长度满足
    strcat(r,s);
 //用malloc
    char *r=malloc(100+1);//隐患:malloc可能无法提供请求的内存,此时会返回一个空指针表示内存分配失败,最后要存'\0',所以要在所需的内存的基础上多分配1字节
    if(!r){exit(1);}             //所以在这里要判断一下r非空
         strcpy(r,s);//

    free(r);          //使用完记得free
12.作为参数的数组声明
printf("%s",s);与printf("%s",&s[0]);作用相同
int strlen(char s[]){}与int strlen(char *s){}作用相同
main(int argc,char* argv[])与main(int argc,char** argv)作用相同
但是!!!extern char *hello;与extern char hello[];作用不同
13.避免“举隅法”
char *p, *q;
 p = "xyz";//p的值是一个指向由'x','y','z','\0'组成的数组的起始元素的指针
 q=p;//现在q和p都是一个指向。。。的指针,!赋值指针并不同时复制指针所指向的数据
 q[1]='Y';//错误,ANSIC标准中禁止对stringliteral做修改,试图修改字符串常量的行为是未定义的
14.空指针并非空字符串
当把0或NULL赋给一个一个指针变量时绝对不能企图使用该指针所指向的内存中存储的内容
if(p==(char*)0)..//合法
if(strcmp(p,(char *)0)==0)..//非法,因为strcpy的实现中会包括查看它的指针参数所指向的内存中的内容的操作
若p是一个空指针,printf(p)和printf("%s",p)的行为也未定义,gcc下会打印null
15.边界计算与不对称边界【书7.6节。。没看完】
x>=16且x<=37最好写成x>=16且x<38//这样满足条件的x就有38-16=22个
for(i=0;i<=9;i++)最好写成for(i=0;i<10;i++)
不对称边界:下界是入界点,上界是出界点,这样做的好处:1取值范围的大小就是上界与下界之差2如果取值范围为空那么上界=下界3即使取值范围为空上界也永远不可能小于下界

16.整数溢出
两类整数算术运算:有符号运算和无符号运算,在无符号运算中不存在溢出
如果算数运算符的一个操作数是有符号整数另一个是无符号整数,那么有符号整数会被转换为无符号整数,就不会溢出。当两个都有符号,可能会溢出,溢出行为未定义。

17.连接器
C语言中若干个源程序可以在不同的时候进行单独编译,通过链接器整合在一起。
链接器的输入是一组目标模块和库文件,输出是一个载入模块,可被操作系统直接运行。连接器的一个重要作用是处理多个模块整合成一个载入模块时发生的命名冲突。
没有被声明成static的函数和外部变量被称作外部对象,某些c编译器会将静态函数和静态变量名称做一定改变也认为是外部对象,因为经过名称修饰所以不会和其他源程序中的同名函数或变量发生命名冲突。
18.声明与定义
extern int a;//a是一个外部整型变量,a的存储空间是在尘谷线的其他地方分配的。
从连接器的角度看上述声明是对a的引用而不是定义。即使该声明出现在某个函数的内部 含义一样。
每个外部对象都应该在程序某个地方定义,所以某个别的地方必须包括语句int a;这两个语句可以位于同一个或者不同的源文件中。
如果在其他地方有多个int a;
比如1.cpp中 int a=7;
2.cpp中int a=9;//这种情况与系统有关,大部分系统都拒绝这种情况。规则是【每个外部变量只能够定义一次】
 部分系统接受一个外部变量被定义多次但是只初始化一次的情况。
19.命名冲突与static修饰符
如果一个库函数需要调用另一个未在ANSI C标准中列出的库函数,那么它应该以“隐藏名称”来调用后者。所以程序员可以定义一个与标准库函数名相同的函数而不用担心库函数错调自己写的函数,但大多数c语言不这么做,所以仍要处理这类命名冲突。
static int a;//将a的作用域限制在一个源文件内
static get(int x){}//将get函数的作用域限制在一个源文件中
20.一个函数在被定义或者声明之前被调用那么它的返回值默认为整型
比如double square(double x,double y){return x*y;}未在main()之前声明或定义:
main(){printf("%g\n",square(2,3));}//连接时会出错
c语言允许 double square()类的省略参数的声明,依赖于调用者能够提供数目正确且类型恰当的实参,(
类型恰当不是说类型相同,比如float会自动转为double,short或char会转成int)
但是在调用该函数的其他文件中必须完整声明为isvowel(char c);否则调用者将把传递给square的函数的实参自动转化为int【默认是int ,所以形参如果是int就可以不完整声明】,就与形参不一致了
main()//这样是错的,因为sqrt()接收一个双精度值参数却被传了整型。sqrt返回双精度类型却未声明
{       //一种解决办法是在main()之前加上double sqrt(double)
 double s;
 s = sqrt(2);//最好的解决办法是 #include
 printf("%g\n", s);
}


#include
main()
{
 int i;
 char c;
 for(i = 0; i < 5; i++){
 scanf("%d", &c);//出错,因为c被声明为char而不是int,输入 0 1 2 3 4会输出000001234
 printf("%d", i);//因为scanf并不能分辨这种情况,它只是将指向字符的指针作为指向数字的指针接受
 }//并且在指针之乡的位置存储一个整数,因为整数所占的存储空间要大于字符的,所以字符c附近的内存被覆盖
 printf("\n");
}

21.检查外部类型
1.cpp中 extern int n;
2.cpp中 long n;//不同编译器处理情况不同,可能会提示错误,可能巧合正常工作【32位机int和long长度相同】,可能赋值时把long的低端部分付给了int,可能给一个赋值时效果相当于给另一个赋了一个完全不同的值即程序不能正常工作
所以要保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型

1.cpp中:char filename[] = "/etc/passwd";//filename的类型是字符数组
2.cpp中:extern char* filename;//filename的类型是字符指针,尽管数组和指针非常类似但毕竟不同
更正:在两个cpp中用同样的类型,要么都char filename[],要么都char* filename

main()
{
 double s;
 s = sqrt(2);
 printf("%g\n", s);
}
未在外部声明sqrt,完全等价于下面这个程序
extern int sqrt();
main()
{
 double s;
 s = sqrt(2);
 printf("%g\n", s);
}//当然这么些是错的,sqrt返回double,不明确行为。
较好的解决办法:每个外部对象只在一个头文件中声明,需要用到该外部对象时就包含该头文件,定义该外部对象的模块也要包含该头文件!! 

23.返回整数的getchar函数
main()
 {
 char c;
while((c=getchar()) != EOF)//getchar函数一般情况下返回标准输入文件中的下一个字符,当没有输入时返回一个EOF
 putchar(c);
 }
这个程序的问题在于c是char类型的而不是int类型,意味着c无法容下所有可能的字符,可能无法容下EOF ,这样3种可能1某些合法的输入字符在“被截断”后使得c取值=EOF或者c根本取不到EOF进入死循环3程序巧合工作【某些编译器对getchar函数的返回值进行截断处理并将低端字节赋给c,在程序处理中比较c与!EOF的取值,这样看上去程序可以正常运行】
24.更新顺序文件
为了兼容过去不能同时进行读写操作的程序,一个输入操作不能随后紧跟一个输出操作反之亦然。若要同时进行输入和输出,必须在其中插入fseek函数的调用//int fseek( FILE *stream, long offset, int origin );设置文件流stream的文件位置指示器为offser所指向的值

//更新一个顺序文件中选定的记录
FILE *fp;
struct record rec;
...
while(fread((char*)&rec, sizeof(rec), 1, fp) == 1){
 /*对 rec执行某些操作*/
 if(/*rec必须被重新写入*/){
 fseek(fp, -(long)sizeof(rec), 1);//sizeof返回一个无符号值因此首先必须将其转换为有符号类型才可能将其反号
 fwrite((char*)&rec, sizeof(rec), 1, fp);//如果fwrite函数执行,那么它的下一步是while()中的()函数,会出错。【解决办法,在fwrite()后加入fseek(fp, 0L, 1);后文件可以正常读取】
 }
}
25.缓冲输出与内存分配
程序输出的两种方式:1即使输出//系统负担高 2先暂存起来然后再大块写入。c语言实现通常都允许程序员进行实际的写操作之前控制产生的数据量,通过setbuf(stdout, buf);函数实现,buf的大小由中的BUFSIZ定义:所有写入到stdout的输出都应该使用buf作为缓冲区,直到buf被填满或者fflush函数被调用时,buf中的内容才会实际写入到stdout中
#include
main()
{
 int c;
 char buf[BUFSIZ];
 setbuf(stdout, buf);//buf缓冲区最后一次被清空是在main()函数结束之后,作为程序交回控制给操作系统之前c运行时库所必须进行的一部分清理工作,但是在此之前buf字符数组已经被释放!
 while((c=getchar()) != EOF)
 putchar(c);
}
【解决:】1.让buf成为静态数组,在main()外static char buf[BUFSIZ];
2.动态分配缓冲区,在程序中并不主动释放分配的缓冲区【main()结束时不会释放该缓冲区,这样c运行时库进行清理时就不会发生缓冲区已释放的情况】

26.使用errno检查错误
许多库函数,尤其是与操作系统相关的库函数执行失败时会返回一个名为errno的外部变量
/*调用库函数*/
 if (errno)//这样直接调用是错的,比如fopen函数新建文件时需要先判断是否已存在同名文件,如果存在的话将其删除然后新建,所以他需要调用别的库函数,假设不存在同名文件,该库函数会返回errno,但是此时fopen新建一个不存在的文件并没有错误发生,但程序的errno仍然被设置了
 /*处理错误*/
【解决:先检测库函数的返回值,确认程序已经失败了再检查errno找出错误原因】
/*调用库函数*/
if(返回的错误值)
    检查errno

27.库函数signal//#include
通过signal(signal type, handler function);//signal type标识signal函数要捕获的信号类型,handler function指当指定的事件发生时,将要加以调用的事件处理函数
因为一个信号可能在程序执行期间的任何时刻发生,所以信号处理函数不应调用例如malloc的复杂库函数


28.预处理器
定义宏时不能忽视宏定义中的空格,否则实际意思可能与想表达的意思相去甚远。
宏并不是函数,也不是语句,也不是类型定义,最好使用typedef进行类型定义

29.可移植性缺陷
(unsigned) c;//若c时字符变量,想将其转换为无符号整数,这样会失败,因为c会先被转换为int
正确的做法是:
(unsigned char)e;//一个unsigned char类型的字符再转换为无符号整数时,无需先被转化为int
30.移位运算符
向右移位时,如果被移位的对象时无符号数,那么空出的位置将由0填充,
           如果是有符号数,那么既可以用0填充,也可以用符号位的副本填充
移位计数的取值范围:如果被移位对象的长度是n位,那么移位计数必须大于等于0且严格小于n
mid = (low + high) >>1;与mid = (low + high) / 2;完全等效但是前者的执行速度快得多
31.内存位置0
null指针并不指向任何对象,如果p,q是null指针,那么strcmp(p,q)的行为是未定义的,某些编译器对内存位置0只允许读不允许写,某些编译器如果发现读取内存位置0就终止执行

32.除法运算时发生的截断
q = a / b;
r = a % b;//希望满足1 q * b + r == aˈ 2改变a的正负号时希望可以改变q的正负号,但不会改变q的绝对值 3当b>0时希望保证r>=0且r

33.大小写转换
int toupper(int c) 或tolower这个函数被调用的开销远大于该函数内部的计算开销,所以希望将其定义为宏
#define _toupper(c)((c)+'A'-'a')

34.首先释放,然后重新分配【11.10没看完】
malloc\realloc\free//调用realloc需要将一块已分配内存的区域指针以及这块内存新的大小作为参数传入,就可以调整这块内存区域为新的大小,这个过程中可能涉及到内存的拷贝

35.c语言容易出现的问题

  1 内存重叠的处理

  2 临时变量太多或者没有安全释放

 3 没有测试内存越界

  4 指针操作不熟悉

 

 


 

你可能感兴趣的:(C/C++)