C语言常见易混易犯错误(C语言陷阱与缺陷前三章基础内容总结)

概览

在这里插入图片描述

C语言陷阱与缺陷之词法陷阱
=不同于==
  • 比较运算误写为赋值运算
while(c=' '|| c=='/t'||c=='/n')
c=getc(f);

如上例代码,原来应该为c==’ '为判断,但是改为赋值语句后就变为死循环一直执行下去。

  • 赋值运算写成比较运算
if(filedesc==open(agrv[i],0)<0)
error

本代码原来应该是’=’,用来检查open返回的数是否为正数和0。但是改为赋值语句后,变为比较运算,值只可能为0或1,没有小于0的数,条件语句失效。

  • 编译器在编译执行时一般也会提醒警告,甚至是报错。所以一定要注意这两者的区别,否则在很长的代码里影响是无法估计的。
词法分析中的“贪心法”

C语言的某些符号,例如/、* 、和=,只有一个字符长,称为单字符符号。而C语言中的其他符号,例如/* 和==,以及标识符,包括了多个字符,称为多字符符号。当C编译器读入一个字符’/‘又跟了一个字符’ * ', 那么编译器就必须做出判断:是将其作为两个分别的符号对待,还是合起来作为一个符号对待。C语言对这个问题的解决方案可以归纳为一个很简单的规则:每一个符号应该包含尽可能多的字符。也就是说编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。这个处理策略有时被称为“贪心法”。((也就是说,只要能合起来组成一个字符的都继续读,直到新读入的字符不能组成新的字符为止)
举例:

y=x/*p

在编译过程中,因为/ 和* 可以组成一个字符。所以实际的结果是这样的:y=x /* p;那么就无法执行。
其原意是想将x除以p所指向的值的结果赋给y.
那么可以这样修改:

y=x /  *p;

/和空格不能组成一个字符,所以/为一个字符。,这样就符合原意。
或者:

y=x/(*p);
字符与字符串

单引号引起字符和双引号引起字符区别
用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。因此, 对于采用ASCII字符集的编译器而言, ‘a’的含义与0141(八进制)或者97(十进制)严格一致。
用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针,该数组被双引号之间的字符以及一个额外的二进制值为零的字符\0’初始化。(注意指针指向的地址是第一个字符的地址,末尾有’\0’字符对应二进制为0)。
例如:

printf("hello,world\n");

等价于

char hello[]={'h','e','l','l','o',' , ','w','o','r','l','d','\n','\0'};
printf(hello);

因为用单引号括起的一个字符代表一个整数,而用双引号括起的一个字符代表一个指针,如果两者混用,那么编译器的类型检查功能将会检测到这样的错误。
例如:

char*slash='/';

在编译时将会生成一条错误消息,因为’/‘并不是一个字符指针。然而,某些C编译器对函数参数并不进行类型检查, 特别是对printf函数的参数。因此, 如果用
printf(’\n’) ;来代替正确的printf(”\n”) ;则会在程序运行的时候产生难以预料的错误,而不会给出编译器诊断信息。

C语言陷阱与缺陷之语法陷阱
运算符优先级问题
  • C语言运算符优先级表
    在这里插入图片描述

优先级比较:前述运算符>单目运算符>双目运算符(双目运算符中运算符的优先级最高,移位运算符次之,关系运算符再次之)>逻辑运算符>赋值运算符(=)>条件运算符(三目运算符)
注意几点:
1.任何一个逻辑运算符的优先级比任何一个关系运算符低
2.移位运算符(<< ,>>)的优先级要比算术运算符低,但是比关系运算符要高
3.所有赋值运算符的优先级是一样的,他们的结合方式是从右往左
例如:

home_score=visitor_score=0;

可以等价于

visitor_score=0;
home_score=visitor_score
  • 优先级易混
while(c=getc(in)!=EOF)
put(c,out)

愿意是将getc返回的值赋值给c然后与eof比较,但是由于!=比较运算符的优先级高于赋值运算符,所以c的值为0或1,与愿意不符。
改为

while((c=getc(in))!=EOF)
put(c,out)

再看一个例子:

if(t=BTYPE(pt1->aty)==strty||t==unionty)

本意是将函数调用的值赋值给t,然后看t是否等于strty或unionty.但是由于==的优先级高于=,所以t的值被赋值为0或1,与原来的效果完全不同。

作为语句结束标志的分号
  • 多增加一个分号
if(x[i]>n);
n=x[i];

可以看出,条件语句就完全失效,而且编译器也不会发出警告
正确的是:

if(x[i]>n)
n=x[i];

这样条件满足就可以执行。

  • 遗漏分号
if(n<3)
return
log rec.date=x[0] ;
log rec.time=x[1] ;
log rec.code=x[2] ;

return后少了一个分号,看似后面没有跟任何值,实际上相当于

if(n<3)
return log rec.date=x[0] ;
log rec.time=x[1] ;
log rec.code=x[2] ;

编译器就可能因为返回值的类型和函数声明的类型不一致而报错。然而,如果一个函数不需要返回值(即返回值为void) , 我们经常在函数声明时省略了返回值类型, 但是此时对编译器而言会隐含地将函数返回值类型视作int类型。如果是这样, 上面的错误就不会被编译器检测到。在上面的例子中,当n>=3时,第一个赋值语句会被直接跳过,由此造成的错误可能会是一个潜伏很深、极难发现的程序Bug。

struct grec(){
int date;
int time;
int code
}
main()
{

}

因为}后少了一个分号,所以main函数会返回一个logrec类型的值,如果有分号,返回值类型会缺省定义为int类型。

switch语句

switch语句中break的作用

switch(color) {
case1:printf(”red”) ;
break;
case2:printf(“yellow”);
break;
case3:printf(“blue”) ;
break;

这样,就能实现按照1,2,3三个序号分别打印red,yellow,blue.但如果把break省略,那么我们输入1,就会打印出red,yellow,blue。不能实现分别打印,即执行完第一个case例程还会继续向下执行。

  • 省略break
    在某些程序中,将break省略掉显然就会丧失语句原来固有的功能,但在某些程序中会故意省略break来实现一些特别到功能。(注,故意省略break时一定要用注释标明)
    例如:
    考虑这样一段代码,它的作用是一个编译器在查找符号时跳过程序中的空白字符。这里,空格键、制表符和换行符的处理都是相同的,除了当遇到换行符时程序的代码行计数器需要进行递增:
case\n':
line count++;
/*此处没有break语句*/
Case‘\t':
/*此处没有break语句*/
case'  ':
/*此处没有break语句*/
,,,,,,,,,,

