“黑箱”中的计算机系统

输入与输出

无论计算机如何复杂,对操作者来说,能感知到的就只有自己进行操作的外设与获得反馈的外设。

例如键盘、屏幕、鼠标、音箱等等。没有类似的设备,我们是没办法操作电脑的,而我们对于电脑的感知,往往也停留在这些外设上。

“科技以人为本”,一台无法给到人类感知的机器,是没有意义的。

将向电脑输入信息的设备叫“输入设备”,例如鼠标、键盘;

电脑将对输入信息进行处理,其产生结果要给到人反馈的,这些为了实现给人反馈的设备叫“输出设备”。

可以说,工程师们更希望电脑对用户来说就像一个“黑箱”。用户不需要知道这个“黑箱”里有什么,只要知道把东西放进去再拿出来就会得到想要的结果就够了。

这样的设计控制了对用户的复杂度,把复杂的底层逻辑抽象成更简单、更容易理解的概念。工程师们这样的行为无疑是必要的,否则用户就不得不抱着可能几千页的工程手册来使用电脑——这对用户来说学习成本是巨大的。

但这样也带来了一些麻烦,尤其是对于需要成为工程师的人来说。

我们现在的大多数工作甚至生活都已经离不开计算机,但是它们的底层逻辑大多数时候对我们来说都是“透明”的,即从我们的视角看是看不到的。例如你在键盘上敲击,几乎是瞬间屏幕上就会显示出你敲了哪几个键。或许有些人就会认为这是键盘直接把信息给到屏幕上去了,但是实际上从键盘上敲下按键到屏幕上显示出字符,这中间有一个漫长但是迅速的过程。

输入输出数据.png

如上图,计算机系统通过输入设备获得人输入的信息,然后计算机将合适的结果输出并通过输出设备输出使得人可以获得想要的数据结果。

所以键盘输入的数据在抵达屏幕之前,实际上计算机在其中完成了许多工作,或许可以浪漫地称之为“一段数据的艺术之旅”。

如何实现这样的过程呢?

计算机正如它的名字,只是“计算”的机器,只是通过设定好的程序,将一堆数据通过计算形成另一堆数据。正如上一篇中我们讲图灵机时在大脑内的模拟,我们可以试着在脑海中简单的进行这样的转换工作。

一串数据的艺术之旅

首先,我们要知道键盘输入了什么,以及屏幕显示得又是什么,以及如何用数据表示他们。

如果拿着显微镜来看,发光的屏幕看上去就像一个个发光的小灯泡,只是这些灯泡的大小和发出的光颜色有区别。物理学告诉我们,只需要有限的几种光进行混合搭配,就可以组合出许多种不同颜色的光出来。

例如:

红色光和绿色光混合形成黄色光;

绿色光和蓝色光混合形成青色光;

红色光和蓝色光混合形成紫色光……

现代的彩色显示屏上,就是分别由一个的红、绿、蓝色小灯泡组成一个单元(这样一个单元也被叫做一个像素点),再由许多这样的单元按一定的排列方式铺满一块屏幕的底座。当然此外还有线路来控制这些小灯泡,还有另外的一些线路来给它们功能,再比如还有保护它们不被轻易压碎的保护层……

最后在这些元件的共同作用下,通过控制这些小灯泡发出的光,来组成我们想要的图形以及颜色。这就是一块屏幕了

假如说,当只有红色小灯泡亮起来,一个像素点就发出红光;当只有绿色小灯泡亮起来,一个像素点就发出绿光;当只有蓝色小灯泡亮起来,一个像素点就发出蓝光;如果需要更复杂的颜色,就让颜色之间进行组合搭配,就可以发出更多颜色的光了。

如果说要显示一个蓝色的字母“A”,我们可以在屏幕上划出“A”的轮廓,然后让轮廓中框选出来的像素点亮起来,并且主要发出蓝色光就好了。

为了能控制屏幕,我们首先要用数据表示框选出的像素点在屏幕上的坐标。

