目录

✊ 一、理解函数声明

☝️ 二、运算符的优先级问题

✌️ 三、注意作为语句结束标志的分号


✊ 一、理解函数声明

先来看下面这条语句:

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

这样的表达式有没有让你感到不寒而栗呢?其实大可不必,因为构造这类表达式其实只有一条简单的规则:按照使用的方法来声明。

任何C变量的声明都由两部分组成:类型以及一组类似表达式的声明符(declarator)。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。最简单的声明符就是单个变量,例如:

float f, g;

这个声明的含义是:当对其求值时,表达式f和g的类型为浮点数类型(float)因为声明符与表达式相似,所以我们也可以在声明符中任意使用括号:

float ((f));

这个声明的含义是:当对其求值时,((f))的类型为浮点类型,由此可以推知,f也是浮点类型。同样的逻辑也适用于函数和指针类型的声明,例如:

float ff();

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

float *pf;

这个声明的含义是:*pf是一个浮点数,也就是说,pf是一个指向浮点数的指针。以上这些形式在声明中还可以组合起来,就像在表达式中进行组合一样。因此,

float *g(), (*h) ();

表示*g()与(*h)()是浮点表达式。因为()结合优先级高于*,*g()也就是*(g()):g是一个函数,该函数的返回值类型为指向浮点数的指针。同理,可以得出h是一个函数指针,h所指向函数的返回值为浮点类型

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

float (*h) ();

表示h是一个指向返回值为浮点类型的函数的指针,因此,

(flaot (*) ())

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

有了这些预备知识,我们现在可以分两步来分析表达式 (*(void(*)())0)()。

第一步,假定变量fp是一个函数指针,那么如何调用fp所指向的函数呢?调用方法如下:

(*fp) ();

因为是 fp 一个函数指针,那么*fp 就是该指针所指向的函数,所以(*fp)()就是调用该函数的方式。ANSI C标准允许程序员将上式简写为 fp() ,但是一定要记住这种写法只是一种简写形式。

在表达式(*fp)()中,*fp两侧的括号非常重要,因为函数运算符()的优先级高于单目运算符*,如果 *fp 两侧没有括号,那么 *fp() 实际上与 *(fp()) 的含义完全一致,ANSIC把它作为 *((*fp)()) 的简写形式。 7简得

现在,剩下的问题就只是找到一个恰当的表达式来替换 fp。我们将在分析的第二步来解决这个问题。如果C编译器能够理解我们大脑中对于类型的认识,那么我们可以这样写:

(*0) ();

上式并不能生效,因为运算符*必须用一个指针来作为操作数。不仅如此,这个指针还应该是一个函数指针,这样经运算符*作用后的结果才能作为函数被调用。因此,在上式中必须对0作类型转换,转换后的类型可以大致描述为“指回返回值为void类型的函数的指针”。

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

void (*fp) ();

因此,我们可以用下式来调用存储位置为0的子例程

void (*fp) ();
(*fp) ();

译注:此处作者假设fp默认初始化为0,这种写法不宜提倡

这种写法的代价是多声明了一个“哑”变量。

我们一旦知道如何声明一个变量,自然也就知道如何对一个常数进行类型转换,将其类型转换为该变量的类型:只需要在变量声明中将变量名去掉即可。因此,将常熟0转型为“指向返回值为 void的函数的指针”类型,可以这样写:

(void (*) () ) 0

因此,我们可以用 (void(*)())0 来替换 fp ,从而得到:

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

末尾的分号使得表达式成为了一个语句。

在我当初解决这个问题的时候,C 语言中还没有 typedef 声明。尽管不用 typedef 来解决这个问题对剖析本例的细节而言是一种很好的方式,但无疑使用 typedef 能够使表述更加清晰:

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

这个棘手的例子并不是孤立的,还有一些C程序员经常遇到的问题,实际上和这个例子是同一个类型的。例如,考虑 signal 库函数,在包括该函数的C编译器实现中,signal 函数接受两个参数:一个是代表需要“被捕获”的特定 signal 的整数值;另一个是指向用户提供的函数的指针。该函数用于处理“捕获到”的特定 signal ,返回值类型为 void 。我们将会在后面的详细讨论该函数。