即对各个字符处理是相同的,精简了程序

悬挂else引发的问题

唉,这个问题这是,,,,,刚开始学C的时候一直犯这个错误。

if(x==0)
    if(y==0) erro();
else{
z=x+y;
f(&z);
}

如上,原意是当x=0时对y进行条件判断,如果不为0,就执行else操作。但是c语言有这样的规则,else始终与同一括号内最近的未匹配的if·结合。所以说,其实代码是这样的:

if(x==0)
    if(y==0) erro();
else{
z=x+y;
f(&z);
}
if(x==0)
    if(y==0)
       erro();
    else{
    z=x+y;
    f(&z);
   }

如果要按照原意进行,可以这样修改:

  if(x==0){
    if(y==0)
    erro();
   }
   else{
    z=x+y;
    f(&z);
   }
C语言陷阱与缺陷之语义陷阱
指针和数组
  • C语言中指针和数组看起来是两种不同的语法,但其关系是密不可分的,甚至有些时候可以互相代替。这种密切的关系,以至于如果不能理解一个概念,就无法彻底理解另一个概念。
  • 简接运算符’ * ‘和取地址符’& ’
    这一点是非常容易混淆的。
int *ip;   //定义一个指向整型变量的指针
int i;   //d定义一个变量
ip=&i; //取出变量i的地址赋给指针ip
*ip=17;//赋值给指针ip所指的变量。

