第一篇: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
第二篇: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)的商标。
第三篇:语言的功能和陷阱
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
你同意演讲者关于“语言陷阱”的观点吗?为什么?
第四篇:王蒙《语言的功能和陷阱》(模版)
王蒙《语言的功能与陷阱》
语言对人来说是太重要了,可以说是人与非人之间的一个非常重大的区别,当然我们首先倡导的学说是劳动创造了人,但是从一定意义上也可以说语言和劳动一起创造了人,人创造了语言,语言反过来又创造了人。人性化离不开语言。所以,我仍然假定,发达的语言,尤其是文字是人与非人,与其他动物,更不用说是植物和矿物的一个重大的区别。
语言的功能
第一,语言的人性化。之前我曾说,有三个词我老是烦它,一个是:芝麻开花节节高,这个我觉得也太俗了。你说你情况越来越好就是好就完了,说进步就是进步,提升就是提升了,富裕了就是富裕了,你干吗还芝麻开花节节高呀?芝麻开花节节高能高到什么程度,这个我不喜欢;另外一个词就是我们的一个固定说法,形容房屋很多,叫鳞次栉比。我说你形容房子多无论如何不能用鱼鳞来形容,用鱼鳞来形容我有一种生理上的不舒服的感觉;第三个我不喜欢的词就是天麻麻亮,天亮了就是亮了,没亮就是没亮,就是用书面语来说是拂晓也行,用破晓也可以,为什么要麻,而且一连麻两次呢?我就说老实话,说东方显出了鱼肚白,这个鱼肚白我也不喜欢,因为它让我想到的是死鱼。
这些都没有什么道理,都不是语言学,我只是说语言里面包含着一些很人性的东西。我喜欢什么样的词语呢?我随便给你们举几个例子,我特别喜欢‚你好‛,而且我认为这是受了苏联的影响,它完全是受俄语的影响。甚至于俄语里面有更好的表示,它说:‚你好,爸爸‛,‚你好,妈妈‛,‚你好,列宁同志。‛哎呀,我觉得这个词好;我喜欢‚再见‛,我喜欢说‚我想你‛,我觉得说‚我想你‛甚至比说‚我爱你‛还好,我还喜欢‚我们都老了‛,这话特别有感情,说这话的人一定是老相识,很可能是老情人,见到一个老情人你说什么呢?‚啊,我们都老了!‛我觉得这个特别的人性化。我还喜欢一个一个书面的说法,一个小说里的、话剧里的语言,这样说有点酸,但是也没有办法,这就是语言的陷阱。一个本来很好的语言呢,场合不对它说出来就显得有点酸。说有个人呢,他原来跟我很好,感情很好,但是后来呢他离开了我,在我最生活困难的时候离开了我。那么我会说什么呢?我会说‚你曾经转过脸去了‛,当我说‚你曾经转过脸去了‛的时候,眼泪已经在我的眼睛里逛荡了,我觉得这句话太好了。
在这里我加一个文言文,我读文言文作品的时候,我不知道为什么我那么喜欢一句话,那就是‚先生别来无恙乎?‛就冲这句话,我就觉得中国的文言文写的是太棒了。‚别来无恙‛现在白话文里也可以考虑,完全可以说,这个你不可以按文字来讲。‚先生别来无恙乎?‛这个英语里它没法翻译,英语变成什么呢?‚Are you Ok?‛它没有那种感情。‚先生别来无恙乎?‛,说明这两个人已经离别很久了,而用‚无恙‛,表达一种人世的沧桑的态度;还有一句就是‚谁知道呢?‛,‚谁知道呢?‛尤其是我读肖洛霍夫的《被开垦的处女地》第二部,它的那个主人公拉古尔洛夫是一个贫农团的团长,一个天天搞革命的农民,但是他跟一个富农出身的,一个很不检点的女人,两个人有点难分难解,然后这个女人是更喜欢一个富农,而那个富农呢,实际上是已经作了一些破坏集体农庄的反革命事情,但是他最后决定把这个他心爱的政治上犯有错误的,而且牵扯着刑事案件的女人放掉。最后他说这个时刻,这个富农女人对这个很粗鲁,很野蛮的,也不会穿衣服,说话大大咧咧,也不懂女性心理的拉古尔洛夫的感觉,也许有一些不同吧?谁知道呢?我觉得这句很棒!同样反过来呢,我对英文的这个‚Who knows?‛也有兴趣‚Who knows?‛他回答得很潇洒,有时候需要互相发现,有的时候北京人回答一件事的时候说‚没戏‛,这个我们听起来很贫哪!是胡同里我们才这么说,但是一个英国人他告诉我,说这个北京人的文化积淀太深了!当他们说一件事办不成的时候,他们不说‚impossible‛,他们说‚No theater‛,所以他们对‚没戏‛的感觉跟我对‚Who knows?‛的感觉是相同的。
我的英语知识很少,但是我有多次和外国人一起生活的经历,我比较喜欢他们的‚Why not ?‛。‚Why not ?‛你没法翻(译)。你比如说他说,老王,今晚上我那有个party ,有一个鸡尾酒会,甚至会有个自助餐,你晚上来吗?我怎么回答呢?我说:‚Ok, I will go!‛,这太干巴巴了,所以我一定回答:‚Yes, Why not ?‛但你不能翻,一翻就酸得你的牙都掉了,英语有些词,在我有限的接触当中,也有些我非常喜爱的说法。比如说甭管它,爱怎么怎么着,‚Let it be!‛这怎么翻呀?爱怎么怎么着,这还凑合。女作家刘索拉把它翻译成‚随他妈的去!‛这就过啦,这个语感就过啦,它不准。所以语感这个东西它是很精微的,添一份它就肥了,减一份它就瘦了。我也喜欢‚So do I.‛,它反过来说,它不说‚I do so‛,它就比较俏皮,你听着就比较舒服。我就是说这个人性化的问题呢,它跟语言有时候联接的比较紧密。所以这个语言的表达的功能不像我们想得那么简单,语言的表达,它表现着人性。
第二,语言的记忆功能。世界上一切的东西他都是变动不息的,所谓‚俯仰之间,已成陈迹‛。其实我们在这说着话,说着说着话这一下午就这么过去了。2004年4月14日,这一下午就这么过去了,有一个姓王的老头子,在这里说的这些话,音波也就在空气中消散了,就变成陈迹了,就变成了历史了。变成历史之后有什么东西留下来了呢?有一些物质的,你比如说房屋、用具,如果你真要追究起来的话,比如说这有一只王蒙用过的茶杯,但主要的是文本留下来了,实际上许许多多东西对我们来说是文本,历史教科书实际上有很多东西是文本。有实物又有文本的东西是给我们留下印象最深的。有文本没有实物的东西给我们留下的印象是次深的,有实物没有文本的东西是哑巴东西,你怎么解释都行,假如五百年后有人对这只茶杯感兴趣的话,假如这个茶杯上没有写中国海洋大学,它是哑巴。我们讲中国史、外国史,实际上他们最后都变成了语言,变成了文本。而很多东西之所以能够有很高的价值,是由于文本的可爱,而不是由于别的。你比如说,我们的祖国有许许多多的旅游点,有很多美好的地方,有文本的跟没有文本的给人的感觉是不一样的。比如西湖,古来吟咏西湖的诗就特别多,包括白娘子的故事,包括秋瑾的诗,对我们来说都是文本!白素贞永镇雷锋塔,有一次我们跟韩国人一起开会,正好赶上这种开会的天气,天天有雨,有很多活动就取消了,韩国人就抢着背诵:‚欲把西湖比西子,浓妆淡抹总相宜‛,‚水光潋滟晴方好,山色空濛雨亦奇‛。韩国人就抢着背,有了这个文本之后啊,就使我们中国的东道主对连续三天不能好好的畅游西湖啊,我们有了底气,有文本为证:‚雨亦奇‛,‚山色空濛‛,‚浓妆淡抹总相宜‛。这次我们就是用淡妆来欢迎韩国朋友,有了这个,你的底气都不一样啊。岳阳楼,现在你都分不清楚是为了看楼还是为了复习范仲淹的《岳阳楼记》,特别是它的‚先天下之忧而忧,后天下之乐而乐‛。尤其是黄鹤楼,黄鹤楼的遗迹实际上早就不存在了,尤其解放以后,苏联专家帮着咱们修了长江大桥,这一带的地理环境其实都已经改变了,现在的黄鹤楼是又选了一个址,现在的黄鹤楼修了有20多年了,当时我们的国家也比较困难,标准也不是很高,怎么不高呢?就是那个木头柱子,它全部是洋灰,全部是钢筋水泥,然后再刷上红漆,但是它仍然吸引了千千万万的游客,络绎不绝。虽然现在的黄鹤楼是伪黄鹤楼,但是关于黄鹤楼的崔颢和李白的诗却流传至今,‚昔人已乘黄鹤去,此地空余黄鹤楼。黄鹤一去不复返,白云千载空悠悠。晴川历历汉阳树,芳草萋萋鹦鹉洲。日暮乡关何处是,烟波江上使人愁。‛李白的诗呢,简单一点:‚故人西辞黄鹤楼,烟花三月下扬州。孤帆远影碧空尽,惟见长江天际流。‛这样一个记忆的宝库,这样一个文化心理的保存,使黄鹤楼始终生长在我们的心里,那么你具体盖起这个黄鹤楼比如油漆的质量怎么样,伪木柱子的成色如何,我们可以宽容,我们可以原谅,为什么呢?因为黄鹤楼的仙气、灵气在它的文本上,在它的语言上,在李白和崔颢上,所以只要有李白有崔颢,黄鹤楼永垂不朽。只要有我们的汉语、汉字的文本,中华民族永垂不朽!滕王阁也是这样,滕王阁修得很精致,它的自然条件不如黄鹤楼,因为南昌那边滕王阁前面是赣江,就是‚秋水共长天一色‛里的,没有长江的那种气魄。滕王阁是根据梁思成先生当年画的,也就是他考察、推测的图把它修起来的,可是这个建筑的依托我觉得也是王勃的《滕王阁序》,没有他的物华天宝,人杰地灵,没有他的‚落霞与孤鹜齐飞,秋水共长天一色‛,这个滕王阁不会这样的屹立,这样的崇高,这样吸引着我们这些中华儿女。这是不得了的,一个有文本的民族和一个没有文本的民族在世界在人类中的地位是不一样的,一个有无数优秀文本的民族必胜,很难代替。所以我们就看到语言文字的记载所起到的记忆和文化积淀的作用,一个没有文化的积淀的民族就好比一个失去了记忆力的人,每个人都有自己的悲哀,而失去了记忆力实在是一个非常大的悲哀,失去了记忆力你连自个儿是谁都不知道,我们还怎么帮助你?你还能有什么幸福可言?还能有什么不幸可言?
第三,语言是一种修辞的手段。这种修辞是泛修辞,我觉得我们的文化在某种意义上说就是修辞,人都有动物的本能,都有求生的本能,而人的这种本能经过修辞以后,就如毛泽东所说的有‚文野之分‛,有了文化与非文化之分,如果没有修辞手段,你就只能限于本能,这个我想到最多的是阿Q先生,阿Q他是按照人权的观点他当然和我们都一样,他有两次求爱经历,一次是求吴妈,用他的话说就是‚小孤孀‛吴妈,那么他怎么求爱呢?他突然一天晚上就给吴妈跪下了,然后他说:‚吴妈!我要和你困觉!‛哎呀,然后呢,吴妈就哭,要抹脖子上吊,然后大家就都认为阿Q干出了毫无人性、违反道德、不守规矩、欺天害理、不齿于人类的这种事情,阿Q没有写检讨因为他不识字,但是他表示了检讨之意,而且还赔了钱,把一年的工钱都给了吴妈,而吴妈却一直在那里哭,哭,哭。如果阿Q在语言文字的修辞上能够到咱们中文系上两节课,能来这听讲座,他绝对就不会用这种话了!如果他读过徐志摩的诗呢?那么他见到吴妈就会说:‚我是天空里的一片云,偶尔投影在你的波心,你不必讶异更无须欢喜,在转瞬间消灭了踪影。你我相遇在黑夜的海上,你有你的,我有我的方向……‛嘿,他可能就成功了!阿Q和徐志摩作为男人,他们想择偶,想求爱,这是天经地义的,阿Q对吴妈连性骚扰也谈不上,因为他是跪下的,他又没有摸人家,也没怎么着人家。性骚扰是阿Q对小尼姑,他去摸人家的脑袋,而且最为恶劣的是他说:‚和尚摸得为什么我摸不得?‛他缺乏修辞。修辞能使很多事情甚至于发生本质的变化,从野蛮到文化,从野兽到文明的人,可以有很大的变化,不过我们往宽里说的话,很多事情就是一种修辞,我们可以说它是生活的修辞,比如说婚姻,该有的这些仪式、程序,互相还写点情书,然后你再进教堂,还要奏结婚进行曲,非常雄壮,我听这声音一直以为是大军出营征战。经过这样一个修辞的手段他就不一样,所以有时候我们从一个人的语言上,所谓的谈吐上就能看出这个人素质是高还是低,对人是粗暴的还是温和的这样一些印象。所以说语言的修辞作用是太大了,为什么我说语言塑造了人呢?这是因为人的许多一些本能的欲望,这些欲望既谈不上坏、罪恶,也谈不上好、文明,经过修辞的作用,经过这种长期的修辞的实践,修辞的习惯,修辞的素养,他变成了一个有教养的,有文化的人。
第四,语言的政治功能。语言在政治当中起到相当作用,政治家基本上或是按照一般来说他应该是个演说家,比如说用语言来选举,用语言来起鼓动的作用。《文心雕龙》一开始就说‚鼓天下之动者存乎辞‛,能发动天下的是‚辞‛,以‚辞‛来打败政治上强有力的对手。‚起来!饥寒交迫的奴隶‛,这个语言的力量太大啦!雷霆万钧,如火如荼啊!就冲这一句话就该热血沸腾了。古代‚吾与汝偕亡‛我没有能力把你消灭,怎么办呢?我跟你一块儿死!还有‚人生自古谁无死,留取丹心照汗青‛,而且‚留取丹心照汗青‛这个说法不仅中国有,菲律宾有一个反抗西班牙统治的民族英雄,他又是一个诗人,他在就义前写过一首诗,这首诗里有一句话说‚我流出的血将染红明天的朝霞‛,这就是‚留取丹心照汗青‛;革命烈士夏明翰的诗‚砍头不要紧,只要主义真;杀了夏明翰,还有后来人‛。有一些话我觉得提得真精彩,比如说《共产党宣言》讲社会主义、无产阶级革命说是‚无产阶级在这场革命中失去的是(只有)锁链,而得到的(将)是全(整个)世界。‛哎呀,这种语言力透纸背,千钧之力啊!还有他最后有一句话说:‚全世界无产者联合起来!‛这个是中文的翻译更悲壮,‚全世界无产者联合起来!‛他原来德语的原文我不会,但是英语我见过,因为我去过马克思墓,那上边写的太简单了,就是说‚All workers unite!‛很简单,‚所有的工人要联合‛。我还想到一些很有名的词儿,比如说,你们这个年龄的人肯定也知道季米特洛夫,他是共产国际的一个活动家,是保加利亚的一个领导人,这个季米特洛夫曾经被希特勒的国会纵火案所诬陷,不知道怎么有个疯子一把火把德国国会给烧了,于是法西斯集团就借这个机会镇压所有的左翼政党和工人团体,逮捕了季米特洛夫,季米特洛夫在被审判当中他不要律师,自我辩护,把敌人的审判变成了他的讲台,在那里慷慨激昂地讲话,所以他真是当时那个时候共产主义的一个明星,他一句话说‚当中世纪的教会把主张地动说的科学家烧死的时候,这个科学家说:‘你可以烧死我,但是它在转动着!’‛它在转动着!多棒啊!还有他说‚无产阶级和资产阶级决战的时刻到来了!到时候我们不做铁锤便做铁砧‛,也就是你不砸他他就砸你,当然从现在来看对这个事情还是应该一分为二,他没有充分考虑事物的复杂性,但是从语言来说它是非常有力的,这是政治上的一种情况。还有一种情况就是这种语言和文字又会变成政治家的一种武器,变成了政治家的一种障眼法,他用来遮蔽事实的真相,用来推迟真相的出场。我们文化大革命中有些人宣传得非常好,而且大家都说这人讲得真好,但是那些话是经不住研究,是经不住推敲的。我这里就不说具体是谁说的了,这人介绍经验的时候说,我们这儿对待自然灾害,我们的经验有三条,第一,要承认它,第二,不怕它,第三,克服它!讲得铿锵有力,而且还有第一第二第三,但是他讲得全是废话,和没讲一模一样啊!什么叫第一承认?你承认不承认山水下来把你房子都已经冲走了你还不承认?就是有时候他讲些同义反复的东西,比如说有些领导人讲话,说什么叫全心全意为人民服务呢?第一,不是三心二意,第二,不是半心半意!这叫什么?这叫同义反复,你说来说去实际上一句话都没有说。前几个月外国媒体评文理不通奖评给拉姆斯菲尔德了,这个拉姆斯菲尔德后来在凤凰台上就多次播放他这个讲话:‚I don’t know what I know or I don’t know, If I think I know I’m not sure I know or I don’t know。‛就是当时人们问他关于伊拉克形势啊,大规模杀伤性武器追究这些事情的时候,他就说我们并不知道我们知道什么,我们也不知道我们不知道什么,我们以为我们知道什么,但是不见得就是我们知道什么。这就是拉姆斯菲尔德说的。有时候搞政治真是练语言啊,我在香港看过老布什竞选的一个新闻片,老布什他讲一个问题,就是要不要加税,他就保证我绝对不加税,他用手指着嘴(动作),说:‚Pay attention to my mouth!I say,NO,NO,NO!‛他说请你们注意我的嘴,注意我的口型,我说了不加税,‚NO NO NO‛,我三次说了不加税,然后他上台三个月后就加税了,然后他又讲,我当时说不加税有当时的情况,现在说加税有现在的情况,情况变了提法自然会有改变!这个语言文字真好啊,没有语言文字怎么办?怎么解释这些事情?中国是很注意外交辞令的,我小学的时候就学过晏子使楚,晏子使楚这一类的很多故事那不就是靠语言吗?他就显示出了语言的魅力,最后那个楚王就说:‚寡人自取辱焉‛,就是等于他承认败在晏子手下。还有说客,《史记》上就有很多说客,说客就靠自己的三寸不烂之舌改变形势。有时候我开玩笑,其实我的话并没有恶意,只是听起来可能有贬义,我发现好多人包括我自己,我说我们到底算什么劳动者?算体力劳动者?不算!算脑力劳动者?不能一天老用脑子,很累的!后来我归结为‚口力劳动者‛,很多人都是口力劳动者,靠口力劳动改变、调整着政策,政策的调整那要靠口力,包括文字也是把口说的东西记载下来,所以我就说这些,政治化的东西我就不多说了,‚天机不可泄露‛。
第五,语言的审美功能。语言产生之后可以成为审美的对象,其实刚才我讲那些我喜欢的词的时候,已经包含了对它的一种审美的感受,一种审美的接受,因为你不能单纯的说哪个词一定好,哪个词一定不好,褒义词都是好词,贬义词都是坏词。语言和文字尤其是像中国的文字有这样巨大的审美功能也是很少有,它的音乐感,我读文学作品包括我自己写东西,我特别追求的就是这种音乐感。好的句子你怎么看怎么舒服,怎么念怎么舒服,别人念你怎么听怎么舒服,而不好的句子他怎么着都别扭。其实拉姆斯菲尔德获得文理不通奖的那一段讲话从单纯的审美意义来说他有一种喜剧的乐趣,用英语来评价的话‚He is funny‛,我们也可以说是‚He is fantastic‛,他是奇妙的,而且是喜人的,这是从单纯的审美意义来说。我有时候喜欢一些不上经传的作品,我就是觉得它的发声好听,内容当然可以,不算深刻,也不算感人,也没有包含巨大的道德的、社会的或是人生的内涵,但它的声音实在是太好听。比如说苏轼的,‚休对故人思故国,且将新火试新茶。诗酒趁年华。‛怎么那么‚帅‛啊这诗?太帅了!‚休对故人思故国,且将新火试新茶。诗酒趁年华‛,唱歌你就唱不到这么好,再比如说‚细雨梦回鸡塞远,小楼吹彻玉笙寒。‛这声音真是太好听了。你就单纯从它的审美的角度,不去管它的内容,而且甚至与那些从内容上来说值得推敲的,不一定很积极,不一定很健康,也不一定同情劳动人民,也不一定代表先进的生产力和先进的文化和人民的最大利益。但是这些词藻,这些词句,这些文字加在一起给你美感。我为什么喜欢李商隐呢?李商隐他能够把他那种消极的、悲哀的、乃至于颓丧的情绪审美化。李商隐他能够用特别美的文字,用特别美的语言,甚至用富贵的词汇,比如说金玉、蝴蝶、花卉等等,用这些东西他来把颓丧的情绪加以包装,实际上颓丧的情绪变成一种曲折有致、美不胜收的这么一座宫殿。譬如说我最喜欢他的两句,这是最颓丧的,‚红楼隔雨相望冷,珠箔飘灯独自归。‛哎呀,这太悲哀啦!‚红楼隔雨相望冷‛,他是不沟通的,他是隔膜的,‚相望冷‛,他是冷雨,‚红楼隔雨相望冷‛,你觉得这个冷啊,读到这里就有点儿冷到骨头里去了。‚珠箔飘灯独自归‛,这么美!他为什么这么美啊?红楼在雨里,他是红楼啊,不是土楼,也不是地堡,‚地堡隔雨相望冷‛,这就不行了,‚珠箔飘灯独自归‛,他这不是纸灯笼,也不是拿着一根蜡,更不是拿着手电筒,‚手持电筒独自归‛,那就完啦!甚至于这种悲哀的情绪,消极的情绪,失望的情绪,软弱的情绪,所谓‚一春梦雨常飘瓦,尽日灵风不满旗‛,这雨都是飘着的,飘到瓦上,不是落到瓦上,雨没有重量,又有风,风也很小,没有大风,大风干脆呼噜呼噜呼噜吹一家伙也可以,他连旗子都不能吹满了,不能吹满旗子迎风飘扬,所以这个语言呐他变成一种审美的呼唤。新诗里头舒婷有两句话,实在是写得非常具有审美的价值,这诗我记得不是很清楚了,她说‚也许有过一次呼唤,却永远没有应许;也许有过一次约定,却永远没有如期。‛Beautiful!太棒了!这就是一种语言的审美化。一念这个我总有一种恶作剧的心理,我就想起我上小学的时候,男生三四年级的时候就开始发坏,我们一个同学就学着唐山味儿教我一句话,‚我说老妹子啊(连读),你咋不爱(发后舌音)我呢(nin音)?‛这就缺少审美价值,显得粗鄙不堪,至少他这是顽童语言,没有什么特别恶劣的,可是如果一个同学在自己私人的感情生活上不太成功,不太顺利,那么你就背诵一下‚也许有过一次呼唤,却永远没有应许‛,‚也许有过一次约定‛,当然也许连约定都没有,‚却永远没有如期‛,是不是你自个儿也显得雅一点儿了?也不必闹得太大发了,也不必太出丑了,语言他就起了这么个作用。有时候语言会变成诗,有时候会变成名句,叫做‚千古丽句‛,有时候变成奇文,有时候变成戏剧的台词。我看曹禺的《雷雨》,我就特别喜欢其中那些最普通的话,特别是《雷雨》的第二场,就是侍萍和周朴园见到了,侍萍说了:‚那已经是几十年以前的事了,那时候还没有用洋火。‛洋火就是火柴,我不知道她是哪一根心弦给我拨动了,她这个‚没有用洋火‛就引起我本身那么多沧桑感,因为我经历过,小时候常说‚买包洋火去吧‛,它叫洋火,不叫火柴,叫火柴已经很先进了,现在火柴都很少用了,因为有了其他的取火的方式,也很难买得到了。然后还有一句话说:‚侍萍老喽,老的连老爷都认不出来了……‛何等的悲凉啊!这很有一种味儿,和那个‚逝者如斯夫,不舍昼夜‛本质是一样的,但是他又是另一种悲凉。所以我们觉得语言文字它不但是有声音,而且是有表情的,它是有动感的,它是有形象的,它是有色彩的,它处处都在感染着你,处处都在触动着你。
第六,语言的神学效应。语言不但有艺术的效应,有艺术的功能,有表意的功能,政治的功能,社会的功能,而且语言有强大的神学的功能。第一,很多神学的最根本的概念,它是语言的产物,是一种语言,譬如说终极,谁看见过终极?终极在哪里?你是活人就看不见终极,你看的只有那几十年,我们假设您长寿,您能活一百五十年,那一百五十一年您都看不到,更不用说终极了。譬如说永恒,你上哪去找永恒?但是永恒是一个词,是一个非常好的词,是一个非常有神学功能的词。本源,至高无上,造物,命运,无限,我们看到的都是有限的,无限是我们思想的产物,因为有这个语言,你的思想才有所丰富。轮回,末日,如果没有这样的一些语言,怎么可能有宗教?怎么可能有人的这种神学的追求和研究,当然还有更严肃的,上帝啊,佛啊,真主啊这样一些词,我们中国除了老天爷,灶王爷,玉皇大帝这些词以外,我们还有一些‚准‛或是‚亚‛神学的一些词,就是说哲学的,具有无限涵盖力的词。譬如说‚道‛,譬如说‚无‛,其实我想来想去这个‚无‛也是看不见摸不着的,你看到的摸到的都是‚有‛而不是‚无‛,但是看不见的摸不着的东西,就是说你经验以外的东西,语言和文字可以创造。语言和文字不但有经验性,而且有‚超验性‛和‚先验性‛,超出你的经验。还有一些伟大的词语,它们本身就具有一种神性,比如说正义,神圣,永生,就义,它有一种超越,对人生经验的一种超越,是对人生经验的一种升华。所以自古以来就有一种把语言神圣化尤其是把文字神圣化的这样一种倾向,世界上各个民族都在寻找一种具有神性的语言。比如说我们所知道的芝麻开门的故事,芝麻开门这就是咒语,你到了一座宝山的紧锁的石洞面前,你叫一声芝麻开门,这门就开了,然后所有的珠宝欢迎你去摘取,去收获,他们寻找这样的语言。福,福禄,福禄其实也离不开字,但是它有一种很特殊的写法,认为它有避邪的效应,过去北京有很多家里头如果有小院子的话,他就立一块石字碑,上边写着‚泰山石敢当‛。就是因为他们认为有些语言文字是有神性的。还有一种魅力,就是有人认为语言文字本身它还有一种神秘,它的背后有一种神的意旨,这个意旨是文字本身所不能直接提供的,是要靠你去钻研和体会,去探索的。中国有《推背图》,所谓‚河出图,洛出书‛,这里头他是一个神秘的东西。要是细讲起来语言和文字真是非常神秘,我相信语言是经过千万年才慢慢形成的,不可能一下子就这么完备,他在刚开始时候肯定有一点道理。比如我们说‚牙齿‛,说的时候这个牙齿的运动,牙齿的显露是很明显的,英语说tooth,teeth,他也有明显的牙齿的动作。‚舌头‛呢?说‚舌‛,这是对舌头的一种使用,英语的tongue,又是一种对舌头的表述。但是时间长了以后,你就只知其然不知其所以然了,而这时候他就神秘了,怎么会有这么多的话这么多的语言,怎么会是语言代表language?老觉得文本后还有天意,而这个天意是不可测的,所以就有测字,所以有《推背图》,就要解释。外国人喜欢搞这个,我看过一本书,叫做《圣经密码》,说他们确认《圣经》是密码,他们请了美国中央情报局的退休的密码专家来研究,找出了它这个密码的规律,根据这个规律,他们发现《圣经》已经预言了所有的事情,预言了苏联的解体,预言了巴以冲突,预言了伊拉克战争,凡是发生的事情,密码都能找得出来。把语言把文字当成一种密码,然后寻找它背后的神秘,这个中国也不是没有这种传统,中国认为语言文字具有神秘性,说仓颉造字的时候‚天雨粟,鬼夜哭‛,啊,因为太智慧了,这个智慧是超人间的,超经验的,所以连天都下小米,鬼夜哭,连鬼都害怕,中国人这么厉害啊,仓颉这么厉害啊,所以说语言有神秘性,有神学的功能。这种把它当密码的游戏表现在占卜上面,他希望通过对一些卦辞的解释、解读能找出对人生、命运的预言来。
第七,语言还有一种心理释放和抚慰作用。语言和人的心理我觉得关系太大了。为什么呢?语言可以使你心里的郁结得到释放。我读过美国八十年代一个很有名的短篇小说家,现在已经去世了,叫做John Chiver,他的女儿叫Susan Chiver,她写过一本书回忆他的爸爸。她在这本书一开始的时候就说‚在我小的时候遇到不愉快的事,我爸爸就让我回到房间跪下来做一会儿祈祷。在我大了以后遇到不愉快的事,我爸爸就让我把这一切都写下来。在祈祷之后,写下来之后,我就变得平安多了。现在我遇到的最不愉快的事就是父亲的逝世,我要把这一切写下来。‛这是很普通的,一个人需要说,需要倾诉,一个能够倾诉的人他是幸福的,一个无人可以倾诉的人,他是可悲的。宗教信徒的忏悔,也起这么个作用。见到神父了,在一个很隐蔽的角落说:‚神父啊,我是有罪的。我的邻居评上高级职称了,我没评上,我真恨他呀,昨天我给他的狗投了毒了。‛神父说:‚啊,我的孩子,你已经有了认识了,你快再买一条好狗送给你的邻居吧。你这个罪不太大,上帝会饶恕你的。‛然后摸摸他的脑袋,他就舒服啦!闹不起神经病来。有时候我觉得语言有一种释毒的作用,他把那种非常负面的,非常消极的有可能成为毒素的,有可能引起癌变的那些经验,那些体验,那些情绪,把它通过一定的语言文字的形式写出来以后,就是经过了这么一个无害化处理的过程。李商隐也是这样,李商隐虽然消极,但是他的诗写得漂亮,他要有很好的音韵,他要有很好的典故,他要有非常精确的对仗,他要有精美的文字的选择,所有这些都是无害的,这是种无害化处理。我还常举一个例子,比如《红楼梦》,晴雯的死宝玉义愤填膺,但是宝玉义愤填膺他又不太可能在荣国府,在大观园掀起一场抗暴,抗谗言,清理小人的这样一场运动,连绝食他都没有,宝玉怎么办呢?他就写了一篇《芙蓉女儿诔》,而且这个《芙蓉女儿诔》也是骈体,写完之后自己在那里摇头摆尾的念,林黛玉还听见了,然后林黛玉就过来了,过来以后还跟他提一提说哪个字该怎么改一改,哪个词该怎么改一改,这样就把对晴雯死的愤激之情转化为对‚诔‛这种文体的一种修辞学的讨论,把它雅化了。不但雅化了,而且释毒了,把它无害化了。所以这本身我就透露了一个消息,语言文字既很好也很厉害,也很冷酷,他把一个对人的死亡的正义的愤怒最后变成了文字的推敲。中国的戏曲和国外的戏曲比较起来,中国戏曲的大仁大孝,大忠大奸,大锣大鼓相对来说比较强烈,什么原因呢?就是因为他的心里上的积蓄太多,压抑太多,他需要语言的宣泄,而语言的宣泄无论如何比他上街宣泄要安全得的多。秦香莲见皇姑,皇姑问‚你为什么不跪?‛秦香莲说:‚按照国法,你是君我是臣,我应该给你跪,按照家法,我是大你是小,你应该给我跪‛(原文记不太清),这个思想本身并不算很先进,但是在当时来说她敢这么说话也还是出气,别的时候不敢说就在戏里头说一说嘛。通过这个东西她能够维持一种心理的健康和平衡,这就是语言的心理的释放乃至于释毒的作用。
第八,语言是一种游戏。语言文字有很大的游戏性,它消遣,有很多纯游戏性的语言游戏和文字游戏,我小时候,学了很多这种游戏性的东西,而且听起来不太有道理的,没有什么意思的这样一些文字,但是它很有游戏性,比如说模拟的快板儿,说‚打竹板儿,迈大步,眼前来到棺材铺,棺材铺的棺材真叫好,一头大来一头小,装上活人跑不了,装上死人活不了。‛我觉得这是很天才的,没有什么意义,也没有攻击棺材铺的意思,也没有为棺材做广告的意思,这是一种游戏。还有很多例子,我的孩子小的时候那么多革命的好的童谣教给他,他不学,他就会说什么呢?‚一个小孩儿学大字,学,学,学不了,了,了,了不起,起,起,起不来,来,来,来上学,学,学,学文化,画,画,画图画,图,图,图书馆,管,管,管不着,着,着,着火了,火,火,火车头,头,头,打你一个大锛儿头!‛这就是游戏,它有什么意义?既不反动,也不革命,也不进步,语言文字有这种游戏功能,人多了一个玩儿的东西。动物吧,一只猫它可以玩儿个球,玩儿个线团线头什么的,再没别的玩儿了它可以
lěi玩儿自己的尾巴。但是人就比较幸福,人可以玩文字,可以玩语言,而且还有各种绕口令,像是‚吃葡萄不吐葡萄皮儿,不吃葡萄倒吐葡萄皮儿‛,这就是游戏,因为这话是不通的,你不吃葡萄怎么吐葡萄皮儿呢?后来我在德国,在波恩一个汉学家的家里,我找到了一个二十年代德国汉学家写的《北京口语词典》,其中有这个绕口令,那时候比较规矩,是‚吃葡萄就吐葡萄皮儿,不吃葡萄不吐葡萄皮儿‛,它是很合理的,但是游戏性不如这个,‚不吃葡萄倒吐葡萄皮儿‛,游戏性就增加了,是把它荒诞化了,这都是一些游戏。中国的游戏还多了,比如说回文诗,这诗从第一个字念可以,从第几个字念也可以,它是循环的,是转的。苏小妹三难新郎,其中就有回文诗,所以极具游戏性。我上小学的时候的大家都喜欢唱岳飞的《满江红》,‚怒发冲冠,凭阑处、潇潇雨歇‛,可是我们的同学给改了,‚来一碗粥,要咸菜不要窝头!‛那么这个同学是不是对岳飞有不敬呢?是不是有私通秦桧的嫌疑呢?没有。真没有。他就是觉得你老唱‚怒发冲冠,凭栏处、潇潇雨歇‛没有人笑啊,改成‚来一碗粥,要咸菜不要窝头!‛大家就一团乐了。解放以后我们有一个歌叫做‚我是一个兵,来自老百姓‛,马上这个同学又给改了:‚我是一块冰,吃了肚子疼!‛人的本性有一种拿语言和文字……,你可以搞得很神圣,也可以把他搞得非常游戏,当成个玩意儿随便玩儿,就像魔方一样,你可以这么拧,也可以那么拧,放地下转一转,碾一碾,搁脚踹一下,总之是玩儿,这是一种游戏。
第九,也是最重要的,语言有一种发展人的能力。它本身有一种发展能力,有一种组合能力,有一种衍生能力。就是说语言本身在人把他创造出来以后变成了一个世界,变成了一个有机的,活的东西,这个本身是在不断的变异,不断的组合,不断的发展,它培养了人,比如说有了数字,它培养了人的条理;慢慢地我们有了反义词的概念,很多新词就创造出来了,譬如说我刚才提到的无限,无限是一个超验的概念,它是怎么来的呢?因为我们有有限的经验,对有限我们是有经验的,我们知道空间是有限的,时间是有限的,您兜里的钱也是有限的,有了对有限的经验,你就想这个有限的反面是什么呢?是无限,长生不老,天地同辉,这是无限。有了短暂的经验,然后想和短暂对立的是什么呢?是永恒,这样我们就创造了永恒。对反义词的思索使我们人产生了超验的概念,有与无,物与神,文与理,都是这样。比如说很多成语,这些成语出现以后,它是经历着一个变异的过程,这个过程我们现在很难作价值判断,它是好还是不好,比如说现在,包括国务院的政府工作报告里经常有这么一个词叫做‚知难而进‛,这个成语原来是‚知难而退‛,但是现在已经快有人不知道‚知难而退‛了,只知道‚知难而进‛,为什么呢?因为毛主席他来了一个‚知难而进‛,毛主席最喜欢改成语,‚前仆后继‛,他来了一个‚前赴后继‛,‚知难而退‛他给改成了‚知难而进‛,他用他的价值观念来改成语。但是起码我刚才说的那两个改法已经被广泛接受了,到处都在讲‚知难而进‛,到处都在讲‚前赴后继‛,比较吉利,而‚前仆后继‛听起来就不怎么吉利。所以语言本身有变异的可能,有衍生的能力,还有自我完善的能力。有时候语言的组合丰富了人的思想,当然首先是人的思想丰富了语言,但是反过来语言的组合又丰富了人的思想。譬如说‚有志者事竟成‛,但是我们马上按照这种模式可以提出几种不同的命题来,一种是‚有志者事不成‛,‚有志者事未成‛,我们可以举出很多的例子,一个是空有大志,到最后蹉跎一生,并无成就,你不能说这没有啊,不能说保证百分之百的‚有志者事竟成‛,能有百分之十的‚有志者事竟成‛就不错了。还有就是‚无志者事竟成‛,有没有这种例子?至少相声里是有,比如说‚黄蛤蟆‛啊等等,有许多无志者他反倒事成了,有志者事反倒不成。还有一种就是‚无志者事不成‛,这和‚有志者事竟成‛没有什么矛盾。还有一种,即不算有志者也不算无志者,事没算成也没算多么不成,这样的一大批。你就从这个‚有志者事竟成‛上,从这种语言的单纯的组合上就可以看出语言本身能够怎样的衍生,怎样的变化。周谷城(副委员长)教授曾经给我讲过一个例子。四九年的时候他去北京看望毛主席,毛主席踌躇意满,说:失败是成功之母,真是这么回事,我们从南昌起义,井冈山,秋收起义,一次一次反围剿,我们失败了多少次,最后我们成功了。周谷城当时来了一句:‚主席,成功也是失败之母!‛——主席略显不悦,主席问‚怎么讲?‛周谷城说‚成功容易骄傲啊,骄傲又使人落后……‛就差不多这么个意思,‚成功了人就容易……腐化啊什么的,这样的话不就引起失败了?……当然,主席例外!‛‚主席例外!‛,他就加了这么一句,这是周谷城亲自对我说的,后来我个人就给他这话作批注——画蛇添足,越描越黑。你如果选择词句,用比较正确的说法,你应该这样,毛主席问‚怎么讲?‛你说‚历史上有过这样的事情,成功了之后注意不够结果失败,但是我们现在吸取了教训和历史经验,我们会摆脱这样的悲剧……‛,这样说就好了!我要是替周谷城来说这话的话,可能还不止当副委员长!但是毛主席当时还算挺好,‚啪‛一拍桌子,说:‚你讲的有理!‛后来我就想成功是失败之母,失败是成功之母,成功也可以是成功之母,一个成功引起一个成功,失败也可是失败之母,或者成功失败各不相干,并无母子关系,这都是可能的。就是说你看着是语言文字的来回的调换,来回的组合,来回的排列,正义词改成反义词,或者是把宾语换成主语,但是本身他丰富了思想。相反的如果我们既没有成功,也没有失败,也没有之母这样的概念,我们的思想会贫乏得多。所以一个没有语言没有文字的人,一个失去了自己的语言和自己的文字的民族是最可悲的。
语言的陷阱
刚才这些是从语言的正面来说的,但是从反过来说语言本身又是一个陷阱。第一,语言不可能完全准确,语言怎么能完全准确呢?我们都知道轮扁论斫
zhuó的故事,有一个造车轮的阿扁,这个阿扁看到齐怀王在读书,他就问怀王读什么书啊?齐怀王说我的是圣贤之书,他说这不过是糟糠而已,齐怀王就问‚有说乎?‛,你有道理吗?给我讲讲。阿扁就说,以做轮子为例,‚斫‛以我的体会是类似于砍但又不太一样的工具,不是斧子,也不是锯,他说你砍的劲儿大了它就苦了,砍的劲儿小了它就甘,这个苦字到现在我们还用,就是说这东西你去的太多了就苦了,做过了就是弄苦了,用的力量不够就会甘,就甜了,现在这个不太用了。但是你怎么能够掌握的合适呢?无法传达,无法用语言来描述。你只能自己慢慢去砍、砍、砍、砍……,然后就掌握住了,他说连做轮子,语言都是无能为力的,那么你写一本书来教人的用处就是很小的,你何况是治国平天下的大事呢?你不能说治国平天下的大事比做轮子简单。所以能够写下来,能够说出来的全是糟粕。所以老子讲,‚智者不辩,辩者不智‛,禅宗讲,‚不可说,不可说‛,越是最精妙的道理越是不能说。至少到了孔子他有‚述而不作‛,说还行,但是不能作,为什么呢?因为‚说‛有很多弹性,有很多灵活性,有语境,有语气,有对象,像我们今天在这里说,有交流,有呼应。但是写成文字了,比如说我今天讲的这个,万一被你们整理出了一个文本,那是很不幸的一件事。你要再看,那就是胡说八道,信口开河啊!是经不住推敲的,这样一来问题就多了。所以它是不准确的。
第二,把任何东西写下来以后,它会简单化,它会教条化,它会呆板化。本来灵活的,极好的一个论断一个见解,写出来以后变成死东西了,它就暗藏着变成教条,变成呆板的可能。
第三,你说的越是普及,就越有降低水平的可能,它被通俗化、被庸俗化了,最后,我有一个不雅的词——被狗屎化了。很好的一个见解,很好的一个说法,尤其是如果一强迫,完了!那么说出来的多半是狗屎,多半不是见解,多半不会有很珍贵的思考的成分在里面,因为它变成人云亦云了,变成各执一词了,就像文化大革命中打语录仗似的,你背一段语录,他那里背一段语录,把语言文字所表达出来的精彩的东西加以歪曲,加以简单化。所以我们在讲到语言文字的重要性的时候,我们无论如何还要看到,尤其在我们中国,有一种反语言文字的,非语言文字的传统。就是我们现在评价一个人往往还用到一个词叫厚重少文,什么样的人厚重呢?他的话不多,而且话文采不多,这人没有什么文采,拙嘴笨腮,那语言,有话不大会说,他的另一面给人的感觉比较厚重,比较稳定,比较可靠,比较忠诚,这个是中国长期形成下来的。正是因为语言文字有这种侧面的反面的东西,你说的太多了令人马上就想到言过其实,你说得的不准确,你说得夸张,你是所谓嘴皮子上的功夫。什么原因呢?因为语言太发达了,他会脱离现实,它和求真务实的精神不符合了。您要是到上级那里开会,比如党委书记去省里开会,你这里天花乱坠的给他讲一段,你可千万别以为会对你印象好,肯定把你从后备名单里头给去掉了。所以我们有很多对语言不利的说法,‚夸夸其谈‛,‚口若悬河‛,‚言语的巨人,行动的矮子‛,那么我们现在批评的是行动的矮子,但是我们老是寻找那种结结巴巴的话也说不清楚的行动的巨人,这个也有点偏颇。演说,答记者问,记者招待会,既需要行动的巨人,也需要言语的大师,我们应该提倡言语的大师和行动的大师。
第四,语言规定了你的思维。我们现代人出生以后接触到的现有的语言信息太多了,根本你想看也看不完,想消化也消化不清楚,所以你只剩下学舌的份儿了,已有的语言已经规定了你的思维,使你的思维不能解放,不可能有什么别的新鲜的想法。本来这是非常好的事,‚床前明月光,疑是地上霜。举头望明月,低头思故乡。‛太好了,现在的孩子一般话还没说全已经都会背这首诗了,然后你一看到月亮就‚低头思故乡‛,那这个月亮到底是什么样儿,你就没有那种真实的原生的感受,你只有这个诗歌里边儿的。我就想我小学学作文的时候我买过一本叫做模范作文读本,现在这一类的东西那就是汗牛充栋了,这个读本给我印象最深就是我学会了一个词叫做‚皎洁‛,皎洁的月亮升起,我过去对月亮有感觉,觉得月亮当然她没有太阳那么亮,又不象星星,不知道该怎么形容,我发现了皎洁之后又发现了月亮,我觉得皎洁这两个字给我的帮助太大了。但是到后来我特别痛恨这两个字,我说你形容月亮,描写月亮,感受月亮你什么都行,老是‚皎洁‛,张三是皎洁的月亮,李四也是皎洁的月亮,弱智者也是皎洁的月亮,汉奸也是看见皎洁的月亮,我就是不‚皎洁‛!皎洁统治了人的思维。所以。已有的语言和文字的信息既是我们的财富,又是我们真正认识世界,进行创造的一个阻隔。
第五,语言还有一个陷阱就是从理论上说一切已经说出来的尤其是写下的东西都有可能被驳倒,我现在不管事实是不是被驳倒,但就是说你要抬杠,那没有一句话是不能抬杠的。说人都要吃饭,他说正绝食的人就不吃饭,刚做完肠胃切除手术的人也不吃饭,年岁过大,靠鼻饲的人也不吃饭。我不想多举了,就是说没有什么话是不可以推敲的,是不可以驳倒的,为什么现在文坛上有一批酷评家?因为语言和文字是最容易驳倒的,一个实验结果你想驳倒没那么容易,除非你自己也做一回试验。一个物理定律你想驳倒他不是那么容易,可是一个语言和文字所构成的,所表达的思想,所表达的命题,你可以攻其一点而不及其余,你可以无限夸张。有时候我们在生活中还可以常常看到两个人在讨论一个事情,本来是一个非常细微的差异的见解,但是两个人各不相让,你抓你的辫子越来越多,他抓他的辫子也越来越多,你说从来没见过!什么叫从来没见过?昨天你还见着呢!那个说你就不信,你不信管什么用啊?毛主席都信!语言文字可以造成人的沟通,又可以引起人的分歧,可以促进我们的思想、我们的感情更加成熟,更加明晰,或是更加敏锐,也可以阻隔我们的思想和感情来制造许许多多的无聊的冲突。我们可以想想在某种意义上说语言文字所制造的废品,所制造的垃圾,并不比语言文字所开放的奇葩少。非常抱歉,我也闹不清我今天讲了半天是不是又是一大堆语言的垃圾,请大家原谅。(完)(温奉桥、侯霞整理)
(本文为王蒙先生2004年4月14日在中国海洋大学的演讲)
第五篇:2014事业单位面试技巧:如何避开语言陷阱
2014事业单位面试技巧:如何避开语言陷阱
语言,是事业单位面试重要的内容之一。有些考生可能不知道,在面试中,考官有时候会设置多重语言陷阱,考察考生的能力,以达到优秀人才的录用。因此,在考场上,考生如何能绕开这些陷阱,更好地展现自己的魅力,取得面试官的青睐就非常重要了。下面,大家就可以跟随中公教育来一起了解有哪些语言陷阱,应对的方法是什么。
一、挑战式语言陷阱
面试时考生经常对自己薄弱的地方表现得不够自信,回答时容易出现纰漏。考官也会问及这方面的东西,例如:对于应届毕业生,面试考官会设问:“你的相关工作经验比较欠缺,你怎么看?” 考生有时不了解考官到底想要了解什么,常会回答:“不见得吧”、“我看未必”或“完全不是这么回事”,那么也许你已经掉进陷阱了,因为对方希望听到的是你对这个问题的看法,而不是简单、生硬的反驳。
对于这样的问题,考生可以用“这样的说法未必全对”、“这样的看法值得探讨”、“这样的说法有一定的道理,但我恐怕不能完全接受”为开场白,然后婉转地表达自己的不同意见。
二、用“激将法”遮蔽的语言陷阱
“激将法”是面试考官用来淘汰应考者的惯用手法。采用这种手法的面试考官,往往在提问之前就会用怀疑、尖锐、咄咄逼人的眼神逼视对方,先令对方心理防线步步溃退,然后冷不防用一个明显不友好的发问激怒对方。如:“你经历太单纯,而我们需要的是社会经验丰富的人”,“你性格过于内向,这恐怕与我们的职业不合适”,“你的专业与所申请的职位不对口”等等。
面对这种咄咄逼人的发问,作为考生,首先要做到的就是无论如何不要被“激怒”,如果你被“激怒”了,那么你就已经输掉了。那么,面对这样的发问,考生将如何应对呢? 考生不要被语言陷阱迷惑,可采用灵活巧妙的方式回避考官提出的问题。例如:考官说:“你的专业与所申请的职位不对口。”你可以巧妙地回答:“21世纪最抢手的就是复合型人才,而外行的灵感也许会超过内行,因为他们没有思维定势,没有条条框框。”
考生若结结巴巴,无言以对,抑或怒形于色,据理力争,异常激动,那就掉进了对方所设置的陷阱。
三、“请君入瓮”式的语言陷阱
在面试的各种语言陷阱中,“请君入瓮”式的语言陷阱使很多考生感到苦恼,因为它所列举的问题都是现实工作中经常遇到的。例如:“如果你拟定了一个方案,你的直接领导不满意,但是局长却非常的满意,你将如何处理? ”考生在回答这类问题时常常回答得不全面,容易出现顾上这头,又忽略了另一头,漏洞百出,容易让考官一眼就能察觉出考生是否具备思考问题的缜密性,甚至从这种压力面试中看到考生是否符合这一职位。考生在回答时要从工作职责上多思考,切勿违背了做人的根本,只要态度积极,原则问题把握准确,也可成功回避这类语言陷阱。
四、诱导式的语言陷阱
这类问题的特点是,面试考官往往设定一个特定的背景条件,诱导对方做出错误的回答,因为也许任何一种回答都不能让对方满意。这时候,考生的回答就需要用模糊语言来表示。
如:“依你现在的水平,报考这一职位没有感觉到屈才吗?有没有想过另谋职位?”考生在回答这类问题时,直接回答是或不是都不是最佳的答案,回答这类问题考生可以先用“不可一概而论”作为开头,然后回答以职位发展、人才培养等长远意义上的内容回答,给考官一个既不偏激又很大体的答案。
还有一种诱导式的语言陷阱是,考官的提问似乎是一道单项选择题,如果考生选了,就会掉进陷阱。比如说,对方问:“你认为金钱、名誉和事业
哪个重要?”很多考生可能会选择说三者都重要来回避考官的陷阱。但有时考官的提问却在误导你,让考生认为“这三者是相互矛盾的,只能选其一”。这时候切不可中了考官的圈套,必须冷静分析,考生可以首先明确指出这个前提条件是不存在的,再解释三者对我们的重要性及其统一性。
五、测试式的语言陷阱
这类问题的特点是虚构一种情况,然后让考生做出回答。比如“今天参加面试的有近10位候选人,如何证明你是最优秀的?”无论你给自己列举多少优点,别人总有你不具备的优点,因此正面回答这样的问题毫无意义,考生可以从侧面回答这个问题,了解考官的真正考点在哪里,然后针对这个考点寻找突破口,就像上面提到的这个问题,它往往是考察应考者随机应变的能力,考生可以从不否认其他人优点的情况下,突出描述一下自己具备的区别于他人的技能,从而使自己在众多候选人中脱颖而出。
面试的成功,是需要考生良好的心理素质和过硬的知识水平的。为了更好的面对考试,取得成功,备考前的练习必不可少。考生一定要把握好这段时间,多积累内容,掌握技巧,争取考试成功!
中公教育新疆事业单位:http:///xinjiang/