C 陷阱与缺陷¶
C Traps and Pitfalls
出版社 | 作者 | 原书年份 |
---|---|---|
人民邮电出版社 | Andrew Koening | 1989 |
想要学好一门编程语言,应该阅读什么样的图书?在大多数场合下,我都会向他推荐市面上最新出版的图书。原因就是:以现在计算机领域内技术的发展速度,几乎每隔一段时间,我们就需要对自己现有的知识进行更新。这样看来,使用一本比较新的图书,里面的东西会比较贴近当前技术的发展,因而也就能够让你更容易掌握所要学的东西。
Ch0. 导读¶
回忆:词法、语法、语义
在进入接下来的章节前,让我们回顾一些知识。
在《计算机科学导论》第 9 章中我们学到,程序设计语言的编译步骤由以下四个步骤组成:
- 词法分析器
- 语法分析器
- 语义分析器
- 编译模块
请回忆它们的工作和作用。
本章简单介绍了全书各个部分将要讨论的内容。
Ch1. 词法陷阱¶
本章我们关注字符是如何组成符号的。
- 术语 token (符号)是语言的基本表意单元。字符组成符号。例子:
->
、file
都是符号.- 同一组字符序列在不同上下文中可能属于不同符号。
我们来看几个例子,请思考这些程序的行为(注意运算符优先级)。
=
与 ==
混用¶
while(c = ' ' || c == '\t')
c = getc(f);
if((filedesc == open(argv[i], 0)) < 0)
error();
open()
函数
这是一个系统调用:
#include <fcntl.h>
int open(const char *pathname, int flags)
我们将在 Linux 进阶中学习到与系统调用有关的知识。
贪心法¶
C 的词法分析遵循贪心法:
- 如果该字符可能组成符号,那么再读入下一个字符,直到读入的字符串已经不可能再组成一个有意义的符号。
我们举几个可能不恰当的例子来说明贪心法:
a---b
a -- - b
a - -- b
y = x/*p /* p point to the dividen */
y = x / *p
n-->0
n-- >0
n- -> 0
a+++++b
请思考上面几个表达式的行为。
整形常量¶
10
与010
的含义截然不同。- ANSI C 以后禁止在八进制数中使用字符
8
和9
。
混用的情况可能在需要格式对齐时出现,此时不要为数字添 0
,可能造成八进制结果。
字符串和字符常量¶
单引号引起的字符实际上代表一个整数,而整数的存储空间(16 位或 32 位)可以同时容纳多个字符。因此,有些 C 编译器允许在一个字符常量中包括多个字符。但对于这样的行为没有准确的定义
'yes'
有的编译器可能忽略后面的字符,有的编译器可能用后面的字符逐个覆盖前面的字符。GCC 和 Clang 采用后一种方法。
Ch2. 语法陷阱¶
本章我们关注符号是如何组成声明、表达式、语句和程序的。
函数声明¶
Question
请设计一个 C 语句,调用首地址为 0 的函数。
对于上面这个问题,标准答案是 (*(void(*)())0)()
。
接下来我们一步步解释这一函数调用。
声明的组成¶
本书将声明看做由:类型和一组类似表达式的声明符组成。简单的逻辑就是:对声明符求值时应当返回指定类型的结果。
float f, g
对f
和g
求值应返回float
float ff()
对ff()
求值应返回float
float *pf
对*pf
求值应返回float
float *g(), (*h)()
对*(g())
和h()
求值应返回float
(本例中,注意运算符优先级和函数指针的意义)
函数指针
应当始终理解:函数指针实际表达的意义是 (*fp)()
。fp()
只是简写。在声明中,必须使用前一种形式。这是因为 ()
的优先级比 *
高,所以必须用圆括号将指针 (*fp)
括起来。事实上,K&R C 不允许第二种形式的使用。
再回忆一些相关的知识:
- 函数名可以代表函数的地址
- 函数指针类型名最好这样声明:
V_FP_CHARP
,即说明返回值、名称、参数列表。
类型转换符¶
把声明中的变量名去掉,再用括号封装,就变成了类型转换符,比如对上面的每一例:
(float)
(float (*)())
(float *)
(float *())
,(float (*)())
我们要想调用地址为 0 的函数,可能会这样想:
(*0)()
但是,0
应当使用强制类型转换为我们需要的类型,类型转换符是:(void(*)())
使用 typedef
可以更清晰地解决该问题:
typedef void (*funcptr)();
(*(funcptr)0)();
理解 typedef
¶
本书给出的理解声明的方式也能让我们更好地理解 typedef
:直接在声明语句前面加上 typedef
就将该声明符作为该声明符的类型的替代。
typedef float *pf
中pf
代表float *
typedef float *g()
中g
代表float *()
例子:signal()
¶
signal()
函数
声明在 <signal.h>
中,它接受一个代表需要被捕获的特定 signal 的整数值,和一个用户提供的用于处理 signal 函数的指针。
我们先从用户定义的函数开始设计。用户可能会定义这样的处理函数:
void sigfunc(int);
因此,我们将 signal
函数声明为:
void (*signal(int, void(*)(int)))(int);
解释:传递 (int, void(*)(int))
参数以调用 signal
函数,对 signal
的返回值(是一个函数指针类型)解引用得到一个函数,然后传递一个整数给这个函数,最后返回值是 void
。
运算符优先级¶
(略)
分号¶
有几种情况下的分号会导致重大问题:
if
和while
等语句的结尾- 上面语句和
return
的结合,如:
if(n<3)
return
logrec.date = x[0];
logrec.time = x[1];
- 结构声明的结尾,再碰到函数定义:
struct logrec{
int date;
}
main ()
{
//...
}
这种情况下结构会被当做函数的返回值。
switch
语句¶
活用 switch
语句处理步骤比较相近的分支。
悬挂的 else
语句¶
牢记:else
与括号内最近的一个 if
匹配,并在多重选择、嵌套的情况下用好花括号即可避免该错误。
Ch3. 语义陷阱¶
本章考察看起来一切都显得合情合理,但在所有 C 实现中都是未定义的行为。
指针与数组¶
本节重点理解数组名与指针的概念、数组与指针的转化。
-
除了被用作
sizeof
的参数这一情形,其他情况下数组名都代表指向数组首元素地址的指针。-
注意,这个首元素可能还是数组,让我们来看一个多维数组的情况:
int calendar[12][31]; p = calendar;
这里的
calendar
是一个指向数组的指针。
-
-
还有
&array_name
的情况,得到一个指向数组的指针。 - 因为
*(a+i)
与*(i+a)
含义相同,因此a[i]
与i[a]
相同。
对于上面的日历多维数组,我们写一个你可能从来不会这样写的循环:
int (*montp)[31];
for (monthp = calendar; monthp < &calendar[12]; monthp++){
int *dayp;
for (dayp = *monthp; dayp < &(*monthp)[31]; dayp++)
*dayp = 0;
}
这个例子仅用于揭示数组与指针的关系,在实际写代码时,千万不要这样做。
非数组的指针¶
本节主要讨论字符串的大小、空字符的影响,略过。
作为参数的数组声明¶
将数组作为参数毫无意义,将被自动转换为指向首元素的指针。
但是,请不要假设在其他情况下也有这样的转换。以下两个语句有着天壤之别:
extern char *hello;
extern char hello[];
Note
编写函数声明时,选择能最清晰表达自己意图的方式。
避免“举隅法”¶
举隅法的意思是,以隐喻表示指代物与被指代物之间的关系。本节主要讨论混淆指针和指针所指向数据的情况,略过。
空指针与空串¶
清楚这两者的不同即可。