定义了一个指向整型变量的指针。那么这里就要区分:
‘* ip’代表的意思是取出存储在地址中的对应值。而ip是一个指针,指针真能被赋值为地址,而不能直接被赋值。而通常我们总是会将’ * '遗漏或者添加,那么编译器就会报错,很多时候也很难察觉。
如果写成这样

int *ip;   //定义一个指向整型变量的指针
int i;   //d定义一个变量
*ip=&i; //*ip对应的是是指针所指向的值,不是地址
ip=17;//ip是一个指针,不能直接赋值

注:
int * ip;这里要区分,int* 是在一起的,代表指针类型,p指的是指针变量。

  • 指针与数组的关系
int *p;
int a[3]

如果一个指针指向的是数组中的一个元素,那么我们只要给这个指针+1,就能得到该数组中下一个元素的指针。同样,减1可以到该数组上一个元素的指针。
由上例可知,a是一个拥有3个整型元素的数组。数组名就可以做为一个指向该数组第一个元素的指针。所以数组名是指针
因此我们可以这样写

p=a;

指针指向的是地址,所以可以相互赋值。可是我们为什么不用取地址符号呢?
假如我们写成

p=&a;

因为&a是一个指向数组的指针,而p是一个指向整型变量的指针,类型不匹配。因此&a被视作非法,或者就等于a.
现在,p指向数组下标为0的元素,那么p+1就指向下标为1的元素,依次类推,,,,(p+1等同于p++)
那么,a是指向数组元素的地址,那么* a就是对数组下标为0的元素的引用,同理 * (a+1)就是对数组下标为1元素的引用,* (a+i)就是对第i个元素的引用,这也就是我们数组中常用的a[i]的写法。

  • 二维数组
int calendar[12][31];
int *p;
int i;

因为calendar是一个有着12个数组类型元素的数组,它的每个数组类型元素 又是一个有着31个整型元素的数组,所以calendar[4]是calendar数组的第5个元 素,是calendar数组中12个有着31个整型元素的数组之一。因此,calendar[4] 的行为也就表现为一个有着31个整型元素的数组的行为。例如,sizeof(calendar[4]) 的结果是31与sizeof(int)的乘积。
如果calendar^]是一个数组,我们当然可以通过下标的形式来指定这个数组 中的元素,就像下面这样,
i. = calendar [4] [7];
我们也确实可以这样做。还是与前面类似的道理,这个语句可以写成下面这样而 表达的意思保持不变:
i = * (calendar[4]+7);
这个语句还可以进一步写成:
i = * (* (calendar+4)+7);
从这里我们不难发现,用带方括号的下标形式很明显地要比完全用指针来表 达简便得多。

非数组的指针(字符串数组的使用及动态分配)
  • 假定我们有两个这样的字符串s和t,我们希望将这两个字符串连接成单个字 符串r。要做到这一点,我们可以借助常用的库函数strcpy和strcat。下面的方法 似乎一目了然,可是却不能满足我们的目标:
char *r;
strcpy(r, s);
strcat(r, t);

之所以不行的原因在于不能确定r指向何处。我们还应该看到,不仅要让r 指向一个地址,而且r所指向的地址处还应该有内存空间可供容纳字符串,这个 内存空间应该是以某种方式已经被分配了的。
给r一定的内存空间:

char r[100];
strcpy(r, s);
strcat(r, t);

但是,有时我们不确定字符串的大小,如果字符串的 长度过小而分配的空间很大,那么就会造成空间的极大浪费。所以我们引入动态配置,来避免空间的浪费。
在这之前,我们还需要了解一点:
在C语言中,字符串常量代表了一块包括字符串中所有字符以及一个空字符 (’\0’)的内存区域的地址。因为C语言要求字符串常量以空字符作为结束标志。也就是说字符串都以’\0’字符结尾。所以给字符串动态分配内存时应注意这一点。

  • 改进
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy (r, s);
strcat(r, t);

