正规军和泥腿子
http://fabiensanglard.net/c/ http://poj.org/problem?id=1088
作者:邱超凡 链接:https://www.zhihu.com/question/37304655/answer/71527606 来源:知乎 著作权归作者所有,转载请联系作者获得授权。
不敢说自己懂C语言,权当抛砖引玉吧。
为什么要学C语言?
在你学习C语言之前,你必须明白C语言是用来干什么的。也许你看了书上的范例代码,浑浑噩噩,印象里好像可以写一个诸如华氏度转摄氏度的东西,或者是给一堆数排个序之类的。总之,语言是用来写程序的。
那什么是程序呢?我不知道你对计算机有多少了解,不过我猜想你应该知道,计算机的处理器能够处理一条一条的指令,这些指令对应着不同的对数据的操作。计算机运行一个程序,就是把这些指令和数据加载到内存里面,然后一条一条地执行。
我相信你也知道计算机内部全都是用二进制表示信息。在机器中,每一条指令也是用二进制数来表示的。我们很难记住这些数和指令的对应关系,它的表现也不够直观。所以计算机科学家们开始用助记符(比如mov、jmp等)来表示指令。这被称作汇编语言。尽管相比于徒手撸机器码有进步,但是说到底汇编语言也只是很浅的一层抽象。实现一个复杂的功能,也许要写很长的一段代码。更可怕的是,许多汇编代码移植性不好,切换到另一个平台上需要改很多代码。因此开发大型软件很不方便。
在C语言出现之前,已经有过许多在汇编语言基础上进一步抽象的高级语言,比如FORTRAN、COBOL等等,而且许多至今都还在某些领域有各自的一席之地。而C的抽象在它出生的那个年代做得恰到好处,既有高级语言的简洁和表达力,又不失贴近底层系统的直接,再加上它与UNIX系系统的密切关系,C在之后许多年里成为了系统编程和软件开发的首选语言。C的代码相比于之前的汇编,在可移植性上已经有了诸多进步,切换到另一个平台的程序,只要那个平台上有C的编译器,不需要修改或者只需要修改少量的代码。我们可以看到在程序语言发展的过程中一个很明确的趋势就是抽象层次愈来愈高,比如后来又出现了号称“一次编写,处处运行”的依靠虚拟机的Java和众多的脚本语言,当然那是后话了。
忘说了,在程序语言的发展之路上,还有另一条分叉,函数式语言,典型代表是lisp。它的思维方式同传统的基于指令的语言截然不同,知乎上有大牛说过,函数式语言就是数学,只要数学不过时,函数式编程语言就不会过时。不过在C诞生那个年代呢,由于机器条件所限,函数式编程语言还没有那么得到人们的重视。
好了好了,讲历史的部分到此打住,在今日软件项目的实际开发中,C语言所占的比重早已不能和八九十年代同日而语了。之所以还要学C语言呢,就是因为它跟底层有天生的亲近感,C语言的许多特性和“坑”,深挖下去,其实也是计算机体系结构的问题。《C标准库》的作者在书里打趣说,C大概是少数能自我实现标准库的语言了。所以,学C的时候,如果关注点仅仅是语法本身,那还不如python来得亲切。(不过我想说国内大学里有些教C语言的老师恐怕连python是什么都不知道)
什么是Hello, world!
大多数初学C语言的人写的第一个程序都是所谓的“Hello, world!”。简言之,就是一个能在屏幕上输出这句话的程序。这句话的起源是C语言之父(可不是谭浩强哦)Dennis Ritchie和Brain Kernighan的名著《C程序设计语言》。从此以后成为了所有程序设计语言学习者的第一课。
题主说自己是软件工程专业的学生,不知道你是哪个学校的。不过在我校呢,软件相比于牛气冲天的土木和建院实在是太渺小了,许多同学甚至是选专业失败被调剂来的。也许你也像他们一样,对所谓的“程序”或者说“软件”,在之前并没有一个明确的概念或者印象。也许你会认为软件是像网易云音乐一样的,或者说是手机上的微信一样的,有一个漂漂亮亮的窗口,还有按钮,还有标签什么的东西。但当你费尽千辛万苦,照着书上敲完这一段也许还不能一次敲对的代码,结果发现只有一个“黑漆漆”的屏幕的时候,你会不会略感失望呢?这就是我要的程序吗,为什么长这样?
如果你去问老师,说自己不想要这种程序。脾气不好的老师也许会直接骂你一顿,有耐心点的老师可能会告诉你学会了这个才打好了基础。可是我想你还是不明白这个黑漆漆的窗口到底代表着什么。这也难怪,因为你用的是Windows,如果之前没有弄过什么bat脚本的话,也许都从来没有接触过终端这个东西。不过我想你可以给家里人打个电话聊聊。如果你爸妈在九十年代的时候学过计算机的话,他们应该对DOS的命令行界面还有印象。说不定小时候偷看过亲戚玩电脑的你也有。事实上,在所谓的“图形用户界面”出现以前,一个程序的输入和输出基本只能依靠命令行中的键盘和屏幕。你在Windows的资源管理器里对着a.txt右键点击删除,和你在DOS的黑屏幕里敲del a.txt然后回车是一个效果。只不过,前者可能对用户而言更加友好。
所以你明白了吧,由于早期计算机的显示技术和设计理念的限制,输入输出采用的是命令行的方式。而你跑这个Hello, world!程序时候出现的黑窗口,实际上可以理解为是Windows对早期命令行系统的一种模拟。如果你还知晓有一种语言叫VB,它的Hello, world!可以用创建一个窗体,然后在上面画一个Label的方式来实现。对比一下,可以发现,其实这两者之间并无什么本质区别。之所以我们喜用在命令行下输出Hello, world!的程序来教授编程的第一课,原因首先是它简单,不用考虑输出的布局、格式,仅仅是内容就可以了;然后是C语言的标准库也并没有给我们提供这样一个图形化的接口。事实上设计一个图形界面的程序也没有那么简单,你需要设定好布局,要为每个组件编写事件响应接口……作为初学者,你应当知晓的是,之所以用命令行,并不是因为C语言不能做图形界面,而是因为那跟我们在初学的时候需要挖掘的东西相隔太远。不管是写一个命令行的Hello, world!,还是点击按钮弹出Hello, world!,甚至是用电脑的蜂鸣器bee一声,本质都是一样的。
编程的意义
事实上,写程序和设计的目的是一样的,都是为了解决问题。(尽管有些人喜欢欣赏所谓“纯粹的技术”)国内高校教材最大的缺点之一,就是只告诉你这个是什么,却从来不告诉你学了有什么用。即使讲了所谓的用途,给人的感觉也只是凑数用的。不信?可以拿我校数学系编的那本紫皮的《线性代数》和国外优秀的线代教材比,看看两者差距有多大。
尽管C语言相比python、Java等语言缺少许多必要的“轮子”,不过对于初学者来说,写点能运行剪刀石头布的代码,或者说能够从某个文本文件里读入生词然后考查你背得如何的程序,都可以带来巨大的成就感。上编程课和上高等数学不一样,它不应该有什么进度之分。在了解完基本的语法之后就可以动手实现了,期间遇到什么问题再去网上查。写完了再去跟别人写得好的对比,看自己哪里不足。
初学编程,关键在练习。
语法有什么用?
似乎谭浩强和其它诸多所谓国内“专家”编写的教材,把C语言当做数学来讲,按照不同的语法内容划分成章节,有些学校说不定最后还会有笔试。有些学校甚至还叫学生上交手抄代码。我不太能够理解和认同这种行为。作为初学编程的你,大概早就受够了这种被动式的接受,在心里反问自己:就算我都看完了,能写出个什么呢?
确实写不出什么东西。因为在这种情况下,读者很难理解到这些语法的含义。比如,你看到书上讲分支语句和循环语句的时候,有想过它们有什么用吗?我不用分支行不行,不用循环行不行?
试想我们有一个程序,让我们在输入的一堆数里面找出最大的那个。假设输入是定死的,第三个数最大,那我完全可以接收到第三个输入的时候直接输出,结束程序了对不对?你可能觉得我这样是在耍流氓,哪里会有这样的程序。事实上,这也是所谓条件语句存在的本质——程序需要接受不同的输入(任何形式)。如果输入是固定的,那么理论上我可以改写为没有条件语句的代码。变量的概念,也是因为它所包含的值不是确定的。话说回来,只有用if语句才能实现分支结构吗?你让我求一年每个月多少天,我是不是可以开一个数组来保存,然后单独判断闰年就可以了?你让我输出从1到100任意一个整数的平方,理论上我用switch...case语句或者if也能做,可直接输出n*n,不是更好理解也更方便吗?循环其实是类似的道理,只是它的目的在于重复执行某一段代码,而重复次数是不固定的。我们说过,编程的目的是解决问题,不是让你在写代码的时候故意卖弄一些什么东西上去把代码搞乱,像写高中的语文作文一样。
每学到某个语法概念的时候,都要问一问自己,如果没有这个东西,有没有什么程序是写不出来的?或者说,如果没有了这个语法,我写程序的时候会增加多少麻烦?理论上,一门编程语言只需要非常简单的语法,就可以写出任何形式的程序。(可以搜一下“图灵完备”)但是为什么还有这么多编程语言呢?为什么有的编程语言适用于某个特定领域呢?为什么人们会觉得用python写点小程序比C要来得方便呢?C语言和现代高级的脚本语言比,语法实在可以用“简陋”来形容,标准库提供的东西也不多,实际编程要用到的一些数据结构都要自己去实现。在自己“造轮子”的过程当中,你会自己去感受诸如内存分配这样的事情,在这个基础上理解其他语言一些高级数据结构的实现原理。比如说链表,比如说内存池。
公式和代码的区别
虽然说一开始学习编程的时候,许多同学都会把C语言当做一个计算器来用,比如说我输入一个变量a,然后计算它的一个函数再输出。在这个层面上,我们可以说,如果不考虑溢出的情况,数学公式(或者说理论上的算法吧)和实际代码可能是对等的。这是非常简单的情况。但是当我们的程序越来越大,步骤越来越复杂,写起代码来可能就没那么简单了。比方说,我们用C来写高精度相乘,最简单的情况是两个数组,每个元素代表一位。如果在脑子里想,可能会很简单:就是逐位乘然后相加嘛。可如果你真动手写了,你会面临这样的问题: 既然我的数占不满整个数组,那到底是靠左还是靠右? 乘法的过程不能一次结束,我需要一个临时数组来容纳中间结果吗? 我怎么知道这一轮乘的数是十位、百位,还是千位?相乘的函数应该有几个参数? 如果题目包含小数,我怎么知道最终结果小数点后有多少位?我应该怎么输出? …… 尽管在搞竞赛的人看来这简直就是入门都算不上的题,但是还是有很多人不能一次写对的(poj1001,比如我……)。数学上的算法和公式是普适的,但是要实现成程序,就必须要落脚到具体语言的语法上。为什么人们说要学好编程要多练习?因为练习就是一个踩坑的过程。我这一次踩到这个坑了,知道这里容易出错,我总结下来,以后也是如此。天下大事必作于细,所谓的代码能力就是这个。对编程的初学者来说,知道不等于会做。所以如果有什么程序自己老是写不对,不要一下子就灰心丧气,但是要对它有个印象。比如说数组不会自动初始化,比如if不加花括号就只执行后面一句。
C语言与底层系统
我们讲,C语言在出生的时候,因为作了适当层次的抽象,才能抓住历史潮流成为最流行的语言之一。这个所谓的“适当层次”,意思就是既避免了直接面向机器指令,又不完全将计算机运行的原理隐藏起来。于是你稍微学深入了,会产生许多疑问…… 为什么printf("%u", -1);结果会输出4294967295? 为什么数组访问a[-1]但程序没有出错? 为什么char str[]="abcd";我就可以修改str[0],而对于char str="abcd";我这样做程序就会出错? 为什么我#define G 3+3而GG的结果却是15? 为什么scanf的参数名要加一个&号? 为什么printf和scanf的参数可以是不固定的? 为什么一个浮点数加了10次0.1之后结果却是0.99999...? 为什么一个局部数组开太大了程序会爆掉? …… 多数问题,深挖下去都和计算机体系结构、计算机内部数据表示、可执行文件格式、编译原理等内容相关。知其然而不知其所以然,就很难说自己了解C语言。
数据结构
作为一个看见算法就晕的非ACMer,我想我本来没有什么资格说数据结构。不过我想换个角度谈谈这个问题。数据结构和算法的重要性自不必说,太多人都会像一个长者一样叮嘱你要想程序写得好,数据结构最重要。数据结构的确重要。可是你有没有想过,到底什么是数据结构,数据结构的本质是什么?
有一大批讲授数据结构的书籍一开始就把各种数据结构的概念灌输给你,然后带着你用实际的代码去实现这些结构(多半是C++或者Java的书吧)。不过我觉得这些作者仍然忽略了一个问题,即:这些东西有什么用?
事实上,每种数据结构都可以看作是对现实生活中物件的刻画。为什么会有栈?因为有很多场景需要先进后出。更本质地说,是在这些场景里面,先进的需要包含后进的才能完成。比方说C语言函数的调用栈,逆序表达式的求值。或者在生活当中,假设我们要拆一台电脑,我们得先打开它的外壳,然后拆下大零件,最后弄下大零件里包含的小零件。如果我们要装配回去,我们要先装上小的,再装上大的,这里有一个先后顺序。我们就可以说,当我们拆机的时候,各个零件组成了一个栈,外面的壳在栈底,里面的小部件在栈顶。当然,在修电脑的时候,我完全可以把所有的零件一通乱地扔在我的床上,要装上去的时候再找,不用像数组一样有序排列。为什么?因为当我完成步骤k的时候,我内心清楚k+1步需要哪个零件。而写程序的时候,比如求逆序表达式的值,我就没有办法知道下一个“零件”是什么,我只能猜测是栈顶元素。因此,在实际程序中,它必须有序。这就是栈这种数据结构的来源,它是一种抽象。本质上讲,这也是个信息的问题。
其他的数据结构,比如队列、树、哈希表等等,都一样可以看作是对现实生活中事物运转方式的刻画。比如树是一个极其典型的递归结构,想一想中世纪欧洲的封建制度,其原则是“我只需要对我下面一级的人发号施令就行了”。所谓二叉树,它的特殊性在于它是树的最简形式,节点有两个子节点的情况是生活中最常见的,比如我们的四则运算就可以用树来表示。嗯,说到四则运算,我们可以把普通的中序表达式转化为逆序表达式,本质上也就是把一棵二叉树变成了栈,感受一下这个算法。
功利一点?
其实如果仅就题主的问题本身,我想我没有必要说那么多。我个人感觉,我校软院对大一新生的C语言考察要求还是不高的(可能是我校太渣?)。期末上机三道题,第一题无脑,第二题要考到C语言里的许多语法内容,第三题会涉及到数据结构,不过不懂的人凭感觉写估计也能过几组数据。所以如果说你的目的仅仅是通过考试拿一个不错的成绩的话,大概没有必要花这么多时间去看这些“有的没的”的东西。后面一些回答提到的东西大概是从从事C开发的专业人士的角度阐述的。作为大一新生,其实你就像我,不知道自己以后会从事哪方面的工作,也许去干前端也说不定。我绝对不相信一个对操作系统没什么了解的大学生真能像某些人建议的一样啃下APUE,更何况一个连C编程和Linux操作都不熟悉的呢,是吧?
如果你所在的学校还没有脑残到要笔试学生C语言的程度,并且你们用的教材是谭浩强那本的话,我觉得你可以扔掉它了,真的。谭老对我国计算机普及作出的贡献当然很大。但是都到2015年了,学校还在用这本书,我看这不是谭浩强之耻,是学校之耻!我没有看过C Primer Plus这本书,写作这个回答的时候到学校图书馆翻了翻,感觉还可以。学计算机的话,就不要畏惧这些大部头了。厚不一定代表难读。大学的课堂老师一学期也讲不了多少内容,多数还得自己去看。一个好的老师,只能说给学生以最正面的引导,让他们少走弯路。
现在这个时代,大学生可以接触到的项目很多,主要的类型无非是跟Web相关,其中的大多数也用不到C语言。不过不要因此就否定C/C++的价值,对此知乎上的大牛们讲的很多了。
还有,千万记住,C和C++是两种语言,C++也不完全兼容C. 不信?在C和C++里分别输出sizeof('Z')试试。
建议书目
Code: The Hidden Language of Computer Hardware and Software The Information: A History, A Theory, A Flood Head First C C Programming: A Modern Approach The C Programming Language Computer Systems: A Programmer's Perspective Expert C Programming: Deep C Secrets Pointers on C C Traps and Pitfalls