一般情况下,程序员并不主动声明signal函数,而是直接使用系统头文件 signal.h 中的声明。那么,在头文件 signal.h 中,signal 函数是如何声明的呢?

首先,让我们从用户定义的信号处理函数开始考虑,这无疑是最容易解决的。该函数可以定义如下:

void sigfunc(int n)
{/*特定信号处理部分*/
}

函数 sigfunc 的参数是一个代表特定信号的整数值,此时我们暂时忽略它。

上面假设的函数体定义了 sigfunc 函数,因而 sigfunc 函数的声明可以如下:

void sigfunc(int);

现在假定我们希望声明一个指向 sigfunc 函数的指针变量,不妨命名为 sfp 。因为 sfp 指向 sigfunc函数,则 *sfp 就代表了 sigfunc 函数,所以 *sfp 可以被调用。又假定 sig 是一个整数。则 (*sfp)(sig) 的值为 void 类型,因此我们可以如下声明:

void (*signal(something)(int));

此处的 something 代表了 signal 函数的参数类型,我们还需要进一步了解如何声明它们。上面声明可以这样理解:传递适当的参数以调用 signal 函数,对 signal 函数返回值(为函数指针类型)解除引用 (dereference),然后传递一个整型参数调用解除引用后所得函数,最后返回值为 void 类型。因此,signal 函数的返回值是一个指向返回值为 void 类型的函数的指针。

那么,signal 函数的参数又是如何呢?signal 函数接受两个参数:一个整型的信号编号,以及一个指向用户定义的信号处理函数的指针。我们此前已经定义了指向用户定义的信号处理函数的指针 sfp :

void (*sfp)(int);

sfp 的类型可以通过将上面声明中的 sfp 去掉而得到,即 void(*)(int) 。此外,signal 函数的返回值是一个指向调用前的用户定义信号处理函数的指针,这个指针的类型与 sfp 指针类型一致。因此,我们可以如下声明 signal 函数:

void (*signal(int, void(*)(int))) (int);

同样地,使用 typedef 可以简化上面的函数声明:

typedef void (*HANDLER) (int);
HANDLER signal(int,HANDLER);

☝️ 二、运算符的优先级问题

假设存在一个已定义的常量 FLAG ,它是一个整数,且改整数的二进制表示中只有某一位是1,其与各位均为0,亦即该整数是2的某次幂。如果对于整型变量 flags ,我们需要判断它在常量 FLAG 为1的那一位上是否同样为1,通常可以这样写:

if (flags & FLAG)...

上式的含义是判断 flags 按位与 FLAG 这个表达式的结果是否为0,考虑到可读性,如果对表达式的值是否为0的判断能够显示的加以说明,无疑使得代码自身就起到了注释该段代码意图的作用,其写法如下:

if (flags & FLAG != 0)...

这是一个错误的语句,因为 != 运算符的优先级要高于 & 运算符,所以上式的结果被解释为:

if (flags & (FLAG != 0) )...

这个表达式含义是先判断 FLAG 的值是不是等于0,再去执行 & 运算。当 FLAG 的值等于0时,0不等于0表达式为假跳出,除了 FLAG 恰好为真的时候,FLAG 为其他数时这个表达是都是错误的。

⚠️ 注意:& 是按位(二进制位)与的意思。

例如:

#include<stdio.h>int main()
{int a = 3;//a的二进制序列:00000000000000000000000000000011int b = 5;//b的二进制序列:00000000000000000000000000000101int c = a & b;//c的二进制序列:00000000000000000000000000000001//将c的二进制序列转为十进制就是1printf("%d\n", c);return 0;
}

按位与 (&) 的作用是:二进制位只要有0,按位与出来的结果就是0。

看下面这个例子:

 r = hi << 4 + low;

该表达式的本意是将 hi 的二进制位向左移动4位再加上 low 的值,但是很遗憾,这样写是错误的。因为加法运算的优先级要比移位运算的优先级高,因此本例实际相当于:

 r = hi << (4 + low);