这个例子还是错的,原因归纳起来有三个:
1.malloc函数有可能 无法提供请求的内存,这种情况下malloc函数会通过返回一个空指针来作为“内 存分配失败”事件的信号。
2.给r分配的内存在使用完之后应该及时释放,这一点务必要记 住。因为在前面的程序例子中r是作为一个局部变量声明的,因此当离开r作用 域时,r自动被释放了。修订后的程序显式地给r分配了内存,为此就必须显式地 释放内存。
3.前面的例程在调用malloc函数时并未 分配足够的内存。前面我们已经提到过字符串都会以’\0’。库函数 strlen返回参数中字符串所包括的字符数目,而作为结束标志的空字符并未计算在 内。因此,如果strlen(s)的值是n,那么字符串实际需要n+1个字符的空间。
所以,应改为

char *r, *malloc();
r = malloc(strlen(s)+strlen(t))+1);
if(!r) {
complain();
exit(1);
}//判断内存是否分配成功
strcpy(r,s);
strcat(r,t);

/* 一段时间之后再使用*/

free(r);//释放内存

作为参数的数组声明

C语言中会自动地将作为参数的数组声明转换为相应的指针声明。也就是说,像这样的写法:

int strlen(char s[])
{
/*具体内容*/
}

与下面的写法完全相同:

int strlen(char* s)
{
/*具体内容*/
}

但是不要将下面种定义方式混淆

extern char *hello;//定义一个字符类型的指针
extern char hello [];//定义一个字符类型的数组(当然这样表示也会报错,没有指定数组的大小,这里只是为了方便区别)
  • 指针参数代表数组
main(int argc, char* argv[])
{
/*具体内容*/
}

这种写法与下面的写法完全等价:

main(int argc, char** argv)
{
/*具体内容*/
}

需要注意的是,前一种写法强调的重点在于argv是一个指向某数组的起始元 素的指针,该数组的元素为字符指针类型。因为这两种写法是等价的,所以读者可以任选一种最能清楚反映自己意图的写法。

指针混淆
  • 混淆指针和指针所指向的数据
char *p,*q;
p="xyz";

尽管某些时候我们可以不妨认为,上面的赋值语句使得P的值就是字符串 “xyz”,然而实际情况并不是这样,记住这一点尤其重要。实际上,p的值是一个 指向由x,y,z和\0,4个字符组成的数组的起始元素的指针。因此,如果我们执 行下面的语句:

q=p;

那么p和q就是指向同一内存地址的指针。如图所示
在这里插入图片描述

同时记住,复制指针并不是复制指针中的数据。

边界问题
  • ‘<’和’<=’
int i, a[10];
for (i=1; i<=10;i++)
a[i] = 0;

在C语言中,这个数组的下标范围是从0到9。一个拥有10个元素的数组中, 存在下标为0的元素,却不存在下标为10的元素。C语言中一个拥有n个元素的 数组,却不存在下标为n的元素,它的元素的下标范围是从0到n-1为此,由其 他程序语言转而使用C语言的程序员在使用数组时特别要注意。
所以,上面的实例是不规范的,这就属于边界问题。
那么C语言中下标为什么是从0开始,我们来探讨一下:
16<=x<=37,那么16和17有多少个元素?我们的第一印象肯定是37-16=21个元素。但其实应是37-21+1=22,共有22个元素。所以这样表示并不能说是一种边界反而易于迷惑人。
我们换种表示方法使之成为边界。让下界>=16,上界38,即16<=x<38,那么我们就能轻易得出中间共有38-16=22个元素。所以,C语言正是利用了这种边界问题。
对于像C这样的数组下标从0开始的语言,不对称边界给程序设计带来的便 利尤其明显:这种数组的上界(即第一个’'出界点”)恰是数组元素的个数!因此, 如果我们要在C语言中定义一个拥有10个元素的数组,那么0就是数组下标的 第一个“入界点”(指处于数组下标范围以内的点,包括边界点),而10就是数组 下标中的第一个“出界点”(指不在数组下标范围以内的点,不含边界点)。正因 为此,我们这样写:

int a[10], i;
for (i = 0; i < 10; i++) 
a[i]= 0;

而不是写成下面这样:

int a[10],i;
for (i = 0; i <=9; i + + )
a[i]=0;

文章引用:C语言陷阱与缺陷[美]

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值