| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> 《C语言陷阱与缺陷》第二章【语法陷阱】上 -> 正文阅读 |
|
[C++知识库]《C语言陷阱与缺陷》第二章【语法陷阱】上 |
? ?目录 ??一、理解函数声明先来看下面这条语句:
这样的表达式有没有让你感到不寒而栗呢?其实大可不必,因为构造这类表达式其实只有一条简单的规则:按照使用的方法来声明。 任何C变量的声明都由两部分组成:类型以及一组类似表达式的声明符(declarator)。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。最简单的声明符就是单个变量,例如:
这个声明的含义是:当对其求值时,表达式f和g的类型为浮点数类型(float)。因为声明符与表达式相似,所以我们也可以在声明符中任意使用括号:
这个声明的含义是:当对其求值时,((f))的类型为浮点类型,由此可以推知,f也是浮点类型。同样的逻辑也适用于函数和指针类型的声明,例如:
这个声明的含义是:表达式ff()的求值结果是一个浮点数,也就是说,ff是一个返回值为浮点类型的函数。类似地,
这个声明的含义是:*pf是一个浮点数,也就是说,pf是一个指向浮点数的指针。以上这些形式在声明中还可以组合起来,就像在表达式中进行组合一样。因此,
表示*g()与(*h)()是浮点表达式。因为()结合优先级高于*,*g()也就是*(g()):g是一个函数,该函数的返回值类型为指向浮点数的指针。同理,可以得出h是一个函数指针,h所指向函数的返回值为浮点类型。 一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可。例如,因为下面的声明:
表示h是一个指向返回值为浮点类型的函数的指针,因此,
表示一个“指向返回值为浮点类型的函数的指针”的类型转换符。 有了这些预备知识,我们现在可以分两步来分析表达式 (*(void(*)())0)()。 第一步,假定变量fp是一个函数指针,那么如何调用fp所指向的函数呢?调用方法如下:
? ? ? ? ?因为是?fp?一个函数指针,那么*fp?就是该指针所指向的函数,所以(*fp)()就是调用该函数的方式。ANSI?C标准允许程序员将上式简写为?fp()?,但是一定要记住这种写法只是一种简写形式。? ? ? ? ? ?在表达式(*fp)()中,*fp两侧的括号非常重要,因为函数运算符()的优先级高于单目运算符*,如果?*fp?两侧没有括号,那么?*fp()?实际上与?*(fp())?的含义完全一致,ANSIC把它作为?*((*fp)())?的简写形式。?7简得 ? ? ? ? 现在,剩下的问题就只是找到一个恰当的表达式来替换 fp。我们将在分析的第二步来解决这个问题。如果C编译器能够理解我们大脑中对于类型的认识,那么我们可以这样写:
? ? ? ?上式并不能生效,因为运算符*必须用一个指针来作为操作数。不仅如此,这个指针还应该是一个函数指针,这样经运算符*作用后的结果才能作为函数被调用。因此,在上式中必须对0作类型转换,转换后的类型可以大致描述为“指回返回值为void类型的函数的指针”。 ? ? ? ?如果fp是一个指向返回值为void类型的函数的指针,那么?(*fp)()?的值为void,fp 的声明如下:
因此,我们可以用下式来调用存储位置为0的子例程
这种写法的代价是多声明了一个“哑”变量。 ? ? ? ?我们一旦知道如何声明一个变量,自然也就知道如何对一个常数进行类型转换,将其类型转换为该变量的类型:只需要在变量声明中将变量名去掉即可。因此,将常熟0转型为“指向返回值为?void的函数的指针”类型,可以这样写:
因此,我们可以用?(void(*)())0?来替换?fp?,从而得到:
末尾的分号使得表达式成为了一个语句。 ? ? ? ?在我当初解决这个问题的时候,C?语言中还没有?typedef?声明。尽管不用?typedef?来解决这个问题对剖析本例的细节而言是一种很好的方式,但无疑使用?typedef?能够使表述更加清晰:
? ? ? ? 这个棘手的例子并不是孤立的,还有一些C程序员经常遇到的问题,实际上和这个例子是同一个类型的。例如,考虑?signal?库函数,在包括该函数的C编译器实现中,signal?函数接受两个参数:一个是代表需要“被捕获”的特定?signal?的整数值;另一个是指向用户提供的函数的指针。该函数用于处理“捕获到”的特定?signal?,返回值类型为?void?。我们将会在后面的详细讨论该函数。 ? ? ? ? ?一般情况下,程序员并不主动声明signal函数,而是直接使用系统头文件?signal.h?中的声明。那么,在头文件?signal.h?中,signal?函数是如何声明的呢? ? ? ? ? 首先,让我们从用户定义的信号处理函数开始考虑,这无疑是最容易解决的。该函数可以定义如下:?
函数?sigfunc?的参数是一个代表特定信号的整数值,此时我们暂时忽略它。 ? ? ? ?上面假设的函数体定义了?sigfunc?函数,因而?sigfunc?函数的声明可以如下:
? ? ? ? 现在假定我们希望声明一个指向?sigfunc?函数的指针变量,不妨命名为?sfp?。因为?sfp?指向?sigfunc函数,则?*sfp?就代表了?sigfunc?函数,所以?*sfp?可以被调用。又假定?sig?是一个整数。则?(*sfp)(sig)?的值为?void?类型,因此我们可以如下声明:
? ? ? ? 此处的 something?代表了?signal?函数的参数类型,我们还需要进一步了解如何声明它们。上面声明可以这样理解:传递适当的参数以调用?signal?函数,对?signal?函数返回值(为函数指针类型)解除引用 (dereference),然后传递一个整型参数调用解除引用后所得函数,最后返回值为?void?类型。因此,signal?函数的返回值是一个指向返回值为?void?类型的函数的指针。 ? ? ? ? 那么,signal?函数的参数又是如何呢?signal?函数接受两个参数:一个整型的信号编号,以及一个指向用户定义的信号处理函数的指针。我们此前已经定义了指向用户定义的信号处理函数的指针?sfp?:
? ? ? ? sfp?的类型可以通过将上面声明中的?sfp?去掉而得到,即?void(*)(int)?。此外,signal?函数的返回值是一个指向调用前的用户定义信号处理函数的指针,这个指针的类型与?sfp?指针类型一致。因此,我们可以如下声明?signal?函数:
同样地,使用?typedef?可以简化上面的函数声明:
?? 二、运算符的优先级问题? ? ? 假设存在一个已定义的常量 FLAG ,它是一个整数,且改整数的二进制表示中只有某一位是1,其与各位均为0,亦即该整数是2的某次幂。如果对于整型变量 flags ,我们需要判断它在常量 FLAG 为1的那一位上是否同样为1,通常可以这样写:
? ? ? ?上式的含义是判断 flags 按位与 FLAG 这个表达式的结果是否为0,考虑到可读性,如果对表达式的值是否为0的判断能够显示的加以说明,无疑使得代码自身就起到了注释该段代码意图的作用,其写法如下:
这是一个错误的语句,因为 != 运算符的优先级要高于 & 运算符,所以上式的结果被解释为:
? ? ? ? 这个表达式含义是先判断 FLAG 的值是不是等于0,再去执行 & 运算。当 FLAG 的值等于0时,0不等于0表达式为假跳出,除了 FLAG 恰好为真的时候,FLAG 为其他数时这个表达是都是错误的。 ???注意:& 是按位(二进制位)与的意思。 例如:
按位与 (&) 的作用是:二进制位只要有0,按位与出来的结果就是0。 看下面这个例子:
? ? ? ? ?该表达式的本意是将 hi 的二进制位向左移动4位再加上 low 的值,但是很遗憾,这样写是错误的。因为加法运算的优先级要比移位运算的优先级高,因此本例实际相当于:
? ? ? ?对于这种情况,有两种更正方法:第一种方法是加括号;?第二种方法意识到问题出在程序员混淆了算术运算与逻辑运算,于是将原来的加号改为按位逻辑或,?但这种方法牵涉到的移位运算与逻辑运算的相对优先级就更加不是那么明显。两种方法如下:
? ? ? ?用添加括号的方法虽然可以完全避免这类问题,但是表达式中有了太多的括号反而不容易被理解。因此,记住C语言中运算符的优先级是有益的。 ??注意:<< 是左移操作符,移动的的是二进制序列。 例如:
下表是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.任何一个逻辑运算符的优先级低于任何一个关系运算符; ? ? ? ? 属于同一类型的各个运算符之间的相对优先级,理解起来一般没有什么困难。乘法、除法和求余优先级相同,加法、减法的优先级相同,两个移位运算符的先级也相同。? ?1/2*a 的含义是 (1/2)*a ,而不是 1/(2*a) ,这一点也许会让某些人吃惊其实在这方面C语言与Fortran语言、Pascal语言以及其他程序设计语言之间的为表现并无差别。 ? ? ? ?但是,6个关系运算符的优先级并不相同,这一点或许让人感到有些吃惊。运算符 == 和 != 的优先级要低于其他关系运算符的优先级。因此,如果我们要比a与b的相对大小顺序是否和c与d的相对大小顺序一样,就可以这样写:
? ? ? ? 任何两个逻辑运算符都具有不同的优先级。所有的按位运算符优先级要比顺序运算符的优先级高,每个“与”运算符要比相应的“或”运算符优先级高,而按位异或运算符 (^运算符) 的优先级介于按位与运算符和按位或运算符之间。 ??注意:按位异或运算符(^运算符)是将二进制位按位异或。 例如:
? ? ? ? 这些运算符的优先顺序是由于历史原因形成的。B语言是C语言的“祖先”,B语言中的逻辑运算符大致相当于C语言中的 & 和 | 运算符。虽然这些运算符从定义上而言是按位操作的,但是当它们出现在条件语句的上下文中时,B语言的编译器会将它们作为相当于现在C语言中的 && 和 || 运算符来处理。而到了C语言中,这两种不同的用法被区分开来,丛兼容性的角度来考虑,如果对它们优先顺序的改变过大,将是一件危险的事。 例如: B语言中的 if (a > b & a > c)... 相当于C语言中的 if (a > b && a > c)... 。 ? ? ? ? 在本节到现在为止提及的所有运算符中,三条件运算符的优先级最低。这就允许我们在三目条件运算符的条件表达式中包括关系运算符的逻辑组合,例如:
? ? ? ? 当前语句中的优先级 && 最高?,其次是条件运算符,最低是 = 运算符。因此,当前语句的含义是,如果income 大于4000并且residency 小于5,就将3.5赋给tax_rate,否则就将2.0赋给tax_rate。 ? ? ? ? 本例其实还说明了赋值运算符的优先级低于条件运算符的优先级是具有意义的。此外,所有赋值运算符的优先级是一样的,而且它们的结合方式是自右向左,因此,
当前语句的含义是,将0赋给 visitor_score ,再将 visitor_score 的值赋给 home_score 。 与下面这两条语句所表达的意思是相同的:
例如: a = b = 10; 相当于 b = 10;?a = b; ? ? ? ? 在所有的运算符中,逗号运算符的优先级最低。这一点很容易记住,因为在需要一个表达式而不是一条语句时,经常使用逗号运算符来替换作为语句结束标志的分号。逗号运算符在宏定义中特别有用,这一点在后面的内容还会进一步讨论。 ? ? ? ? 在涉及赋值运算符时,经常会引起优先级的混淆。考虑下面这个例子,例子中循环语句的本意是复制一个文件到另一个文件:
? ? ? ? 在 while 语句的表达式中,c似乎是首先被赋予函数 getc(in) 的返回值,然后与 EOF 比较是否到达文件结尾以便决定是否终止循环。然而,由于赋值运算符的优先级要低于任何一个比较运算符,因此c的值实际上是函数 getc(in) 的返回值与 EOF 比较的结果。此处函数 getc(in) 的返回值只是一个临时变量,在与 EOF 比较后就被“丢弃”了。因此,最后得到的文件“副本”中只包括了一组二进制值为1的字节流。 上例实际应该写成:
如果表达式再复杂一点,这类错误就很难被察觉。 例如,第4章首提及的 lint 程序的一个版本,在发布时包括了下面一行错误代码:
?? 三、注意作为语句结束标志的分号? ? ? ? 在C程序中,如果不小心多写了一个分号,可能不会造成什么不良后果:这个分号也许会被视作一个不会产生任何实际效果的空语句;或者编译器会因为这个多余的分号而产生一条警告信息,根据警告信息的提示能够很容易去掉这个分号。一种重要的例外情形是在 if 或者 while 语句之后需要紧跟一条语句时,如果此时多了一个分号,那么原来紧跟在 if 或者 while 子句之后的语句就是一条单独的语句,与条件判断部分没有了任何关系。考虑下面的这个例子:
? ? ? ? 编译器会正常的接受第一行代码中的分号而不会提示任何警告信息,因此编译器对这段程序代码的处理与对下面这段代码的处理就大不相同:
前面第一个例子(即在 if 之后多加了一个分号的例子)时间上相当于
当然,也就等于(除非 x 、i 或者big 是有副作用的宏)
如果不是多写了一个分号,而是遗漏了一个分号,同样会招致麻烦,例如:
此时的return语句后面遗漏了一个分号,然而这段程序代码仍然会顺利通过编译而不会抱错,只是将语句
当做了return语句的操作数。上面这段代码时间相当于:
? ? ? ? 如果这段代码所在的函数声明其返回值为 void ,编译器会因为实际返回值的类型与声明返回值的类型不一致而报错。然而,如果一个函数不需要返回值(即返回值为 void ),我们通常会在函数声明时省略返回值类型,但是此时对编译器而言会隐含地将函数返回值类型视作 int 类型。如果是这样,上面的错误就不会被编译器检测到。在上面的例子中,当 n>=3 时,第一个赋值语句会被直接跳过,由此造成的错误可能会是一个潜伏很深、极难发现的程序 Bug 。 ? ? ? ? 当一个声明的结尾紧跟一个函数定义时,有分号与没分号的实际效果相差极为不同?如果声明结尾的分号被省略,编译器可能会把声明的类型视作函数的返回值类型。考虑下面的例子:
? ? ? ? 在第一个 } 与紧随其后的函数 main 定义之间,遗漏了一个分号。因此,上面代码段的实际效果是声明函数 main 的返回值是 struct?logrec 类型。写成下面这样,会看得更清楚:
如果分号没有被省略,函数 main 的返回值类型会缺省定义为 int 类型。 ? ? ? ? ?在函数 main 中,如果本应返回一个 int 类型数值,却声明返回一个 struct?logrec 类型的结构,会产生怎样的效果呢?我们把它留作本章结尾的一个练习。虽然刻意地往消极面去联想也许有些“病态”,但对于要考虑到各种意外情形的程序设计来说 (比如航空航天或医疗仪器的控制程序),却是不无裨益的。 ? |
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 18:32:00- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |