专栏首页微光点亮星辰校长讲堂第五讲

校长讲堂第五讲

句法缺陷

要理解 C 语言程序,仅了解构成它的关键字是不够的。还要理解这些关键字是如何构成声明、表达式、语句和程序的。尽管我们可以很清楚的找到这些关键字的定义以及用法,但这些定义有时候是有悖于直觉的。 在这一节中,我们将着眼于一些不明显句法构造。

2。1 理解函数声明

我曾经和一些人聊过天,他们那时在书写在一个小型的微处理器上单机运行的 C 程序。当这台机器的开关打开的时候,硬件会调用地址为 0 处的子序。

为了模仿电源打开的情形,我们要设计一条 C 语句来显式地调用这个子程序。经过一些思考,我们写出了下面的语句:

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

这样的表达式会令 C 程序员心惊胆战。但是,并不需要这样,因为他们可以在一个简单的规则的帮助下很容易地构造它:以你使用的方式声明它。

每个 C 变量声明都具有两个部分:一个类型和一组对该类型求值的特定表达式。最简单的表达式就是一个变量:

float f, g;

说明表达式 f 和 g(变量可以近似认为省略表达式)在求值的时候为float类型。由于待求值的时表达式,因此可以自由地使用圆括号:

float ((f));

这表示((f))为 float 类型,因此通过推断,f 也是一个 float。 同样的逻辑用在函数和指针类型。例如:

float ff();

表示表达式 ff()是一个 float,因此 ff 是一个返回一个 float 的函数。类似地,

float *pf;

表示*pf 是一个 float 并且因此 pf 是一个指向一个 float 的指针。

这些形式的组合声明对表达式是一样的。因此,

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

表示*g()和(*h)()都是 float 表达式。

由于()比*绑定得更紧密,*g()和*(g())一样,g是一个返回指 float 指针的函数,而 h 是一个指向返回 float 的函数的指针。

当我们知道如何声明一个给定类型的变量以后,就能够很容易地写出一个类型的模型(cast):只要删除变量名和分号并将所有的东西包围在一对圆括号中即可。因此,由于

float *g();

声明 g 是一个返回 float 指针的函数,所以(float *())就是它的模型。

有了这些知识的武装,我们现在可以准备解决(*(void(*)())0)()了。 我们可以将它分为两个部分进行分析。首先,假设我们有一个变量 fp,它包含了一个函数指针,并且我们希望调用 fp 所指向的函数。可以这样写:

(*fp)();

如果 fp 是一个指向函数的指针,则*fp 就是函数本身,因此(*fp)()是调用它的一种方法。(*fp)中的括号是必须的,否则这个表达式将会被分析为*(fp())。我们现在要找一个适当的表达式来替换 fp。

这个问题就是我们的第二步分析。如果 C 可以读入并理解类型,我们可以写:

(*0)();

但这样并不行,因为*运算符要求必须有一个指针作为他的操作数。另外,这个操作数必须是一个指向函数的指针,以保证*的结果可以被调用。因此,我们需要将 0 转换为一个可以描述“指向一个返回 void 的函数的指针”的类型。

如果 fp 是一个指向返回 void 的函数的指针,则(*fp)()是一个 void 值,并且它的声明将会是这样的:

void (*fp)();

因此,我们需要写:

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

来声明一个哑变量。

一旦我们知道了如何声明该变量,我们也就知道了如何将一个常数转换为该类型:只要从变量的声明中去掉名字即可。因此,我们像下面这样将 0 转换为一个“指向返回 void 的函数的指针”:

(void(*)())0

接下来,我们用(void(*)())0 来替换 fp:

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

结尾处的分号用于将这个表达式转换为一个语句。

在这里,我们就解决了这个问题时没有使用 typedef 声明。通过使用它,我们可以更清晰地解决这个问题: typedef void (*funcptr)(); (*(funcptr)0)();

2.2 运算符的优先级问题

