关键词不能为空

当前您在: 主页 > 英语 >

C语言缺陷与陷阱(笔记)

作者:高考题库网
来源:https://www.bjmy2z.cn/gaokao
2021-03-03 12:50
tags:

-

2021年3月3日发(作者:草原土拨鼠)


C


语言缺陷与陷阱(笔记)


< br>C


语言像一把雕刻刀,锋利,并且在技师手中非常有用。和任何锋利的工具一样,


C


会伤到那些


不能掌握它的人。本文介 绍


C


语言伤害粗心的人的方法,以及如何避免伤害。

< p>


第一部分研究了当程序被划分为记号时会发生的问题。

< br>第二部分继续研究了当程序的记号被编译


器组合为声明、


表达式和语句时会出现的问题。


第三部分研究了由多个部分组成、


分别编译并绑


定到一起的


C


程序。第 四部分处理了概念上的误解:当一个程序具体执行时会发生的事情。第


五部分研究了我们 的程序和它们所使用的常用库之间的关系。


在第六部分中,


我们 注意到了我们


所写的程序也许并不是我们所运行的程序;


预处理 器将首先运行。


最后,


第七部分讨论了可移植

< br>性问题:一个能在一个实现中运行的程序无法在另一个实现中运行的原因。



词法分析器(


lexical


ana lyzer


):检查组成程序的字符序列,并将它们划分为记号(


token


)一


个记号是一个由一个或多个字符构成的序列,


它在语言被编译时具有一个


(相关地)


统一的意义。



C


程序被两次划分为记 号,


首先是预处理器读取程序,


它必须对程序进行记号划分以发 现标识宏


的标识符。


通过对每个宏进行求值来替换宏调用,


最后,


经过宏替换的程序又被汇集成字符流送


给编译器。编译器再第二次将这个流划分为记号。



1.1=


不是



==



C


语言则是用


=


表示赋值而用


==


表示比较。这是因为赋值的频率要高于比较 ,


因此为其分配更短的符号。


C


还将赋 值视为一个运算符,因此可以很容易地写出多重赋值(如


a



=


b


=


c


),并且可以将赋值嵌入到一个大的表达式中。



1.2


&



|


不是


&&



||



1.3


多字符记号



C


语言参考手册说明了如何决定:


< br>如果输入流到一个给定的字符串为止已经被识别为记号,则


应该包含下一个字符以 组成能够构成记号的最长的字符串




最长子串原则




1.4


例外




组合赋值运算符如


+=


实际上是两个记号。因此,

< p>


a


+


/*


strange


*/


=


1





a


+=


1



是一个意思。看起来像一个单独的记号而实际上是多个记号的只有这一个特例。特别地,



p


-


>


a



是不合法的。它和



p


->


a



不是同义词。



另一方面,有些老式编 译器还是将


=+


视为一个单独的记号并且和

+=


是同义词。



1.5


字符串和字符



包围在单引号中的一个 字符只是编写整数的另一种方法。


这个整数是给定的字符在实现的对照序


列中的一个对应的值。


而一个包围在双引号中的字符串,


只是编写一个有双引号之间的字符和一


个附加的二进制值为零的字符所初始化的一个无 名数组的指针的一种简短方法。



使用一个指针来代替一个整数 通常会得到一个警告消息(反之亦然),使用双引号来代替单


引号也会得到一个警告消息 (反之亦然)。但对于不检查参数类型的编译器却除外。



由于 一个整数通常足够大,以至于能够放下多个字符,一些


C


编译器 允许在一个字符常量中


存放多个字符。这意味着用


'yes'< /p>


代替



将不会被发现。后者意味着



分别包含


y



e



s


和一个


空字符的四个连续存储器区域中的第一个的地址



而前者意味着



在一些实现定义 的样式中表示


由字符


y



e



s


联合构成的一个整数



。这两者之间的任何一致性都纯属巧合。


2


句法缺陷



理解这些记号是如何构成声明、表达式、语句和程序的。



2.1


理解声明


< br>每个


C


变量声明都具有两个部分:一个类型和一组具有特 定格式的、期望用来对该类型求值


的表达式。



float


*g(),


(*h)();



表示


*g()



(*h)()


都是


float


表达式。由于


()



*


绑定得更紧密,


* g()



*(g())


表示同样的东西 :


g


是一个返回指


float


指针的函数,而


h


是一个指向返回


float


的函数的指针。



当我们知道如何声明一个给定类型的变量以后,


就能够很容易地写出一个类型的模型< /p>



cast




只要删除变量名和分号并将所有的东西包围在一对圆括号中即可。



float


*g();



声明


g


是一个返回


f loat


指针的函数,所以


(float


*())


就是它的模型。


< p>
(*(void(*)())0)();


硬件会调用地址为


0


处的子程序



(*0)();


但这样并不行,


因为


*


运算符要求必须有一个指针作为它的操作数。


另外,


这个操作数必


须是一个指向函数的指针,


以保证


*


的结果可以被调用。


需要将


0


转换为一个可以描述



指向一个


返回


void


的函数的指针



的类型。


( Void(*)())0



在这里,我们解决这个问题时没有使 用


typedef


声明。通过使用它,我们可以更清晰地解决这 个


问题:



typedef


void


(*funcptr)();


//


typedef


funcptr


void


(*)();


指向返回


void


的函数的指针


< br>(*(funcptr)0)();


//


调用地址为


0


处的子程序



2.2


运算符并不总是具有你所想象的优先级



绑定得最紧密的运算符并不是真正的运算符:


下标、


函数调用 和结构选择。


这些都与左边相关联。



接下来是一元运算符。


它们具有真正的运算符中的最高优先级。


由于函数调用比一元运算符绑定


得更紧密,你必须写


(*p)( )


来调用


p


指向的函数;


*p()


表示


p


是一个返回 一个指针的函数。转换


是一元运算符,并且和其他一元运算符具有相同的优先级。一元运 算符是右结合的,因此


*p++


表示


* (p++)


,而不是


(*p)++


。< /p>



在接下来是真正的二元运算符。


其中数 学运算符具有最高的优先级,


然后是移位运算符、


关系运


算符、逻辑运算符、赋值运算符,最后是条件运算符。需要记住的两个重要的东西是:



1.



所有的逻辑运算符具有比所有关系运算符都低的优先级。



2.



移位运算符比关系运算符绑定得更紧密,但又不如数学运算符。



乘法、


除法和求余具有相同的优先级,


加法和减法具有相同的优先级,


以及移位运算符具有相同


的优先 级。



还有就是六个关系运算符并不具有相同的优先级:


==



!=


的优先级 比其他关系运算符要低。



在逻辑运算符中,

< br>没有任何两个具有相同的优先级。


按位运算符比所有顺序运算符绑定得都紧密,< /p>


每种与运算符都比相应的或运算符绑定得更紧密,并且按位异或(


^


)运算符介于按位与和按位


或之间。




三元运算符的优先级比我们提到过的所有运算符的优先级都低。



这个例子还说明了赋值运算符具有比条件运算符更低的优先级是有意义的。另外,所有的 复


合赋值运算符具有相同的优先级并且是自右至左结合的



具有最低优先级的是逗号运算符。赋值是另一种运算符,通常具有混合的优先级。

< p>


2.3


看看这些分号!



或者是一个空语句, 无任何效果;或者编译器可能提出一个诊断消息,


可以方便除去掉它。

< br>一个


重要的区别是在必须跟有一个语句的


if

< p>


while


语句中。


另 一个因分号引起巨大不同的地方是函


数定义前面的结构声明的末尾,考虑下面的程序片段 :



struct


foo


{



int


x;


}



f()


{


...


}



在紧挨着


f


的第一个


}


后面丢失了一个分号。它 的效果是声明了一个函数


f


,返回值类型是

struct



foo


,这个结构 成了函数声明的一部分。如果这里出现了分号,则


f


将被定义为 具有默认的整型


返回值


[5]




2.4


switch


语句


< br>C


中的


case


标签是真正的标 签:控制流程可以无限制地进入到一个


case


标签中。




看看另一种形式,假设

< p>
C


程序段看起来更像


Pascal




switch(color)


