第一篇:C语言缺陷与陷阱
C语言陷阱和缺陷
[译序]
那些自认为已经“学完”C语言的人,请你们仔细读阅读这篇文章吧。路还长,很多东西要学。我也是„„
[概述]
C语言像一把雕刻刀,锋利,并且在技师手中非常有用。和任何锋利的工具一样,C会伤到那些不能掌握它的人。本文介绍C语言伤害粗心的人的方法,以及如何避免伤害。
[内容]
0 简介 1 词法缺陷 1.1 = 不是 == 1.2 & 和 | 不是 && 和 || 1.3 多字符记号 1.4 例外
1.5 字符串和字符 2 句法缺陷 2.1 理解声明
2.2 运算符并不总是具有你所想象的优先级 2.3 看看这些分号!2.4 switch语句 2.5 函数调用
2.6 悬挂else问题 3 链接
3.1 你必须自己检查外部类型 4 语义缺陷
4.1 表达式求值顺序 4.2 &&、||和!运算符 4.3 下标从零开始
4.4 C并不总是转换实参 4.5 指针不是数组 4.6 避免提喻法
4.7 空指针不是空字符串 4.8 整数溢出 4.9 移位运算符 5 库函数
5.1 getc()返回整数 5.2 缓冲输出和内存分配 6 预处理器 6.1 宏不是函数 6.2 宏不是类型定义 7 可移植性缺陷
7.1 一个名字中都有什么? 7.2 一个整数有多大?
7.3 字符是带符号的还是无符号的? 7.4 右移位是带符号的还是无符号的? 7.5 除法如何舍入? 7.6 一个随机数有多大? 7.7 大小写转换
7.8 先释放,再重新分配 7.9 可移植性问题的一个实例 8 这里是空闲空间 参考 脚注
0 简介
C语言及其典型实现被设计为能被专家们容易地使用。这门语言简洁并附有表达力。但有一些限制可以保护那些浮躁的人。一个浮躁的人可以从这些条款中获得一些帮助。
在本文中,我们将会看一看这些未可知的益处。这是由于它的未可知,我们无法为其进行完全的分类。不过,我们仍然通过研究为了一个C程序的运行所需要做的事来做到这些。我们假设读者对C语言至少有个粗浅的了解。
第一部分研究了当程序被划分为记号时会发生的问题。第二部分继续研究了当程序的记号被编译器组合为声明、表达式和语句时会出现的问题。第三部分研究了由多个部分组成、分别编译并绑定到一起的C程序。第四部分处理了概念上的误解:当一个程序具体执行时会发生的事情。第五部分研究了我们的程序和它们所使用的常用库之间的关系。在第六部分中,我们注意到了我们所写的程序也不并不是我们所运行的程序;预处理器将首先运行。最后,第七部分讨论了可移植性问题:一个能在一个实现中运行的程序无法在另一个实现中运行的原因。词法缺陷
编译器的第一个部分常被称为词法分析器(lexical analyzer)。词法分析器检查组成程序的字符序列,并将它们划分为记号(token)一个记号是一个有一个或多个字符的序列,它在语言被编译时具有一个(相关地)统一的意义。在C中,例如,记号->的意义和组成它的每个独立的字符具有明显的区别,而且其意义独立于->出现的上下文环境。
另外一个例子,考虑下面的语句:
if(x > big)big = x;
该语句中的每一个分离的字符都被划分为一个记号,除了关键字if和标识符big的两个实例。
事实上,C程序被两次划分为记号。首先是预处理器读取程序。它必须对程序进行记号划分以发现标识宏的标识符。它必须通过对每个宏进行求值来替换宏调用。最后,经过宏替换的程序又被汇集成字符流送给编译器。编译器再第二次将这个流划分为记号。
在这一节中,我们将探索对记号的意义的普遍的误解以及记号和组成它们的字符之间的关系。稍后我们将谈到预处理器。
1.1 = 不是 == 从Algol派生出来的语言,如Pascal和Ada,用:=表示赋值而用=表示比较。而C语言则是用=表示赋值而用==表示比较。这是因为赋值的频率要高于比较,因此为其分配更短的符号。
此外,C还将赋值视为一个运算符,因此可以很容易地写出多重赋值(如a = b = c),并且可以将赋值嵌入到一个大的表达式中。
这种便捷导致了一个潜在的问题:可能将需要比较的地方写成赋值。因此,下面的语句好像看起来是要检查x是否等于y:
if(x = y)foo();
而实际上是将x设置为y的值并检查结果是否非零。在考虑下面的一个希望跳过空格、制表符和换行符的循环:
while(c == ' ' || c = 't' || c == 'n')c = getc(f);
在与't'进行比较的地方程序员错误地使用=代替了==。这个“比较”实际上是将't'赋给c,然后判断c的(新的)值是否为零。因为't'不为零,这个“比较”将一直为真,因此这个循环会吃尽整个文件。这之后会发生什么取决于特定的实现是否允许一个程序读取超过文件尾部的部分。如果允许,这个循环会一直运行。
一些C编译器会对形如e1 = e2的条件给出一个警告以提醒用户。当你趋势需要先对一个变量进行赋值之后再检查变量是否非零时,为了在这种编译器中避免警告信息,应考虑显式给出比较符。换句话说,将:
if(x = y)foo();
改写为:
if((x = y)!= 0)foo();
这样可以清晰地表示你的意图。
1.2 & 和 | 不是 && 和 || 容易将==错写为=是因为很多其他语言使用=表示比较运算。其他容易写错的运算符还有&和&&,或|和||,这主要是因为C语言中的&和|运算符于其他语言中具有类似功能的运算符大为不同。我们将在第4节中贴近地观察这些运算符。
1.3 多字符记号
一些C记号,如/、*和=只有一个字符。而其他一些C记号,如/*和==,以及标识符,具有多个字符。当C编译器遇到紧连在一起的/和*时,它必须能够决定是将这两个字符识别为两个分离的记号还是一个单独的记号。C语言参考手册说明了如何决定:“如果输入流到一个给定的字符串为止已经被识别为记号,则应该包含下一个字符以组成能够构成记号的最长的字符串”。因此,如果/是一个记号的第一个字符,并且/后面紧随了一个*,则这两个字符构成了注释的开始,不管其他上下文环境。
下面的语句看起来像是将y的值设置为x的值除以p所指向的值:
y = x/*p /* p 指向除数 */;
实际上,/*开始了一个注释,因此编译器简单地吞噬程序文本,直到*/的出现。换句话说,这条语句仅仅把y的值设置为x的值,而根本没有看到p。将这条语句重写为:
y = x / *p /* p 指向除数 */;
或者干脆是
y = x /(*p)/* p指向除数 */;
它就可以做注释所暗示的除法了。
这种模棱两可的写法在其他环境中就会引起麻烦。例如,老版本的C使用=+表示现在版本中的+=。这样的编译器会将
a=-1;
视为
a =-1;或
a = a> a
是不合法的。它和
p-> a
不是同义词。
另一方面,有些老式编译器还是将=+视为一个单独的记号并且和+=是同义词。
1.5 字符串和字符
单引号和双引号在C中的意义完全不同,在一些混乱的上下文中它们会导致奇怪的结果而不是错误消息。
包围在单引号中的一个字符只是书写整数的另一种方法。这个整数是给定的字符在实现的对照序列中的一个对应的值。因此,在一个ASCII实现中,'a'和0141或97表示完全相同的东西。而一个包围在双引号中的字符串,只是书写一个有双引号之间的字符和一个附加的二进制值为零的字符所初始化的一个无名数组的指针的一种简短方法。
线面的两个程序片断是等价的:
printf(“Hello worldn”);
char hello[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', 'n', 0 };printf(hello);
使用一个指针来代替一个整数通常会得到一个警告消息(反之亦然),使用双引号来代替单引号也会得到一个警告消息(反之亦然)。但对于不检查参数类型的编译器却除外。因此,用
printf('n');
来代替
printf(“n”);
通常会在运行时得到奇怪的结果。
由于一个整数通常足够大,以至于能够放下多个字符,一些C编译器允许在一个字符常量中存放多个字符。这意味着用'yes'代替“yes”将不会被发现。后者意味着“分别包含y、e、s和一个空字符的四个连续存贮器区域中的第一个的地址”,而前者意味着“在一些实现定义的样式中表示由字符y、e、s联合构成的一个整数”。这两者之间的任何一致性都纯属巧合。句法缺陷
要理解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的幂是不行的[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)++。
在接下来是真正的二元运算符。其中数学运算符具有最高的优先级,然后是移位运算符、关系运算符、逻辑运算符、赋值运算符,最后是条件运算符。需要记住的两个重要的东西是:
所有的逻辑运算符具有比所有关系运算符都低的优先级。
一位运算符比关系运算符绑定得更紧密,但又不如数学运算符。
在这些运算符类别中,有一些奇怪的地方。乘法、除法和求余具有相同的优先级,加法和减法具有相同的优先级,以及移位运算符具有相同的优先级。
还有就是六个关系运算符并不具有相同的优先级:==和!=的优先级比其他关系运算符要低。这就允许我们判断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相等。而实际的效果却大不相同[3]。
C中的逻辑运算符的优先级具有历史原因。B——C的前辈——具有和C中的&和|运算符对应的逻辑运算符。尽管它们的定义是按位的,但编译器在条件判断上下文中将它们视为和&&和||一样。当在C中将它们分开后,优先级的改变是很危险的[4]。
2.3 看看这些分号!
C中的一个多余的分号通常会带来一点点不同:或者是一个空语句,无任何效果;或者编译器可能提出一个诊断消息,可以方便除去掉它。一个重要的区别是在必须跟有一个语句的if和while语句中。考虑下面的例子:
if(x > big);big = x;这不会发生编译错误,但这段程序的意义与: if(x > big)big = x;就大不相同了。第一个程序段等价于: if(x > big){ } big = x;也就是等价于: big = x;(除非x、i或big是带有副作用的宏)。另一个因分号引起巨大不同的地方是函数定义前面的结构声明的末尾[译注:这句话不太好听,看例子就明白了]。考虑下面的程序片段: struct foo { int x;} f(){...} 在紧挨着f的第一个}后面丢失了一个分号。它的效果是声明了一个函数f,返回值类型是struct foo,这个结构成了函数声明的一部分。如果这里出现了分号,则f将被定义为具有默认的整型返回值[5]。2.4 switch语句 通常C中的switch语句中的case段可以进入下一个。例如,考虑下面的C和Pascal程序片断: switch(color){ case 1: printf(“red”);break;case 2: printf(“yellow”);break;case 3: printf(“blue”);break;} case color of 1: write('red');2: write('yellow');3: write('blue');end 这两个程序片断都作相同的事情:根据变量color的值是1、2还是3打印red、yellow或blue(没有新行符)。这两个程序片断非常相似,只有一点不同:Pascal程序中没有C中相应的break语句。C中的case标签是真正的标签:控制流程可以无限制地进入到一个case标签中。看看另一种形式,假设C程序段看起来更像Pascal: switch(color){ case 1: printf(“red”);case 2: printf(“yellow”);case 3: printf(“blue”);} 并且假设color的值是2。则该程序将打印yellowblue,因为控制自然地转入到下一个printf()的调用。这既是C语言switch语句的优点又是它的弱点。说它是弱点,是因为很容易忘记一个break语句,从而导致程序出现隐晦的异常行为。说它是优点,是因为通过故意去掉break语句,可以很容易实现其他方法难以实现的控制结构。尤其是在一个大型的switch语句中,我们经常发现对一个case的处理可以简化其他一些特殊的处理。例如,设想有一个程序是一台假想的机器的翻译器。这样的一个程序可能包含一个switch语句来处理各种操作码。在这样一台机器上,通常减法在对其第二个运算数进行变号后就变成和加法一样了。因此,最好可以写出这样的语句: case SUBTRACT: opnd2 =-opnd2;/* no break;*/ case ADD:...另外一个例子,考虑编译器通过跳过空白字符来查找一个记号。这里,我们将空格、制表符和新行符视为是相同的,除了新行符还要引起行计数器的增长外: case 'n': linecount++;/* no break */ case 't': case ' ':...2.5 函数调用 和其他程序设计语言不同,C要求一个函数调用必须有一个参数列表,但可以没有参数。因此,如果f是一个函数,f();就是对该函数进行调用的语句,而 f;什么也不做。它会作为函数地址被求值,但不会调用它[6]。2.6 悬挂else问题 在讨论任何语法缺陷时我们都不会忘记提到这个问题。尽管这一问题不是C语言所独有的,但它仍然伤害着那些有着多年经验的C程序员。考虑下面的程序片断: if(x == 0)if(y == 0)error();else { z = x + y;f(&z);} 写这段程序的程序员的目的明显是将情况分为两种:x = 0和x!= 0。在第一种情况中,程序段什么都不做,除非y = 0时调用error()。第二种情况中,程序设置z = x + y并以z的地址作为参数调用f()。然而,这段程序的实际效果却大为不同。其原因是一个else总是与其最近的if相关联。如果我们希望这段程序能够按照实际的情况运行,应该这样写: if(x == 0){ if(y == 0)error();else { z = x + y;f(&z);} } 换句话说,当x!= 0发生时什么也不做。如果要达到第一个例子的效果,应该写: if(x == 0){ if(y ==0)error();} else { z = z + y;f(&z);} 3 链接 一个C程序可能有很多部分组成,它们被分别编译,并由一个通常称为链接器、链接编辑器或加载器的程序绑定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现的错误。在这一节中,我们将看到一些这种类型的错误。有一些C实现,但不是所有的,带有一个称为lint的程序来捕获这些错误。如果具有一个这样的程序,那么无论怎样地强调它的重要性都不过分。3.1 你必须自己检查外部类型 假设你有一个C程序,被划分为两个文件。其中一个包含如下声明: int n;而令一个包含如下声明: long n;这不是一个有效的C程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一个文件的内容。因此,检查类型的工作只能由链接器(或一些工具程序如lint)来完成;如果操作系统的链接器不能识别数据类型,C编译器也没法过多地强制它。那么,这个程序运行时实际会发生什么?这有很多可能性: 实现足够聪明,能够检测到类型冲突。则我们会得到一个诊断消息,说明n在两个文件中具有不同的类型。你所使用的实现将int和long视为相同的类型。典型的情况是机器可以自然地进行32位运算。在这种情况下你的程序或许能够工作,好象你两次都将变量声明为long(或int)。但这种程序的工作纯属偶然。n的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有效。这可能发生,例如,编译器可以将int安排在long的低位。不论这是基于系统的还是基于机器的,这种程序的运行同样是偶然。n的两个实例以另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在这种情况下,程序可能失败。这种情况发生的里一个例子出奇地频繁。程序的某一个文件包含下面的声明: char filename[] = “etc/passwd”;而另一个文件包含这样的声明: char *filename;尽管在某些环境中数组和指针的行为非常相似,但它们是不同的。在第一个声明中,filename是一个字符数组的名字。尽管使用数组的名字可以产生数组第一个元素的指针,但这个指针只有在需要的时候才产生并且不会持续。在第二个声明中,filename是一个指针的名字。这个指针可以指向程序员让它指向的任何地方。如果程序员没有给它赋一个值,它将具有一个默认的0值(null)[译注:实际上,在C中一个为初始化的指针通常具有一个随机的值,这是很危险的!]。这两个声明以不同的方式使用存储区,他们不可能共存。避免这种类型冲突的一个方法是使用像lint这样的工具(如果可以的话)。为了在一个程序的不同编译单元之间检查类型冲突,一些程序需要一次看到其所有部分。典型的编译器无法完成,但lint可以。避免该问题的另一种方法是将外部声明放到包含文件中。这时,一个外部对象的类型仅出现一次[7]。4 语义缺陷 一个句子可以是精确拼写的并且没有语法错误,但仍然没有意义。在这一节中,我们将会看到一些程序的写法会使得它们看起来是一个意思,但实际上是另一种完全不同的意思。我们还要讨论一些表面上看起来合理但实际上会产生未定义结果的环境。我们这里讨论的东西并不保证能够在所有的C实现中工作。我们暂且忘记这些能够在一些实现中工作但可能不能在另一些实现中工作的东西,直到第7节讨论可以执行问题为止。4.1 表达式求值顺序 一些C运算符以一种已知的、特定的顺序对其操作数进行求值。但另一些不能。例如,考虑下面的表达式: a < b && c < d C语言定义规定a < b首先被求值。如果a确实小于b,c < d必须紧接着被求值以计算整个表达式的值。但如果a大于或等于b,则c < d根本不会被求值。要对a < b求值,编译器对a和b的求值就会有一个先后。但在一些机器上,它们也许是并行进行的。C中只有四个运算符&&、||、?:和,指定了求值顺序。&&和||最先对左边的操作数进行求值,而右边的操作数只有在需要的时候才进行求值。而?:运算符中的三个操作数:a、b和c,最先对a进行求值,之后仅对b或c中的一个进行求值,这取决于a的值。,运算符首先对左边的操作数进行求值,然后抛弃它的值,对右边的操作数进行求值[8]。C中所有其它的运算符对操作数的求值顺序都是未定义的。事实上,赋值运算符不对求值顺序做出任何保证。出于这个原因,下面这种将数组x中的前n个元素复制到数组y中的方法是不可行的: i = 0;while(i < n)y = x[i++];其中的问题是y的地址并不保证在i增长之前被求值。在某些实现中,这是可能的;但在另一些实现中却不可能。另一种情况出于同样的原因会失败: i = 0;while(i < n)y[i++] = x;而下面的代码是可以工作的: i = 0;while(i < n){ y = x;i++;} 当然,这可以简写为: for(i = 0;i < n;i++)y = x;4.2 &&、||和!运算符 C中有两种逻辑运算符,在某些情况下是可以交换的:按位运算符&、|和~,以及逻辑运算符&&、||和!。一个程序员如果用某一类运算符替换相应的另一类运算符会得到某些奇怪的效果:程序可能会正确地工作,但这纯属偶然。&&、||和!运算符将它们的参数视为仅有“真”或“假”,通常约定0代表“假”而其它的任意值都代表“真”。这些运算符返回1表示“真”而返回0表示“假”,而且&&和||运算符当可以通过左边的操作数确定其返回值时,就不会对右边的操作数进行求值。因此!10是零,因为10非零;10 && 12是1,因为10和12都非零;10 || 12也是1,因为10非零。另外,最后一个表达式中的12不会被求值,10 || f()中的f()也不会被求值。考虑下面这段用于在一个表中查找一个特定元素的程序: i = 0;while(i < tabsize && tab!= x)i++;这段循环背后的意思是如果i等于tabsize时循环结束,元素未被找到。否则,i包含了元素的索引。假设这个例子中的&&不小心被替换为了&,这个循环可能仍然能够工作,但只有两种幸运的情况可以使它停下来。首先,这两个操作都是当条件为假时返回0,当条件为真时返回1。只要x和y都是1或0,x & y和x && y都具有相同的值。然而,如果当使用了出了1之外的非零值表示“真”时互换了这两个运算符,这个循环将不会工作。其次,由于数组元素不会改变,因此越过数组最后一个元素进一个位置时是无害的,循环会幸运地停下来。失误的程序会越过数组的结尾,因为&不像&&,总是会对所有的操作数进行求值。因此循环的最后一次获取tab时i的值已经等于tabsize了。如果tabsize是tab中元素的数量,则会取到tab中不存在的一个值。4.3 下标从零开始 在很多语言中,具有n个元素的数组其元素的号码和它的下标是从1到n严格对应的。但在C中不是这样。一个具有n个元素的C数组中没有下标为n的元素,其中的元素的下标是从0到n'a';return c;} 在很多C实现中,为了减少比实际计算还要多的调用开销,通常将其实现为宏: #define toupper(c)((c)>= 'a' &&(c)<= 'z' ?(c)+('A''a')#define tolower(c)((c)+ 'A''a' :(c))#define tolower(c)((c)>= 'A' &&(c)<= 'Z' ?(c)+ 'a''a';return c;} tolower()类似。这个改变带来更多的问题,每次使用这些函数的时候都会引入函数调用开销。我们的英雄认为一些人可能不愿意支付这些开销,因此他们将这个宏重命名为: #define _toupper(c)((c)+ 'A''A')这就允许用户选择方便或速度。这里面其实只有一个问题:伯克利的人们和其他的C实现者并没有跟着这么做。这意味着一个在AT&T系统上编写的使用了toupper()或tolower()的程序,如果没有为其传递正确大小写字母参数,在其他C实现中可能不会正常工作。如果不知道这些历史,可能很难对这类错误进行跟踪。7.8 先释放,再重新分配 很多C实现为用户提供了三个内存分配函数:malloc()、realloc()和free()。调用malloc(n)返回一个指向有n个字符的新分配的内存的指针,这个指针可以由程序员使用。给free()传递一个指向由malloc()分配的内存的指针可以使这块内存得以重用。通过一个指向已分配区域的指针和一个新的大小调用realloc()可以将这块内存扩大或缩小到新尺寸,这个过程中可能要复制内存。也许有人会想,真相真是有点微妙啊。下面是System V接口定义中出现的对realloc()的描述: realloc改变一个由ptr指向的size个字节的块,并返回该块(可能被移动)的指针。在新旧尺寸中比较小的一个尺寸之下的内容不会被改变。而UNIX系统第七版的参考手册中包含了这一段的副本。此外,还包含了描述realloc()的另外一段: 如果在最后一次调用malloc、realloc或calloc后释放了ptr所指向的块,realloc依旧可以工作;因此,free、malloc和realloc的顺序可以利用malloc压缩存贮的查找策略。因此,下面的代码片段在UNIX第七版中是合法的: free(p);p = realloc(p, newsize);这一特性保留在从UNIX第七版衍生出来的系统中:可以先释放一块存储区域,然后再重新分配它。这意味着,在这些系统中释放的内存中的内容在下一次内存分配之前可以保证不变。因此,在这些系统中,我们可以用下面这种奇特的思想来释放一个链表中的所有元素: for(p = head;p!= NULL;p = p->next)free((char *)p);而不用担心调用free()会导致p->next不可用。不用说,这种技术是不推荐的,因为不是所有C实现都能在内存被释放后将它的内容保留足够长的时间。然而,第七版的手册遗留了一个未声明的问题:realloc()的原始实现实际上是必须要先释放再重新分配的。出于这个原因,一些C程序都是先释放内存再重新分配的,而当这些程序移植到其他实现中时就会出现问题。7.9 可移植性问题的一个实例 让我们来看一个已经被很多人在很多时候解决了的问题。下面的程序带有两个参数:一个长整数和一个函数(的指针)。它将整数转换位十进制数,并用代表其中每一个数字的字符来调用给定的函数。void printnum(long n, void(*p)()){ if(n < 0){(*p)('-');n =-n;} if(n >= 10)printnum(n / 10, p);(*p)(n % 10 + '0');} 这个程序非常简单。首先检查n是否为负数;如果是,则打印一个符号并将n变为正数。接下来,测试是否n >= 10。如果是,则它的十进制表示中包含两个或更多个数字,因此我们递归地调用printnum()来打印除最后一个数字外的所有数字。最后,我们打印最后一个数字。这个程序——由于它的简单——具有很多可移植性问题。首先是将n的低位数字转换成字符形式的方法。用n % 10来获取低位数字的值是好的,但为它加上'0'来获得相应的字符表示就不好了。这个加法假设机器中顺序的数字所对应的字符数顺序的,没有间隔,因此'0' + 5和'5'的值是相同的,等等。尽管这个假设对于ASCII和EBCDIC字符集是成立的,但对于其他一些机器可能不成立。避免这个问题的方法是使用一个表: void printnum(long n, void(*p)()){ if(n < 0){(*p)('-');n =-n;} if(n >= 10)printnum(n / 10, p);(*p)(“0123456789”[n % 10]);} 另一个问题发生在当n < 0时。这时程序会打印一个负号并将n设置为-n。这个赋值会发生溢出,因为在使用2的补码的机器上通常能够表示的负数比正数要多。例如,一个(长)整数有k位和一个附加位表示符号,则-2k可以表示而2k却不能。解决这一问题有很多方法。最直观的一种是将n赋给一个unsigned long值。然而,一些C便一起可能没有实现unsigned long,因此我们来看看没有它怎么办。在第一个实现和第二个实现的机器上,改变一个正整数的符号保证不会发生溢出。问题仅出在改变一个负数的符号时。因此,我们可以通过避免将n变为正数来避免这个问题。当然,一旦我们打印了负数的符号,我们就能够将负数和正数视为是一样的。下面的方法就强制在打印符号之后n为负数,并且用负数值完成我们所有的算法。如果我们这么做,我们就必须保证程序中打印符号的部分只执行一次;一个简单的方法是将这个程序划分为两个函数: void printnum(long n, void(*p)()){ if(n < 0){(*p)('-');printneg(n, p);} else printneg(-n, p);} void printneg(long n, void(*p)()){ if(n <=-10)printneg(n / 10, p);(*p)(“0123456789”[-(n % 10)]);} printnum()现在只检查要打印的数是否为负数;如果是的话则打印一个符号。否则,它以n的负绝对值来调用printneg()。我们同时改变了printneg()的函数体来适应n永远是负数或零这一事实。我们得到什么?我们使用n / 10和n % 10来获取n的前导数字和结尾数字(经过适当的符号变换)。调用整数除法的行为在其中一个操作数为负的时候是实现相关的。因此,n % 10有可能是正的!这时,-(n % 10)是正数,将会超出我们的数字字符数组的末尾。为了解决这一问题,我们建立两个临时变量来存放商和余数。作完除法后,我们检查余数是否在正确的范围内,如果不是的话则调整这两个变量。printnum()没有改变,因此我们只列出printneg(): void printneg(long n, void(*p)()){ long q;int r;if(r > 0){ r-= 10;q++;} if(n <=-10){ printneg(q, p);}(*p)(“0123456789”[-r]);} 8 这里是空闲空间 还有很多可能让C程序员误入迷途的地方本文没有提到。如果你发现了,请联系作者。在以后的版本中它会被包含进来,并添加一个表示感谢的脚注。参考 《The C Programming Language》(Kernighan and Ritchie, Prentice-Hall 1978)是最具权威的C著作。它包含了一个优秀的教程,面向那些熟悉其他高级语言程序设计的人,和一个参考手册,简洁地描述了整个语言。尽管自1978年以来这门语言发生了不少变化,这本书对于很多主题来说仍然是个定论。这本书同时还包含了本文中多次提到的“C语言参考手册”。《The C Puzzle Book》(Feuer, Prentice-Hall, 1982)是一本少见的磨炼人们文法能力的书。这本书收集了很多谜题(和答案),它们的解决方法能够测试读者对于C语言精妙之处的知识。《C: A Referenct Manual》(Harbison and Steele, Prentice Hall 1984)是特意为实现者编写的一本参考资料。其他人也会发现它是特别有用的——因为他能从中参考细节。------------------脚注 1.这本书是基于图书《C Traps and Pitfalls》(Addison-Wesley, 1989, ISBN 0-201-17928-8)的一个扩充,有兴趣的读者可以读一读它。2.因为!=的结果不是1就是0。3.感谢Guy Harris为我指出这个问题。4.Dennis Ritchie和Steve Johnson同时向我指出了这个问题。5.感谢一位不知名的志愿者提出这个问题。6.感谢Richard Stevens指出了这个问题。7.一些C编译器要求每个外部对象仅有一个定义,但可以有多个声明。使用这样的编译器时,我们何以很容易地将一个声明放到一个包含文件中,并将其定义放到其它地方。这意味着每个外部对象的类型将出现两次,但这比出现多于两次要好。8.分离函数参数用的逗号不是逗号运算符。例如在f(x, y)中,x和y的获取顺序是未定义的,但在g((x, y))中不是这样的。其中g只有一个参数。它的值是通过对x进行求值、抛弃这个值、再对y进行求值来确定的。9.预处理器还可以很容易地组织这样的显式常量以能够方便地找到它们。10.PDP-11和VAX-11是数组设备集团(DEC)的商标。
第二篇:C语言陷阱和缺陷
0 简介
C语言及其典型实现被设计为能被专家们容易地使用。这门语言简洁并附有表达力。但有一些限制可以保护那些浮躁的人。一个浮躁的人可以从这些条款中获得一些帮助。
在本文中,我们将会看到这些未可知的益处。正是由于它的未可知,我们无法为其进行完全的分类。不过,我们仍然通过研究为了一个C程序的运行所需要做的事来做到这些。我们假设读者对C语言至少有个粗浅的了解。
第一部分研究了当程序被划分为记号时会发生的问题。第二部分继续研究了当程序的记号被编译器组合为声明、表达式和语句时会出现的问题。第三部分研究了由多个部分组成、分别编译并绑定到一起的C程序。第四部分处理了概念上的误解:当一个程序具体执行时会发生的事情。第五部分研究了我们的程序和它们所使用的常用库之间的关系。在第六部分中,我们注意到了我们所写的程序也许并不是我们所运行的程序;预处理器将首先运行。最后,第七部分讨论了可移植性问题:一个能在一个实现中运行的程序无法在另一个实现中运行的原因。词法缺陷
编译器的第一个部分常被称为词法分析器(lexical analyzer)。词法分析器检查组成程序的字符序列,并将它们划分为记号(token)一个记号是一个由一个或多个字符构成的序列,它在语言被编译时具有一个(相关地)统一的意义。在C中,例如,记号->的意义和组成它的每个独立的字符具有明显的区别,而且其意义独立于->出现的上下文环境。
另外一个例子,考虑下面的语句:
if(x > big)big = x;
该语句中的每一个分离的字符都被划分为一个记号,除了关键字if和标识符big的两个实例。
事实上,C程序被两次划分为记号。首先是预处理器读取程序。它必须对程序进行记号划分以发现标识宏的标识符。它必须通过对每个宏进行求值来替换宏调用。最后,经过宏替换的程序又被汇集成字符流送给编译器。编译器再第二次将这个流划分为记号。
在这一节中,我们将探索对记号的意义的普遍的误解以及记号和组成它们的字符之间的关系。稍后我们将谈到预处理器。
1.1 = 不是==
从Algol派生出来的语言,如Pascal和Ada,用:=表示赋值而用=表示比较。而C语言则是用=表示赋值而用==表示比较。这是因为赋值的频率要高于比较,因此为其分配更短的符号。
此外,C还将赋值视为一个运算符,因此可以很容易地写出多重赋值(如a = b = c),并且可以将赋值嵌入到一个大的表达式中。
这种便捷导致了一个潜在的问题:可能将需要比较的地方写成赋值。因此,下面的语句好像看起来是要检查x是否等于y:
if(x = y)
foo();
而实际上是将x设置为y的值并检查结果是否非零。再考虑下面的一个希望跳过空格、制表符和换行符的循环:
while(c == ' ' || c = '/t' || c == '/n')
c = getc(f);
在与'/t'进行比较的地方程序员错误地使用=代替了==。这个“比较”实际上是将'/t'赋给c,然后判断c的(新的)值是否为零。因为'/t'不为零,这个“比较”将一直为真,因此这个循环会吃尽整个文件。这之后会发生什么取决于特定的实现是否允许一个程序读取超过文件尾部的部分。如果允许,这个循环会一直运行。
一些C编译器会对形如e1 = e2的条件给出一个警告以提醒用户。当你确实需要先对一个变量进行赋值之后再检查变量是否非零时,为了在这种编译器中避免警告信息,应考虑显式给出比较符。换句话说,将: if(x = y)
foo();改写为:
if((x = y)!= 0)
foo();
这样可以清晰地表示你的意图。
1.2 & 和 | 不是 && 和||
容易将==错写为=是因为很多其他语言使用=表示比较运算。其他容易写错的运算符还有&和&&,以及|和||,这主要是因为C语言中的&和|运算符于其他语言中具有类似功能的运算符大为不同。我们将在第4节中贴近地观察这些运算符。
1.3 多字符记号
一些C记号,如/、*和=只有一个字符。而其他一些C记号,如/*和==,以及标识符,具有多个字符。当C编译器遇到紧连在一起的/和*时,它必须能够决定是将这两个字符识别为两个分离的记号还是一个单独的记号。C语言参考手册说明了如何决定:“如果输入流到一个给定的字符串为止已经被识别为记号,则应该包含下一个字符以组成能够构成记号的最长的字符串”([译注]即通常所说的“最长子串原则”)。因此,如果/是一个记号的第一个字符,并且/后面紧随了一个*,则这两个字符构成了注释的开始,不管其他上下文环境。
下面的语句看起来像是将y的值设置为x的值除以p所指向的值: y = x/*p
/* p 指向除数 */;
实际上,/*开始了一个注释,因此编译器简单地吞噬程序文本,直到*/的出现。换句话说,这条语句仅仅把y的值设置为x的值,而根本没有看到p。将这条语句重写为: y = x / *p
/* p 指向除数 */;或者干脆是
y = x /(*p)
/* p指向除数 */;它就可以做注释所暗示的除法了。
这种模棱两可的写法在其他环境中就会引起麻烦。例如,老版本的C使用=+表示现在版本中的+=。这样的编译器会将 a=-1;视为 a =-1;或
a = a> a
是不合法的。它和
p-> a
不是同义词。
另一方面,有些老式编译器还是将=+视为一个单独的记号并且和+=是同义词。
1.5 字符串和字符
单引号和双引号在C中的意义完全不同,在一些混乱的上下文中它们会导致奇怪的结果而不是错误消息。
包围在单引号中的一个字符只是编写整数的另一种方法。这个整数是给定的字符在实现的对照序列中的一个对应的值。因此,在一个ASCII实现中,'a'和0141或97表示完全相同的东西。而一个包围在双引号中的字符串,只是编写一个有双引号之间的字符和一个附加的二进制值为零的字符所初始化的一个无名数组的指针的一种简短方法。
下面的两个程序片断是等价的:
printf(“Hello world/n”);
char hello[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '/n', 0 };printf(hello);
使用一个指针来代替一个整数通常会得到一个警告消息(反之亦然),使用双引号来代替单引号也会得到一个警告消息(反之亦然)。但对于不检查参数类型的编译器却除外。因此,用 printf('/n');来代替 printf(“/n”);通常会在运行时得到奇怪的结果。([译注]提示:正如上面所说,'/n'表示一个整数,它被转换为了一个指针,这个指针所指向的内容是没有意义的。)
由于一个整数通常足够大,以至于能够放下多个字符,一些C编译器允许在一个字符常量中存放多个字符。这意味着用'yes'代替“yes”将不会被发现。后者意味着“分别包含y、e、s和一个空字符的四个连续存储器区域中的第一个的地址”,而前者意味着“在一些实现定义的样式中表示由字符y、e、s联合构成的一个整数”。这两者之间的任何一致性都纯属巧合。句法缺陷
要理解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的幂是不行的[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)++。
在接下来是真正的二元运算符。其中数学运算符具有最高的优先级,然后是移位运算符、关系运算符、逻辑运算符、赋值运算符,最后是条件运算符。需要记住的两个重要的东西是:
所有的逻辑运算符具有比所有关系运算符都低的优先级。
移位运算符比关系运算符绑定得更紧密,但又不如数学运算符。
在这些运算符类别中,有一些奇怪的地方。乘法、除法和求余具有相同的优先级,加法和减法具有相同的优先级,以及移位运算符具有相同的优先级。
还有就是六个关系运算符并不具有相同的优先级:==和!=的优先级比其他关系运算符要低。这就允许我们判断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相等。而实际的效果却大不相同[3]。
C中的逻辑运算符的优先级具有历史原因。B语言——C的前辈——具有和C中的&和|运算符对应的逻辑运算符。尽管它们的定义是按位的,但编译器在条件判断上下文中将它们视为和&&和||一样。当在C中将它们分开后,优先级的改变是很危险的[4]。
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将被定义为具有默认的整型返回值[5]。
2.4 switch语句
通常C中的switch语句中的case段可以进入下一个。例如,考虑下面的C和Pascal程序片断:
switch(color){ case 1: printf(“red”);
break;case 2: printf(“yellow”);
break;case 3: printf(“blue”);
break;}
case color of 1: write('red');2: write('yellow');3: write('blue');end
这两个程序片段都作相同的事情:根据变量color的值是1、2还是3打印red、yellow或blue(没有新行符)。这两个程序片段非常相似,只有一点不同:Pascal程序中没有C中相应的break语句。C中的case标签是真正的标签:控制流程可以无限制地进入到一个case标签中。
看看另一种形式,假设C程序段看起来更像Pascal:
switch(color){ case 1: printf(“red”);case 2: printf(“yellow”);case 3: printf(“blue”);}
并且假设color的值是2。则该程序将打印yellowblue,因为控制自然地转入到下一个printf()的调用。
这既是C语言switch语句的优点又是它的弱点。说它是弱点,是因为很容易忘记一个break语句,从而导致程序出现隐晦的异常行为。说它是优点,是因为通过故意去掉break语句,可以很容易实现其他方法难以实现的控制结构。尤其是在一个大型的switch语句中,我们经常发现对一个case的处理可以简化其他一些特殊的处理。
例如,设想有一个程序是一台假想的机器的翻译器。这样的一个程序可能包含一个switch语句来处理各种操作码。在这样一台机器上,通常减法在对其第二个运算数进行变号后就变成和加法一样了。因此,最好可以写出这样的语句:
case SUBTRACT:
opnd2 =-opnd2;
/* no break;*/ case ADD:
...另外一个例子,考虑编译器通过跳过空白字符来查找一个记号。这里,我们将空格、制表符和新行符视为是相同的,除了新行符还要引起行计数器的增长外: case '/n':
linecount++;
/* no break */ case '/t': case ' ':
...2.5 函数调用
和其他程序设计语言不同,C要求一个函数调用必须有一个参数列表,但可以没有参数。因此,如果f是一个函数,f();
就是对该函数进行调用的语句,而
f;
什么也不做。它会作为函数地址被求值,但不会调用它[6]。
2.6 悬挂else问题
在讨论任何语法缺陷时我们都不会忘记提到这个问题。尽管这一问题不是C语言所独有的,但它仍然伤害着那些有着多年经验的C程序员。
考虑下面的程序片断:
if(x == 0)
if(y == 0)error();else {
z = x + y;
f(&z);}
写这段程序的程序员的目的明显是将情况分为两种:x = 0和x!= 0。在第一种情况中,程序段什么都不做,除非y = 0时调用error()。第二种情况中,程序设置z = x + y并以z的地址作为参数调用f()。
然而,这段程序的实际效果却大为不同。其原因是一个else总是与其最近的if相关联。如果我们希望这段程序能够按照实际的情况运行,应该这样写:
if(x == 0){
if(y == 0)
error();
else {
z = x + y;
f(&z);
} }
换句话说,当x!= 0发生时什么也不做。如果要达到第一个例子的效果,应该写: if(x == 0){
if(y ==0)
error();} else {
z = z + y;
f(&z);} 3 连接
一个C程序可能有很多部分组成,它们被分别编译,并由一个通常称为连接器、连接编辑器或加载器的程序绑定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现的错误。
在这一节中,我们将看到一些这种类型的错误。有一些C实现,但不是所有的,带有一个称为lint的程序来捕获这些错误。如果具有一个这样的程序,那么无论怎样地强调它的重要性都不过分。
3.1 你必须自己检查外部类型
假设你有一个C程序,被划分为两个文件。其中一个包含如下声明: int n;
而令一个包含如下声明:
long n;
这不是一个有效的C程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一个文件的内容。因此,检查类型的工作只能由连接器(或一些工具程序如lint)来完成;如果操作系统的连接器不能识别数据类型,C编译器也没法过多地强制它。
那么,这个程序运行时实际会发生什么?这有很多可能性:
实现足够聪明,能够检测到类型冲突。则我们会得到一个诊断消息,说明n在两个文件中具有不同的类型。
你所使用的实现将int和long视为相同的类型。典型的情况是机器可以自然地进行32位运算。在这种情况下你的程序或许能够工作,好象你两次都将变量声明为long(或int)。但这种程序的工作纯属偶然。
n的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有效。这可能发生,例如,编译器可以将int安排在long的低位。不论这是基于系统的还是基于机器的,这种程序的运行同样是偶然。
n的两个实例以另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在这种情况下,程序可能失败。
这种情况发生的里一个例子出奇地频繁。程序的某一个文件包含下面的声明: char filename[] = “etc/passwd”;而另一个文件包含这样的声明:
char *filename;
尽管在某些环境中数组和指针的行为非常相似,但它们是不同的。在第一个声明中,filename是一个字符数组的名字。尽管使用数组的名字可以产生数组第一个元素的指针,但这个指针只有在需要的时候才产生并且不会持续。在第二个声明中,filename是一个指针的名字。这个指针可以指向程序员让它指向的任何地方。如果程序员没有给它赋一个值,它将具有一个默认的0值(NULL)([译注]实际上,在C中一个为初始化的指针通常具有一个随机的值,这是很危险的!)。
这两个声明以不同的方式使用存储区,它们不可能共存。
避免这种类型冲突的一个方法是使用像lint这样的工具(如果可以的话)。为了在一个程序的不同编译单元之间检查类型冲突,一些程序需要一次看到其所有部分。典型的编译器无法完成,但lint可以。
避免该问题的另一种方法是将外部声明放到包含文件中。这时,一个外部对象的类型仅出现一次[7]。语义缺陷
一个句子可以是精确拼写的并且没有语法错误,但仍然没有意义。在这一节中,我们将会看到一些程序的写法会使得它们看起来是一个意思,但实际上是另一种完全不同的意思。
我们还要讨论一些表面上看起来合理但实际上会产生未定义结果的环境。我们这里讨论的东西并不保证能够在所有的C实现中工作。我们暂且忘记这些能够在一些实现中工作但可能不能在另一些实现中工作的东西,直到第7节讨论可以执行问题为止。
4.1 表达式求值顺序
一些C运算符以一种已知的、特定的顺序对其操作数进行求值。但另一些不能。例如,考虑下面的表达式:
a < b && c < d
C语言定义规定a < b首先被求值。如果a确实小于b,c < d必须紧接着被求值以计算整个表达式的值。但如果a大于或等于b,则c < d根本不会被求值。
要对a < b求值,编译器对a和b的求值就会有一个先后。但在一些机器上,它们也许是并行进行的。
C中只有四个运算符&&、||、?:和,指定了求值顺序。&&和||最先对左边的操作数进行求值,而右边的操作数只有在需要的时候才进行求值。而?:运算符中的三个操作数:a、b和c,最先对a进行求值,之后仅对b或c中的一个进行求值,这取决于a的值。,运算符首先对左边的操作数进行求值,然后抛弃它的值,对右边的操作数进行求值[8]。
C中所有其它的运算符对操作数的求值顺序都是未定义的。事实上,赋值运算符不对求值顺序做出任何保证。
出于这个原因,下面这种将数组x中的前n个元素复制到数组y中的方法是不可行的: i = 0;while(i < n)
y[i] = x[i++];
其中的问题是y[i]的地址并不保证在i增长之前被求值。在某些实现中,这是可能的;但在另一些实现中却不可能。另一种情况出于同样的原因会失败: i = 0;while(i < n)
y[i++] = x[i];
而下面的代码是可以工作的: i = 0;while(i < n){
y[i] = x[i];
i++;}
当然,这可以简写为: for(i = 0;i < n;i++)
y[i] = x[i];4.2 &&、||和!运算符
C中有两种逻辑运算符,在某些情况下是可以交换的:按位运算符&、|和~,以及逻辑运算符&&、||和!。一个程序员如果用某一类运算符替换相应的另一类运算符会得到某些奇怪的效果:程序可能会正确地工作,但这纯属偶然。
&&、||和!运算符将它们的参数视为仅有“真”或“假”,通常约定0代表“假”而其它的任意值都代表“真”。这些运算符返回1表示“真”而返回0表示“假”,而且&&和||运算符当可以通过左边的操作数确定其返回值时,就不会对右边的操作数进行求值。
因此!10是零,因为10非零;10 && 12是1,因为10和12都非零;10 || 12也是1,因为10非零。另外,最后一个表达式中的12不会被求值,10 || f()中的f()也不会被求值。
考虑下面这段用于在一个表中查找一个特定元素的程序:
i = 0;while(i < tabsize && tab[i]!= x)
i++;
这段循环背后的意思是如果i等于tabsize时循环结束,元素未被找到。否则,i包含了元素的索引。
假设这个例子中的&&不小心被替换为了&,这个循环可能仍然能够工作,但只有两种幸运的情况可以使它停下来。
首先,这两个操作都是当条件为假时返回0,当条件为真时返回1。只要x和y都是1或0,x & y和x && y都具有相同的值。然而,如果当使用了除1之外的非零值表示“真”时互换了这两个运算符,这个循环将不会工作。
其次,由于数组元素不会改变,因此越过数组最后一个元素前进一个位置时是无害的,循环会幸运地停下来。失误的程序会越过数组的结尾,因为&不像&&,总是会对所有的操作数进行求值。因此循环的最后一次获取tab[i]时i的值已经等于tabsize了。如果tabsize是tab中元素的数量,则会取到tab中不存在的一个值。
4.3 下标从零开始
在很多语言中,具有n个元素的数组其元素的号码和它的下标是从1到n严格对应的。但在C中不是这样。
一个具有n个元素的C数组中没有下标为n的元素,其中的元素的下标是从0到n'a';
return c;}
在很多C实现中,为了减少比实际计算还要多的调用开销,通常将其实现为宏:
#define toupper(c)((c)>= 'a' &&(c)<= 'z' ?(c)+('A''a')#define tolower(c)((c)+ 'A''a' :(c))#define tolower(c)((c)>= 'A' &&(c)<= 'Z' ?(c)+ 'a''a';
return c;}
tolower()类似。
这个改变带来更多的问题,每次使用这些函数的时候都会引入函数调用开销。我们的英雄认为一些人可能不愿意支付这些开销,因此他们将这个宏重命名为:
#define _toupper(c)((c)+ 'A''A')这就允许用户选择方便或速度。
这里面其实只有一个问题:伯克利的人们和其他的C实现者并没有跟着这么做。这意味着一个在AT&T系统上编写的使用了toupper()或tolower()的程序,如果没有为其传递正确大小写字母参数,在其他C实现中可能不会正常工作。
如果不知道这些历史,可能很难对这类错误进行跟踪。
7.8 先释放,再重新分配
很多C实现为用户提供了三个内存分配函数:malloc()、realloc()和free()。调用malloc(n)返回一个指向有n个字符的新分配的内存的指针,这个指针可以由程序员使用。给free()传递一个指向由malloc()分配的内存的指针可以使这块内存得以再次使用。通过一个指向已分配区域的指针和一个新的大小调用realloc()可以将这块内存扩大或缩小到新尺寸,这个过程中可能要复制内存。
也许有人会想,真相真是有点微妙啊。下面是System V接口定义中出现的对realloc()的描述:
realloc改变一个由ptr指向的size个字节的块,并返回该块(可能被移动)的指针。在新旧尺寸中比较小的一个尺寸之下的内容不会被改变。
而UNIX系统第七版的参考手册中包含了这一段的副本。此外,还包含了描述realloc()的另外一段:
如果在最后一次调用malloc、realloc或calloc后释放了ptr所指向的块,realloc依旧可以工作;因此,free、malloc和realloc的顺序可以利用malloc压缩存贮的查找策略。
因此,下面的代码片段在UNIX第七版中是合法的:
free(p);p = realloc(p, newsize);
这一特性保留在从UNIX第七版衍生出来的系统中:可以先释放一块存储区域,然后再重新分配它。这意味着,在这些系统中释放的内存中的内容在下一次内存分配之前可以保证不变。因此,在这些系统中,我们可以用下面这种奇特的思想来释放一个链表中的所有元素: for(p = head;p!= NULL;p = p->next)
free((char *)p);
而不用担心调用free()会导致p->next不可用。
不用说,这种技术是不推荐的,因为不是所有C实现都能在内存被释放后将它的内容保留足够长的时间。然而,第七版的手册遗留了一个未声明的问题:realloc()的原始实现实际上是必须要先释放再重新分配的。出于这个原因,一些C程序都是先释放内存再重新分配的,而当这些程序移植到其他实现中时就会出现问题。
7.9 可移植性问题的一个实例
让我们来看一个已经被很多人在很多时候解决了的问题。下面的程序带有两个参数:一个长整数和一个函数(的指针)。它将整数转换位十进制数,并用代表其中每一个数字的字符来调用给定的函数。
void printnum(long n, void(*p)()){
if(n < 0){
(*p)('-');
n =-n;
}
if(n >= 10)
printnum(n / 10, p);
(*p)(n % 10 + '0');}
这个程序非常简单。首先检查n是否为负数;如果是,则打印一个符号并将n变为正数。接下来,测试是否n >= 10。如果是,则它的十进制表示中包含两个或更多个数字,因此我们递归地调用printnum()来打印除最后一个数字外的所有数字。最后,我们打印最后一个数字。
这个程序——由于它的简单——具有很多可移植性问题。首先是将n的低位数字转换成字符形式的方法。用n % 10来获取低位数字的值是好的,但为它加上'0'来获得相应的字符表示就不好了。这个加法假设机器中顺序的数字所对应的字符数顺序的,没有间隔,因此'0' + 5和'5'的值是相同的,等等。尽管这个假设对于ASCII和EBCDIC字符集是成立的,但对于其他一些机器可能不成立。避免这个问题的方法是使用一个表:
void printnum(long n, void(*p)()){
if(n < 0){
(*p)('-');
n =-n;
}
if(n >= 10)
printnum(n / 10, p);
(*p)(“0123456789”[n % 10]);}
另一个问题发生在当n < 0时。这时程序会打印一个负号并将n设置为-n。这个赋值会发生溢出,因为在使用2的补码的机器上通常能够表示的负数比正数要多。例如,一个(长)整数有k位和一个附加位表示符号,则-2k可以表示而2k却不能。
解决这一问题有很多方法。最直观的一种是将n赋给一个unsigned long值。然而,一些C便一起可能没有实现unsigned long,因此我们来看看没有它怎么办。
在第一个实现和第二个实现的机器上,改变一个正整数的符号保证不会发生溢出。问题仅出在改变一个负数的符号时。因此,我们可以通过避免将n变为正数来避免这个问题。
当然,一旦我们打印了负数的符号,我们就能够将负数和正数视为是一样的。下面的方法就强制在打印符号之后n为负数,并且用负数值完成我们所有的算法。如果我们这么做,我们就必须保证程序中打印符号的部分只执行一次;一个简单的方法是将这个程序划分为两个函数: void printnum(long n, void(*p)()){
if(n < 0){
(*p)('-');
printneg(n, p);
}
else
printneg(-n, p);}
void printneg(long n, void(*p)()){
if(n <=-10)
printneg(n / 10, p);
(*p)(“0123456789”[-(n % 10)]);}
printnum()现在只检查要打印的数是否为负数;如果是的话则打印一个符号。否则,它以n的负绝对值来调用printneg()。我们同时改变了printneg()的函数体来适应n永远是负数或零这一事实。
我们得到什么?我们使用n / 10和n % 10来获取n的前导数字和结尾数字(经过适当的符号变换)。调用整数除法的行为在其中一个操作数为负的时候是实现相关的。因此,n % 10有可能是正的!这时,-(n % 10)是负数,将会超出我们的数字字符数组的末尾。
为了解决这一问题,我们建立两个临时变量来存放商和余数。作完除法后,我们检查余数是否在正确的范围内,如果不是的话则调整这两个变量。printnum()没有改变,因此我们只列出printneg():
void printneg(long n, void(*p)()){
long q;
int r;
if(r > 0){
r-= 10;
q++;
}
if(n <=-10){
printneg(q, p);
}
(*p)(“0123456789”[-r]);} 这里是空闲空间
还有很多可能让C程序员误入迷途的地方本文没有提到。如果你发现了,请联系作者。在以后的版本中它会被包含进来,并添加一个表示感谢的脚注。
参考
《The C Programming Language》(Kernighan and Ritchie, Prentice-Hall 1978)是最具权威的C著作。它包含了一个优秀的教程,面向那些熟悉其他高级语言程序设计的人,和一个参考手册,简洁地描述了整个语言。尽管自1978年以来这门语言发生了不少变化,这本书对于很多主题来说仍然是个定论。这本书同时还包含了本文中多次提到的“C语言参考手册”。
《The C Puzzle Book》(Feuer, Prentice-Hall, 1982)是一本少见的磨炼人们文法能力的书。这本书收集了很多谜题(和答案),它们的解决方法能够测试读者对于C语言精妙之处的知识。
《C: A Referenct Manual》(Harbison and Steele, Prentice Hall 1984)是特意为实现者编写的一本参考资料。其他人也会发现它是特别有用的——因为他能从中参考细节。
脚注
1.这本书是基于图书《C Traps and Pitfalls》(Addison-Wesley, 1989, ISBN 0-201-17928-8)的一个扩充,有兴趣的读者可以读一读它。
2.因为!=的结果不是1就是0。
3.感谢Guy Harris为我指出这个问题。
4.Dennis Ritchie和Steve Johnson同时向我指出了这个问题。
5.感谢一位不知名的志愿者提出这个问题。
6.感谢Richard Stevens指出了这个问题。
7.一些C编译器要求每个外部对象仅有一个定义,但可以有多个声明。使用这样的编译器时,我们何以很容易地将一个声明放到一个包含文件中,并将其定义放到其它地方。这意味着每个外部对象的类型将出现两次,但这比出现多于两次要好。
8.分离函数参数用的逗号不是逗号运算符。例如在f(x, y)中,x和y的获取顺序是未定义的,但在g((x, y))中不是这样的。其中g只有一个参数。它的值是通过对x进行求值、抛弃这个值、再对y进行求值来确定的。
9.预处理器还可以很容易地组织这样的显式常量以能够方便地找到它们。
10.PDP-11和VAX-11是数组设备集团(DEC)的商标。
本文来自CSDN
博
客,转
载
请
标
明
出
处
:http://blog.csdn.net/milan25429688/archive/2005/03/24/328944.aspx#contents
第三篇:语言的功能和陷阱
n
语言的功能和陷阱
n
王蒙
n
一、焦点问题
Ø
思考语言的社会功能问题,以及由语言而引发的社会问题。
Ø
演讲的基本特点和要求。
n
二、王蒙其人
王蒙,1934年生于北京
n
5岁上小学。
n
10岁时跳级考入中学。
n
1948年,14岁,王蒙参加地下党。
n
1949年,15岁调入新民主主义青年团(后改名为共产主义青年团)北京市委工作。
n
1953年,19岁,长篇小说《青春万岁》获得成功。
n
1956年,参加全国第一届青年作者会议。
n
1956年秋,发表《组织部来了个年轻人》,引起极大反响。
n
1958年,24岁,被错划为右派。
n
1958年,赴北京郊区劳动。
n
1962年,赴新疆劳动。
n
1963年起在伊犁地区农村劳动多年。
n
n
三、王蒙成就
n
其间曾任自治区文联编辑、维吾尔语翻译。1979年调回北京,任北京市文联专业作家。中国作家协会副主席。
n
自20世纪50年代以来,发表作品共一千余万字。
n
被翻译成英、法、德、俄、日、韩、意、西班牙、等二十余种语言文字。
n
曾获意大利蒙德罗文学奖、日本创作学会和平与文化奖。
n
学术著作《〈红楼梦〉启示录》。
n
担任十余所大学教授、名誉教授、顾问。
n
曾应邀访问世界各大洲四十多个国家。曾任哈佛大学燕京学院特邀访问学者、美国三一学院校长级学者
n
三、文本分析
n
为什么关注语言?
“语言是存在的揭示、澄明、到达”
“语言是存在的家,人就居住在这家中”。
——海德格尔
王蒙这篇演讲的前一部分主要讲述语言、尤其是文学语言的基本功能。
Ø
(一)王蒙提出语言的三种功能:
Ø
现实有用的功能;
Ø
生发和促进的功能(推进思想、推进感情、推进文化、创造文化);
Ø
浪漫的功能(语言和文字离开了现实或者超出了现实的功能)。
Ø
(二)如何理解王蒙的观点:
Ø
①
语言创造了人:(反对语言工具论)提倡语言本体论,如“皎洁”。
Ø
②
没有语言就没有记忆:(反对语言交际论)提倡语言文化论,如“我们俩困觉”。
Ø
③
语言的审美化:(反对语言反映论)提倡语言形象论,如“吃葡萄”。
Ø
(三)王蒙这篇演讲的后一部分主要讲述语言的陷阱。
语言的陷阱:
Ø
语言和现实和你的思想感情脱节;
Ø
脱离生活,变成反面的东西;
Ø
异化、狗屎化效应、被语言文字主宰,扼杀创造性,扼杀活泼的生机。
n
四、怎样认识语言的功能和陷阱
Ø
(1)语言决定、生产意义。
Ø
(2)语言是思想的物质现实:维特根斯坦说,我的语言的局限就是我的世界的局限。例如,现代“时间”是一种空间化的隐喻,“自……以来”;但是,在美国印第安的霍皮族那里,没有昨天、今天、明天的概念。同样,不同的语言体系生产出不同的“宇宙”,也就有了不同的生命观、宇宙观和哲学。
Ø
(3)语言限定体验:语言在瞬间体验中起着决定性的作用,历史性的养成人们感受的习性,如明月、流水;也横向地限定了人们的体验能力和方式,如月色、五味。
Ø
(4)语言本身也可以是美的形象。
Ø
(5)语言可以“修改”现实:王蒙是一个经历了“反右”和十年“文化大革命”的作家,特殊的经历和遭遇使他对“语言”的负面功能有着特殊的认识。他说,“语言文字可以反过来主宰我们,扼杀我们的创造性,扼杀我们活泼的生机”。
n
五、“概念恐惧”与语言的权利
存在主义哲学家基尔克郭尔提出“概念恐惧”认为,“恐惧”和“畏惧”不同,前者是对没有具体对象的恐惧。在十年“文化大革命”中,很多语言就是这样造成一种“恐惧”,这些语言并没有创造“实体”,比如“地富反坏右”、“牛鬼蛇神”等。“一个青年在街上走”,这说明了语言本身隐藏着权力,影响我们的认识和思考。事实上,有“语言”的地方,就存在着权力的妥协、对立和斗争,就存在着“扼杀”和对“扼杀”的反抗。
n
六、再谈谈讲演
讲演稿也叫演说词,是在较隆重的集会和会议上发表的讲话文稿。可以用来交流思想、感情,表达主张、见解,具有宣传、鼓动和教育作用。
演讲是一种沟通。在古代希腊,演讲被称之为“诱动术”。这包含了三个意思:
(1)广场性:利用话语修辞,调动公众情绪的相互感染;
(2)单向性:含有表演性质的独白话语行为;
(3)
共谋性:演讲是一种修辞性的“共谋”策略的实施。
七、王蒙讲演的风格:
外松内紧
亦庄亦谐
取譬引喻
n
八、思考与讨论
l
找出表现演讲者机智的句子。
l
怎样区分演讲中的幽默与噱头?
l
举例说明演讲者是怎样不断调动、活跃场内气氛的。
l
你同意演讲者关于“语言陷阱”的观点吗?为什么?
第四篇:面试中要注意的语言陷阱与应答技巧
就业面试经典问题及最佳答案 工作动机 个人愿望篇
自考生就业面试经典问题”见招拆招“
工作动机、个人愿望·
请给我们谈谈你自己的一些情况·你是哪年出生的?你是哪所大学毕业的?问题:请给我们谈谈你自己的一些情况
回答:简要的描述你的相关工作经历以及你的一些特征,包括与人相处的能力和个人的性格特征。如果你一下子不能够确定面试者到底需要什么样的内容,你可以这样说:”有没有什么您特别感兴趣的范围?“
点评:企业以此来判断是否应该聘用你。通过你的谈论,可以看出你想的是如何为公司效力还是那些会影响工作的个人问题。当然,还可以知道你的一些背景。
·请谈一下你对公司的看法,为什么你想来工作?
问题:你是哪年出生的?你是哪所大学毕业的?等等
回答:我是XXXX年出生的。我是XX大学毕业的。
点评:这类问题至为关键的是要针对每个问题简洁明了的回答,不可拖泥带水,也 不必再加什么说明。完全不必再画蛇添足的说”我属X,今年XX岁“之类的话。至于专业等或许主考官接下来的问题就是针对此而言的,故而不必迫不及待和盘托出。
·你认为对你来说现在找一份工作是不是不太容易?
问题:你认为对你来说现在找一份工作是不是不太容易,或者你很需要这份工作?
回答:
1.是的。
2.我看不见得。
点评:
一般按1回答,一切便大功告成。
有些同学为了显示自己的”不卑不亢“,强调个人尊严,故按2回答。结果,用人单位打消了录用该生的念头,理由是:”此人比较傲“一句话,断送了该生一次较好的就业机会。
·你是怎么应聘到我们公司的?
问题:你是怎么应聘到我们公司的?
回答:贵公司是国际上有名的汽车工业公司,虽然我学的专业不是汽车专业,但我一直留意、关心贵公司的发展,特别是贵公司注重对员工的培训,更让我心动,另外象贵公司这样大的企业,我想是各种专业人才都需要的,便毅然前来应聘。
点评:该毕业生的专业虽然不是该公司紧缺的专业,但他分析了公司招聘职位的具体要求,认为可以应试该公司的某一种职位要求。(如管理、营销、秘书),如食品工程专业的求职面远不只局限于食品的加工企业,可延伸至饮品、酒类、保健品、调味品甚至酒楼等多个行业。都会有适合自己的职位。·请你谈谈对我单位的看法·
问题:请你谈谈对我单位的看法
回答:我对贵单位还没什么了解,故谈不出看法
点评:象这样的回答,一般面试不成功多,如你很想进入该单位,就不妨实地去单位”侦察“一番,或收集有关的资料。如有一位毕业生,他有意去国家进出口银行工作,便通过朋友的关系弄到了一本进出口银行的基本业务材料,从而在面试中对答如流,赢得了招聘单位的赏识。并能以自身的优势来说明为何应聘这工作,做到有的防矢,给主考官留下了深刻的印象。因此,收集资料,了解单位,可以帮助求职者认清主要方向,更精确,更客观地审视主聘单位,选择适合自己发展的单位,避免走弯路。
你完全可以到大公司任职,你怎么想到我们小企业?
问题:以你的资历条件,完全可以到大公司任职,你怎么想到我们小企业?回答:
1.哎,没办法,一时没有应聘到大企业,况且,毕业时间又到了,否则只能回当地就业,因此先就业再说。
2.小企业有他自己的优势,在用人方面非常重视,自己虽然资历条件尚可,我想,在你们这样的企业更能发挥自己的作用。
点评:一个还未工作就想以后跳槽的员工,是无论如何不能指望他尽心尽力的干好工作的,因此,即使有此想法,也不能说出来,说不定工作后受到企业重用,本人的作用也发挥的特别好,而不想再走了呢?
·你为什么希望到我们公司工作?
问题:你为什么希望到我们公司工作?
回答:我觉得贵公司力量雄厚,领导得力,上下一心,适于一切有才干的人发展。
忌:”我是学电子的,我到这里才是专业对口。“看情况而定。
”我来这里上班离家近。“
”我喜欢你们这儿。“
”听说你们公司月薪较高。“
点评:回答问题要从对方入题,引起对方好感,使对方感到你能尊重,关心公司的需要,愿为公司尽微薄之力。
·如果公司录用你,你最希望在哪个部门工作?
问题:如果本公司录用你,你最希望在哪个部门工作?
回答:
忌:”到哪个部门都行“
应:”本人希望 到XX部门,但也很乐意接受公司的其他安排。
点评:不要说得太随意,太肯定。比较稳妥的办法是首先表明自己的志向和兴趣,再表示服从安排。
·你愿意被外派工作吗?你愿意经常出差吗?
问题:你愿意被外派工作吗?你愿意经常出差吗?
回答:愿意,反正我无牵无挂,到哪儿工作都可以。
点评:这是主试者通过提问来透露他要找的是什么样的人,此信息已经很明白地告诉你,他所期待的回答是什么。对于此类问题应聘者留意倾听。从“话中之话”中找出应试者实际需要的线索。
我怎样相信对这个职位你是最好的人选呢?
问题:我怎样相信对这个职位你是最好的人选呢?
回答:根据这个职位的性质和我们刚才的谈话,我推断你需要的是工作积极的人,能够设定目标,不惧怕挑战的人。我就具有这些品质,让我再告诉你一些我在校时的经历,它们能说明我确实是你所需要的最好的人选。
点评:设身处地替面试官想一想,考虑一下招聘者需要什么样的人,你又在哪些方面符合他们的要求。根据要求,谈出自己应聘的优势。
·如果我能给你任何你想要的工作,你会选择什么?
问题:如果我能给你任何你想要的工作,你会选择什么?你真正想做的是什么工作?
回答:就是这份工作。
点评:你可能觉得这是个怪问题,事实上常有这样的问题。这个问题是假设每个人都有未实现的梦想,都不能做他真正想做的事,亦即或多或少每个人都在妥协。若你真的谈了你的梦想,而他只会为圆你梦想的梦,而不录用你。因此,你确实要这份工作,那么答案只有一个。
·为什么你还没有找到工作?
问题:为什么你还没有找到工作?
回答:我正在谨慎选择我的工作,本来我可以选择别的工作的,可是那些工作和现在这一个不同,我实在看不出它们会对我的事业进展有帮助。
点评:如果你真的拒绝了其他人的录取,那是再好不过了,如果其他企业都没有录取你,哪也不一定有问题。别人不能只因为你现在没有工作,就断定都没有人录取你,不要给人这样的错觉。
你对我们公司有多少了解?
问题:你对我们公司有多少了解?
回答:
1.完全不了解。
2.因为对贵公司有关方面相当有兴趣,所以才来应聘。
点评:若回答1.那就没有必要再说下去了,但录用的机会也就小了。最好的回答是2,这是公司想测试应聘者对公司的兴趣,关注程度,以后进公司工作的意愿的问题,因此,最好要稍稍记住公司的简介内容和招聘人事广告内容。你对公司有何印象?
问题:你对公司有何印象?
回答:感觉很好,在其他公司没有这样的感受。
点评:或者说出面试当天的印象就可以了,因为还没有正式进入公司上班,所以主试者也不会太过刁难。
·你谈谈选择这份工作的动机?
问题:你谈谈选择这份工作的动机?
回答:“这个职位刚好是我的专业对口,能把学的书本知识在实践中更好地应用。”
“我虽然学的专业与这职位有区别,但我对这方面的能力较强,相信自己能干好这份工作。
点评:这是测试面试者对这份工作的理解程度及热忱,并筛选因一时兴起而来应聘的人。
你家在外地,单位无住宿条件,你如何看待呢?
问题:你家在外地,我们单位无住宿,你如何看待呢?
回答:家在外地,贵单位无住宿条件,这些都不影响我来应聘贵公司,住宿我可以自己解决,无须单位操心,我看重贵公司的发展前途。
点评:不要因为个人生活上的小问题,而错失良机。主试者也想看看你对困难的看法,自信心程度。
我们不限定固定职位,你认为自己最适合做什么?
问题:我们不限定固定职位,你认为自己最适合做什么?
回答:
忌:”公司安排我做什么就做什么!“太随意。
”理想的职位就是有机会让我一展专长,为公司的发展贡献自己的学识。“太空。
应:我学的是XX专业,我认为XX职位比较适合我。
点评:主试者问你问题,就是想要一个明确的答案,且明确的回答给人以有思想、有主见、有活力的印象。象上面的回答,是犯了一个错误,然而几乎每个人都会犯同样的错误,他们总是说自己干什么都可以。因此,回答这样的问题,干脆用自己的心里话表白,实事求是,至少让主试者听起来感到舒服些。你希望从事什么样的工作?
问题:你希望从事什么样的工作?
回答:根据贵公司的招聘职位,我认为**职位可能比较适合我,有利于我的能力的发挥。当然,其他有些职位也是可做的,人贵在学习。
点评:应试者可以应聘的职位作出大致的设想,让主试人了解自己的抱负与努力方向。由于每个单位都有自己的人事政策,其工作安排未必能完全与求职者的愿望相一致,尤其对一个初出茅庐的大学生来说,从基层做起,从小事做起也是应该的。但是,又不能随便回答:”到哪里工作都可以。“这让人觉得像在”乞讨工作“,被人看轻。所以要掌握分寸。
你为什么要应聘我们公司?
问题:你为什么要应聘我们公司?
回答:看了贵公司的广告及要求,感到自己比较符合公司的招聘条件,另外,对贵公司也有些了解,自己若能有幸成为贵公司的一员,是能有助于自己能力的发挥与发展的。
点评:这样的回答,可显示出自己积极进取的态度。在谈论用人单位时,态度要诚恳、谦和。不论大单位或小单位,都有其优胜和劣势,应试者应视其实际情况,提出自己的见解,不要牵强附会,如果一味往对方脸上贴金,反而会令人反感。
·你在以前实习的公司从事什么样的工作?
问题:你在以前实习的公司从事什么样的工作?
回答:在具体说明对工作的理解程度和熟悉度时,回答要领有三个方面:担任的工作内容、职务、成绩三项。
点评:这个问题可以让公司知道面试者是否符合所要招聘的职位,以前在其他公司的职位是否重要,来判断应聘者的发展可能。
你为何选择应聘我们公司?
问题:你为何选择应聘我们公司?
回答:我对贵公司有一定的了解,特别对公司的XX经营理念,产品质量及员工培训比较看好。
点评:为了表明应聘原因及工作意愿,应聘者在回答时最好要了解企业状况,不要笼统回答因为自己将来有发展,更不要回答为了安定等答案。
·在公司想做什么样的工作?
问题:在公司想做什么样的工作?
回答:现在想在某工作方面冲刺,将来则希望能在某方面努力等。朝自己想要的目标陈述即可。
点评:同时招聘很多职种的公司,最有可能问到这样的问题,这是判断应聘者个人的能力倾向。面试者如果不论职种都回答”可以“的话,反而会让人怀疑工作态度。如果这家公司只招聘一个职种,还是被问到这个问题时,是为了确认应聘者有无犹豫,应聘者只要清楚的叙述自己想做的事就可以了。
·你为何要跳槽?
问题:你为何要跳槽?
回答:虽然在前面公司工作挺顺的,同事间合作也很愉快,但我感到贵公司更适合我的发展。
点评:公司根据你跳槽原因,意在了解你的就业动机。
·请问你有什么样的工作观?
问题:请问你有什么样的工作观?
回答:我认为工作是为了实现自己的人生价值,发挥自己的最大潜能,解决自己的生活问题。
点评:此话是问工作在你的生活中意味着什么?为何而工作?从工作中得到了什么?几年后想变成怎样等。因此,别把它想得太复杂,可根据自己的具体情况回答。
·你是否可以接受加班?
问题:你是否可以接受加班?
回答:我愿意接受挑战。在自己责任范围内的工作,不能算是加班。
点评:这是面试者针对应聘者的工作热忱而提的问题,因无理的加班不一定是好的。
你认为这份工作最重要的是什么?
问题:你认为这份工作最重要的是什么?
回答:最重要的是对自己的挑战和提高。
点评:对工作要加上自己的看法。
第五篇:Chapter1语言的功能与陷阱
大学语文题库
一.语言的功能与陷阱 大学语文主要培养的是(C)。A、背诵 B、书写 C、语感 D、文采
(往年考过)2.(镜像问题)王蒙的(A)这部作品给使他被错划为右派。A、《组织部来了个年轻人》 B、《青春万岁》 C、《春尽江南》 D、《中国天机》
王蒙写作的新中国历史上第一部校园小说是(C)。A、《组织部来了个年轻人》 B、《语言的功能和陷阱》 C、《青春万岁》 D、《恋爱的季节》
王蒙的第一部作品是()。A A.青春万岁
B.组织部来了个年轻人 C.班主任 D.青春之歌
3.《人论》是(D)的作品。A、笛卡尔 B、黑格尔 C、笛卡尔 D、卡西尔
4.“幸福”一词在中国的广泛使用源于(C)国家的影响。A、美国 B、德国 C、苏联 D、日本
5.(镜像问题)“春心莫共花争发,一寸相思一寸灰”是(C)的作品。A、李白 B、李贺 C、李商隐 D、李隆基
“春心莫共花争发,一寸相思一寸灰”是李商隐的作品。(是)
(往年考过)6.“言不尽意”最早是(B)意识到的问题。A、孔子 B、老子 C、孟子 D、屈原
下面(C)最早提出了言不尽意的观点。A、王蒙 B、苏轼 C、老子 D、孔子
(往年考过)7.红色文学的主题是(C)。A、爱情 B、青春 C、革命 D、农村
8.(镜像问题)“写小说就是写语言”是(B)的名言。A、巴金 B、汪曾祺 C、郭沫若 D、矛盾
(往年考过)“想象一种语言,就是想象一种社会生活”是(C)的观点。A、康德 B、萨特
C、维特根斯坦 D、尼采
(往年考过)“语言的局限就是我们全部世界的局限”是(D)的观点。A、马克思 B、费尔巴哈 C、黑格尔 D、恩格斯
(D)提出了“如果说不清楚就说明没有想清楚,如果写不清楚就说明没有说清楚”。A、王蒙 B、徐志摩 C、臧克家 D、闻一多
(A)曾说过写小说就是写语言。A、汪曾祺 B、王蒙 C、普罗普 D、林风眠
“语言是思想的物质的、直接的现实”是(D)的观点。A、弗洛伊德 B、王蒙 C、郭沫若 D、恩格斯 9.(镜像问题)辨认色彩最强的是(B)人。A、亚洲 B、欧洲 C、非洲 D、美洲
中国人辨别色彩的能力强于欧洲人,是因为汉语中表示色彩的词汇非常丰富。()我的答案:×
10.《团结一切抗日力量,反对反共顽固派》是毛泽东的作品,这篇文章的语言是面向(B)群体。
A、知识分子 B、农民 C、工人 D、官方
(往年考过)11.小品《主角和配角》反应了(C)时期两种群体力量的博弈。A、抗战 B、大跃进 C、改革开放 D、现代化建设
12.小品《主角和配角》体现了语言和(C)的关系。A、思想 B、意义 C、权力 D、情感
13.蒋介石在大陆第一次作为正面形象出现的电视剧是(D)。A、《闯关东》 B、《席卷大西南》 C、《亮剑》
D、《长沙保卫战》
(往年考过)14.《热血、辛劳、汗水和眼泪》是(D)的演讲。A、希特勒 B、斯大林 C、毛泽东 D、丘吉尔
《热血、辛劳、汗水和眼泪》是()时期的演讲。C A 法国大革命 B 一战 C 二战
D 解放战争
15.(D)真正带来了长篇小说的繁荣。A、毛笔写作 B、钢笔写作 C、沾笔写作 D、电脑写作
16.(B)因写作京剧《沙家浜》被摘掉了右派的帽子。A、王蒙 B、汪曾祺 C、丁玲 D、艾青
17.下面诗句中的月亮不代表思乡之情的是(A)。A、月明星稀,乌鹊南飞 B、海上生明月,天涯共此时 C、举头望明月,低头思故乡 D、露从今夜白,月是故乡明
18.王蒙总结了语言的三种功能,其中不包括(B)。A、交流功能 B、区别功能 C、推动思想功能 D、浪漫功能
19.《侠客行》中,只有不识字的小孩认出了蝌蚪文,这体现了语言的(D)。A、言不尽意 B、言过其实 C、可替代性
D、对思想的束缚
20.老一辈的人不懂玛丽苏、大叔控等词的意思,体现了语言(D)。A、是交流工具
B、是表达感情的媒介 C、具有大众性 D、具有时代性
21.(镜像问题)艺术的真正魅力来源于(D)。A、觉悟 B、修养 C、艺术技巧 D、语言技术
艺术的真正魅力来自于语言。我的答案:√
22.下面词语被语言赋予时间流逝感受的是(B)。A、月亮 B、流水 C、桃花 D、梅花
23.下面不能体现时间是空间的隐喻的是(C)。A、一顿饭的功夫 B、从前
C、9点10分 D、自古以来
24.毛泽东发表文章团结广大群众抗日时使用的语言是(C)。A、严肃正规的语言 B、冷静的语言 C、市井化的语言 D、有逻辑的语言
25.下面不能体现语言中包含着权力的是(B)。A、青年特指男性
B、俄罗斯的叙事诗很长 C、小老婆 D、叫花子
26.红色文学的主题是()。C A 爱情 B 青春 C 革命 D 农村
27.美国“垮掉的一代”反抗的是(C)。A、政府 B、体制 C、父辈 D、战争
28.下面不能体现语言性别歧视的是(A)。A、女人不能骂人 B、默认青年为男性
C、女人没有按照正常人类的形式被命名 D、生男孩是可好,生女孩是也好
29.(镜像问题)下面不属于演讲技术特点的是(D)。A、感染性 B、单向性 C、共谋性 D、冷静性
演讲需要从几个方面增强感染力,其中不包括(C)。A、感情 B、独语论断 C、道理
D、自我打动
30.语言的权力效益体现在(D)。A、让人觉得屈辱 B、让人觉得愤怒 C、让人觉得激动 D、以上都是
31.下面不能作为公共象征的是(B)A、热血 B、电脑 C、长城 D、五星红旗 我的答案:B(往年考过)32.《日喻说》是(A)的文章。A、苏轼 B、韩愈 C、柳宗元 D、杜甫
33.黑格尔提出,人和动物最重要的区别是(D)。
A、劳动 B、思维 C、情感 D、语言
(往年考过)34..王蒙认同语言工具论的提法。我的答案:×
35.王蒙提出人与动物最重要的区别是语言。()我的答案:×
(往年考过)36.《语言的功能与陷阱》是王蒙的一篇演讲词。我的答案:√
(往年考过)37.语言会激发思想,但也会扼杀人的创造力。我的答案:√
38.语言的“狗屎化效应”指语言说得多了,语言往往就失去意义了。我的答案:√
(往年考过)39.语言规定了种种瞬间的体验,甚至可以创造其本身不具有的意味。我的答案:√
(往年考过)40.在文学创作中,永远是内容决定形式,形式服务于内容。我的答案:×
(往年考过)41.语言不需要表达对象,本身就具有独立的审美意义。我的答案:√
(往年考过)42.演讲是一种公共话语行为。语言组织总是暗含着鼓动性和说服性。我的答案:√
43.在演讲中,理性的说服比感情的感染更有效。我的答案:×
44.(镜像问题)语言一定要符合现实生活,否则会产生消极的后果。我的答案:×
45语言来源于生活,不能脱离现实。()我的答案:×
(往年考过)46.语言本身隐藏着权力,本身可以影响我们的认识和思考。我的答案:√
(往年考过)47.一般来讲,作家的理论思考能力都很好。我的答案:×
48.语言有帮助思想、推动思想的功能。我的答案:√
(往年考过)49.语言一定要符合现实生活,否则会产生消极的后果。我的答案:×
50.想象一种语言,就是想象一种社会生活。我的答案:√
(往年考过)51.语言本身隐藏着权力,本身可以影响我们的认识和思考。我的答案:√
52.作家写作都是事先在脑子里构思好情节大纲才开始写的。()我的答案:×
53“不着一字,尽得风流”体现了言不尽意的魅力。()我的答案:√
54.任何时候言过其实都会让人反感,要尽量避免言过其实。()我的答案:×
55.不同的语言代表了不同的文化和社会地位。()我的答案:√
56.语言可以催生感情,即使是哈哈大笑也可能传递悲凉的情绪。我的答案:√
57.市场社会的代表语言就是官方语言。我的答案:×
58演讲中主体置换的目的是改变听众的利益需求。()我的答案:×
59人们对客观事物的感情很多时候会受到语言的影响。()我的答案:√
60虚假提问能够增强说话的气势。()我的答案:√
61画国画的对墨色的分辨能力要强于画油画的 我的答案:√
63语言的形式可以决定语言的内容。()我的答案:√
64蒋介石在大陆第一次作为正面人物出现的电视剧是《长沙保卫战》
我的答案:√
(往年考过)65从语言的使用上来讲,使用反问句去询问他人,通常是不礼貌的。我的答案:√
(往年考过)66在一个民主、自由的社会里,言论自由是社会得以正常运作的基石,正是因为倡导言论自由,因而也就排除了贵族或特权阶层的存在可能,毕竟言论自由不等于言论特权。
我的答案:√
67王蒙曾在《语言的功能与陷阱中》举了诸葛亮斩马谡的故事,这个例子说明的是语言的哪个陷阱?言过其实
68王蒙曾在《语言的功能与陷阱中》举了阿Q和徐志摩分别向吴妈示爱的例子,作者意图要说明语言的哪种功能?修辞
69王蒙在《语言的功能与陷阱》中举了“失空斩“的例子,用来说明语言的哪种问题?言过其实
70王蒙在《语言的功能与陷阱》中举了”轮扁斫轮"的例子,用来说明语言的哪种陷阱?言不能达意
71王蒙在《语言的功能与陷阱》中曾提到李商隐的诗歌,是为了说明语言哪方面的问题?语言具有艺术和审美功能的问题 72列哪位作者曾经担任过中国的文化部长?王蒙