对于这种情况,有两种更正方法:第一种方法是加括号; 第二种方法意识到问题出在程序员混淆了算术运算与逻辑运算,于是将原来的加号改为按位逻辑或, 但这种方法牵涉到的移位运算与逻辑运算的相对优先级就更加不是那么明显。两种方法如下:

r = (hi << 4) + low;//法1:加括号r = ji << 4 | low;//法2:将原来的加号改为按位逻辑或

用添加括号的方法虽然可以完全避免这类问题,但是表达式中有了太多的括号反而不容易被理解。因此,记住C语言中运算符的优先级是有益的。

⚠️注意:<< 是左移操作符,移动的的是二进制序列。

例如:

#include<stdio.h>int main()
{int a = 1;//a的二进制序列:00000000000000000000000000000001int b = a << 1;//b的二进制序列:00000000000000000000000000000010//是将a的二进制序列左移后得到的printf("b = %d\n", b);printf("a = %d\n", a);return 0;
}

下表是C语言运算符优先级表。

如果把这些运算符恰当分组,并且理解了各组运算符之间的相对优先级,那么这张表其实不难记住。

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

单目运算符的优先级仅次于前述运算符。在所有真正意义上的运算符中,它们的优先级最高。因为函数调用的优先级要高于单目运算符的优先级,所以如果 p 是一个函数指针,要调用 p 所指向的函数,必须这样写:(*p)() 。如果写成 *p() ,编译器会解释成 *(p()) 。类型转换也是单目运算符,它的优先级和其他单目运算符的优先级一样。单目运算符是自右向左结合。因此 *p++ 上会被编译器解释成 *(p++) ,即取指针p所指向的对象,然后将 p 递增1;而不是 (*p)++ , 即取指针 p 所指向的对象,然后将该对象递增1。 后面的内容还会进一步指出 p++ 的含义有时会出人意料。

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

译注:原书如此,条件运算符实际应为三目运算符。

⚠️注意:

我们需要记住的最重要的两点是:

1.任何一个逻辑运算符的优先级低于任何一个关系运算符;
2.移位运算符的优先级比算术运算符要低,但是比关系运算符要高。

属于同一类型的各个运算符之间的相对优先级,理解起来一般没有什么困难。乘法、除法和求余优先级相同,加法、减法的优先级相同,两个移位运算符的先级也相同。   1/2*a 的含义是 (1/2)*a ,而不是 1/(2*a) ,这一点也许会让某些人吃惊其实在这方面C语言与Fortran语言、Pascal语言以及其他程序设计语言之间的为表现并无差别。

但是,6个关系运算符的优先级并不相同,这一点或许让人感到有些吃惊。运算符 ==!= 的优先级要低于其他关系运算符的优先级。因此,如果我们要比a与b的相对大小顺序是否和c与d的相对大小顺序一样,就可以这样写:

a < b == c < d

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

⚠️注意:按位异或运算符(^运算符)是将二进制位按位异或

例如:

#include<stdio.h>int main()
{int a = 3;int b = 5;int c = a ^ b;//写出a和b的二进制序列//a:00000000000000000000000000000011//b:00000000000000000000000000000101//对应的二进制位相同为0,相异为1//c:00000000000000000000000000000110//转化为十进制是6printf("%d\n", c);return 0;
}

这些运算符的优先顺序是由于历史原因形成的。B语言是C语言的“祖先”,B语言中的逻辑运算符大致相当于C语言中的 &| 运算符虽然这些运算符从定义上而言是按位操作的,但是当它们出现在条件语句的上下文中时,B语言的编译器会将它们作为相当于现在C语言中的 &&|| 运算符来处理。而到了C语言中,这两种不同的用法被区分开来,丛兼容性的角度来考虑,如果对它们优先顺序的改变过大,将是一件危险的事。

例如:

B语言中的 if (a > b & a > c)... 相当于C语言中的 if (a > b && a > c)... 。

在本节到现在为止提及的所有运算符中,三条件运算符的优先级最低。这就允许我们在三目条件运算符的条件表达式中包括关系运算符的逻辑组合,例如:

