1.5 注解与习题
经过前几个小节的学习,相信读者已经初步了解顺序结构程序设计和分支结构程序设计的核心概念和方法,然而对这些知识进行总结,并且完成适当的练习是很必要的。
为了突出实践的重要性,本章从一开始就不加解释地给出了一段程序,并鼓励读者暂时忽略不理解的细节,把注意力集中在变量、表达式、赋值等核心内容。然而,实践的步伐也不是越快越好,因此笔者在每章的最后加入一些理论知识,供读者在实践之余稍加注意。也可以直接跳到第2章继续阅读,以后再阅读(并且实践)这些文字。
1.5.1 C语言、C99、C11及其他
本书的前4章介绍C语言,更具体地说是介绍C99标准中对算法竞赛而言最核心的部分。C语言的历史和特点不难在网上以及其他书籍中找到,并且本书的前言中也详细叙述了为什么要介绍C语言,因此这里唯一想讲的是C99和编译器。
什么是编译器?简单地说,编译器的任务就是把人类可以看懂的源代码变成机器可以直接执行的指令。“机器可以直接执行的指令”很抽象,并且笔者也无意在这里进行进一步的解释——但有一点可以说明,那就是这里的“机器”有很多种,甚至还可以是非物理的虚拟机器。诚然,让同一段程序完美地运行在千差万别的机器上并不是容易的事情,但编译器仍然大大减轻了工作量。
C语言并不是只有一种编译器(7),例如gcc和微软的Visual C++系列(8)。为了避免同一段程序被不同的编译器编译成截然不同的机器指令,C语言标准诞生了。目前最新的是C11,其次是C99。考虑到C11的新特性未影响算法竞赛(9),因此这里仍然讨论C99。正如前言中所说,本书介绍C语言只是为学习C++做铺垫。C99中最常用的特性已经基本包含在了C++中(例如64位整数、随处声明变量、单行注释),所以在前4章中无须过多地关注哪些特性是C99新增的,哪些是ANSIC(即C89)中已经包含的特性,把更多的注意力放在代码和算法本身。
本书介绍C语言的目的是为C++语言铺垫(因为后面章节的代码用了很多C++特性),但是有读者仍然希望先学习到“纯粹的C”,所以在写作本书时确保了前4章中的代码全部能使用gcc -std=c99编译通过(10)。“与C99兼容”是要付出代价的。例如,在C99中,double的输出必须用%f,而输入需要用%lf,但是在C89和C++中都不必如此——输入输出可以都用%lf。为了保持与C99兼容,不得不向这种不一致性妥协。如果一开始就使用C++,则不必拘泥于C99,把所有代码以.cpp而不是.c为扩展名保存,用C++编译器编译即可。本书前4章中的代码均可以直接用C++编译器编译。不仅如此,多数比赛中的C语言都是指ANSI C,即C89而不是C99,在参加比赛时也需要把C语言程序当作C++程序提交。
是不是很晕?没关系,只要你不是一个纯粹主义者,作者最推荐的方式就是:从现在开始直接认为你学的不是C语言,而是C++语言中与C兼容的部分。这样一来,ANSI C、C99之类的名词都和你无关了。
1.5.2 数据类型与输入格式
在继续学习之前,强烈建议读者完成以下两个实验。它们不仅能帮助你搞清楚数据类型以及输入输出的一些细节,还能培养你的实践习惯,锻炼实践能力。
数据类型实验。本章中涉及的int和double并不能保存任意的整数和浮点数。它们究竟有着怎样的限制呢?不必解释背后的原因,但需注意现象。
实验A1:表达式11111*11111的值是多少?把5个1改成6个1呢?9个1呢?
实验A2:把实验A1中的所有数换成浮点数,结果如何?
实验A3:表达式sqrt(-10)的值是多少?尝试用各种方式输出。在计算的过程中系统会报错吗?
实验A4:表达式1.0/0.0、0.0/0.0的值是多少?尝试用各种方式输出。在计算的过程中系统会报错吗?
实验A5:表达式1/0的值是多少?在计算的过程中系统会报错吗?
输入格式实验。本章介绍了scanf和printf这两个最常见的输入输出函数。考虑下面的函数段,可以从实验结果总结出什么样的规律?
程序1-15 输入输出实验
#include<stdio.h>
int main()
{
int a, b;
scanf("%d%d", &a, &b);
printf("%d %d\n", a, b);
return 0;
}
实验B1:在同一行中输入12和2,并以空格分隔,是否得到了预期的结果?
实验B2:在不同的两行中输入12和2,是否得到了预期的结果?
实验B3:在实验B1和B2中,在12和2的前面和后面加入大量的空格或水平制表符(TAB),甚至插入一些空行。
实验B4:把2换成字符s,重复实验B1~B3。
输出技巧。读者有没有注意到在本章中所有的printf中,双引号中的内容总是以\n结尾?\n是一个特殊字符,叫做“换行符”,其中n是英文单词newline(换行)的首字母。换句话说,在输出的最后加一个\n会在输出结束后换行。既然“换行”只是一个特殊字符,完全可以用printf("1\n2\n")分两行输出1和2,并且用“printf("1\n\n2\n");”分三行输出1和2,并且在1和2中间换一行。更多的特殊字符将在第3章中介绍。但是这样一来,问题出现了:如果真的要输出斜线“\”和字符n,怎么办?方法是“printf("\\n");”,编译器会把双斜线“\\”理解成单个字符“\”(11)。
最后请读者思考这样一个问题:如何连续输出“%”和d两个字符?不难发现使用“printf("%d\n");”是不行的,那么应该怎样办呢?读者可以自行尝试,也可以查阅printf的资料(12)。从一开始就养成查文档的好习惯是有益的。
1.5.3 习题
程序设计是一门实践性很强的学科,读者应在继续学习之前确保下面的题目都能做出来。请先独立完成,如果有困难可以翻阅本书代码仓库中的答案,但一定要再次独立完成。
习题1-1 平均数(average)
输入3个整数,输出它们的平均值,保留3位小数。
习题1-2 温度(temperature)
输入华氏温度f,输出对应的摄氏温度c,保留3位小数。提示:c=5(f-32)/9。
习题1-3 连续和(sum)
输入正整数n,输出1+2+…+n的值。提示:目标是解决问题,而不是练习编程。
习题1-4 正弦和余弦(sin和cos)
输入正整数n(n<360),输出n度的正弦、余弦函数值。提示:使用数学函数。
习题1-5 打折 (discount)
一件衣服95元,若消费满300元,可打八五折。输入购买衣服件数,输出需要支付的金额(单位:元),保留两位小数。
习题1-6 三角形(triangle)
输入三角形3条边的长度值(均为正整数),判断是否能为直角三角形的3个边长。如果可以,则输出yes,如果不能,则输出no。如果根本无法构成三角形,则输出not a triangle。
习题1-7 年份(year)
输入年份,判断是否为闰年。如果是,则输出yes,否则输出no。
提示:简单地判断除以4的余数是不够的。
接下来的题目需要更多的思考:如何用实验方法确定以下问题的答案?注意,不要查书,也不要在网上搜索答案,必须亲手尝试——实践精神是极其重要的。
问题1:int型整数的最小值和最大值是多少(需要精确值)?
问题2:double型浮点数能精确到多少位小数?或者,这个问题本身值得商榷?
问题3:double型浮点数最大正数值和最小正数值分别是多少(不必特别精确)?
问题4:逻辑运算符号“&&”、“||”和“!”(表示逻辑非)的相对优先级是怎样的?也就是说,a&&b||c应理解成(a&&b)||c还是a&&(b||c),或者随便怎么理解都可以?
问题5:if(a)if(b)x++;else y++的确切含义是什么?这个else应和哪个if配套?有没有办法明确表达出配套方法?
1.5.4 小结
对于不少读者来说,本章的内容都是直观、容易理解的,但这并不意味着所有人都能很快地掌握所有内容。相反,一些勤于思考的人反而更容易对一些常人没有注意到的细节问题产生疑惑。对此,笔者提出如下两条建议。
一是重视实验。哪怕不理解背后的道理,至少要清楚现象。例如,读者若亲自完成了本章的探索性实验和上机练习,一定会对整数范围、浮点数范围和精度、特殊的浮点值、scanf、空格、TAB和回车符的过滤、三角函数使用弧度而非角度等知识点有一定的了解。这些内容都没有必要死记硬背,但一定要学会实验的方法。这样即使编程时忘记了一些细节,手边又没有参考资料,也能轻松得出正确的结论。
二是学会模仿。本章始终没有介绍“#include<stdio.h>”语句的作用,但这丝毫不影响读者编写简单的程序。这看似是在鼓励读者“不求甚解”,但实为考虑到学习规律而作出的决策:初学者自学和理解能力不够,自信心也不够,不适合在动手之前被灌输大量的理论。如果初学者在一开始就被告知“stdio是standard I/O的缩写,stdio.h是一个头文件,它在XXX位置,包含了XXX、XXX、XXX等类型的函数,可以方便地完成XXX、XXX、XXX的任务;但其实这个头文件只是包含了这些函数的声明,还有一些宏定义,而真正的函数定义是在库中,编译时用不上,而在连接时……”多数读者会茫然不知所云,甚至自信心会受到打击,对学习C语言失去兴趣。正确的处理方法是“抓住主要矛盾”——始终把学习、实验的焦点集中在最有趣的部分。如果直观地解决方案行得通,就不必追究其背后的原理。如果对一个东西不理解,就不要对其进行修改;如果非改不可,则应根据自己的直觉和猜测尝试各种改法,而不必过多地思考“为什么要这样”。
当然,这样的策略并不一定持续很久。当学生有一定的自学、研究能力之后,本书会在适当的时候解释一些重要的概念和原理,并引导学生寻找更多的资料进一步学习。要想把事情做好,必须学得透彻,但没有必要操之过急。
————————————————————
(1) 但也有不少语言会严格区分整数除法和浮点数除法。
(2) 在学习编程时,“明知故犯”是有益的:起码你知道了错误时的现象。这样,当真的不小心犯错时,可以通过现象猜测到可能的原因。
(3) 有的读者可能会有math.h中定义的常量M_PI,但其实这个常数不是ANSIC标准的。不信可以用gcc-ansi编译试试。
(4) 如果是网络竞赛,还可以向组织者发信,在论坛中提问或者拨打热线电话。
(5) 这个方法还有一个“变种”:用异或运算“^”代替加法和减法,还可以进一步简写成a^=b^=a^=b,但不建议使用。
(6) 单行注释原先只有C++支持,后来已成为C99的标准的一部分。
(7) 事实上,它甚至有多种解释器——无须编译直接执行的C语言解释器,例如Ch和TCC。
(8) Visual C++不仅包含IDE,也包含C和C++编译器。
(9) 有一个例外:gets在C11中被移除了。详见第3章。
(10) 如果使用其他编译器,请自行查阅相关文档,确保代码按照C99标准编译,否则可能会出现编译错误。
(11) 这是一个很有意思的设计,建议读者花时间琢磨一下这样做的用意。
(12) 例如http://en.wikipedia.org/wiki/Printf。