C程语言序模式
一个语言的程序设计有一些常用的基本模式,这些模式是许多人长期工作的总结。在这个语言的程序中到处可见。对于一些典型问题,采用适当的模式是最容易把程序写好、写正确的。在这里我们将从简单到复杂,分门别类地列处一些C程序模式,供大家参考。这里列出的东西选自各种材料(包括《从问题到程序》书中)。这里列出的东西是很初步的。如果你认为有些东西也很重要,请给我们提示。
在所有模式描述中,用$$符号括起来的一段段文字表示的是应该实际写出的东西。
有关解释如果写了页数,请参考《从问题到程序》书中相关的页。
--------------------------------------------------------------------------------
简单输出程序模式:
#include ;
main () {
$一个或几个输出语句,例如printf("Hello, world!/n");$
}
----------------------------------------------------
例:
#include ;
main() {
printf("Welcome/n");
printf("to/n");
printf("Beijing!/n");
}
--------------------------------------------------------------------------------
简单表达式计算模式:
#include ;
main() {
printf($格式描述串$, $一个或几个表达式$);
}
---------------------------------------------------
注意:格式描述串中转换描述与参数个数一致,类型一致。
---------------------------------------------------
#include ;
main() {
printf("%f * %f = %f", 3.14, 6.5, 3.14 * 6.5);
}
--------------------------------------------------------------------------------
简单算术计算函数定义:
double fun ($一个或几个参数的说明$) {
return $计算表达式$;
}
----------------------------------------------------
1,函数返回值类型根据需要写出,未必是double;
2,参数写出类型和参数名的对,多个参数用逗号分隔;
3,计算表达式描述如何由参数出发算出函数值;
4,如果计算中需要用数学函数,应该在程序开始写一行
#include ;
----------------------------------------------------
例:
double circle_area (double r) {
return r * r * 3.14159265;
}
--------------------------------------------------------------------------------
有函数定义的一般程序:
#include …
…… /* 函数定义写在这里(可以有一个或几个) */
main () {
…… /* 主程序体,通常包含对函数的调用 */
}
--------------------------------------------------------------------------------
常用while循环形式:
n = 1; /* 初始化循环中使用的变量 */
while (n <= 100) { /* 已知次数的循环用for结构写得更多 */
/* 其他语句 */
n = n + 1; /* 循环最后更新辅助变量 */
}
注:这种更常见的是采用for循环形式写。见下。
----------------------------------------------------
/* 有关变量的初始化 */
while ($表达式$ >;= 1E-6) {
/* 循环中的计算应该影响“表达式”的值,否则将导致无限循环 */
}
--------------------------------------------------------------------------------
常用for循环形式:
for (n = 0; n < N; n++) {
$循环体里的语句$
}
---------------------------------------------------
注意:C语言里一般不用(除非有特殊目的):
for (n = 1; n <= N; n++) {
$循环体里的语句$
}
--------------------------------------------------------------------------------
给程序计时:
#include ;
#include ;
/* 其他定义 */
main () {
/* 其他程序变量的定义 */
double x;
x = clock() / CLOCKS_PER_SEC;
/* 需要计时的程序片段 */
x = clock() / CLOCKS_PER_SEC - x;
printf("Timing: %f/n", x); /* 输出形式自己确定 */
}
---------------------------------------------------
注意:不同系统的计时精度可能不同。有些系统还有非标准的计时功能。
--------------------------------------------------------------------------------
输入和处理由标准输入文件来的一系列字符
int c;
/* 其他定义或语句 */
while ((c = getchar()) != EOF) {
/* 对读入字符的处理 */
}
---------------------------------------------------
1,接收读入字符的变量(例如上面的c)必须用int类型;
2,注意while条件中的括号,不能写错;
3,应该写 #include ;
--------------------------------------------------------------------------------
最常见的数组操作
for (i = 0; i < N; i++) {
.... ....
.... a .... b
.... ....
}
---------------------------------------------------
1,循环范围不能超出数组下标的范围
--------------------------------------------------------------------------------
典型的处理数组的函数
double sqsum(double a[], int n) {
double x = 0.0;
int i;
for (i = 0; i < n; i++)
x += a * a;
return x;
}
---------------------------------------------------
1,注意数组参数的写法;
2,增加一个描述数组长度的参数,借助于它控制对数组元素的处理;
3,通过这种函数可以改变实际参数数组。
--------------------------------------------------------------------------------
处理字符串的典型函数
void str_copy (char s[], char t[]) {
int i;
for (i = 0; t != '/0'; i++)
s = t;
s = '/0';
}
---------------------------------------------------
void str_copy1 (char *s, char *t) {
for (; *t != '/0'; s++, t++)
*s = *t;
*s = '/0';
}
---------------------------------------------------
1,参数用 char s[] 或者 char *s 描述,两者等价,但用 char s[] 更具有提示性;
2,用遇到空字符'/0'作为循环结束条件;
3,还有些更紧凑的写法,参见有关书籍
--------------------------------------------------------------------------------
逐个处理命令行参数
int main (int argc, char *argv[]) {
int i;
... ...
for (i = 0; i < argc; i++) {
.... argv ....
}
... ...
}
---------------------------------------------------
int main (int argc, char *argv[]) {
... ...
for (; *agrv != NULL; argv++) {
... *argv ...
}
... ...
}
---------------------------------------------------
循环中的 *argv 将依次取各个命令行参数字符串。
注意:编号0的参数是命令名本身,如果不处理它应该先跳过去。
--------------------------------------------------------------------------------
使用动态存储分配函数malloc或calloc
if ((p = (... *)malloc(...)) == NULL) {
.. ... /* 对分配未成功情况的处理 */
}
---------------------------------------------------
1,指针应当具有合适的类型;
2,在malloc名前写合适的转换描述;
3,malloc的参数应当借助sizeof计算。
--------------------------------------------------------------------------------
链接表的处理模式(以扫描的方式逐个处理表中的数据)
for (p = head; p != NULL; p = p->;next) {
.... p->;data ....
}
---------------------------------------------------
假设head是指向表头结点的指针。
结点中的next成分是指向下一个结点的指针,data是数据。
--------------------------------------------------------------------------------
文件使用的基本模式
FILE *fp;
.... ....
if ((fp = fopen($文件名$, $打开方式$)) == NULL) {
/* 当文件打不开时的处理 */
}
... ... /* 对文件的各种操作 */
fclose(fp);
---------------------------------------------------
执行文件打开操作之后,必须检查操作是否正常完成。
--------------------------------------------------------------------------------
使用;里的bsearch和qsort,比较函数的基本写法
1,确定被比较对象的顺序关系;
2,比较函数总用两个const void* 参数;
3,在函数开始时将参数正确转换到指向数组元素类型的指针;
4,在第一个参数大于、等于、小于第二个时分别返回1、0、-1;
5,注意使用方式。
---------------------------------------------------
例子:假定被排序的是整数数组。定义比较函数:
int icmp(const void *p, const void *q){
const int *m = p, *n = q;
return *m >; *n ? 1 : (*m == *n ? 0 : -1);
}
使用:
int *p, a[] = {5, 6, 3, 28, 23, 34, 7, 9, 6, 14}, k = 7;
int main () {
... ...
qsort(a, sizeof(a)/sizeof(int), sizeof(int), icmp);
/* 这样,数组a的元素已经按照上升顺序排列好了 */
p = bsearch(&k, a, sizeof(a)/sizeof(int), sizeof(int), icmp);
/* 指针p将指向数组a中元素7的位置 */
... ...
}
---------------------------------------------------
请注意在这里如何为bsearch和qsort提供所要求的各个参数。
写好C程序的10+大要点
要写好C语言程序,最重要的当然是把要解决的问题分析清楚,设计好解决问题的方案和通过计算实现求解的过程,对问题的求解过程进行科学的结构化的分解。在此基础上进一步考虑如何写程序的时候,下面的建议可能有所帮助。
这些建议中有些是一般性的,不仅仅对C语言程序设计有效;也有些是特别针对C语言程序设计的。这个表还会进一步修改和扩充,欢迎提出意见。
--------------------------------------------------------------------------------
1)应该特别注意程序的书写格式,让它的形式反映出其内在的意义结构。
程序是最复杂的东西(虽然你开始写的程序很简单,但它们会逐渐变得复杂起来),是需要用智力去把握的智力产品。良好的格式能使程序结构一目了然,帮助你和别人理解它,帮助你的思维,也帮助你发现程序中不正常的地方,使程序中的错误更容易被发现。
人们常用的格式形式是:逻辑上属于同一个层次的互相对齐;逻辑上属于内部层次的推到下一个对齐位置。请参考本课程的教科书或《C程序设计语言》(The C Programming Language,Brian W. Kernighan & Dennis M. Rirchie,清华大学出版社,大学计算机教育丛书(影印版,英文),1996。)
利用集成开发环境(IDE)或者其他程序编辑器的功能,可以很方便地维护好程序的良好格式。请注意下面这几个键,在写程序中应该经常用到它们:Enter键(换一行),Tab键(将输入光标移到下一个对齐位置——进入新的一个层次),Backspace键(回到前一个对齐位置——退到外面的一个层次)。
--------------------------------------------------------------------------------
2)用最规范的、最清晰的、最容易理解的方式写程序。注意人们在用C语言写程序的习惯写法,例如教科书中解决类似问题时所使用的写法,《C程序设计语言》一书中有许多极好的程序实例。在这里有一个关于程序模式的相关网页,里面也列出了一些常用的模式。
C语言是一个非常灵活的语言,你可能在这里用许多非常隐晦的方式写程序,但这样写出的程序只能是作为一种玩意儿,就像谜语或者智力游戏。这些东西可以用于消磨时间,但通常与实际无缘。在我们的C语言讨论组里提到过这种东西。
--------------------------------------------------------------------------------
3)在编程中,应仔细研究编译程序给出的错误信息和警告信息,弄清楚每条信息的确切根源并予以解决。特别是,不要忽略那些警告信息,许多警告信息源自隐含的严重错误。我们有许多办法去欺骗编译程序,使它不能发现我们程序中的错误,但这样做最终受到伤害的只能是自己。
--------------------------------------------------------------------------------
4)随时注意表达式计算过程和类型。注意运算符的优先级和结合顺序,不同类型的运算对象将怎样转换,运算的结果是什么类型的,等等。在必要的时候加上括号或显式的类型强制转换。
C语言的运算符很多,优先级定义也不尽合理,很难完全记清楚,因此要特别注意。需要时查一查(不要怕麻烦,相关网页有运算符表),或者直接按照自己的需要加上几个括号。
--------------------------------------------------------------------------------
5)绝不去写依赖于运算对象求值顺序的表达式。对于普通二元运算符的运算对象,函数调用的各个实际参数,C语言都没有规定特定求值顺序。因此,我们不应该写那种依赖于特定求值顺序的表达式,因为不能保证它一定得到什么结果。例如下面的表达式和函数调用都是不合适的,很可能产生你预料不到的结果:
scanf("%d %d", i++, a);
m = n * n++;
--------------------------------------------------------------------------------
6)总保证一个函数的定义点和它的所有使用点都能看到同一个完整的函数原型说明。参看《从问题到程序》第103-107页。
--------------------------------------------------------------------------------
7)总注意检查数组的界限和字符串(也以数组的方式存放)的结束。C语言内部根本不检查数组下标表达式的取值是否在合法范围内,也不检查指向数组元素的指针是不是移出了数组的合法区域。写程序的人需要自己保证对数组使用的合法性。越界访问可能造成灾难性的后果。
例:在写处理数组的函数时一般应该有一个范围参数;处理字符串时总检查是否遇到空字符'/0'。
--------------------------------------------------------------------------------
8)绝不对空指针或者悬空的指针做间接访问。这种访问的后果不可预料,可能造成系统的破坏,也可能造成操作系统发现这个程序执行非法操作而强制将它终止。
--------------------------------------------------------------------------------
9)对于所有通过返回值报告运行情况或者出错信息的库函数,都应该检查其执行是否正常完成。如果库函数没有完成操作(可能因为各种原因),随后的操作有可能就是非法的。这种错误也可能在程序运行中隐藏很长时间,到很后来才暴露出来,检查错误非常困难。
--------------------------------------------------------------------------------
10)在带参数宏的定义字符串中,一般应该给整个字符串和其中出现的每个参数都加括号。
C语言预处理程序是个简单的文本替换程序,它根本不知道C语言的语法结构、优先级规则等。不写括号有时会产生我们不希望的代换结果。
--------------------------------------------------------------------------------
11)所有外部变量名字、所有函数名字,应该只靠前6个字符就能够互相区分。因为有些老的编译程序只关注这些名字的前6个字符。如果不注意这个问题,就可能引起隐含的连接错误。
--------------------------------------------------------------------------------
有关如何写好程序,如何将自己发展成为一个高水平的计算机工作者(真正的高级程序工作者,而不是那种拿证书的所谓“高级程序员”)还需要进一步学习和实践。如果希望向这个方向努力,我翻译的一本书可能对你有所帮助:
《程序设计实践》,(The Practice Of Programming, Brian W. Kernighan & Bob Pike 1999)。机械工业出版社2000。这本书讨论了程序设计的许多重要的实践性问题,值得每个学过一个程序设计语言,有些程序设计经验,热爱或梦想在计算机领域工作的人阅读参考。在这里可以看到原书的前言和章节表。还有我为该书中文版写的译者序言。
写坏C程序的几大诀窍
如果你常常按照下面方式写程序,可以保证你的程序经常出错,你也经常头疼。
--------------------------------------------------------------------------------
1) 根本不管程序的格式,把程序写得老师也看不懂(自己当然更看不懂)。
用格式迷惑自己也是弄坏程序的绝招。例如:
if (a >; 0)
if (x == y) {
....
};
else {
....
}
请自己分析这到底是什么意思。
--------------------------------------------------------------------------------
2)不关心scanf或者printf中格式串和对应参数类型匹配的问题。例如(假设x, y是double类型,n是int类型)写:
printf("%d, %f", x, n);
scanf("%d %f", &x, &y, &n);
这样可以保证输入输出总出现莫名其妙的问题,而且你和别人都找不到原因。
--------------------------------------------------------------------------------
3)写scanf时,在接收输入值的变量名之前不写&符号。
这样做常常还可以顺便摧毁你所使用的计算机系统,给自己再多找点麻烦,有自制病毒之妙。
--------------------------------------------------------------------------------
4)写注释时随便地忘记几个结束符号。例如:
x = y + 1; /* ha ha ha
z = x * 2; /* fine fine fine */
这样可以保证编译程序“按照你的指示”把你的一段代码吃掉。
--------------------------------------------------------------------------------
5)在比较的时候用 = 代替 ==。例如:
if (x = y)
z = x + 5;
这可以保证x和y一定相同,而是否执行赋值就看你的运气了。
--------------------------------------------------------------------------------
6)定义局部变量后,不初始化就使用。例如:
int fun(int n) {
int m;
return n + m;
}
如果对指针变量这样做,常可以带来隐秘的破坏效果,让最有经验的人也难找到毛病的根源。例如写:
int fun(int n) {
int* p;
*p = n*n;
return *p + n;
}
这样做有时可以顺便摧毁你用的计算机系统,再给自己多找点麻烦。
--------------------------------------------------------------------------------
7)函数定义为有返回值的,但(有时)却不去写return(101-102页)。
这样做可以保证你的程序不时出现古怪行为。
--------------------------------------------------------------------------------
8)不关心变量的范围,例如对循环次数很多的变量用 short 类型做循环等。
这样做可以得到不明不白的结果;有时还可以使程序永不停止,直到你用的计算机累死为止。
--------------------------------------------------------------------------------
9)用sizeof运算符去计算函数的数组参数的大小,或者计算字符串的大小。
这样做一般可以保证计算结果错误,出现的问题难以预料。
--------------------------------------------------------------------------------
10)不写函数原型说明,采用过时原型说明形式(103-107页),或者故意写错误的原型说明。
这样做一般都能骗过编译程序,阻止它帮助你检查程序错误,使你自己麻烦多多。
--------------------------------------------------------------------------------
11)定义带参数宏的时候尽量节约括号,省得写起来麻烦。
这样可以保证在使用宏的地方不时地出现隐含错误,就像埋下的地雷,检查源程序也很难发现。
--------------------------------------------------------------------------------
写坏C程序的秘诀还有很多,我们将在这里总结交流如何写坏程序,使自己头疼,给老师找麻烦的经验。欢迎大家投稿。来函请发“坏程序经验”。
查找和排除程序编译中的错误(主要是语法错误)
(在建设和不断修改中,欢迎提出意见)
学生: 怎么办啊?我的程序编译通不过,出了这么多错误信息!
老师: 不要慌!你先集中力量找出第一个错误的真正原因,并将这个问题解决掉。然后再分析后面的错误。如果看不清楚下面的问题,那么就重新编译一下,并按照同一个方式重复去做。
--------------------------------------------------------------------------------
不能通编译过的程序实际上还不是合法的程序,因为它不满足C语言对于程序的基本要求。
检查语法错误的第一要义:集中力量检查系统发现的第一个错误,弄清并改正它。
在编译过程中系统发现的错误主要有两类:基本语法错误和上下文关系错误。这些错误都在表面上,可以直接看得见。也是比较容易弄清,比较容易解决的。关键是需要熟悉C语言的语法规定和有关上下文关系的规定,按照这些规定检查程序正文,看看存在什么问题。
编译中系统发现错误都能指出错误的位置。不同系统在这方面的能力有差异,在错误定位的准确性方面有所不同。有的系统只能指明发现错误的行,有的系统还能够指明行内位置。
一般说,系统指明的位置未必是真实错误出现的位置。通常情况是错误出现在前,而系统发现错误在后,因为它检查到实际错误之后的某个地方,才能确认出了问题,因此报出错误信息。要确认第一个错误的原因,应该从系统指明的位置开始,在那里检查,并从那里开始向前检查。
系统的错误信息中都包含一段文字,说明它所认定的错误原因。应该仔细阅读这段文字,通常它提供了有关错误的重要线索。但也应该理解,错误信息未必准确,有时错误确实存在,但系统对错误的解释也可能不对。也就是说,在查找错误时,既要重视系统提供的错误信息,又不应为系统的错误信息所束缚。
发现了问题,要想清楚错误的真正原因,然后再修改。不要蛮干。在这时的最大诱惑就是想赶快改,看看错误会不会消失。但是蛮干的结果常常是原来的错误没有弄好,又搞出了新的错误。
另一个值得注意的地方:程序中的一个语法错误常常导致编译系统产生许多错误信息。如果你改正了程序中一个或几个错误,下面的弄不清楚了,那么就应该重新编译。改正一处常常能消去许多错误信息行。
--------------------------------------------------------------------------------
解决语法错误
常见语法错误:
1)缺少语句、声明、定义结束的分号。
2)某种括号不配对。C语言中括号性质的东西很多,列举如下:
( ), [ ], { }, ' ', " ", /* */
在不同位置的括号不配对可能引起许多不同的错误信息。
3)关键字拼写错误。
较难认定的典型错误:
1)宏定义造成的错误。这种东西不能在源程序文件中直接看到,是在宏替换之后出现的。常见的能引起语法错误的宏定义错误:宏定义中有不配对的括号,宏定义最后加了不该有的分号,……
--------------------------------------------------------------------------------
解决上下文关系错误
1)变量没有定义。产生这个问题的原因除了变量确实没有大意外,还可能是变量的拼写错误,变量的作用域问题(在不能使用某个变量的地方想去用那个变量)。
2)变量重复定义。例如在同一个作用域里用同样名字定义了两个变量,函数的局部变量与参数重名等。
3)函数的重复定义。可能是用同一个名字定义了两个不同的函数。或者是写出的函数原型在类型上与该函数的定义不相符。有时没有原型而直接写函数调用也可能导致这种错误信息,因为编译程序在遇到函数调用而没有看到函数原型或函数定义时,将给函数假定一个默认原型。如果后来见到的函数定义与假定不符,就会报告函数重复定义错误。
4)变量类型与有关运算对运算对象或者函数对参数的要求不符。例如有些运算(如 %)要求整数参数,而你用的是某种浮点数。
5)有些类型之间不能互相转换。例如你定义了一个结构变量,而后要用它给整数赋值。系统容许的转换包括:数值类型之间的转换,整数和指针之间的转换,指针之间的转换。其余转换(无论是隐含的,还是写出强制)都不允许。参见《C语言程序设计》(K&R)197-199页。
--------------------------------------------------------------------------------
如何看待编译警告
当编译程序发现程序中某个地方有疑问,可能有问题时就会给出一个警告信息。警告信息可能意味着程序中隐含的大错误,也可能确实没有问题。对于警告的正确处理方式应该是:尽可能地消除之。对于编译程序给出的每个警告都应该仔细分析,看看是否真的有问题。只有那些确实无问题的警告才能放下不管。
注意:经验表明,警告常常意味着严重的隐含错误。
常见警告:
1)(局部自动)变量没有初始化就使用。如果对局部指针变量出现这种情况,后果不堪设想。对于一般局部自动变量,没有初始化就使用它的值也不会是有意义的。
2)在条件语句或循环语句的条件中写了赋值。大部分情况是误将 == (等于判断)写成 = 了。这是很常见的程序错误,有些编译程序对这种情况提出警告。
查找连接错误
(在建设和不断修改中,欢迎提出意见)
学生: 我的程序编译没错,为什么系统说不能连接,不能产生可执行程序?
老师: 那说明程序的外部变量、函数的名字方面出了些问题。这些问题比较简单,但修改时也要特别小心,不要把其他正确的东西改错了。
--------------------------------------------------------------------------------
连接是编译完成后的下一个程序加工步骤。在这个步骤中,连接程序的工作对象是:1)由你所编写的程序源文件产生的目标文件(一个或者几个);2)语言系统提供的一些目标代码文件,包括基本运行模块(也称为运行系统)和库文件。
连接程序的工作包括两部分:1)将所有需要的目标代码拼装到一个文件中(这是最后可执行文件的基础);2)将外部对象的使用和定义连接起来,包括所有函数调研的实际调用代码的建立,正确设置所有外部变量的使用。
连接错误有两类:
1)缺定义。当程序中出现对某个外部对象的使用,而连接程序找不到对应的定义时,将产生这个错误。
2)重复定义。当被连接的各个部分中出现某个名字的多个定义时,将产生这个错误。
--------------------------------------------------------------------------------
缺定义错误的常见原因:
1)名字拼写错误。例如将 main 拼写为 mian,连接时就会产生缺定义的连接错误。因为程序的基本允许模块里有一个对 main 的调用,连接程序需要找它的定义而没有找到。调用自己的函数名字写错的情况也很常见。
2)真的就是没有定义。例如你想调用一个非标准的C库函数,而你所用的系统里没有。或者是你要调用的自定义函数忘了定义。有时也有这样的情况,系统里有这个函数,但需要特别设置才能够找到它(因为函数放在某个非标准的库文件里),解决这种问题需要查阅有关的系统手册,这里不讨论了。
如果真是缺了定义,那就只能设法补上。
--------------------------------------------------------------------------------
重复定义错误的常见原因:
1)重复定义可能是自己(在不同源文件里)定义的两个东西采用了同样的名字,或者是自己定义的东西恰好与C语言系统内部定义的某个东西重名。这时都需要改名字。
2)可能你在一个文件里定义了某个变量,而在另一个文件里需要使用它,但却忘记在变量说明前加 extern 关键字。
3)有些连接程序只按照外部名字的前X个(常见的是前6个,这是C语言标准的最低要求)字符考虑连接问题。如果你程序里有多个对象前6个字符相同,或者恰好某个对象名字的前6个字符与编译系统所提供模块里的某个名字相同,那么就可能出问题。
解决办法:找出出现冲突的名字,系统地将它们改为另外的名字。请注意,在改名字时一定要特别小心,如果更改不当或者不彻底,就会引进错误(某个使用实际用的可能不是你所希望的东西)。
利用C语言的static功能,将所有只在一个文件里使用的外部对象定义为static,可以避免自己在多个文件里定义的东西互相冲突(即使是用同样的名字,如果都是static也不会冲突,因为static的外部名字只在本文件里可以看见)。
查找和排除程序运行中发现的错误(debugging)
(在建设和不断修改中,欢迎提出意见)
学生: 我的程序编译连接都通过了,为什么运行中系统还会报出错误?
老师: 这一般说明你的程序在某些方面做了违反C语言规定(违规)的事情,而这种情况是在运行的动态过程中出现的。你现在用的系统能够检查出这个问题,所以就报了错误。
学生: 我的程序编译运行都正常,怎么就是结果不对?(怎么就不能完成我想做的事情?)
老师: 出现这种问题的可能原因很多,解决起来也比较困难些。你最好首先认真读一读你的程序,想一想它可能存在什么问题,然后再…………
--------------------------------------------------------------------------------
从外面看,运行时发现的错误可以分为两类:一类是系统能够检查并产生信息的错误;另一类是系统不检查也不产生运行错误信息的错误。不同的系统在检查哪些问题方面可能差别很大。
抽象地看,运行时发现的程序错误可以分为两类:一类是程序中某些地方执行了违反语言规定的操作,由此产生某种影响导致程序出错;另一类问题出在程序本身,例如程序的算法不对,或者是程序写的不对(没有表达你所想说的东西),这些一般称为逻辑错误。这种问题分类只有提示性,并不是绝对的,有时也很难划分清楚。
--------------------------------------------------------------------------------
违规型的错误
最常见的违规错误是非法地址访问。有些系统(例如DOS)对这类错误完全不检查,可能造成很严重的后果,常常会破坏系统,造成死机或者非常奇怪的系统行为。有些系统(例如 Windows NT)管理比较严格,可能确认程序非法访问而将其kill掉。
1)对空指针、未初始化的指针的间接访问。这涉及到对指针值所确定地址的访问,常常是非法的。
2)把整数或者其他变量当作指针使用,造成访问非法地址的情况,例如,假定 n 和 x 分别是整的和双精度的变量;下面语句将它们的值当作指针值使用,形成非法访问:
scanf("%d %lf", n, x);
3)数组的越界访问。效果无法预料,有时可能被系统检查出来,有时可能检查不出来,造成奇怪的程序行为。
--------------------------------------------------------------------------------
逻辑型的错误(语义错误)
一类常见错误是计算溢出、除零等。C语言对于无符号数的上溢出(超出表示范围)自动丢掉最高位,对于一般整数类型、浮点数类型,语言的标准本身并没有明确规定,不同C语言系统的处理方式可能不同。大部分C语言系统忽略整数溢出的情况。无论如何,出现溢出往往会造成结果与预想的东西不符。
--------------------------------------------------------------------------------
排除程序错误的基本方法
--------------------------------------------------------------------------------
排错系统(debugger)的使用,原理、技术、方法。局限性。
--------------------------------------------------------------------------------
参考材料:《程序设计实践》第5章:排错。
ANSI C 与 K&R C
C语言由Dennis M. Ritchie在1973年设计和实现。从那以后使用者逐渐增加。到1978年Ritchie和Bell实验室的另一位程序专家Kernighan合写了著名的《The C Programming Language》,将C语言推向全世界,许多国家都出了译本,国内有一些C语言书就是这本书的翻译或者编译。由这本书定义的C语言后来被人们称作 K&R C。
随着C语言使用得越来越广泛,出现了许多新问题,人们日益强烈地要求对C语言进行标准化。这个标准化的工作在美国国家标准局(ANSI)的框架中进行(1983-1988),最终结果是1988年10月颁布的ANSI标准X3.159-1989,也就是后来人们所说的ANSI C标准。由这个标准定义的C语言被称作ANSI C。
ANSI C标准很快被采纳为国际标准和各国的标准。国际标准为ISO/IEC 9899-1990,中国国家标准GB/T 15272-94是国际ISO标准的中文翻译。
ANSI C标准化工作的一个主要目标是清除原来C语言中的不安全、不合理、不精确、不完善的东西。由此也产生了ANSI C与K&R C之间的差异。从总体上看,这些差异反应的是C语言走向完善、走向成熟。
--------------------------------------------------------------------------------
ANSI C 对 K&R C 的修订
(本段根据《C Programming Language》和C语言标准整理。不求完整,希望列出最常见的差异)
对于源文件内部的标识符,有效的最小长度扩充到31个字符。文件间连接时,标识符的最小有效长度仍然为6个字符。(许多实现都支持更大的长度)
增加了几个新关键字:void,const,volatile,signed,enum。抛弃了老关键字entry。
在换意字符 / 之后写非规定的序列,其作用确定为无定义。
规定8和9都不是八进制数的合法字符。
引进了数的后缀字符:整数的U和L,浮点数的F和L。
规定连续出现的字符串常量将被拼接在一起。
引进了“宽字符”的概念。
将字符也确定为带符号(signed)和不带符号(unsigned)的。
丢弃了long float(原来作为double的同义词)。
引入了void类型,用 (void*) 表示通用指针的类型(过去人们通常用 (char*))。
对算术类型规定了最小表示范围。要求每个C语言系统用头文件(;和;)说明实现中的具体规定。
引进了枚举定义enum。
采用了来自C++的类型修饰符,如const。
规定字符串常量是不可修改的。
改变了算术类型的隐含转换规则。
删去了一些过时赋值运算符,如 =+。规定赋值运算符都是基本单词,如 += 之间不能有空格分隔。
引进了与一元 - 运算符对应的一元 + 运算符。
指向函数的指针可以直接放在函数调用的位置,不必显式地写间接操作。
允许结构地整体赋值,作为函数参数和返回值传递。
允许将取地址运算符作用于数组,得到的是指向有关数组的指针。
标准规定 sizeof 运算符的返回值为 size_t 类型(某个无符号整型),这一类型在标准头文件;里定义。同时在那里定义的还有 ptrdiff_t 类型,它是指针减运算的结果类型。
规定取地址运算符不能作用于 register 变量。
规定移位表达式的类型为其左运算对象的类型。
允许建立指向过数组末元素一个位置的指针,以及对它的算术运算和关系运算。
(从C++)引进了包含参数类型的函数原型概念,引进了变长参数表函数的概念。仍允许老的形式,但仅仅是作为过时形式保留。
标准规定任何局部声明的作用域仅仅是当前的块(复合语句)。
规定函数参数作为加入函数体(复合语句)的声明,因此不能用变量声明去覆盖。
有关名字空间的规定:所有结构、联合和枚举标记在一个名字空间里,标号是另一个名字空间。
联合变量在定义时也可以初始化,规定初始化其第一个成分。
自动结构、联合和数组也可以初始化,但限制其初始化方式(其中只能包含常量表达式)。
带大小描述的字符数组也可以用大小与之相同的字符串常量初始化(结束的 /0 被删除)。
开关语句的控制表达式和case标号可以是任何整型的(包括字符类型)。
Turbo C(TC)系统的浮点连接错误
用TC-2.0系统编写小的C程序,如果程序里用到浮点输入,有时运行中会出现下面错误信息:
scanf : floating point formats not linked
Abnormal program termination
这个错误信息的意思是:scanf的浮点格式转换程序没有连接。
TC开发时(80年代)DOS下的存储资源紧缺,因此TC在编译时尽量不加入无关部分。在没发现需要做浮点转换时,就不将这个部分安装到可执行程序里。但有时TC不能正确识别实际确实需要浮点转换,因此就会出现上面错误。
解决方法:设法告诉TC需要做浮点数输入转换。下面例子里增加了一个double变量并用它输入。
大程序里由于变量很多,只要有了线索,TC就会把浮点转换连上,因此反而不常遇到这个问题。
--------------------------------------------------------------------------------
/* 能导致出现运行错误的程序例子。
在这里用的一个结构数组,结构里面有double类型的成分,TC不能正确识别和处理,因此会导致上述问题。*/
#include ;
#define NUM 4
struct entry {
int inum; /* 商品编号 */
int pc; /* 件数 */
double price;/* 价钱 */
} st[NUM]; /* st是个商品表 */
int main () {
int i;
for (i = 0; i < NUM; i++)
scanf("%d %d %lf", &st.inum, &st.pc, &st.price);
for (i = 0; i < NUM; i++)
printf("total price of item %d: %f/n",
st.inum, st.pc * st.price);
return 0;
}
/* 这个程序编译正常,运行中会出现上面错误信息 */
--------------------------------------------------------------------------------
/* 修改的程序,其中增加了一个double变量x。问题就解决了 */
#include ;
#define NUM 4
struct entry {
int inum;
int pc;
double price;
} st[NUM];
int main () {
int i;
double x;
for (i = 0; i < NUM; i++) {
scanf("%d %d %lf", &st.inum, &st.pc, &x);
st.price = x;
}
for (i = 0; i < NUM; i++)
printf("total price of item %d: %f/n",
st.inum, st.pc * st.price);
return 0;
}