假设有一个声明了的常量 FLAG 是一个整数,其二进制表示中的某一位被置位(换句话说,它是 2 的某次幂),并且你希望测试一个整型变量 flags 该位是否被置位。通常的写法是:

if(flags & FLAG) ...

其意义对于很多 C 程序员都是很明确的:if 语句测试括号中的表达式求值的结果是否为 0。出于清晰的目的我们可以将它写得更明确:

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

这个语句现在更容易理解了。但它仍然是错的,因为!=比&绑定得更紧密,因此它被分析为:

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

这(偶尔)是可以的,如 FLAG 是 1 或 0(!)的时候,但对于其他 2 的幂是不行的。 假设你有两个整型变量,h 和 l,它们的值在 0 和 15(含 0 和 15)之间,并且你希望将 r 设置为 8位值,其低位为 l,高位为 h。一种自然的写法是:

r = h << 4 + 1;

不幸的是,这是错误的。加法比移位绑定得更紧密,因此这个例子等价于:

r = h << (4 + l);

正确的方法有两种:

r = (h << 4) + l; r = h << 4 | l;

避免这种问题的一个方法是将所有的东西都用括号括起来,但表达式中的括号过度就会难以理解,因此最好还是是记住 C 中的优先级。

不幸的是,这有 15 个,太困难了。然而,通过将它们分组可以变得容易。

绑定得最紧密的运算符并不是真正的运算符:下标、函数调用和结构选择。这些都与左边相关联。

接下来是一元运算符。它们具有真正的运算符中的最高优先级。由于函数调用比一元运算符绑定得更紧密,你必须写(*p)()来调用 p 指向的函数;*p()表示 p 是一个返回一个指针的函数。转换是一元运算符,并且和其他一元运算符具有相同的优先级。一元运算符是右结合的,因此*p++表示*(p++),而不是 (*p)++。

在接下来是真正的二元运算符。其中数学运算符具有最高的优先级,然后是移位运算符、关系运算符、 逻辑运算符、赋值运算符,最后是条件运算符。需要记住的两个重要的东西是: 1. 所有的逻辑运算符具有比所有关系运算符都低的优先级。 2. 一位运算符比关系运算符绑定得更紧密,但又不如数学运算符。

在这些运算符类别中,有一些奇怪的地方。乘法、除法和求余具有相同的优先级,加法和减法具有相同的优先级,以及移位运算符具有相同的优先级。 还有就是六个关系运算符并不具有相同的优先级:==和!=的优先级比其他关系运算符要低。这就允许我们判断 a 和 b 是否具有与 c 和 d 相同的顺序,例如:

a < b == c < d

在逻辑运算符中,没有任何两个具有相同的优先级。按位运算符比所有顺序运算符绑定得都紧密,每种与运算符都比相应的或运算符绑定得更紧密,并且按位异或(^)运算符介于按位与和按位或之间。

三元运算符的优先级比我们提到过的所有运算符的优先级都低。这可以保证选择表达式中包含的关系运算符的逻辑组合特性,如:

z = a < b && b < c ? d : e

这个例子还说明了赋值运算符具有比条件运算符更低的优先级是有意义的。另外,所有的复合赋值运算符具有相同的优先级并且是自右至左结合的,因此

a = b = c

b = c; a = b;

是等价的。 具有最低优先级的是逗号运算符。这很容易理解,因为逗号通常在需要表达式而不是语句的时候用来替代分号。 赋值是另一种运算符,通常具有混合的优先级。例如,考虑下面这个用于复制文件的循环: while(c = getc(in) != EOF) putc(c, out); 这个 while 循环中的表达式看起来像是 c 被赋以 getc(in)的值, 接下来判断是否等于 EOF 以结束循环。 不幸的是,赋值的优先级比任何比较操作都低,因此 c 的值将会是 getc(in)和 EOF 比较的结果,并且会被抛弃。因此,“复制”得到的文件将是一个由值为 1 的字节流组成的文件。 上面这个例子正确的写法并不难: while((c = getc(in)) != EOF) putc(c, out);