tax_rate = income > 4000 && residency < 5 ? 3.5 : 2.0;

当前语句中的优先级 && 最高 ,其次是条件运算符,最低是 = 运算符。因此,当前语句的含义是,如果income 大于4000并且residency 小于5,就将3.5赋给tax_rate,否则就将2.0赋给tax_rate。

本例其实还说明了赋值运算符的优先级低于条件运算符的优先级是具有意义的。此外,所有赋值运算符的优先级是一样的,而且它们的结合方式是自右向左,因此,

home_score = visitor_score = 0;

当前语句的含义是,将0赋给 visitor_score ,再将 visitor_score 的值赋给 home_score

与下面这两条语句所表达的意思是相同的:

visitor_score = 0;
home_score = visitor_score;

例如:

a = b = 10; 相当于 b = 10; a = b;

在所有的运算符中,逗号运算符的优先级最低。这一点很容易记住,因为在需要一个表达式而不是一条语句时,经常使用逗号运算符来替换作为语句结束标志的分号。逗号运算符在宏定义中特别有用,这一点在后面的内容还会进一步讨论。

在涉及赋值运算符时,经常会引起优先级的混淆。考虑下面这个例子,例子中循环语句的本意是复制一个文件到另一个文件

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

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

上例实际应该写成:

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

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

例如,第4章首提及的 lint 程序的一个版本,在发布时包括了下面一行错误代码:

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

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

✌️ 三、注意作为语句结束标志的分号

在C程序中,如果不小心多写了一个分号,可能不会造成什么不良后果:这个分号也许会被视作一个不会产生任何实际效果的空语句;或者编译器会因为这个多余的分号而产生一条警告信息,根据警告信息的提示能够很容易去掉这个分号。一种重要的例外情形是在 if 或者 while 语句之后需要紧跟一条语句时,如果此时多了一个分号,那么原来紧跟在 if 或者 while 子句之后的语句就是一条单独的语句,与条件判断部分没有了任何关系。考虑下面的这个例子:

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

编译器会正常的接受第一行代码中的分号而不会提示任何警告信息,因此编译器对这段程序代码的处理与对下面这段代码的处理就大不相同:

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

前面第一个例子(即在 if 之后多加了一个分号的例子)时间上相当于

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

当然,也就等于(除非 x 、i 或者big 是有副作用的宏)

 big = x[i];

如果不是多写了一个分号,而是遗漏了一个分号,同样会招致麻烦,例如:

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

此时的return语句后面遗漏了一个分号,然而这段程序代码仍然会顺利通过编译而不会抱错,只是将语句

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

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

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

在第一个 } 与紧随其后的函数 main 定义之间,遗漏了一个分号。因此,上面代码段的实际效果是声明函数 main 的返回值是 struct logrec 类型。写成下面这样,会看得更清楚:

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

如果分号没有被省略,函数 main 的返回值类型会缺省定义为 int 类型。

在函数 main 中,如果本应返回一个 int 类型数值,却声明返回一个 struct logrec 类型的结构,会产生怎样的效果呢?我们把它留作本章结尾的一个练习。虽然刻意地往消极面去联想也许有些“病态”,但对于要考虑到各种意外情形的程序设计来说 (比如航空航天或医疗仪器的控制程序),却是不无裨益的。

