C语言的词法陷阱

简介: C语言的词法陷阱



前言

       要理解一个C程序,仅仅理解组成该程序的符号,程序员还必须理解这些符号如何组成声明、表达式、语句和程序的。虽然这些组合方式的定义都很完善,几乎无懈可击,但有时这些定义与人们的直觉相悖,或者容易引起混淆。本篇将讨论一些用法和意义与我们想当然的认识不一致的语法结构。

1、理解函数声明

      任何一个C变量的声明都由两部分组成:类型一组类似表达式的声明符。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果.

最简单的声明符

最简单的声明符就是单个变量:

float f,g;

含义:当对其求值时,表达式f和g的类型都是浮点数类型

因为声明符与表达式相似,所以我们也可以在声明符中使用一些有意义的括号:

float ((f));

含义:当对其求值时,((f))的类型为浮点型,即f的类型为浮点型

函数和指针类型

float ff();

含义:表达式ff()的求值结果是一个浮点数,即ff是一个返回值为浮点类型的函数

float *pf

含义: *pf是一个浮点数,即pf是一个指向浮点数的指针

其它组合

以上这些声明形式还可以组合起来,就像在表达式中进行组合一样:

float *g();

解释:*g()与(*h)()都是浮点表达式,因为()的结合优先级高于*,*g()等价于*(g()) ,g是一个函数,该函数的返回类型为指向浮点数的指针;

Void (* signal(int ,void(*)(int) ) (int)

解释:signal是一个函数名,(int , void(*)(int))是signal函数的两个参数,一个是int型,另一个是函数指针类型,该函数指针类型指向的函数参数是int型,返回类型是void,signal(int,void(*)(int))就相当于对signal函数的声明,如果我们把这个函数声明的整体假设为m,那么这行代码就会变成void(* m)(int),*m就是一个新的函数指针,该函数指针指向的函数参数是int型,返回的类型为void型。

typedef简化

typedef可以简化上面的函数声明:

//未简化版本
Void (* signal(int ,void(*)(int) ) (int)
typedef void (*HANDLER)(int);
//简化版本
HANDLER signal (int,HANDLER);

解释:void (*)(int) 定义为 HANDLER 类型,这是因为void(*)(int)是一个无主的函数指针类型我们现在将它的主人规定为HANDLER,同样的将原来式子变为Void (* signal(int ,HANDLER ) (int)后我们可以将signal(int ,HANDLER )看作一个声明符k,那么这时候就变成了void (*k)(int),所以此时k的类型就是void (*)(int)也就是HANDLER,即HADNLER k,最后将k替换为原式子即可

2、运算符优先级

C语言运算符优先级表

运算符 结合性
()、[ ]、->、. 自左向右
!、~、++、-、(类型)、*、&、sizeof 自右向左
*、/、% 自左向右
+、- 自左向右
<<、>> 自左向右
>、>=、<、<= 自左向右
==、!= 自左向右
& 自左向右
^ 自左向右
| 自左向右
&& 自左向右
|| 自左向右
?: 自右向左
assignments 自右向左
=、/=、*=、%=、+=、-=、<<=、>>=、&=、^=、|= 自右向左
, 自左向右

如果想要查看更详细的优先级表格请查看:C语言进制转换、操作符万字详解

补充内容

1、优先级最高者其实并不是真正意义上的运算符,包括数组下标、函数调用操作符、各结构体成员选择操作符。它们都是自左向右结合,因此a.b.c的含义是(a.b).c,而不是a.(b.c)

2、单目运算符的优先级仅次于前述运算符,在所有真正意义上的运算符中,它们的优先级最高

3、类型转换也是单目运算符,它的优先级和其它单目运算符的优先级一样,单目运算符是自右向左结合,因此*p++会被编译器解释称*(p++),即取指针p所指向的对象,然后将p递增1;而不是(*p)++,即取指针p所指向的对象,然后将该对象递增1

4、运算符优先级:单目运算符>算术运算符>移位运算符>关系运算符>逻辑运算符>赋值运算符>条件运算符(三目运算符)

5、最需要注意的是它们的优先级,算术运算符>移位运算符>关系运算符>逻辑运算符

6、同一类型的各个运算符之间的相对优先级相同,但是6个关系运算符的优先级并不完全相同,关系运算符==和!=的优先级要低于其它关系运算符

7、任何两个逻辑运算符都具有不同的优先级,所有的按位与运算符优先级要高于顺序运算符,任何的与运算符要比或运算符优先级高,按位异或运算符的优先级小于按位与运算符大于按位或运算符

8、赋值运算符的优先级都一样,结合方式均为自右向左,涉及赋值运算符时经常会引起优先级的混淆,比如我们想要利用循环语句复制一个文件到另一个文件时,写出来这样的语句:

while(c=getc(in) != EOF)
    putc(c,out);

       c似乎时先被赋予getc(in)的返回值,然后与EOF比较是否到达文件结尾以便决定是否终止循环,然而由于赋值运算符的优先级要低于任何一个比较运算符,因此c的值实际上是函数getc(in)的返回值与EOF比较的结果,此处函数getc(in)的返回值只是一个临时变量,在与EOF比较后就被“丢弃”了,因此,最后得到的文件“副本”中只包括了一组二进制值为1的字节流,故实际上代码应该写成:

while((c=getc(in)) != EOF)
    putc(c,out);

如果表达式再复杂一点,这类错误就很难被察觉,例如:

if( (t = BTYPE(pt1->aty) == STRTY)  || t == UNIONTY)

这段代码的本意是首先赋值给t,然后判断t是否等于STYPR或者UNIONTY,但是实际结果却大相径庭:根据BTYPE(pt1->aty)的值是否等于STRTY,t的取值或者为1或者为0;如果t取值为0,还将进一步与UNIONTY比较

3、作为语句结束标志的分号

多写分号

       在C程序中,如果不小心多写了一个分号,可能不会造成什么不良后果,这个分号可能会被视为一个不会产生任何实际效果的空语句;或者编译器会因为这个多余的符号报错,根据报错信息很容易就能去掉这个分号,但是在if或while语句之后仅跟一条语句时,如果此时多了一个分号,那么原来紧跟在if与while之后的语句就是一条单独的语句,与条件判断部分没有了任何关系:

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

它相当于:

if (x[i] > big) {}
    big = x[i];

漏写分号

如果不是多写一个分号,而是遗漏了一个分号,同样会引起不必要的麻烦:

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

这段代码可以正常运行,只不过将logrec.date = x[0];当作了return语句的操作数,即:

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

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

分号与函数声明

当一个声明的结尾紧跟一个函数定义时,有分号与没分号的实际效果相差极为不同,如果声明结尾的分号被省略,编译器可能会把声明的类型视作函数的返回值类型:

struct logrec{
        int date;
        int time;
        int code;
        ...
}
main()
{
    ...
}

在第一个}与main之间遗漏了一个分号,因此,上面代码段的实际效果是声明函数main的返回值是struct logrec类型 ,这样会产生报错,同时需要注意的是根据 C 语言标准,如果 main() 函数没有显式指定返回值或者使用了不匹配的返回类型,那么编译器会默认将其视为具有 int 返回类型

4、switch语句

case语句是switch语句的优势所在,但也是它的一大弱点

       说是它的一大弱点,是因为程序员很容易遗漏各个case部分的break语句,造成一些难以理解的程序行为,说它是优势所在,是因为如果程序员有意去忽略一个break语句,则可以表达出一些采用其他方式很难方便地加以实现地程序控制结构。特别是对于一些大地switch语句,我们常常会发现各个分支地处理大同小异:对某个分支情况的处理只要稍微改动,剩余部分就完全等同于另一个分支情况下地处理。

       考虑这样一段代码,它的作用是一个编译器在查找符号时跳过程序中的空白字符空格键、制表符和换行符的处理都是相同的,不过在遇到换行符时,程序的代码行计数器需要进行递增:

case ‘\n’:
       linecount++;
        /*这里没有break*/
case ‘\t’://不执行任何具体语句,只需要在遇到换行符时linecount++
case ‘\t’://不执行任何具体语句,只需要在遇到换行符时linecount++
       ...

5、函数调用

       与其它程序设计语言不同,C语言要求:在函数调用时,即使函数不带参数,也应该包括参数列表。因此,如果f是一个函数,那么:

f()

是一个函数调用语句,而:

f;

却是一个什么都不做的语句,确切的说,这个语句计算函数f的地址,但并不调用该函数......

6、“悬挂”else引发的问题

问题产生原因:else会与离其最近的if语句匹配

看这样一段代码:

//当if个数大于else个数时就会出现悬空else问题,具体情况如下:
//代码运行结果为空
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if(a == 1)
    if(b == 2)
        printf("hehe\n");   
else
    printf("haha\n");
return 0;
}

这段代码的实际情况是:

//代码运行结果为空
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if(a == 1)   //a != 1,表达式结果为假内部if...else...语句不执行
 {
    if(b == 2)
        printf("hehe\n");   
    else
        printf("haha\n");
 }
return 0;
}