然而,这种错误在很多复杂的表达式中却很难被发现。例如,随 UNIX 系统一同发布的 lint 程序通常带有下面的错误行: if (((t = BTYPE(pt1->aty) == STRTY) || t == UNIONTY) {

这条语句希望给 t 赋一个值,然后看 t 是否与 STRTY 或 UNIONTY 相等。而实际的效果却大不相同。

C 中的逻辑运算符的优先级具有历史原因。B语言——C语言 的前辈,具有和 C 中的&和|运算符对应的逻辑运算符。尽管它们的定义是按位的 ,但编译器在条件判断上下文中将它们视为和&&和||一样。当在 C 中将它们分开后,优先级的改变是很危险的。

2.3 注意标志语句结束的分号

C 中的一个多余的分号通常会带来一点点不同:或者是一个空语句,无任何效果;或者编译器可能提出一个诊断消息,可以方便除去掉它。一个重要的区别是在必须跟有一个语句的 if 和 while 语句中。考虑下面的例子:

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

这不会发生编译错误,但这段程序的意义与:

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

就大不相同了。第一个程序段等价于:

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

也就是等价于:

big = x[i];(除非 x、i 或 big 是带有副作用的宏)。

另一个因分号引起巨大不同的地方是函数定义前面的结构声明的末尾[译注:这句话不太好听,看例子就明白了]。考虑下面的程序片段:

struct foo { int x; } f() { ... }

在紧挨着f 的第一个}后面丢失了一个分号。它的效果是声明了一个函数 f,返回值类型是 struct foo,这个结构成了函数声明的一部分。如果这里出现了分号,则 f 将被定义为具有默认的整型返回值。

本文分享自微信公众号 - 微光点亮星辰(SandTower)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2016-10-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java中的位运算符

    大家在接触运算符的时候通常都已经学完了变量的使用,对于算术以及赋值运算的感觉就是So easy!这不就是小学的知识嘛,对于逻辑运算符的部分依然无压力,这不就是中...

    聚沙成塔
  • Linux进入单用户模式加密

    在默认情况下,系统会在3(完整的多用户模式)和5(带界面的操作模式)两个级别下运行。在之前的文章中已经和大家介绍了如何将系统切换至单用户模式下运行,在单用户模...

    聚沙成塔
  • 校长讲堂第七讲

    一个句子可以是精确拼写的并且没有语法错误,但仍然没有意义。在这一节中,我们将会看到一些程序的写法会使得它们看起来是一个意思,但实际上是另一种完全不同的意思。 我...

    聚沙成塔
  • 硬件描述语言VHDL——运算符

    版权声明:本文为博主原创文章,转载请注明博客地址: https://blog.csdn.ne...

    zy010101
  • C语言中的运算和运算符

    Zoctopus
  • C优先级

    圆括号【()】、下标运算符【[]】、分量运算符的指向结构体成员运算符【->】、结构体成员运算符【.】

    开源519
  • python运算符

    & 按位与运算符:参与运算的两个值,如果两个相应位都为1,则该位的结果为1,否则为0

    老雷PHP全栈开发
  • 【程序源代码】《零基础学编程-python》第2期20200622

    什么是运算符?运算符用于执行程序代码运算,会针对一个以上操作数项目来进行运算。例如:2+3,其操作数是2和3,而运算符则是“+”。在vb2005中运算符大致可以...

    程序源代码
  • PHP 基本语法篇:运算符

    介绍完了 PHP 中的基本数据类型和数组,今天我们结合数据类型介绍 PHP 语言中的运算符和流程控制,非常简单,我们简单过一下即可。

    学院君
  • PHP运算符

    只能放在变量、常量、函数和include调用之前等。不能用在函数和类定义之前以及条件结构之前使用

    白胡杨同学

扫码关注云+社区

领取腾讯云代金券