《C语言陷阱与缺陷》第二章【语法陷阱】上相关推荐

  1. 《C陷阱与缺陷》----第二章 语法陷阱

    第二章 语法陷阱 2.1 理解函数声明 2.1.1 如何理解函数声明 2.1.2 举例理解声明 2.1.2.1 例子1 2.1.2.2 例子2 2.2 运算符的优先级 2.2.1 常见错例 2.2.1 ...

  2. 基于python的界面自动化测试-基于Python语言的自动化测试实战第二章(上)

    原标题:基于Python语言的自动化测试实战第二章(上) 测试环境搭建 2.1 Windows 下的环境搭建 如果想要学习一门编程语言,对于新手来说只需到其官方网站上去下载最新版本安装即可,但对于想要 ...

  3. python语言程序设计2019版第二章课后答案-python语言程序设计基础课后答案第二章...

    python语言程序设计基础课后答案第二章 以下合法的用户自定义标识符是____________. 导入模块或者模块中的元素要使用关键字________ . 下列哪个函数是用来控制画笔的尺寸的____ ...

  4. python语言程序设计2019版第二章课后答案-python语言程序设计基础(嵩天)第二章课后习题...

    **第二学期第一周学习总结 一. 本周学习内容总结 一维数组,了解了一维数组的定义(定义一个数组,需要明确数组变量名,数组元素的类型和数组大小,即数组中元素的数量) 一维数组定义的一般形式为:类型名, ...

  5. python数据结构题目_《数据结构与算法Python语言描述》习题第二章第三题(python版)...

    ADT Rational: #定义有理数的抽象数据类型 Rational(self, int num, int den) #构造有理数num/den +(self, Rational r2) #求出本 ...

  6. Java语言程序设计(基础篇) 第二章

    第二章 基本程序设计 2.2 编写简单的程序 1.变量名尽量选择描述性的名字(descriptive name). 2.实数(即带小数点的数字)在计算机中使用一种浮点的方法来表示.因此,实数也称为浮点 ...

  7. 关于c语言的基本知识,第二章_关于C语言的基本知识.ppt

    第二章_关于C语言的基本知识.ppt 函数 函数说明 例2.3 分析下面的运行结果. main() { printf("\"123\"\\\"456\" ...

  8. C语言程序设计教程_第二章:程序设计起步_笔记整理

    第二章 程序设计起步[

  9. C陷阱与缺陷代码分析之第2章语法陷阱

    作者:刘昊昱 博客:http://blog.csdn.net/liuhaoyutz 陷阱1 理解函数声明 作者提出一个问题:有一个首地址为0的函数,该函数返回值类型为void,没有参数.怎样用C语言的 ...

  10. C陷阱与缺陷 第2章 语法“陷阱” 2.6 “悬挂”else引发的问题

    "悬挂"else引发的问题     if (x == 0)      if (y == 0) error();     else {         z = x + y;     ...

最新文章

  1. Unit05: 创建和访问数组 、 数组的常用方法_1
  2. 开机后台运行jupyter_手机重启=关机再开机?原来差别竟这么大,很多人都不知道!...
  3. windows 2008 R2下安装Exchange 2010(单域环境下)
  4. 其中一个页签慢_VBA实战技巧15:创建索引页
  5. 数据仓库事实表分类[转]
  6. Python编程系列教程第13讲——隐藏数据和封装
  7. AES和RSA前后端加解密
  8. Linux 下杀毒软件 CPU 占用率为何持续升高
  9. 7.25 8figting!
  10. 本地通过Eclipse链接Hadoop操作Mysql数据库问题小结
  11. 图像旋转(任意角度)matlab
  12. 光模块基础知识【快速入门】02
  13. 2021年了,对话系统凉透了吗?
  14. 英语中提醒注意安全句子
  15. 崩坏3新版本服务器维护多久,崩坏3 3月14日版本更新维护通知
  16. 微信小程序跳转公众号图文内容
  17. 派工单系统 源码_青鸟报修云酒店设备报修管理系统
  18. Linux 磁盘动态扩容 PVM(转载)
  19. 如何学习理财知识,零基础怎么学习理财
  20. DOM是什么意思-前端入门

热门文章

  1. eclipse 安装反编译工具
  2. 华为荣耀计算机历史记录,华为荣耀手机怎么将文件导出到电脑
  3. 三极管常用电路_三极管偏置电路
  4. 【KALI网络安全】DNS攻击(劫持和欺骗)与网络钓鱼的模拟和预防(1)
  5. 让Gmail自动转发邮件到多个邮箱
  6. pt 软件安装及pt-kill 用法
  7. 学成在线首页pnf素材
  8. html各种遍历,Jq遍历、HTML操作(示例代码)
  9. opencv证件照背景替换
  10. 09中国IC老杳榜5:2010年八大IPO