【C陷阱与缺陷】----语法陷阱

简介: 由于一个程序错误可以从不同层面采用不同方式进行考察,而根据程序错误与考察程序的方式之间的相关性,可以将程序错误进行划分为各种陷阱与缺陷

💯💯💯


要理解一个C程序,必须理解这些程序是如何组成声明,表达式,语句的。虽然现在对C的语法定义很完善,几乎无懈可击,大门有时这些定义与人们的直觉相悖,或容易引起混淆。语法细节决定语义,本篇总结C语法陷阱中的诸多细节,以供参考。


导言:


由于一个程序错误可以从不同层面采用不同方式进行考察,而根据程序错误与考察程序的方式之间的相关性,可以将程序错误进行划分为各种陷阱与缺陷:


①.词法“陷阱”

②.语法“陷阱”

③.语义“陷阱”

④.连接问题

⑤.库函数问题

⑥.预处理器问题

⑦.可移植性缺陷


Ⅰ. 理解函数的声明


(*(void(*)()))0();


你知道这个表达式表示什么吗?


:调用一个首地址为0的函数。


要理解这个表达式我们需要从两个方面入手:函数如何声明的,与类型如何转换的。


1.1函数的声明


任何C变量的声明都是由两部分组成:类型以及变量。


float f,g;


这个声明的含义是:当对其求值时,表达式f和g的类型为浮点数类型


float ff();


这个声明的含义是:表达式ff()求值结果是一个浮点数,也就是说ff是一个返回值为浮点数类型的函数。


float *pf;


这个声明的函数是*pf是一个浮点数,也就是说,pf指向的数是个浮点数。pf是一个指向浮点数的指针。


float *f(), (*h)();


同理,那*f() ,(*h)(),就是浮点表达式


因为函数调用()结合优先级是高于解引用 *,*f(),也就是 *(f()):f是个函数,返回值是一个指向浮点数的指向。


h呢是一个函数指针,h指向的函数的返回值是浮点类型。


如果假设pf为函数指针,那么如何调用fp所指向的函数呢?


首先pf就是指针指向的函数,那么对它调用就可以了。


不过注意要这样写:(*pf)();


因为函数运算符()的优先级高于单目运算符。如果pf两侧没有括号,那么 pf()就与 (pf())一样了。


那我们如果想调用一个首地址为0的函数应该如何调用呢?


这样?:(* 0)();


上式并不能生效,因为运算符必须要一个指针来做操作数。


而且这个指针还应该是一个函数指针,这样经过运算符作用后的结果才能能作为函数被调用。


所以必须要对上式的0进行类型转换。


转换的发现我们可以描述为:指向函数值为void,参数为void的函数的指针。


也就是这个0必须转换为函数指针,而这个函数指针指向的函数参数为void,返回值也为void。


那该如何转换呢?


1.2类型转换


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


float (*pf)();


pf表示的是一个指向函数指针,指向的是函数参数是void,返回值为float。


也就是指向返回值为浮点类型的函数的指针。


而float (*)()去掉变量名与分号


再加上一个括号( float (*)() )


这就表示一个“指向返回值为浮点类型的函数的指针”的类型转换符。


所以我们如果将0类型转换为“一个指向返回值为void的函数的指针”类型,就首先要知道一个该类型是如何声明。


如果pf是一个指向返回值为void类型的函数指针,那么(*pf)()的值为void,pf的声明如下:


void (*pf)();


所以该类型的类型转换符为:


( void (*)() )


所以将0强制类型转换为“指向返回值为void的函数指针”则为如下表示


( void (*)() )0;


而对于该函数指针要是调用该函数指针所指向的函数的话,应该如下表示:


(*( void (*)() )0)();


所以该表达式表达的也就是,调用一个函数,该函数的首地址为0,返回值为0,参数也为0.


1.3规则:


按照使用的方式来声明


Ⅱ. 运算符的优先级问题


2.1不同类型的运算符优先级问题


运算符优先级有那么多,记住它们并不是一件容易的事


1304cf40ba88434db1459f0ad54580c1.png


所有以我们应该对它们进行恰当的分组,理解各组运算符之间的相对优先级。这样记忆起来就很快了。


优先级最高:() [ ] .


优先级最高的其实并不是真正意义上的运算符,包括:数组下标,函数调用操作符,结构体成员访问操作符。它们都是从左向右结合的。所以a.b.c的含义是(a.b).c


第二高:单目


单目运算符的优先级仅次于前述运算符。


在所有真正意义上的运算符中,它们的优先级最高。因为函数调用的优先级要高于单目运算符的优先级。


类型转换()也是单目运算符,它的优先级和其他单目运算符的优先级一样。单目运算符是自右向左结合,因此*p++会被编译器解释为 *(p++),即p的地址+1,而不是p指向的对象+1


第三高:双目


优先级比单目运算符要低的,接下来就是双目运算符。双目运算符中,算术运算符的优先级最高,移位运算符次之,关系运算符再次之,接着解释逻辑运算符,赋值运算符,最后是条件运算符。


我们需要记住的两点就是


1.任何一个逻辑运算符的优先级低于任何一个关系运算符

2.移位运算符的优先级比算术运算符要低,但是比关系运算符高。

算术>移位>关系>逻辑>赋值>条件


2.2同类运算符之间相对优先级问题


属于同一类型的各个运算符之间的相对优先级,理解起来一般没有什么困难。但是,6个关系运算符的优先级并不相同。


1.运算符==和!=的优先级要低于其他关系运算符的优先级。

