目前几乎所有理工科的学生都要学习 C 语言编程,有很多同学会感觉C语言很难入门。下面我将从我自己的学习经验出发,总结在 C 语言学习的过程中,怎么才能少走些弯路。 首先必须说一点,要想学好 C 语言,最好的办法是:上机!上机!上机!重要的事说三遍,离开编程实践,是没有办法学好一门语言的。像所有工具一样,最好的学习办法就是多用。上机多了,对编程的感觉自然而然会上来。 不管你的期末考试的形式是什么样的,哪怕就告诉你,考题就出复习材料中的原题,也千万不要试图去背答案。背答案永远是最差的解决方法。常言道授人以鱼不如授人以渔,学会一种解决问题的方法,学会一门编程语言,对所有理工科的学生来说,都是一种在日后不一定什么时候就能用到的技能。 下面进入正题。这里我不喜欢按照教材的顺序讲解,学一门语言要融汇贯通,要站在一个更高的视角反观正在学习的内容。 先说基本的程序结构,只有涉及到控制流(分支、循环等)和函数调用时,才有程序结构这一概念。 不要按照 "解决这个问题我需要用哪个语法" 来想问题,语法或者代码本身永远是最后最后才考虑的问题。我带过一个学生,我带他做题时,我都会先问题他:这个问题有思路吗?结果他上来就说:用 for 对吗?这说明他的思维没有上高度,眼光只盯着语法来想问题。 比较好的思维方式时,先想好大致的思路,这个思路是用自然语言描述的,不是用具体的编程语言描述的。例如,如果有一个问题,让你求 a 和 b 的最大公约数。有一点数论背景的人都会想到,用辗转相除法(广义欧几里德除法),算 a mod b,余数计为 q,则有 (a, b) = (b, q),继续算 b mod q,一环一环算下去,当整除时,除数就是最大公约数。上面这个就是我所谓的 "思路",不涉及具体的编码。先有思路,再转换为编程语言的具体代码,这样一来思路清晰,二来不易出错。根据这个思路,很容易写成代码: while (b) { int q = a % b; a = b; b = q; } 再说说我的另一个观点:语言的细节是通过编程尝试学习的。比如,printf 语句中,格式字符串中如果有 %s, %15s, %-15s, %15.5s, %.5s, %-15.5s,分别什么意思?死记硬背肯定已经背晕了,但是如果你写出这样的一个程序: main() { char *str = "1234567890abcdefg"; printf("%sn", str); printf("%15sn", str); printf("%-15sn", str); printf("%15.5sn", str); printf("%.5sn", str); printf("%-15.5sn", str); return 0; } 运行一下,很容易就能得到你想要的答案。而且,这与人的记忆方式有关,你这样试过之后,这个结果很容易形成你的长期记忆,不需要像背书一样地来背什么负号表示左对齐,.表示精度,.前面的表示宽度等等。 又如,我上面两个程序都用了语言固有的一些约定:上面那个,把一个整数作为判断条件,涉及到 C 语言中 bool 值的表示方法:非 0 表示真,0 表示假。下面那个,函数返回值类型默认为 int。这些也都可以通过编程实践试出来,不需要死记硬背。 又如,for ( ; ; ) 表示一个死循环,而 while() 会导致编译错误,也就是说 for 语句的循环判断条件可以省略,while 语句不能省,要想用 while 语句构造死循环,必须写成 while (1) 。 还有,你能不能一眼就看出来 for(;;); 跟 for(;;) 的区别?对,差了一个分号。没有足够多的编译实践,没有无数次的因为一个分号导致编译错误,你是无法具有这样一双慧眼的。这也就是为什么编程上机这么重要。 要学精 C 语言,要了解一些系统底层的知识。说实话,我个人也认为,对于毫无编程经验的人来说,入门第一门语言学 C 的确有些偏难了,因为 C 语言太过底层了。但是这也未尝不是一件好事——如果把 C 语言能学精,那么以后学习任何一门别的语言都会觉得很容易。要学好 C 语言,你需要知道,在程序运行时,进程的内存布局分为哪些块:有代码区、静态数据区、栈区、堆区等等,当你知道这些后,你自然而然就会清楚,为什么说可以把 static 变量看作是全局变量,为什么 malloc 得到的东西如果不 free 就会永远存在,为什么我们的局部变量也叫自动变量,何为 "传值不传参"。了解一些体系结构的知识,你才能知道 register 关键字代表什么含义。了解一个 hello, world 程序从源代码到 .exe 文件经历了哪些变化过程,知道什么叫编译,什么叫链接,你才能明白 extern 关键字的意义。等等。其实单为了学习一门语言,要看三四本书当然有点过了,但是简单地通过搜索引擎或者通过请教老师,把底层的一些知识搞懂,才能真的说你很懂 C 语言。 "指针是 C 语言的灵魂。" 这是我的《计算机导论》老师反复跟我们强调的一句话。直到使用 C 语言一年多之后,我才深刻地体会到了这句话的含义。没有指针,C 语言就是渣;有了指针,C 语言就是一门万能的语言。 初学 C 语言,到了指针这里,如果对 int **ppi, int ***pppi, int (*pa)[], int *ap[] 这样的声明都一点都不晕,才能说指针学得达到入门的水平了。一个能减轻你的大脑负担的方式是,见到指针之后,把指针变量本身画出来,把它指的内容画出来,用一个箭头指过去,这样就清晰明了了。我说的 "画出来",指的是内存,这样在操作指针时,你就知道,你操作的到底是什么东西,为什么它变了,为什么它没变。上机实践够多之后,这些东西就都熟悉了,就可以不过脑子直接写出正确的代码了。 给几个我们老师反复强调的口诀: 指针就是地址。 数组名就是指针。 指针就是数组,数组就是指针。 函数名就是指针。 把这些东西都理解透,你就能很容易地理解,如果有这样的声明 int a[100], *pa = a; 那么,a[i], pa[i], *(a+i), *(pa+i) 都是同一个意思。 要理解函数指针,第一步,熟练使用 库中的 bsearch 函数和 qsort 函数;第二步,自己写一个 bsearch 和 qsort,注意参数要给一个函数指针。完成这两步,对于函数指针是什么东西,你就一点都不会晕了。其实只是那一句话:函数名就是指针。 浅谈递归 初学者遇到递归都会晕,甚至我的同学中,有的已经写了两年代码了,见到递归还是晕。其实大可不必害怕递归这个东西,用已有的思维方式,可以非常容易的理解它。 有的读者可能不知道我在说什么,递归就是一个函数调用它自己。比如下面这段求 Fibonacci 数列的程序: fibonacci(int n) { if (n <= 1) return 1; return fibonacci(n-1) + fibonacci(n-2); } 就这个例子我们可以看到递归的要素: 1、递归必须有终止条件。这很好理解,如本例中的 if 语句,如果没有,那么会无限递归下去,最后程序会因为栈溢出而崩溃。 2、函数调用自己。 要理解递归,只要按照高中数学的思维想问题就可以,递归相当于递推公式,或者可以理解成数学归纳法。给一个初始条件,凡是后面的都根据前面的能推出来。我们写程序时,只需要把初始条件给好,把递推规则写好,剩下的,"交给上帝来做"。 看一看递归的运行栈,历史状态都在运行栈里,函数一层一层调用下去,再一层一层返上来,一点一点走。可以自己把运行栈画出来,一目了然。 递归能够简化我们的编码,但是带来的函数调用的开销是很大的,特别是当递归层数过多时,有可能因为递归太深导致运行栈崩溃。我们完全可以不用递归来写递归程序,这时要我们自己维护一个栈,记录中间的状态。这个编程难度对初学者来说有点大,只需要理解即可。 至于对基本语法已经很熟悉的同学,如果想深入学习,建议学一下数据结构。这可以强化你对递归和指针的理解,自己手动实现一些基本的数据结构,可以强化 C 语言的编程能力。 推荐的数组结构书目: 《数据结构与算法分析(C语言描述)》,机械工业出版社。 推荐的 C 语言教材:《C程序设计语言(第2版)》,机械工业出版社。 这本书是 C 语言的设计者写的,目前几乎全世界所有的 C 语言教材都以这本书为蓝本,大师的文字读起来就是一种享受,语言精练、易懂,内容全面。