若想要else与第一个if匹配只需要这样改:

//代码运行结果为空
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if(a == 1)   //a != 1,表达式结果为假内部if...else...语句不执行
 {
    if(b == 2)
        printf("hehe\n");   
    else
        printf("haha\n");
 }
return 0;
}

结论:if语句中对于{}的运用是十分重要的,可以增强代码的可读性以及减少问题的发生

~over~

相关文章
|
自然语言处理 编译器 C语言
C语言编程陷阱:词法陷阱
推荐一个零声学院免费教程,个人觉得老师讲得不错, 服务器课程
46 0
|
编译器 C语言
03 C语言 - 程序结构
03 C语言 - 程序结构
31 0
|
16天前
|
存储 编译器 C语言
【C语言】C语言的变量和声明系统性讲解
在C语言中,声明和定义是两个关键概念,分别用于告知编译器变量或函数的存在(声明)和实际创建及分配内存(定义)。声明可以多次出现,而定义只能有一次。声明通常位于头文件中,定义则在源文件中。通过合理组织头文件和源文件,可以提高代码的模块化和可维护性。示例包括全局变量、局部变量、函数、结构体、联合体、数组、字符串、枚举和指针的声明与定义。
44 12
|
7月前
|
C语言
C语言最基本程序控制结构
C语言最基本程序控制结构
61 0
|
4月前
|
存储 C语言 容器
C语言中的变量作用域
C语言中的变量作用域
|
7月前
|
程序员 C语言
C语言中的条件语句技术详解
C语言中的条件语句技术详解
161 2
|
7月前
|
程序员 C语言
C语言中的控制结构
C语言中的控制结构
192 1
|
6月前
|
C语言
|
7月前
|
存储 C语言
C语言中的switch语句编程
C语言中的switch语句编程
218 0
|
7月前
|
编译器 数据处理 C语言
C语言运算符的深入探究
C语言运算符的深入探究
40 0