在前文的引入数据和数据类型的基础上,本文将逐步说明编程语言的数理逻辑。
在传统的计算机体系结构中,数理逻辑建立在二值逻辑的基础上。但在“通用编程语言技术”系列文章中,我并不打算讲解传统的二值逻辑体系下的数理逻辑。相反,我将通用编程语言技术建立在三值逻辑体系上。因为三值逻辑对二值逻辑进行了部分拓展。
本文的重点在于讲解“通用编程语言技术”中由数学原理向上层过度,这一部分也是在所有编程语言学习过程中令所有学生感到最无奈的一部分,因为在大学,根本不会讲授数学与编程的关系,在软件工程专业更不会讲授哲学与数学的关系。
这一点我深有感受。记得大二在一次上机实践课时,有一道经典问题。按照我的做法,先列了一个二元一次方程组,然后编写代码,而老师在我身后提醒我这不是数学题。
故而在本文开始之前,有一个重要的说明:即“所有的计算机问题都是数学问题。”
一、三值布尔逻辑
布尔逻辑是计算机体系结构的重要基础。三值布尔逻辑是对二值逻辑的拓展。
在二值逻辑中,有真、假逻辑。在三值逻辑中允许存在一个非真非假逻辑,英文中可以使用“unkown”来表示,我们姑且称为“空逻辑”(毕竟叫“不明”逻辑既冗长又难听)。
真、假逻辑之间的逻辑运算仍应当符合二值逻辑运算。空逻辑在运算过程中不影响逻辑结果。而在程序运行过程中,空逻辑应当对应异常、警告或者程序错误。注意此处的程序错误指的是数据运算中允许存在的原始性错误,而不是致使程序崩溃、中断的错误。
在软件中,三值逻辑的运算形式对于程序的运行逻辑影响并不是很大,最多是影响程序运行时的精准程度。反观硬件中,三值逻辑逻辑的影响则具体。至此,可能知识面较广的小伙伴会联想到量子计算机。
然而,本系列文章中认为,量子计算机可能仍然属于二进制计算机的范畴,即使运算形式采用三进制也仍未能突破其固有的局限性;更准确的说法是量子计算机是由二进制转向三进制的过度形态。(这里可能是个深渊,毕竟量子计算机现有的发展不容诋毁。)
作者个人认为:在所有的计算模型中,所有数据单元的运算必须是可监测的;运算方式或运算形式的变化不能影响随机监测的结果,且随机监测的结果应当在监测预算之内。
二、表达式
在计算机程序中,表达式是有限数量运算单位进行有效的算术逻辑或比较逻辑的语句。表达式不能、也不应该造成上下文环境的变化。
这里点名JavaScript,因为作者在“微信小程序”和“Vue课程”中遇到很多同学在回调函数中使用this时程序报错,而我给他们的建议时,遇到内部需要使用this的,尽量使用箭头函数而非匿名函数。箭头函数属于表达式,而匿名函数属于函数定义。
计算机程序中的表达式一般都会产生一个可接收的返回值。无论是否接受这个返回值,它都会存在,只是用户(程序员)无法直观判断这个返回值存储的位置。
另外,表达式也是数据处理过程中最基本的语句。表达式在原则上可以书写与程序的任何位置。但是为了方便简明扼要的体现程序流程,不同编程语言会有不同的约束。
1、算数表达式
算术表达式的书写应至少满足基本的数学运算规律,即算数运算符应当匹配数学中的运算符优先级。同时算术表达式的计算精度应当在可接受的范围内,不同的可接受范围适用于不同的程序设计场景。特别注意,算数表达式的算术逻辑必须是无条件严格的。
2、逻辑表达式
计算机程序中逻辑表达式应至少包含数学中二值逻辑。故而在C语言中,除逻辑与、逻辑或、逻辑非外,增加了符合机器特性的按位与、按位或、按位非。
表达式是对底层逻辑(数学原理)进行的最基本的封装。
三、函数与接口
函数同样可以在数学中找到原型。只是数学中的函数必然有一个返回值(或称为结果),显然这样类比是不合适的。
我更愿意这样理解:函数的思想是数学中代数表达式,而其来源无关紧要。
例如:
数学中比较常见的公式
(
a
+
b
)
(
a
−
b
)
=
a
2
−
b
2
(a+b)(a-b)=a^2-b^2
(a+b)(a−b)=a2−b2
这个公式可以理解成是函数设计的过程。以C语言为例(因为涉及精度的问题)
代码1:
double squareDiff(double num1, double num2) {
return pow(num1, 2) - pow(num2, 2);
}
代码2:
int squareDiff(int num1, int num2) {
return (num1 + num2) * ( num1 - num2);
}
/**
* pow()函数由C语言库<math.h>/C++库<cmath>提供,原型如下:
* float pow ( float base, float exp );
* float powf( float base, float exp ); (C++11 起)
* double pow ( double base, double exp ); (C语言)
* long double pow ( long double base, long double exp );
* long double powl( long double base, long double exp ); (C++11 起)
* float pow ( float base, int iexp ); (C++11 前)
* double pow ( double base, int iexp ); (C++11 前)
* long double pow ( long double base, int iexp ); (C++11 前)
* Promoted pow ( Arithmetic1 base, Arithmetic2 exp );
*/
各位小伙伴上面那个版本的代码更好呢?
倘若为了可读性,应当首推第一个版本。但是如果涉及运算精度和运行效率,则首推第二个。
下面进行分析:(首先要知道加减的效率高于乘除法和函数调用)。
函数的调用过程:
1、获取上下文,即函数名
2、跳转至函数所在地址
3、保护上下文环境,即将之前的调用位置保存在栈中
4、为局部数据开辟空间
5、执行函数体
6、将返回值保存在RAX寄存器中(可能有、可能没有)
7、释放局部数据的空间(开辟多少就释放多少)
8、预备恢复上下文环境,将保存在栈中的前一个调用位置取出。
9、恢复上下文环境,即跳转至前一个调用位置
注意:在大学实践课中,所言针对数据的“压栈”或“弹栈”的行为可能并不存在,根据gcc编译器编译所得汇编代码,只针对函数调用初保存环境变量时存在压栈行为、针对预备恢复上下文环境时存在弹栈行为。故而在上述过程中,没有使用“压栈”、“弹栈”等词汇。(可能在汇编语言中,push或pop指令的效率要低于mov指令。当然,这就属于编程语言的优化细节了。函数的调用行为本来就是人为规定的。可能习惯上仍说“压栈”“弹栈”之类的,我更喜欢“入栈”“出栈”的叫法。)
总之,无论上一段文字的争论如何,函数调用、传递参数及其局部数据都是在栈上进行处理的。
下面给出函数的定义:函数是对程序中一段有效表达式的封装,函数即是接口,函数的参数就是接口的输入,函数的返回值就是接口的输出。
注意:这里的接口不是面向对象中接口,更不是Java等某一具体的编程语言中的接口。
在本文中,可争议的地方较多,而理解时更需要各位小伙伴结合数学经验。加之个人思维特点,可能每个人的理解角度都不一样,所以在探讨相关问题时,希望能够和大家进行有效的交流,尽可能的求同存异。同时也希望我国量子计算工程能够再获突破。
至此,在通用编程语言技术中,机器特性以及相关底层细节已经能够完全被隐藏。核心基础章节至此也就结束了。