| date: | Jun 17, 2018 UTC+08:00 |
|---|---|
| license: | CC BY-ND 4.0 |
| authors: | 直播的参与者们和 vc |
| updated: | Jun 17, 2018 |
本次直播主要面向需要参加「大学计算机基础」等课程的非计算机相关专业大一学生,意在厘清编程的一些基本概念和流程,并纠正(由这些课程带来的)一些误会。
展示课件内容
展示「大学计算机基础」第 5 章「C 程序设计基础」课件。(了解到机类约 1 千同学参与了这门课。)
课件上是这样介绍「程序设计」的:
用程序设计语言编写代码来驱动计算机完成特定的功能
问题求解过程的关键步骤之一
然而相比于将程序设计作为「问题求解的关键步骤之一」,整个课程的设计对其他步骤的介绍甚少,更接近于传达「编程是问题求解的唯一关键步骤」,很容易造成严重误会。
一般来说,借助计算机通过程序设计求解工程问题和实际问题至少有如下几步:
展示课件内容
程序的汇编、编译、连接程序、库等定义。
这部分内容放在课件里,可能不是很妥。不是内容正确与否的问题,而是这几步的抽象介绍根本不足以让人理解它们是什么。考虑到这也并不是必须的前置知识,不如直接跳过。
展示课件内容(例 5-1 在屏幕上显示 Hello World)
/* 多行注解
在屏幕上显示“Hello World”
*/
#include <stdio.h> // 文件包含 单行注解
int main( )
{
printf("Hello World!\n");
return 0;
}
(已确认课堂上展示效果确实如此)
这部分代码实在是非常惊悚。难以想象这种入门级别的代码都会有这么大的问题。和 repl.it 网站提供的 C 语言代码初始程序相比:
#include <stdio.h>
int main(void) {
printf("Hello World\n");
return 0;
}
大家可以观察到,缩进方面有很明显的区别。缩进,或者说代码排布在编码过程中很重要,它直接影响人对代码的理解。糟糕的代码排布会误导读者,也会误导作者自己。
众所周知,代码是写给人看的。而很多年来,人们已经总结出代码编排的大量规则,遵循这些规则可以写出比较好读的代码。就如同数学课本上的公式,大多也是等价的形式里面,最适合教学时让读者理解的那种。
反例:goto fail
糟糕的代码风格造成严重后果的知名例子之一是 goto fail (CVE-2014-1266):
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0) goto fail; if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) goto fail; goto fail; if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0) goto fail;
连续的第二个 goto fail; 没有正确缩进,否则更容易一眼看出问题。
展示课件内容(例 5-3 输入两个整数并求和)
#include <stdio.h>
int main()
{
int a,b,sum; /*定义变量*/
scanf("%d%d",&a,&b); //输入变量
sum=a+b;
printf("sum is %d\n",sum);
return 0;
}
先不考虑代码没有对齐的问题。这里 scanf 的第一个参数,学名是格式化字符串。格式化字符串是 C 语言的糟粕之一,并不是说它一无是处,而是并不适合在大规模的普及类的课程里做入门内容。这和格式化字符串出现的背景和解决的特定问题有关。
在一定的程度上可以说,C 语言里面用到的格式化字符串已经不是已经不是一般意义上的 C 语言了。它本身是一个模板语言,用来描述如何将计算机内存储的数据展示成文本(以及使用同样的对应关系,将文本转换回数据),但它不是解决这个问题唯一的方法,在如今看来,也并不是最好的那个,例如 C++ 风格的输入输出:
int x, y; std::cin >> x >> y;
在各种常见场景下,就优于格式化字符串控制的输入输出格式,原因之一是,std::cin >> x >> y; 这个写法本身是 C++ 语言的一部分,而不像格式化字符串几乎是——当然实际不是——一个独立于 C 语言的领域特定语言。也就是说,这样的 C 语言代码,虽然看起来用着错误的格式化字符串,却是可以通过编译的:
#include <stdio.h>
int main(void) {
printf("%d%d");
return 0;
}
编译后的程序虽然看起来可以运行,但输出了很奇怪的结果。这种看似奇怪的输出,实质代表着安全漏洞(读到了内存不该读的地方)。实践中避免这样严重的安全漏洞的方法之一,就是避免使用这种不安全的语言和写法。
如果课程中教授了 C 语言部分知识,却不告知各种写法容易带来的安全问题,这是很不负责任的。C 语言可能并不适合大部分只是「将电脑当作计算器使用」的人,用计算器的人不需要太了解计算器的原理,但用 C 语言几乎是一定要了解计算机很多内部原理才可以的。
使用编程手段解决问题的第一步是明确问题。很多时候同学们写代码发现很难下手,原因是问题不明确,没有说清楚到底需要求出什么、没有给出明确的界限范围、对于可能无法顺利完成计算的输入没有给出处理建议。
问题不明确有多种原因。工程问题和实际问题很多是开放式问题,并不在刚开始就知道要什么。但教科书问题如果还不明确,那很可能是出题人水平有限了。一般所说的「编程难」并不包括这部分。
明确问题之后需要思考解决问题的思路、用到的基本算法和原理等,这些思考过程最好以注释等形式体现在最终的代码里。
展示课件内容(求 Fibonacci 数列前 40 个数)
#include <stdio.h>
int main()
{ int f1=1,f2=1,f3; int i;
printf("%12d\n%12d\n",f1,f2);
for(i=1; i<=38; i++)
{ f3=f1+f2; printf("%12d\n",f3);
f1=f2; f2=f3;
}
return 0;
}
(已保持课件中原始的排版格式,仅改为等宽字体)
至于这样的代码,根本不是给人读的。以这种风格写代码,不出问题只能叫巧合。对于这种乱七八糟毫无背景也不愿意写注释的代码,不要浪费时间去读它。不要试着读懂它,这没意义。
有时间,读一些相对优秀的代码,比如,算最大公约数的代码。比如遇到了这样的问题,不要去找一些乱七八糟的鬼知道从哪里实验报告抄来的博客,去找一些久经考验的代码,例如来自 Linux 内核的最大公约数算法实现,看看这样的代码是怎么做的。
这段代码开头的注释介绍了它解决什么问题、用了什么方法:
/* * This implements the binary GCD algorithm. (Often attributed to Stein, * but as Knuth has noted, appears in a first-century Chinese math text.) * * This is faster than the division-based algorithm even on x86, which * has decent hardware division. */
也解释了没有用辗转相除法的原因是,即使在有硬件除法器的机器上,这个算法也更快。总之,把读代码的人一下子要问的问题,都回答了。
如果一段代码,既不说自己是解决了什么问题,又不说用了什么方法,那这段代码基本上可以删了,因为别人还得花费相当的精力才能读懂。总收益是负的。
之前提到了代码的缩进和样式问题。理论上,风格之类的只要易读,自己的代码之间一致且可自圆其说即可。实践中,可以参考他人总结的、已经成文并且在实际项目里大量使用的代码风格,然后遵照执行。有的样式风格文档会详细描述为什么这么做,比如可能是因为另外一种写法比较容易导致错误。在工程上,我们可以避免特定的容易出错的(error-prone)写法,类似电工总结的防止触电常识。
展示实验报告内容
这里要说明的代码风格问题,和之前介绍的 goto fail 问题基本类似,故略去。
照着这一步步,实现了程序代码之后,事情还没完。在工程中,多采用自动化测试的方法来检查代码是否符合要求(而不是手工运行一遍把截图贴在实验报告里这样)。自动化测试的原理很简单,比如如果按照要求实现了 int max(int a, int b),那就用一些实际例子来调用这个函数,检查结果是否符合预期。
测试做得最好的软件之一是 SQLite,也就是几乎每部智能手机里自带的嵌入式数据库。按照 SQLite 网站上的介绍,它的测试代码,是它本身代码的 711 倍这么多。就是因为它有非常非常充足的测试,别人才敢用它。
对于更重要的代码,甚至测试还不够,要做形式验证,保证代码绝对符合预期。但是形式验证是很贵很贵的,一般只用在飞机控制系统和航天器这样完全不敢出错,或者 CPU 电路设计这样如果出错会造成很大损失的地方。这个涉及到的是计算机辅助证明一系列的东西,有点像拿计算机来证明定理。
总结一下,编程的基本方法是:
本文授权「学长享答」网站转载。