0 1 2 3 4 5 6 7
0 (0,0) (0,1) (0,2) (0,3) (0,4) (0,5) (0,6) (0,7)
1 (1,0) (1,1) (1,2) (1,3) (1,4) (1,5) (1,6) (1,7)
2 (2,0) (2,1) (2,2) (2,3) (2,4) (2,5) (2,6) (2,7)
3 (3,0) (3,1) (3,2) (3,3) (3,4) (3,5) (3,6) (3,7)
4 (4,0) (4,1) (4,2) (4,3) (4,4) (4,5) (4,6) (4,7)
5 (5,0) (5,1) (5,2) (5,3) (5,4) (5,5) (5,6) (5,7)
6 (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (6,7)
7 (7,0) (7,1) (7,2) (7,3) (7,4) (7,5) (7,6) (7,7)

如上表,就是一个的8 x 8方阵以及这个方阵中每个点的坐标,我用形似(x,y)的形势来表示坐标,x处表示代表的数字就是点所处的列,y处表示的数字就是点所处的行。

如果说这个方阵里摆放的是一个个像素点,那么当我要找到第三列第四行时只需要告诉说(3,4)就好了。

接着我们还需要用数据来表示像素点中的灯泡发光。这就更简单了,比如说我们用“1”表示灯亮,“0”表示灯不亮,于是当只要蓝色灯泡亮的话,只需要说(0,0,1)即可。

我们只要用(3,4,0,0,1)就可以表示第三列第四行的像素点的蓝色小灯泡要亮起来。

注意:上述的(x,y)这样的表述中,逗号和括号只是为了方便阅读,在逻辑上他们是不存在的。

现在把屏幕想象成一个可以听得懂数字的小妖精,他可以听懂我们用上面用来表示小灯泡亮起来的数据,我们又该告诉他哪几个像素点要亮起来呢?

上面说我们需要亮起一个字母“A”,现在我们可以试着在一个方阵里画出来这个图形。如下图:

A点阵图-2.png

只需要在屏幕上显示上图中涂黑方块坐标相同的像素点,就可以做出一个大写字母“A”了。同时我们又要这个“A”是蓝色的,所以,需要告诉告诉屏幕这个小妖精下面这样的一串数据:

(0,5,0,0,1)(0,6,0,0,1)(0,7,0,0,1)

(1,3,0,0,1)(1,4,0,0,1)(1,5,0,0,1)

(2,1,0,0,1)(2,2,0,0,1)(2,3,0,0,1)(2,4,0,0,1)

(3,0,0,0,1)(3,1,0,0,1)(3,2,4,0,1)

(4,0,0,0,1)(4,1,0,0,1)(4,2,4,0,1)

(5,1,0,0,1)(5,2,0,0,1)(5,3,0,0,1)(5,4,0,0,1)

(6,3,0,0,1)(6,4,0,0,1)(6,5,0,0,1)

(7,5,0,0,1)(7,6,0,0,1)(7,7,0,0,1)

注意:为了方便阅读,上面进行了一定的排版,实际上应该没有换行。

这样的一个阵列并不是唯一的,我们可以根据需要制作许多阵列,例如:

A点阵图.png

我们完成了一个图形的表达,但是在键盘上我们并不需要这样冗余的表达。

一个经典的QWER键盘只有103个按键,也即是说,如果我们给每一个键一个编号,我们也只需要103个数字就可以完全表达整个键盘上的按键了。例如我们给字母“A”编号为“65”,字母“B”编号为“66”……这样,键盘只需要将这个编号输入到计算机系统里,然后由计算机系统自动将编号转换成对应的阵列数据,并告诉屏幕这个小妖精。这样,一个字母就可以显示在屏幕上了。

显然,在上一篇中的“图灵机”就可以完成这样的工作。

计算机系统与函数

直到现在,我们依然将一个具体的计算机系统当做一个“黑箱”,并没有去具体了解它是怎么实现的,实际上这篇C语言教程中几乎所有内容都会保持这样的状态。

之所以要这样最主要的原因就是知识量的问题。这所有内容大概是一个相关大学专业在本科阶段所需要学习的一半内容,笔者如果要去完整描述是几乎不可能的。

而基础的C语言也确实很好地抽象了更底层的内容,让使用者可以去“透明”地使用,至少我们在现阶段还不需要去考虑是否“保持透明”的问题。

而这样将变量输入一个“黑箱”然后得到一个结果的机器(或者说工具),我们很早就有接触,那就是“函数

心动篇 - 图4%0A#card=math&code=y%3Df%28x%29%0A&id=pIHV0)

将一个自变量x代入函数里,就可以得到一个与之对应的因变量y

我们看到上文中描述的计算机系统与输入输出就很类似函数的这个过程。而这也正是计算机的作用之一,将人们从繁琐的函数自变量到因变量的计算中解放出来了,只需要专注于构造函数然后交给计算机去计算。

只是目前来说,还不清楚从自变量x到因变量y之间的过程。而C语言起到的最主要的作用,就是辅助我们设计这个过程。

在C语言中也有名为“函数”的概念,而且与数学中的“函数”作用类似。但是因为实现计算机的硬件电路的限制,其中又多了一些限制,下面就会开始详细讲解。

注意:C语言中的函数与数学中的函数并不完全等价

前面说过,我们编写的C语言程序,要通过编译器来编译成计算机能够理解的样子。而在编译器所使用的规定中,想要开始使用一个函数或者变量,我们需要首先在文档中声明

在正式声明一个函数之前,就不得不引入数据类型的概念。所谓数据类型实际上是计算机系统内部对于数据解释的区别,在后面的章节中会详细讲述。

具体的数据类型比较多,我们现在只需要记住三个,分别是:

数据类型 释义 描述
int 整型 表示整数
double 双精度浮点型 表示小数,但是不应该用于精确值
char 字符型 表示字符,字符不应参与运算,例如’a’,’b’,’c’,’-‘,’*’

接下来,我们通过实际编写一个程序,用来更详细地描述如何使用这些内容。请保证你已经安装好了GCC。

首先,在某个文件夹下新建一个文本文件——打开文件管理器->鼠标右键单击->新建->文本文档

新建文本.png

将文件名修改一下尤其要注意需要添加一个后缀名,如下图,后面添加了一个“.c”。

新建文本2.png

如果文件变得无法打开的话,可以直接用记事本打开。

新建文本3.png

现在可以键入需要编写的程序了,首先输入:

  1. int main()
  2. {
  3. return 0;
  4. }

这里可以试着编译一下(参考初始篇)。

变量与常量

变量是函数中最重要的部分,而在C语言中如果你需要一个变量,首先需要声明

  1. int main()
  2. {
  3. int intVar1,intVar2,intVar3;
  4. return 0;
  5. }

(暂且不用管上述第三句以外的内容)

只需要这样,我们就声明了三个int变量。即如下格式:

  1. [变量类型][空格][变量名1],[变量名2]...;

虽然一次可以选择声明很多个变量,但是一般情况下建议只声明一个,变量名只能使用a-z,0-9以及’_’来命名,且数字不能出现在第一位。

也可以初始化它:

  1. [变量类型][空格][变量名] = [初始值];

除了变量类型和变量名之间必须要空格,其他地方并不需要。

可以用另一个已经初始化的变量以及一个表达式来初始化变量:

  1. int a = 1
  2. int b = a * a + 2;

注意:一般只建议将变量初始化为0

如何使用变量呢?

我们可以试着用变量来计算一些简单的函数,例如:

心动篇 - 图8

  1. int main()
  2. {
  3. double x = 0;
  4. double y = 3.14*x*x;
  5. return 0;
  6. }

上面的程序中,我们声明了两个变量xy;同时还出现了两个常量03.14

在x后方添加了一个“= 0”;y后方添加了“= 3.14xx”。这很像我们日常书写的算式,只是乘号被*代替了。这里的逻辑很容易理解:

  1. 将0通过“=”赋值给变量x
  2. 将3.14xx的值赋值给变量y

实际上变量就类似与图灵机中纸带的格子,可以将一个符合条件的值写入这个变量里,一般通过赋值运算符即“=”表示将一个值赋予某一个变量。

注意:不同类型的值不能随意赋值!

这里我们引入运算符的概念,C语言中,只提供了一些基础的算术运算符,可以在编写的程序中直接使用它们。更复杂的运算就需要通过我们编写程序来实现。

上述程序可以使用GCC来编译(参考基础篇),但是注意他们并不能产生输出,因为我们没有使用相应的函数。读者可以参考下面的程序修改,只需将x的值改成需要的值,就可以用这个函数来计算圆形的面积,只是这个程序每次修改都需要重新编译:

  1. #include<stdio.h>
  2. int main()
  3. {
  4. double x = 0;
  5. double y = 3.14*x*x;
  6. printf("%f",y);
  7. return 0;
  8. }

算术运算符

例如有A和B两个变量,A = 10,B = 20,下表中通过它们列举了C语言支持的算术运算符。

运算符 描述 实例
+ 把两个两个数相加 A + B 将得到 30
- 从第一个操作数中减去第二个操作数 A - B 将得到 -10
* 把两个操作数相乘 A * B 将得到 200
/ 分子除以分母 B / A 将得到 2
% 取模运算符,整除后的余数 B % A 将得到 0
++ 自增运算符,整数值增加 1 A++ 将得到 11
自减运算符,整数值减少 1 A— 将得到 9

函数

经过上述的内容,读者或许已经发现了函数的存在。

实际上之前的main就是一个C语言中的函数。在C语言中,函数由几个部分组成:

  1. [返回类型][函数名]([参数1],[参数2],...)
  2. {[函数主体]}

函数名,即是函数的名字,让编译器能够找到需要的函数,函数名只能使用a-z,0-9以及’_’来命名,且数字不能出现在第一位。大多数情况下,我们应该避免函数重名。至于在什么时候,如何避免,将在下一章讲述。

正如数学中的函数,我们需要通过自变量获得因变量,C语言的函数中,参数就类似于自变量,这些参数经过函数主体的运算之后,得到结果(因变量)传递出去,这就是返回值

参数,参数是可选的,可以从零到许多个参数根据需要设定,参数的作用在于其它函数调用这个函数时,可以通过参数将数据传递到函数主体中。参数的定义和变量的定义格式没有区别。(早期的C语言有其他格式,但现在基本不用了)

函数主体,定义函数执行内容的语句

返回类型,返回类型是函数的返回值的类型,返回值写在函数主体中,使用return语句来声明,例如上面的main函数中出现的“return 0”而返回类型正是表示return语句中计算结果的类型。例如main函数中return跟着0,而且返回类型是int,这就意味着这个函数将返回一个int类型的0出去。再例如:

  1. double DoubleFunction()
  2. {
  3. return 0.01;
  4. }
  5. char CharacterFuntion()
  6. {
  7. return 'a';
  8. }

DoubleFunction()返回了一个’double’类型的值0.01;CharacterFuntion()返回了一个char类型的值’a’。

注意,a两边的单引号是必要的,这表示一个字符类型的值。

为了更好体现函数的作用,我们重新编写一个计算圆形面积的函数。

  1. #include<stdio.h>
  2. double square(double a)
  3. {
  4. return a*a;
  5. }
  6. double CalculateCycleArea(double radius)
  7. {
  8. return square(r)*3.14;
  9. }
  10. int main()
  11. {
  12. double r;
  13. double area;
  14. r = 10;
  15. area = CalculateCycleArea(r);
  16. printf("%f",area);
  17. return 0;
  18. }

如上程序,我们从main开始下的”{“开始阅读它。
一对“{}”中间的内容被称为一个程序块,如果是紧跟着函数的声明,那么他就是函数体。
函数体是由许多分号结尾的一句程序编写的。我们看到每一句都有换行,实际上换行是只是为了增加对人的可读性,机器并不需要换行。

  1. 这里的main函数首先连续声明了两个变量rarea,C语言并只管变量名有没有符合基本的用字要求,你可以取任何你想得到的名字,例如r可以换成banjingarea可以换成mianji

  2. 接下来我们给rarea赋值,r被赋予地是我随意给的数字,关键在于area,可以看到“=”右边是一个函数CalculateCycleArea(r),而这个函数是我们在上面定义的double CalculateCycleArea(double radius),当遇到这种情况时,程序就会从main函数跳转到CalculateCycleArea函数中(main会中止),并且我们定义的参数double radius这时就会得到main中调用时所填入的值,即main中的变量r。我们称这种情况为调用。

  3. 接下来程序会转入CalculateCycleArea中执行,如果CalculateCycleArea中又调用了其他的函数,那么就会再继续到其他函数中执行,直到收到一个返回结果。在这里,CalculateCycleArea调用了square函数。
  4. CalculateCycleArea成功返回之后,area就会获得CalculateCycleArea的返回值,然后在下一句printf函数中,area的值会被输出出去,当我们在cmd或者其他shell中执行这个程序时,就会看到area的值。

函数的一些规则

main函数

main函数是一个特殊的函数,它是程序开始的地方,程序总会从main函数的函数主体中第一句开始,逐句向下执行,如果遇到了当main函数中使用了其他的函数时才会跳转到其他地方,一直到执行至mainreturn语句,程序结束。

函数调用

一般情况下,一旦调用了另一个函数,调用者就需要一直等到被调用者成功返回,然后才会继续执行自己后面的语句。也就是说,直到被调用的函数执行到了return语句,将语句中的表达式计算出来并且返回成功了,函数才会继续执行下一条语句,直到执行至自己的return语句。

或许读者现在依然对函数感觉一头雾水,在下一章,我们会具体使用C语言编写具体的程序,以帮助读者理解。