因此我们如果要比较a与b的相对大小顺序是否和c与d的相对顺序一样,就可以这样写:


a<b==c<d


2.任何两个逻辑运算符都具有不同的优先级。所有的按位运算符优先级要比顺序运算符的优先级高,每个"与"运算符要比对应的"或"运算符优先级高,而按位异或(^运算符)的优先级介于按位与运算符和按位或运算符之间。


3.在所有的运算符中,三目条件运算符优先级最低。这就可以在条件运算符的条件表达式中包含关系运算符的逻辑组合,因为先处理的是关系运算符,最后再处理三目条件逻辑符。


4.所有的赋值运算符的优先级是一样的,而且它们的结合方式是从右到左。


所以
a=b=0;
b=0;
a=b;
表达的意思是一样的。


5.在所有的运算符中,逗号运算符的优先级最低。


Ⅲ. 函数调用


C语言要求:在函数调用的时候,即使函数不带参数,也要将函数参数列表括号写下来。因此,如果 f 是一个函数,f();则表示一个函数调用语句,而f;却是一个什么都不做的语句,这个语句虽然计算函数f的地址,但不调用该函数。


Ⅳ. 注意作为语句结束标志的分号


在C语言中如果不小心多写了一个分号可能不会造成什么不良后果:

1.这个分号可能会被视为一个不会产生任何实际效果的空语句

2.或者编译器会因为这个分号,产生警告信息,根据信息去掉这个分号。


但也会有例外发现,有时会造成很大的差别:


1.如果在if或者while语句之后多了一个分号,那么原来紧跟在if或者while之后的语句就是一个单独的语句,与条件判断部分没有任何关系了。


2.当不是多写了一个分号,而是遗漏一个分号,同样会招致麻烦,比如return 语句后面的分号忘记写了,则会将return 后面的语句作为操作数,进行返回。


3.当一个声明的结尾紧跟一个函数定义时,如果声明结尾的分号被省略,编译器可能会把 声明的类型视为函数的返回值类型。


Ⅴ. “悬挂”else引发的问题


C语言中规定:else始终与同一对括号内最近的未匹配的if结合。


也就是就近原则,它会和离它最近的if相结合。当然这必须在同一个括号里。如果在不同的括号里,那么就不遵循了。


int main()
{
  int a = 0, y = 1;
  if (a == 0)
    if (y == 0)
      printf("正确\n");
  else
    {
      printf("错误\n");
    }
  return 0;
}


比如第一个if里面的判断条件为a是否等于0


该代码的本意是else为a不为0时进行的代码,但真正的是else与第二个if相匹配了,也就是else里的判断条件变成了y不为0时进行的代码。


如果要得到原来的例子中编程者本意的结果,应该这样写:


int main()
{
  int a = 0, y = 1;
  if (a == 0)
  {
    if (y == 0)
      printf("正确\n");
  }
  else
    {
      printf("错误\n");
    }
  return 0;
}


现在,else与第一个if结合,即使它离第二个if更近也不会改变,因为此时第二个if已经被括号“封装”起来了。


Ⅵ. switch语句


switch语句的特点就是包含break;当遇到break,语句立刻结束。


C语言中switch语句的这种特性,既是它的优势,也是它的一大弱点。说它是弱点,是因为程序员很容易遗漏各个case部分的break语句,造成一些难以理解的程序行为。


说它是优势,是因为如果程序员有意的省略一个break语句,就可以表达出一些采用其他方式很难方便地加以实现的程序控制结构。


特别是对于一些大的swtich,我们经常发现各个分支的处理大同小异:对于某个分支情况的处理只要稍作修改,或者不修改,就也符合程序的要求。


比如这样的一段代码,它的作用是查找符号时跳过程序中的空白字符,在这里,空格键,制表符,换行符的处理都是相同的,除了遇到换行符时程序的代码行计数器需要进行递增。其他都是一样,所以我们可以省略break,程序照样可以运行甚至更好。


case '\n':
    linecount++;
    //该处省略break语句
  case '\t':
  case ' ':
相关文章
|
存储 自然语言处理 编译器
C陷阱与缺陷
C陷阱与缺陷
64 0
C陷阱与缺陷
|
6月前
|
测试技术
常见测试陷阱
常见测试陷阱
|
6月前
|
存储 程序员 编译器
C陷阱与缺陷:语法陷阱
C陷阱与缺陷:语法陷阱
55 0
|
6月前
|
自然语言处理 编译器 程序员
C陷阱与缺陷:词法陷阱
C陷阱与缺陷:词法陷阱
51 0
|
编译器 C语言
《C陷阱与缺陷》之“语义”陷阱——数组越界导致的程序死循环问题
《C陷阱与缺陷》之“语义”陷阱——数组越界导致的程序死循环问题
141 0
|
自然语言处理 编译器 程序员
《C陷阱与缺陷》----词法“陷阱”
由于在C语言中赋值操作相对于比较出现更加频繁,所以将字符较少的符号=赋予更常用的含义—赋值操作。
106 0
|
存储 人工智能 自然语言处理
【C缺陷与陷阱】----语义“陷阱”
那获得该下标为0的元素的指针,如果给这个指针加1,就能得到指向该数组中下一个元素的指针。也就是指针+一个整数得到的还是指针,只不过指针的位置发生改变
106 0
|
自然语言处理 编译器 C语言
|
自然语言处理 算法 编译器
|
Kubernetes Shell 网络安全
详解K8s安装及“陷阱”
详解K8s安装及“陷阱”
287 0
详解K8s安装及“陷阱”