{


case


1:


printf


(


case


2:


printf


(


case


3:


printf


(


}



并且假 设


color


的值是


2


。则该程序将打印


yellowblue


,因为控制自 然地转入到下一个


printf()



调用。



这既是


C

语言


switch


语句的优点又是它的弱点。说它是弱点, 是因为很容易忘记一个


break



句 ,从而导致程序出现隐晦的异常行为。说它是优点,是因为通过故意去掉


break


语句,可以很


容易实现其他方法难以实现的控制结构。尤其是在一个 大型的


switch


语句中,我们经常发现对

< br>一个


case


的处理可以简化其他一些特殊的处理。



2.5


函数调用



和其他程序设计语言不同,


C


要求一个函数调用必须有一个参数列表,但可以没有参数。因 此,


如果


f


是一个函数,



f();



就是对该函数进行调用的语句,而



f;



什么也不做。

< br>它会作为函数地址被求值


,但不会调用它


[6]




2.6


悬挂


else


问题



一 个


else


总是与其最近的


if


相关联。



3


连接



一个


C


程序可能有很多部分组成,它们被分别编译,并由一个通常称为连接器、连接编辑器或< /p>


加载器的程序绑定到一起。


由于编译器一次通常只能看到一个文件 ,


因此它无法检测到需要程序


的多个源文件的内容才能发现的错 误。



3.1


你必须自己检查外部类型



假设你有一 个


C


程序,被划分为两个文件。其中一个包含如下声明:



int


n;



而令一个包含如下声明:



long


n;



这不是一个有效的


C


程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多< /p>


实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一个文件的内容。因 此,


检查类型的工作只能由连接器(或一些工具程序如


lint


)来完成;如果操作系统的连接器不能识


别数据类型,


C


编译器也没法过多地强制它。




那么,这个程序运行时实际会发生什么?这有很多可能性:



1.



实现足够聪明,


能够检测到类型冲突。


则我们会得到一个诊断消息,

说明


n


在两个文件中具有不


同的类 型。



2.


< br>你所使用的实现将


int



lo ng


视为相同的类型。典型的情况是机器可以自然地进行


32< /p>


位运算。


在这种情况下你的程序或许能够工作,好象你两次都将变 量声明为


long


(或


int


)。但这种程序


的工作纯属偶然。



3.



n


的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有

< br>效。这可能发生,例如,编译器可以将


int


安排在


long


的低位。


不论这是基于系统的还是 基于机


器的,这种程序的运行同样是偶然。



4.



n


的两个实例以另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在

< br>这种情况下,程序可能失败。



这种情况发生的另一个例 子出奇地频繁。程序的某一个文件包含下面的声明:



char


filename[]


=




而另一个文件包含这样的声明:



char


*filename;




尽管在某些环境中数组和指针的行为非常相似,但它们 是不同的。在第一个声明中,


filenam


e


是一个字符数组的名字。尽管使用数组的名字可以产生数组第一个元素的指针,但这个指针只

< p>
有在需要的时候才产生并且不会持续。


在第二个声明中,

< br>filename


是一个指针的名字。


这个指针


可以指向程序员让它指向的任何地方。如果程序员没有给它赋一个值,它将具有一个默认的


0


值(


NULL


)(


[


译注


]


实际 上,在


C


中一个为初始化的指针通常具有一个随机的值,这是很 危险


的!)。




这两个声明以不同的方式使用存储区,它们不可能共存。




避免这种类型冲突的一个方法是使用像


lint


这样的工具(如果可以的话)。为了在一个程序


的不同编译单元之间检查类型冲突,


一些程序需要一次看到其所有部分。


典型的编译器无法完成,



lint


可以。




避免该问题 的另一种方法是


将外部声明放到包含文件中


。这时,一个外部对 象的类型仅出现


一次


[7]




4


语义缺陷



4.1


表达式求值顺序




一些


C


运算符以一种已知的、特定的顺序对其操作数进行求值。但另一些不能。例如,考虑


下 面的表达式:



a


<


b


&&


c


<


d



C


语言定义规定


a


<


b


首先被求值。

< br>如果


a


确实小于


b



c


<


d


必须紧接着被求值以计算整个表


达式的值。但如果


a


大于或等于


b


,则

c


<


d


根本不会被求值。



要对


a


<


b


求值,编译器对


a



b


的求值就会有一个先后。但在一些机器上,它们也许是


并行进行的。



C


中只有四个运算符


&&



||



?:



,


指定 了求值顺序。


&&



||


最先对左边的操作数进行求值,而


右边的操作数只有在需要的时候才进行求值 。而


?:


运算符中的三个操作数:


a< /p>



b



c


,最先对


a


进行求值,之后仅对


b



c


中的一个进行求值, 这取决于


a


的值。


,

< br>运算符首先对左边的操作数


进行求值,然后抛弃它的值,对右边的操作数进行求值


[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


&&



||



!


运算符



4.3


下标从零开始




在很多语言中,具有


n


个元素的数组其元素的号码和 它的下标是从


1



n

< br>严格对应的。但在


C


中不是这样。



个具有


n


个元素的


C


数组中没有下标为


n


的元素 ,


其中的元素的下标是从


0



n


-


1



因此从其


它语言转到


C


语言的程序员应该特别小心地使用数组:



int


i,


a[10];


for(i


=


1;


i


<=


10;


i++)



a[i]


=


0;



4.4


C


并不总是转换实参




下面的程序段由于两个原因会失败:



double


s;


s


=


sqrt(2);


printf(


s);




第一个原因是


sqrt()< /p>


需要一个


double


值作为它的参数, 但没有得到。第二个原因是它返回一



double

< p>
值但没有这样声名。改正的方法只有一个:



double


s,


sqrt();


s


=


sqrt(2.0);


printf(


s);



C


中有两个简单的规则控制着函数参数的转换:


(1 )



int


短的整型被转换为


int



(2)



double


短的浮点类型被转换为


dou ble


。所有的其它值不被转换。


确保函数参数类型的正确性是 程序员


的责任。



因此,一个程序员如 果想使用如


sqrt()


这样接受一个


double


类型参数的函数,就必须仅传递给



float



double


类型的参数。常数


2


是一个


int


,因此其类型是错误的。




当一个函数的值被用在表达式中时,其值会被自动地转换为适当的类型。然而,为了完成这


个自动转换,


编译器必须知道该函数实际返回的类型。


没有更进一步声名的函数被假设返回


int



因此声名这样的函数并不是必须的。然而,


sqrt()


返回


double


,因此在成功使用它之前必须要声


名。



这里有一个更加壮观的例子:



main()


{



int


i;



char


c;



for(i


=


0;


i


<


5;


i++)


{



scanf(


&c);



printf(


i);



}



printf(


}




表面上看,这个程序从标准输入中读取五个整数并向标 准输出写入


0


1


2


3


4


。实际上,它

< br>并不总是这么做。譬如在一些编译器中,它的输出为


0


0


0


0


0


1


2


3


4





为什么?因为


c


的声名是


char


而不是


i nt


。当你令


scanf()


去读取一 个整数时,它需要一个指


向一个整数的指针。但这里它得到的是一个字符的指针。但


scanf()


并不知道它没有得到它所需


要的:


它将输入看作是一个指向整数的指针并将一个整数存贮到那里。

< br>由于整数占用比字符更多


的内存,这样做会影响到


c


附近的内存。



< br>c


附近确切是什么是编译器的事;在这种情况下这有可能是


i


的低位。因此,每当向


c


中读入< /p>


一个值,


i


就被置零。当程序最后到达文 件结尾时,


scanf()


不再尝试向


c


中放入新值,


i


才可以正

< p>
常地增长,直到循环结束。



4.5


指针不是数组



C

程序通常将一个字符串转换为一个以空字符结尾的字符数组。假设我们有两个这样的字符串

< br>s



t


,并且我们想要将它们连 接为一个单独的字符串


r


。我们通常使用库函数


strcpy()



strcat()


来完成。下面这种明显的方法并不会工作:



char *r;


strcpy(r, s);


strcat(r, t);



这是因为


r

< br>没有被初始化为指向任何地方。尽管


r


可能潜在地表示某 一块内存,但这并不存在,


直到你分配它。




让我们再试试,为


r


分配一些内存:



char r[100];


strcpy(r, s);


strcat(r, t);



这只有在


s

< br>和


t


所指向的字符串不很大的时候才能够工作。不幸的是 ,


C


要求我们为数组指定的


大小是一个 常数,因此无法确定


r


是否足够大。然而,很多


C


实现带有一个叫做


malloc()


的库函

-


-


-


-


-


-


-


-



本文更新与2021-03-03 12:50,由作者提供,不代表本网站立场,转载请注明出处:https://www.bjmy2z.cn/gaokao/700067.html

C语言缺陷与陷阱(笔记)的相关文章

  • 余华爱情经典语录,余华爱情句子

    余华的经典语录——余华《第七天》40、我不怕死,一点都不怕,只怕再也不能看见你——余华《第七天》4可是我再也没遇到一个像福贵这样令我难忘的人了,对自己的经历如此清楚,

    语文
  • 心情低落的图片压抑,心情低落的图片发朋友圈

    心情压抑的图片(心太累没人理解的说说带图片)1、有时候很想找个人倾诉一下,却又不知从何说起,最终是什么也不说,只想快点睡过去,告诉自己,明天就好了。有时候,突然会觉得

    语文
  • 经典古训100句图片大全,古训名言警句

    古代经典励志名言100句译:好的药物味苦但对治病有利;忠言劝诫的话听起来不顺耳却对人的行为有利。3良言一句三冬暖,恶语伤人六月寒。喷泉的高度不会超过它的源头;一个人的事

    语文
  • 关于青春奋斗的名人名言鲁迅,关于青年奋斗的名言鲁迅

    鲁迅名言名句大全励志1、世上本没有路,走的人多了自然便成了路。下面是我整理的鲁迅先生的名言名句大全,希望对你有所帮助!当生存时,还是将遭践踏,将遭删刈,直至于死亡而

    语文
  • 三国群英单机版手游礼包码,三国群英手机单机版攻略

    三国群英传7五神兽洞有什么用那是多一个武将技能。青龙飞升召唤出东方的守护兽,神兽之一的青龙。玄武怒流召唤出北方的守护兽,神兽之一的玄武。白虎傲啸召唤出西方的守护兽,

    语文
  • 不收费的情感挽回专家电话,情感挽回免费咨询

    免费的情感挽回机构(揭秘情感挽回机构骗局)1、牛牛(化名)向上海市公安局金山分局报案,称自己为了挽回与女友的感情,被一家名为“实花教育咨询”的情感咨询机构诈骗4万余元。

    语文