1.1 如何从1加到100
计算机语言是用来解决实际问题的,这里有一个很普通的例子:计算从1到100的整数累加和。现在让我们看一看,如果用C语言来编程解决这个数学问题,应该怎么做。
数学家高斯小时候的做法是101乘以50,因为他发现1+100=101,2+99=101,共有50对这样的组合。现在的计算机还缺乏这种自主的思考和分析能力,因此,和高斯的灵机一动不同,我们只能通过编写程序,让计算机老老实实地从1加到100。办法很笨,但是计算机的速度比高斯的大脑和手要快得多得多。
在本书中,存储器特指处理器内部的寄存器、高速缓存或处理器可以直接访问的存储芯片(也就是我们通常所说的内存),除非明确指出,存储器并不包括外部的辅助存储器(比如硬盘和U盘)。存储器由字节单元组成,存储器里的一个字节单元,或者多个连续的字节单元合起来形成一个更大的单元,称为存储区。
如图1-1所示,内存储器由字节单元组成,所有字节单元都按顺序编号,每个字节单元的编号就是它的地址。图中的地址是用十六进制表示的,第一个字节单元的编号是0;第二个字节单元的编号是1,其他以此类推。显然,图中的这个内存储器很大,其最后一个字节单元的地址是FFFFFFFF。
图1-1 内存的组织和处理器
处理器通过地址线和数据线同内存储器相连,如果是按字节访问,则每个地址对应一个字节单元;如果是按长度不同的字来访问,则一个地址将对应2个、4个或8个连续的字节单元。内存储器里存放了数据和机器指令,处理器可以从内存储器里取得指令加以执行,也可以读取内存储器里的数据,或者把数据写入内存储器。
我们知道,处理器可以读写存储区,可以用存储区里的内容进行算术或逻辑操作,可以自动执行程序指令,这都是处理器可以完成的基本动作。因为处理器只会做有限的基本动作,因此,所谓的编程就是组合这些基本的动作,以解决复杂的问题。
比如说,处理器可以从存储区读取数字,也可以把数字写入存储区,还可以做加法和减法,所以,我们可以把这100个数字放到存储区里,第1个存储区放数字1,第2个存储区放数字2……第100个存储区放数字100。然后,编写程序,让处理器自动地按顺序读这些存储区,并把存储区里的数字累加起来。
老实说,这样做没问题,但是很愚蠢,既体现不出计算机的价值,也给编程这门职业抹黑。所以得有巧妙的办法,既节省人力,也能让计算机自动进行操作。最终,我们认为可以让计算机自动按以下步骤来解决问题:
1.在存储器里找两个空闲的存储区,第一个存放数值1,第二个存放数值0;
2.将两个存储区里的数值相加,结果放回第二个存储区;
3.将第一个存储区的数值取出,加1后再存回;
4.如果第一个存储区的数值小于等于100则转到步骤2;否则停止。
注意,上述过程不需要人工干预即能自动执行,当它最终停止后,第二个存储区里的内容就是从1加到100的结果。现在的问题是,如何写一个程序来命令计算机自动重复地做上述工作呢?嗯,这就要依靠程序设计了,也就是写程序,或者叫编程。
我们是说人话的,而计算机只能依靠电信号组成的机器指令工作。早先的时候,只有理解机器指令的程序员才能做编程工作,此时的编程是人类用机器的语言说话。
机器语言是0和1的组合,处理器执行起来干脆利落,快如闪电——不,比闪电还要快无数倍,但对人类来说非常抽象难懂,外行很难快速入门,就算是内行用起来也是烦琐得要命。在这种情况下,最自然的想法就是创造高级一点的编程语言。高级语言的终极目标是让编程像我们平时写文章一样,用自然语言进行,但目前来说还远无法实现,所以我们只能退而求其次,发明一些不那么“高级”的高级语言,这些高级语言比机器语言好懂多了。
用高级语言写的程序只是一些文本,我们能看懂,但计算机看不懂,处理器无法执行,因为那些东西并不是处理器可以识别的机器指令。所以,我们还必须用一个特殊的软件程序,称为翻译器,来将我们写的程序文本翻译成处理器可以执行的机器指令。翻译器也是人类写出来的程序,可以想象,第一个翻译器是用机器语言编写的。
翻译器不是人的大脑,它无法理解人类的自然语言。你可以将翻译器看成学校里用来自动识别答题卡的阅读机,它只能识别具有固定格式的内容。因此,在现阶段用高级语言编程就像在书写一篇具有固定格式的文章。每种高级语言都具有自己的格式,这种格式被称为那种高级语言的语法。现在,你应该明白C语言就是高级语言的一种,学习C语言,说白了就是为了掌握C语言的语法。
写程序和程序的执行既有关系,又很不同。程序在实际执行时,处理器执行这些机器指令,计算机按照程序的指示做各种动作。但是,程序在编写的时候,这一切都还没有发生,还仅仅是在描述那个执行过程。
就拿分配存储区这件事来说,这需要你亲自做出明确的指示。电脑虽然可以做很多事,但它不是你肚子里的蛔虫,你不跟它说,它是不知道你想要什么的。所以,你的C语言程序不但要指明整个累加过程如何进行,还得明确地告诉电脑在存储器里分配两个存储区。
在C语言里,要求分配存储区的事情通常是靠声明来完成的。“声明”的意思就是“告诉”“宣布”或“通知”,它用来指明某个东西是什么。例如:
int obj;
高级语言的特点是屏蔽了底层的细节,使你不需要关心这两个存储区在存储器中的具体位置,况且它们被安排到哪里,只有程序运行的时候才能确定下来。但为了能够访问得到它们,还是得有个凭据,就像人的名字。在C语言里,使用一个符号来代表、表示,或者说指示那个存储区,这里的obj就是一个存储区的名字,或者说它代表着一个存储区。
使用符号当然可以摆脱烦人的存储器位置,但是仅有符号还不够。首先,存储器是按字节划分的,字节是存储器的最小可寻址单位。但是,一个字节能表示的数的大小有限,存储一个数可能需要好几个连续的字节才行。所以问题来了,你这个符号是代表着一个字节的存储区呢,还是几个连续的字节?
除了存储区的大小,还有一个内容的解释问题。当你使用这个符号来访问它对应的存储区时,如何解释它里面的内容呢?
如图1-2所示,这是一个字节的存储区。字节的长度缺乏标准定义,但绝大多数计算机系统都支持将它定义为8个比特,所以我们的这个存储区也是由8个比特组成的。
图1-2 存储区的内容可以有多种解释
由图中可知,这个存储区的内容为二进制序列“11111111”。但是,它的含义是什么呢?是个小数?整数?还是其他什么东西?
如果它是一个整数,那么,它可能是一个无符号整数,也可能是一个有符号整数。有关这一点,相信大家在学习C语言这门课之前都已经有所了解。
如果是一个无符号整数,那么,这8个比特全都用于表示数值,所以这个二进制序列所对应的十进制数字为255。
如果是一个有符号整数,那么,在这8个比特中,有1个用来表示正负,另外7个比特才用来表示数值。假定这里采用的是对2的补码来表示负数,则这个二进制序列所对应的十进制数字为-1。
以上说的,是我们在读一个存储区时必须考虑的问题;当我们往一个符号所指示的存储区里写数字时,由于无符号数和有符号数的表示方法不同,所以也同样有这样的问题。
显然,为了分配一个存储区,仅仅声明一个符号是不够的。为此,C语言要求程序员在声明一个符号时,必须指定它的类型。类型决定了该符号所指示的存储区只能用来读写哪些种类的数据,实际上也就决定了数据以什么样的比特序列存在。
除此之外,C语言的类型还用来决定存储区的大小。数是无限的,无论存储区有多大,占多少个字节,有些数它依然表示不了,容纳不下;另一方面,如果处理的数都很小,而你又分配了一个特别大的存储区,显然是很浪费的。特别是在计算机发展的早期,存储器的容量很小,在存储空间的利用上用“锱铢必较”来形容毫不过分。
所以,对于上述声明,“int”是类型指定符,用于指定obj的类型。在C语言里,整数类型有好多种,而int是其中之一。该声明的完整意思是“声明一个符号obj,它的类型是int”。实际上,符号是没有类型的,而该符号所指示的存储区也没有类型,它只是用来存储电荷、容纳二进制序列的空间。所以,完整的表述应该是“声明一个符号obj,该符号所指示的存储区用来容纳int类型的数据,要用int类型来访问(读和写)”。
一旦把存储区和类型关联起来,那么,就等于约定以后只用这种类型来写入或者读出这个存储区,而且存储区的大小也就确定了。如果你用另一种截然不同的类型来读取或者写入该存储区,将无法保证结果的正确性,也无法预期程序的行为,因为存储区的长度不同,而且存储区的内容用不同的类型来解释将得到不同的值。
每个声明里都会有一些具有固定拼写的部分,在这里是“int”和分号“;”。“int”是C语言里的关键字,关键字就是那些具有固定拼写的单词,在C语言里有特定的含义和用途,其中的一个功能就是充当线索。因为机器不是人,它没有智慧,所以只能依靠关键字来分析你敲入的内容是什么意思。比如在这里,当翻译器看到有一行的开始部分是“int”,它就知道这应当理解为一个声明,并根据声明的语法继续分析该声明的剩余部分。
和很多别的计算机语言不同,C语言是区分大小写的,所以关键字也是大小写敏感的,不能将int写成Int或者INT,等等。
当然,声明里还有一些程序员可以自主决定的部分,比如这里的“obj”,这在C语言里称为标识符。你可以将“obj”改成“i”“x”和“object”等,都没问题,但你不能使用和关键字相同的符号,所以也不允许出现这样的声明:
int int;
简直乱套,这像什么话!要知道,关键字是C语言语法专属的部分,有固定的意义和用途,不能和标识符冲突。
标识符的拼写可以自由决定,但并不是完全没有限制,而且它也是区分大小写的。比较常规的拼写是使用下画线、26个英文小写字母、26个英文大写字母,以及0到9这十个数字字符,但是不能以数字字符打头。所以,_Myid、store、no001、id_ab是合法标识符的例子,而21century、pg dn、go-to、~num和cc*^w都是非法标识符的例子(分别是因为以数字字符打头、中间有空格、使用了“-”“~”“*”和“^”这些不允许的字符)。
标识符的最大长度原则上没有限制,唯一的限制来自你所使用的翻译软件,这是一套软件包,用来将你编写的源程序翻译成可执行程序。世界上存在着多种不同的翻译软件,由不同的人和机构编写,他们在标识符长度的问题上并不统一,你认为31个字符足够,我认为起码得128个。翻译软件在发行时会提供帮助文档,告诉你如何使用该软件,而且在文档中会给出所允许的标识符长度。如果你无法获取这些信息,那么就请记住,将标识符的长度控制在不超过31个字符的范围内一定是安全的,这个长度是C语言对翻译软件的最低要求。
到目前为止,我们一直在使用“存储区”这个词,但是用起来不方便。程序在运行时总是要读写数据的——可能是一个非常小的整数,也可能是一个人的完整履历资料。不管它是什么,是一个数字,还是一整块数据,都要在存储器中分配空间来读取和写入。我们先前称之为存储区,但更经常的叫法是“变量”。
要将C源程序翻译为可执行程序,需要一套翻译软件,但翻译软件需要根据C语言的语法规则来工作,而程序员也需要知道如何用C语言写程序,这就需要一个标准化的文档和依据。C语言刚刚诞生时,它的作者写了一本书,这本书就是事实上的标准。然后,因为这本书太过于简略,很多细节没说清,于是各个翻译软件只能自由发挥,各搞一套。
在这种情况下,C语言的标准化工作就提上了日程。1989年,国际标准化组织推出了第一个C语言的国际标准ISO/IEC 9899:1989,简称C89;1999年,又推出第二个C语言的国际标准ISO/IEC 9899:1999,简称C99;最新的一版是2011年推出的C语言标准ISO/IEC 9899:2011,简称C11。之所以标准一直在更新,是因为很多组织和厂商希望加入新的语言特性。另外,旧标准里有一些不完善的地方也需要加以补充和修改。
在C标准文档的正文里,不使用“变量”一词,而代之以“对象”。当然了,这不是谈恋爱时所找的对象,也不是有些面向对象的编程语言(例如C++和JAVA)里的对象,不要混为一谈。标准文档之所以避免使用“变量”一词,是因为它是一个已经被滥用,但还缺乏标准定义的词。绝大多数教材根本不加解释就用,有的则解释得非常笼统。
在本书中,我们约定,“变量”的含义和C标准文档里的“对象”是等同的,都是指一个存储区,可用来保存值。“值”是计算机操作和加工的对象,是存储在计算机中的、用特定类型来解释的、精度意义上的内容。
一旦我们按照上面的方法声明了符号obj,这个符号就与它所对应的变量之间建立了关联,如图1-3所示。变量只有在程序真正运行时才会分配,但现在还只是在编程阶段,但我们完全可以纸上谈兵,在编写程序时就假定已经有了这个变量,并对它进行并非真实的读写操作,就像它们已经存在一样,这很方便,不是吗?
图1-3 标识符和变量之间的对应关系
不过,obj毕竟不同于它所指示的变量,它仅仅是符号,而变量是在程序运行时才会确定下来的存储区。所以,严格地说,我们只是在编写程序时用一个符号来指示或者表示一个尚不存在的变量,即“obj所指示的变量”或者“obj所代表的变量”。有时候,为了方便起见,会直接说“变量obj”,但你要明白实际上是怎么一回事。
C语言对程序的格式并不在意,只要各语法成分能够互相区分开来就行。例如,类型指定符int和标识符obj一定要用空白字符分开,但标识符obj和后面的分号“;”可以连写,因为标识符不能由分号组成,所以能够被识别为不同的东西,但是用空白字符分开也没问题。空白字符包括空格、换行符、制表符(对应于键盘上的TAB键),等等。所以你可以这样重写前面的声明:
int
obj
;
在这里,分隔字符是换行符。为了告诉翻译器,一个声明已经结束,后面的内容不再是当前声明的一部分,这里需要一个结尾标志。对,就是分号“;”。另外,要注意类型指定符int和标识符obj的相对位置,int应该在前面。