计算机本质
一阴一阳谓之道
对于计算机来说,0
和1
就是计算机的「道」。
道生一,一生二,二生三,三生万物
我们知道,计算机只能识别0
和1
这样的二进制数据。所谓程序,其实就是一堆由0
和1
构成的二进制指令,这些指令可以控制计算机底层数字电路系统,使各个组件间的数据可以相互交换与计算。计算机最重要的交互莫过于 CPU 与 内存 之间的数据交互与计算,通过一些特定指令,就可以实现 CPU 从内存中读取数据,然后将该数据进行计算,最终再写回到内存中,完成一轮计算过程。内存中的数据,本质就是一个数值(二进制数据),内存中不同的地址,可以存储不同的数值,所以内存其实就是一个可以临时存储数值的大仓库。
0
和1
构成了计算机的道,0
和1
这两个数字的任意组合,就可以产生无穷多的数值,我们将真实世界中的事物及其规律以数学的方式进行表达,就可以让计算机模拟真实世界,对真实世界的事物进行计算,最终反馈于真实世界。比如说,对于字符来说,计算机无法直接识别,因此我们将每个字符用不同的数值(二进制)进行表示,比如,对于 ASCII 编码来说,字符a
在内存中的表示就是数值0x61
,也就是十进制的91
,CPU 并不关心数值对应的真实类型是什么,CPU 就只知道二进制数据,CPU 就只能对二进制数据进行代数与逻辑计算,其他的事情 CPU 漠不关心,等待 CPU 将数据处理完成后,将该数值传递给其他输出设备,由输出设备处理该数值的具体显示效果,比如,假设屏幕输出设备接收到0x61
,假设屏幕要进行字符串打印操作,那么屏幕就知道0x61
代表字符a
,就直接在屏幕上打印出字符a
,而不是输出原始数值0x61
,这样就实现了数值与字符的交互。
简单来说,在计算机中,所有的数据存储与计算都只能使用二进制数据,真实世界的事物必须抽象为具体的数值模型,计算机才能进行处理,最终反馈于真实世界,这一过程其实也就是对真实事物的数学建模过程。0
和1
虽然只有两个数字,但他们的组合可以表示无穷范围数值,计算机说到底,就是一台进行计算的机器,而世间一切,万物皆数,一切皆可进行计算,也因此,我们现在生活的环境,其实存在两个世界,一个是真实的物理世界,另一个是由数学构造的虚拟世界,而计算机或者说程序,正是构建虚拟世界的开山战斧,程序,也成为沟通虚拟与现实之间的桥梁。
编程语言
如果说0
和1
是计算机构建虚拟世界的基石,那么程序则是指导计算机构建的图纸。
在计算机角度来说,程序就是一串由0
和1
组合而成的指令集合,指令可以控制计算机底层电路,实现电路不同组件间的数据交互与数值计算...
计算机可以直接识别这些指令,也因此,这些二进制指令被称为「机器语言」。
初代程序员编程都是通过直接输入相应指令来控制计算机,但这种方法太过原始且不符合人的认知模型,因此,之后便发展出了一种符号助记语言:「汇编语言」。
汇编语言采用助记符来代替机器指令的操作码,采用标识符表示指令操作数,大大增加了代码的可读性。但是计算机只能识别二进制数据,因此必然存在一种可以将汇编语言翻译为相应机器指令的工具,而这种工具就是「编译器」,对于汇编语言来说,其学名为「汇编器」。
汇编语言是对机器语言的抽象,但其仍属于低纬度抽象。同机器语言一样,汇编语言也是面向机器的,汇编语言编写程序也是直接对硬件进行操作,也因此,机器语言和汇编语言都属于「低级语言」。
在前一章节中,我们说过,万物皆数,而计算机的本质就是可以对数值进行计算,但是采用机器语言或汇编语言编写的代码,没办法让我们直接的以接近自然语言或数学公式来表达数值计算,比如,对于int x = 1 + 2
,其含义就是计算代数1 + 2
,然后将结果赋值给内存中的一块区域,即将3
赋值给变量x
,如果采用汇编语言编写上述代码,其代码可能如下所示:
MOV R0, 1 # 将立即数 1 存储到寄存器 R0
MOV R1, 2 # 将立即数 2 存储到寄存器 R1
ADD R0, R1, R2 # 将 R0 和 R1 存储的数值相加,然后将结果存储到寄存器 R2
MOV 64, R0 # 选中主内存地址 64
STO R2, R0 # 将 R2 数值写回到内存中
低级语言虽然在效率上十分高效,毕竟直接操作硬件,但是在表达上,却十分低效,我们更需一种具备更高维度抽象的语言,可以让我们以更接近自然语言和数学公式描述来编写代码,而这种面向用户的语言,就称为「高级语言」。
其实汇编语言已经开了个好头,其通过汇编器将符号助记编译为机器指令,只是其抽象维度较低而已,同样的,我们可以使用更加接近人类阅读习惯的文本来表示操作逻辑,然后通过编译器(或解释器)将其最终编译成机器指令即可。
注:实际上,绝大多数编译器都会先生成汇编代码,然后再由汇编器生成机器码。生成汇编代码的原因是方便对代码进行调试。
到这里,我们已经成功将二进制指令编写代码转化为文本编写代码,而由于文本的富表达性,导致描述一个相同的操作,可能存在许多不同的表达,有些编程语言表达很繁杂,有些编程语言表达很简洁,也有些语言在各自的领域内更加擅长...也因此,一个一个的高级语言如雨后春笋般涌现而出,但是对于所有的编程语言,万变不离其宗,无论其高层代码如何表示,最终都会编译为机器指令。
总结一下,计算机的本质工作是进行数值计算,万物皆数,在计算机中,可以表达真实世界一切事物,只需找到真实事物的数学模型。高级语言虽然有很多种,但高级语言的本质是提供富文本来让程序员以接近自然语言的方式来表达程序计算与操作逻辑,或者说更好的对算法进行描述。虽然各个高级语言的表达方式不尽相同,但其思想是一样的。
掌握一门编程语言,其实就是能够使用这门编程语言来表达我们的计算与操作,当然,随着现在数据类型的愈加丰富,高级语言的富表达性可直接让我们表达与操作这些非数值类型数据,但本质上,这些非数值数据最终也会转化为二进制数值。而计算与操作逻辑,跳脱不出这些基本操作,比如,变量、函数、条件处理、流程控制...学会使用某个编程语言的这些基础操作,我们就算入门了该语言,此时我们就能编写出一些小程序,基础的数值计算与逻辑操作是没问题的,当然,要想更好掌握该门语言,还需对该门语言的一些特有或高级特性熟练掌握,这可以让我们写出更简洁与高效的代码。而如果想具备使用该语言进行真正的大型项目开发,排除设计模式、数据结构与算法等非语言因素外,熟练掌握该语言提供的常用库函数以及其他一些第三方库或框架等,这样才能让我们真正具备一定的工程能力。
本篇博文主要介绍高级语言的一些共有特性,这些是编写程序必不可少的功能特性,通常掌握这些内容,使用这门语言进行一些程序编写或代码修改应当是没有什么问题的。另外对大多数高级语言都具备的一些高级特性,也会简单提及下。至于各语言提供的私有特性,这个具体学习时再针对性进行掌握即可。
编程范式
「编程范式」意指某种类型的编程风格或编程方式。
宏观上来看,不同的编程范式代表不同的代码组织与执行方式。比如,对于面向对象编程来说,事物间的交互是一系列不同对象间的通信过程;对于函数式编程来说,数据的流向与处理是通过一系列无副作用的函数进行...
或者可以这样说,不同的编程范式表示程序员使用不同的思维模型进行程序编写。
目前业界支持很多种编程范式,但常见的有如下几种:
-
面向过程(Procedure Oriented):面向过程也被称为「命令式编程」,该种范式关注的是完成一件事情,需要经历的所有具体步骤,即第一步执行什么,第二步执行什么...直至事情完成。
面向过程的语言比较适合解决线性算法问题,因为它强调的是自顶而下有序执行,这与人类对简单事物的处理描述的认知模型是一致的。
常见的面向过程的语言有:机器语言、汇编语言、C语言...
-
面向对象(Object-oriented programming,OOP):面向对象思想将事物以类进行抽象,将同一性质的事物共有特性抽取出来,封装为类,不同的行为则由具体子类进行特异实现,实现封装、继承与多态。类的具体实例被称为对象,对象是程序的基本单元,程序的执行是一系列对象间的通信过程。
面向对象语言将事物看成一个个不同的实体,每个实体代表一个具体的事物,实体本身自成一体,具备数据接收、处理与输出功能,不同实体间通信通过调用对应实体的相关方法即可。
面向对象的思维模式与人类对事物的总结与认知是完全一致的,类就是我们所认知的事物总称,对象就是事物的一个具体实例(比如“人”就是类,“小明”这个人就是一个具体实例)。
面向对象编程应当说是当前最主流的编程范式之一,常见的面向对象的编程语言有:C++、Java、Python...
-
函数式编程(Functional programming):函数式编程中函数指的不是编程语言中的
function
,而是数学中的函数。参数作为自变量,经函数执行后转换为另一个数。在函数式编程中,函数是一等公民,因此几乎可以在任何地方定义和使用函数。且由于其数学原理,一个函数的值只取决于参数的值,不依赖其他状态,即函数是无状态的,因此,函数式编程不会依赖和更改外部状态和数据,没有副作用。
函数式编程强调程序由数据进行驱动,且数据在不同的纯函数间进行流转与加工,每次流转都会生成一个新的数据。每个纯函数视作一个处理单元,数据在经历多个处理单元计算后,就可以完成相关任务,且整个过程不涉及状态间转换。相对于设计一个复杂的执行流程,函数式编程采用数据无状态流转,不断渐进、逐层推导得出结果,整个过程更加清晰与可控。
函数式编程是面向数学的抽象,每个纯函数相当于一个无副作用的数学表达式求值。
常见的函数式编程语言有:Lisp、ML、Haskell、Erlang、Clojure...
一种语言可以支持多种编程范式,这类语言被称为「多范式语言」,不过即使是多范式语言,通常都有主次范式之分,且主范式只有一种。大多数情况下,学习一门编程语言,我们主要掌握该门语言的主要范式编程思维,但是如果本身已经熟悉其他范式编程,且该语言也完全支持该范式,也可使用自己熟悉的范式进行学习。
类型系统
前面我们说过,计算机只能识别比特数据,但是真实生活中,我们有很多非数值类型数据,比如字符、图片、音视频...高级语言的一大特性就是可以让我们能以接近自然语言来描述代码实现,对于这些不同的类型数据,高级语言通常都会内置或支持自定义方式让我们可以使用这些类型数据(当然我们知道,最终这些数据都会翻译为比特数据,这样计算机才能处理),这个特性称为编程语言的「类型系统(type system)」。
简单来说,类型就是对底层内存布局的一个抽象,其实质就是对比特数据的映射,程序员或程序可以通过类型信息,来知晓比特数据用途及其具体操作。
每个编程语言都有属于自己的一套特定类型系统,类型系统的一大好处就是可以进行类型检查,通常类型检查可发生在编译期和运行期,我们将编译期就能够检查类型信息的语言称为「静态语言」,将只有在运行期才能检测类型信息的语言称为「动态语言」。另外,如果一个语言不允许类型隐式转换(或者隐式转换不会丢失信息),则称其为「强类型语言」,反之则称之为「弱类型语言」。
所以,一门语言其实可以细分为:
- 无类型:比如汇编语言,没有类型的概念,所有操作都基于数值。
- 弱静态类型:比如 C 语言,支持类型定义,但不同类型之间可以自动进行转换。
- 强静态类型:比如 Java 语言,支持类型定义,且类型检查发生在编译期。
- 强动态类型:比如 Python 语言,变量类型无需定义,程序运行时由解释器推断出类型信息,且 Python 不支持类型隐式转换(比如:
2 + '2'
会抛出错误) - 弱动态类型:比如 PHP 语言,类型信息在运行期才能知道,且支持类型隐式转换(比如:
2 + '2'
,字符串'2'
会先被隐式转换为整形后,再进行相加,结果为4
)
通常来说,强类型相比于弱类型语言,会更加安全,因为弱类型语言有可能因为未注意到的隐式转换而导致程序结果不符合预期,而强类型会强制让我们显示进行类型转换,避免潜在的错误,因此,目前似乎越来越多的语言都尝试把类型增强。
静态类型语言类型检查发生在编译期间,只有语法和类型检查等都正确后,编译器才会生成可执行代码,且由于在编译期间可完全获取类型信息,因此编译器在编译期间可以对程序代码进行优化,所以静态类型语言生成的代码通常都比动态类型语言执行效率高。
动态类型语言只有在运行期才能知道变量类型信息,因此动态语言采用的是解释执行,即解释器一行一行读取源码文件,实时进行解释执行。由于动态语言采用解释执行方式,因此其程序运行速断会相对较慢,且可能因为一个微小的类型错误就导致程序运行崩溃...但动态语言的优点是变量无需声明,且可以随意更换类型指向,这使得动态语言编写代码比较自由且代码编写效率高。
通常来说,中小程序可以优先采用动态语言进行编码,灵活高效,而对于大型项目,强烈建议使用静态类型语言,严格的类型系统与编译期类型检查可以杜绝因类型错误等原因而导致的生产环境崩溃,同时,静态类型代码更加方便查阅,因为可以直接从源码中获取类型信息,而动态语言采用「鸭子类型」,我们无法从源码中直接知道变量具体类型,只能从源码具体实现中知道类型特性(即类型拥有的相关方法或属性),如果项目存在多种相同鸭子类型,就必须对代码进行追踪,结合上下文推导出具体类型,这对于业务繁杂的大项目来说,无法想象...
数据类型
数据类型就是编程语言实现的类型系统,对大多数编程语言来说,存在两种数据类型:
-
基本数据类型:基本数据类型一般为值数据类型,通常涵盖如下表所示的类型:
类型 常用关键字表示 布尔型 bool
、boolean
字节 byte
字符 char
短整型 short
整型 int
长整型 long
单精度浮点型 float
双精度浮点型 double
引用数据类型:也可称为「抽象数据类型」,可能不表示为值类型数据。常见的自定义类型、类、接口、对象等都属于引用数据类型,一个特殊的引用类型为:空,常用
null
、void
进行表示。
变量
编写程序,少不了变量定义,不同语言有不同的变量声明与定义格式,但主要需要关注变量的作用域,通常有局部变量和全局变量之分。
函数
函数的三要素为:函数名、参数 和 返回值。其中,函数名 和 参数 唯一识别一个函数,这两者共同构成 函数签名,函数签名可区分函数重载(即函数名相同,但参数不同的函数)。
大多数函数为普通函数,关注下不同语言间的定义格式即可。而在支持函数式编程的语言中,函数是一等公民,因此支持高阶函数与闭包等特性。
类
面向对象编程的三大特性为:封装、继承 和 多态。
在支持面向对象编程范式的语言中,通常都会具备 类 这个概念,类是对一些事物共性的描述。
主要关注下编程语言中 类 的定义格式,同时还要区分下 类 和 对象 间的区别:类有类属性(静态成员)和 类方法(静态方法),对象有对象属性(成员变量)和 对象方法(成员方法),两者的定义含义与定义格式要区分下。
运算符 / 操作符
对变量的操作少不了运算符,运算符大致有如下几类:
-
算术运算符(Arithmetic Operators):执行代数运算。常见算术运算符如下表所示:
Operator Description +
加法运算 -
减法运算 *
乘法运算 /
除法运算 %
取余 ^
幂运算 注:除法运算符
/
需要注意下是否是整除(都是整型情况下) -
赋值运算符(Assignment Operators):执行变量赋值操作。常见赋值运算符如下表所示:
Operator Description =
等于(赋值) +=
加等 -=
减等 *=
乘等 /=
除等 %=
余等 ^=
幂等 -
关系/比较运算符(Relational (Comparison) Operators):执行比较操作。常见关系运算符如下表所示:
Operator Description <
小于 >
大于 <=
小于或等于 >=
大于或等于 ==
等于 !=
不等于
-
逻辑运算符(Logical Operators):执行逻辑运算。常见逻辑运算符如下表所示:
Operator Description ||
与运算 &&
或运算 !
非运算 -
位运算符(Bitwise Operators):执行位运算(比特操作)。常见位运算符如下表所示:
Operator Description |
与 &
或 ~
非(取反) ^
异或 <<
左移 >>
右移 -
一元运算符(Unary Operators):有些编程语言支持一元运算符。常见的一元运算符如下表所示:
Operator Description +
正号 -
负号(正负数转换) ++
自增 --
自减 注:一元运算符中的
++
和--
区分下前置和后置功能。 三目运算符:有些编程语言还支持三目运算符,形如:
a > b ? a : b
。
流程控制
高级语言提供的流程控制语句主要有如下几种类型:
条件判断:主要包含
if
语句和switch case
等语句。循环:主要包含
while
语句和for
等语句。-
中断:高级语言主要包含的中断语句如下表所示:
关键字 描述 return
返回(主要用于函数中断与返回) continue
用于继续下一次循环操作 break
中断循环操作并跳出循环
其他特性
其他一些相同的特性包括:注释、代码组织、模块导入等...
私有特性
上述内容都是大多数高级编程语句具备的共有特性,然后每个语言也都会同时具备其他一些语言所没有的特性,我将之称为 私有特性。
比如 Java 语言有注解、字节码等特性,Python 语言有装饰器等特性,Javascript 有原型链等特性...
具体语言的私有特性等学校该门语言时,在自行学习即可。
高级特性
高级特性是大多数编程语言都支持且非常有用,然后相对难掌握的一些特性,包含但不限如下:并发(多线程 + 协程)、异常机制、泛型、序列化、反射、注解/元数据、编译期处理、运算符重载...
高级特性的掌握可以让我们的工程能力有一定的提升。
数据结构
我们前面说过,计算机的本质是数值计算,而在编程中,我们也经常听到一句话:程序 = 数据结构 + 算法。
可以知道,在上层代码编写中,基本上都是围绕数据进行操作,因此使用恰当的数据结构维护这些数据就显得特别重要。
常见的数据结构如下所示:
数组:连续内存,支持随机访问。
链表:插入与删除数据效率高。
栈:先进后出(FILO)。
队列:先进先出(FIFO)。
字符串:在高级语言编程中,字符串的使用频率非常高,说是最常用的一种类型也不为过。字符串的常用操作包含但不限于如下:获取长度、字符串拼接、获取子串、字符串切片、字符串分割、字符串大小写转换、字符串模板...
树:一对多数据结构,主要关注:二叉树、排序二叉树...
哈希表:具备查找的数据结构,性能十分高效。
集合:不含重复元素的数据结构。
主要关注各个数据结构的特性及其增、删、改、查操作。
常用操作
编程时,经常会使用到的一些操作也要熟知,比如有:控制台输出、调试...
常用库/框架
熟悉一门编程语言,除了掌握编程语言的常见特性外,对该语言提供的一些内置库(比如:文件操作,时间处理...)的熟练使用,也是非常重要。除此之外,对一些常用第三方库或主流框架如果也能熟练使用,那么我觉得就可以算的上具备大型项目开发的工程能力了。
参考
- 计算机的本质是什么?逻辑?数学?
- 万物皆数,世界为何是数字组成的?
- 如何理解「万物皆数」?
- CPU的功能和组成
- 再谈编程范式—程序语言背后的思想
- 编程语言里的类型系统