这几年在公司一直带徒弟,每次必教的内容就是C++。在我看来,C++已经有非常好的教材了(注1),实在没有必要从头教起。自学就可以了,可是结果总是不尽人意。
不想再重复一次“把C++当成一门新语言来学习”,自己直接教吧。
总论
C++是一门实践的编程语言,它由数十位工业界的大佬们共同设计出来,它是一种至力于解决问题的语言。我们在学习的过程中,同样也不应纠结于细节,而是专注于如何优雅的解决问题。
C++要解决的基本问题有两个,一是如何处理好int类型;二是如何处理好vector类型。在解决这两个问题的时候,C++的设计者们遇到了相当多的细节问题,不过它们都已经被记录在了C++ programming language一书中。幸好我们不用设计语言,这些问题我们不用再去思考,只需要知道如何做最好(best practice).
在一切开始之前,我们先明确什么是类型(type)?
类型可以粗略的理解成一个概念,比如自然数,动物或宇宙。概念可以由一系列实际的物品举例,但很完全例举。我们说1,2,3,4等都是自然数,但不能把所有自然数都例举出来。这些实例我们称为类型的值。我们可以说1,2,3等大于-32768,小于32767的整数都是int类型,也可以说所有一维有序的数列都是vector类型。同样,可以说int类型的值是所有大于-32768,小于32767的整数。
类型说明这些值之中包括一些关系,计算机科学上也称为操作,比如1+1=2。int类型可以包括算术操作,位操作等等。
计算机中,值都是以内存中的bit来表示,一片保存了值的内存,我们可称为一个对象,为了方便使用,我们为对象取名,以说明它保存了什么用途的值,这个有名字的对象,我们叫变量。
对于有经验的程序员可以关注一下以下名词,来自于A tour of C++。
- 类型(为对象)定义了一组可能的值以及一组可能的操作。
- 值是一组bit的组合,其含义由类型来解释。
- 对象是一片内存,保存了某个类型的一个值。
- 变量是一个有名字的对象。
其次我们需要了解什么是编程?
解决问题的一种方式是依次执行一系列步骤。计算机解决问题正是这种方式,不过它有一些限制。首先,它的每个步骤必须已经定义,我们称为这些已经定义的步骤为基本操作。其次,它的步骤必须有限,不能无穷多。
编程的核心内容就是把问题用有限次的基本操作解决。这是一个很难的工作,同时具有工程特点和艺术性。
在一切编程之前,我们先要定义一个基本操作集合,它可能是一个CPU的指令集,也可以是C++的运行时环境。
如果有这样一个计算机,它除了可以进行整数运算外,还可以接收一个数值n,输出一定数量的1分、2分、5分的硬币,并且数量足够多,以足成n分钱。我们可以这样解决这个问题(注2):
int five-cent-count = n / 5; output-5-cents(five-cent-count); int two-cent-count = (n - 5*five-cent-count) / 2; output-2-cents(two-cent-count); int one-cent-count = n - 5*five-cent-count - 2 * two-cent-count; output-1-cents(one-cent-count);
上面是一个有效C或者C++程序的主体部分,中间部分步骤以int开头,它说明了随后是一个变量,其类型为int。
我们还可以定义这样的一个计算机,它的类型Node的值,包括两个部分:data和 ptr。data为int类型,ptr为另一个Node类型的对象位置(指针)。除了int类型的操作外,我们可能的操作还包括:
- cons(data, ptr) : 由data和ptr来构成一个Node类型的值,并返回对象的位置。
- first(node): 取node的data部分的值。
- rest(node): 取node的ptr部分的值,即指向另一个Node对象的位置。
这个神奇的计算机与我们常见的有像大海一样连续内存的计算机不一样,它的数据像是保存在海岛上,你需要一张机票才能到达。但是,使用这样的计算机编程并没有什么不同,只不过基本的操作不同。
假设有一天,计算机已经相当发达,说明定我们会遇到,能走路,说话,会推理的计算机,但编程不会发生变化,同样只是基本操作不同。
我们了解一下什么是计算机程序?
通常说计算机程序是一个可执行的文档。可执行有两方面的意思,一是计算机认识,二是有效的基本操作序列。C/C++程序一般是原生程序,原生是指它由CPU直接认识,它的有效基本操作序列是CPU的指令集。CPU执行程序的过程是:
读入指令-->执行操作-->输出-->再读入...
我们编程完成的源代码,还不是一个可执行的文档,它要通过编译,把文本变成CPU指令,再连接,连接C++的运行时环境对应的CPU的指令集。
源代码---(编译)-->目标文件---(连接C++运行时环境对应的CPU指集)-->可执行文档。
程序的执行过程一般是:
读入一些数据-->执行操作-->输出--回到开始的状态。
注意到这个过程和一个原生程序的执行过程非常类似,一个程序可以视为一个虚拟机,只是它的基本操作非常有限。图形化程序的输入可以认为是鼠标和键盘的操作。虽然,图形化操作已经非常类似编程。由于这些操作系列不满足有限,因此还不能说图形化操作也是编程。
我们如何设计一个计算机程序?
这个问题等同于“如何用计算机解决一个具体问题?”。简化的步骤如下,
-
首先,把问题已知和未知用计算机形式设计出来,也就是明确它的类型。
-
其次,分析最简单的情况来,找到可能的解法。
-
然后,试图在一般的情况解决它,这里可能需要补充一些中间类型的值。
-
接着,证明解法正确,且在有限的步骤内可以完成。
-
最后,用计算机编程语言来描述它。
例如,输出以下由空格和星号组成的图形,图形的高度小于1000行。
*
***
*****
........
计算机的表示
这个问题的已知是一个高度n,小于1000,所以类型可以是int.
输出是一个基本操作,打印一行文字,共有a个空格和b个星号,我们记作put-stars(a, b)
未知是n行输出。
从简单的情况来看:
n=1时,有1行输出a=0, b=1;
n=2时,有2行输出,第1行a=1,b=1, 第2行a=0,b=3
n=3时,有3行输出,第1行a=2,b=1, 第2行a=1,b=3,第3行a=0,b=5
结合图形的观察,我们可以猜测,n行中每行的a和b都是一个固定序列的第i项。b比较容易看出来,它的通项是2i-1;a可能略难,不过从图形上可以看出,它是n-i。
从一般情况下来看:
当我们知道第n-1行的a=1,b=2(n-1)-1时,不难知道下一行是a=0,b=2n-1。
正确性证明
从图形上来看,每行空格比上一行少一个,星号多两个,可知a序列满足要求,b序列也满足要求。同时,最多需要n次输出操作就可以完成。
最后用计算机语言来实现它:
void draw(int n) { for (int i = 1; i <= n; ++i) { put-stars(n-i, 2*i-1); } }
以后,我们不讨论具体如何分析和设计,只考虑在已知算法的情况下,如何来实现它。比如:C++的习惯上,计数项从0开始到n-1结束,我们可以重新写通项为n - i - 1和2 * i + 1。代码实现为:
1 void draw(int n) 2 { 3 for (int i = 0; i < n; ++i) { 4 put-stars(n-i-1, 2*i+1); 5 6 } 7 }
注1:我认为好的教材有几种:有些经验编程的同学可以用简单明了的Essential C++;经验较少的同学可以用事无巨细的C++ Primer;对于零基础的初学者,按部就班用Programming:principles and practice design using C++更好。
注2:整数除法运算时,小数部分会直接丢弃,而不是通常的四舍五入。