高级语言中的短语和句子——表达式和语句

《高级语言中的短语和句子——表达式和语句》源站链接,阅读体验更佳!

《高级语言中的单词——5种类型的token》一文中,我们介绍了高级语言中的5种类型的token,并指出token是一门高级语言中最底层的语法结构,它们是不可再分的。知道了token就相当于知道了英文中的单词,在学习英文的时候,我们学习完单词之后下一步就是学习一些基本的语法规则,然后根据语法规则来构造更高级的英文结构——短语和句子。

同样的,我们在学习高级程序设计语言的时候,了解完基本的语言结构token之后,下一步就是学习语言的语法,然后使用特定的语法结构排列token来构成更加高级的语法结构,类似于英文中的短语和句子,高级程序设计语言中更高级的语言结构是表达式和语句。

除此之外,只有结构是没有意义的,计算机只能运行二进制的机器码,我们使用高级语言编写的代码最终也应该可以使用某些手段映射到机器码上才能最终被计算机运行,这就是语法存在的意义,语法本质上就是token的排列规则,只有符合规则的token序列才能被语言的编译器或者解释器识别成表达式和语句,最终表达特定的语义。

不同高级语言的语法规则可能是千差万别的,但是所有高级语言语法的目的都是让我们能够用token构建表达式和语句,最终描述我们的语义,而本系列的文章是介绍我对高级程序设计语言的看法,并不拘泥于某一门语言,所以这里我们不对语法进行过多的介绍,而是介绍一下我们在所有语言中都会碰到的表达式和语句这两个概念。

表达式和语句是不同的

在很多编程语言相关的书籍和文献中,表达式和语句这两个术语是突然出现的,而且在使用的时候也是混乱不堪,比如介绍C语言相关的书籍中经常会出现“赋值语句”或者“函数调用语句”这样的术语,其实准确的说,在C语言中赋值操作应该称为“赋值表达式”,而函数调用操作应该称作“函数调用表达式”,只不过大多数情况下它们都是作为“表达式语句”出现的。

为什么我要强调表达式和语句这两个术语呢?因为表达式和语句是不同的,弄清表达式和语句二者的区别,可以让我们在编写代码的时候更加清醒,同时在我们学习一门语言的基本特性的时候让我们更加快速弄明白一些事情。

比如在Kotlin语言中,if…else引导的是一个表达式,但是在Java语言中,if…else引导的是语句,如果我们很清楚表达式和语句二者的区别,那么对于Kotlin语言和Java语言的一些区别我们也可以非常容易弄明白了。

那表达式和语句二者之间到底有何区别和联系呢?最直接的区别就是,**每一个表达式都会产生值,而语句并不会。**知道了这一点之后,有些事情我们自认而然也就想明白了,比如,在程序想要使用一个值的上下文中,我们只能使用表达式,而不能使用语句。

实际上,表达式更加接近机器,几乎所有的表达式在编译之后都会变成具体的机器指令,它承载了程序计算的任务;那语句又有什么用呢?有一部分语句也会被编译成指令,比如一些语言中的分支、循环结构都是语句,而更多的语句是为编译器提供元数据信息的,语句为高级语言提供了更加高层的抽象能力,比如类型声明、代码结构组织等等。

接下来我们就来简单介绍一下表达式和语句这两种高级程序设计语言中都会存在的语言结构。

表达式

表达式是由运算符和其运算对象组成,最简单的表达式是一个单独的运算对象,以此为基础可以创建复杂的表达式

表达式有一个基本特征——每个表达式都会产生一个值。

根据表达式的复杂程度的不同,可以把表达式分为简单表达式和复合表达式:

  • 简单表达式

    简单表达式就是我们上文所说的,只有一个单独的运算对象,而不包含任何的运算符,这样的表达式是不可再分的,可以直接计算的。如下的C语言代码,每一行代表的都是一个简单表达式:

    3
    "Hello world"
    a //a是上文声明的一个int型的变量
    tom.age //tom是一个Person类型的结构,它有一个int类型的成员age
    

    我们可以看出,简单表达式其实可以分为两种——“字面值”和“变量”。对于字面值,其本身就代表了一个值,这没啥好说的;而对于“变量”,我们可以认为变量中存储了一个值,但是这个值是可以动态变化的,在后面我们会有一篇专门介绍变量的文章。

    综上所述,我们可以认为简单表达式就是一个单值,对其进行计算最多也就是一个简单的RHS1,如果其是一个字面值,有可能会被直接处理成低级代码中的立即数,直接存在于机器指令当中

  • 复合表达式

    和简单表达式相对应的,复合表达式就是由运算目标和运算符所构成的表达式,更准确地说,复合表达式是由简单表达式和运算符所构成的更复杂的表达式。

    对于复合表达式,会有一个或者多个子表达式,在整个表达式完成计算之前,其所有的子表达式都会完成计算,也就是说,复合表达式的计算过程中可能会产生一个或者多个中间结果,中间结果再和外层的操作符结合形成新的表达式,继续进行计算,最终得到一个唯一的单值。

表达式的计算

上文提到表达式有一个最重要的特征是:每个表达式都有一个值。表达式可以说是编程中最灵活的东西,比较极端的情况下,一个表达式就能完成一段逻辑(当然我们并不推荐这样的编码方式,毕竟程序不光要给计算机看,也要让人看);这种情况下,一个复合表达式可能是非常复杂的——它可以具有不止一个子表达式,且子表达式可能还会嵌套着子表达式。

我们要计算出一个复合表达式的值之前,它所有的子表达式都需要先计算完成,那这些子表达式的计算顺序是如何确定的呢?主要就是依靠操作符的优先级和结合性来一步一步将一个大的表达式转换成一个小的表达式,而最终,一个复合表达式会被简化成一个简单表达式,最终得到计算结果。

副作用和序列点

在一个复合表达式的计算过程中还会涉及副作用和序列点相关的约定,这一点在不同的语言中也会有所不同。

副作用是对数据存储区域或者是对文件的修改,其实副作用就代表了程序运行过程中的状态变化,副作用最典型的例子就是赋值。

从本质上看,副作用是我们程序执行效果的体现,在使用表达式的时候,我们的目的其实就是使用其副作用。但是副作用这个词好像并不是主要目的的意思。这是因为从语言的角度来讲,表达式的目的是计算值,‘副作用’是在表达式计算过程中自然发生的,所以才称之为’副作用’。

那么程序的副作用何时发生呢?尤其是在一个复杂的表达式中,计算其子表达式也有可能会发生副作用,这些副作用是以什么样的时序发生的呢?

这就牵扯到另一个概念——序列点。

**序列点是程序执行的点,在该点上,所有的副作用都在进入下一步之前发生。**一般来说,一条语句(下面会有对于语句的介绍)标记了一个序列点,意思是在一条语句中,赋值操作、递减、递增等具有副作用的操作的副作用必须在程序执行下一条语句之前发生。

在比较早期的语言中会强调序列点这个概念,比如C语言;但是比较新的语言中副作用都是同步发生的,意思是在计算一个表达式的时候,这个表达式的副作用会在计算下一个表达式之前就发生

如下Java代码:

public static void main(String[] args){
  int a = 1;
  System.out.println(a++ + a); // 3
}

上面的表达式a++ + a的计算结果是3,我们知道a++的计算结果是1,同时a++还会产生一个副作用——自增a,下一步我们又对a进行了RHS查询,发现其值已经变成了2,因为a++ +a的最终结果是3。我们发现,子表达式a++的副作用在其被计算之后就立马发生了。但是这在C语言中是不一定的,因为C语言标准中a++这个子表达式并不是一个序列点,C语言标准只能保证在整条语句结束之后,进入下一条语句之前,副作用会发生,所以在不同的C语言编译器实现中,表达式a++ +a的结果有可能是2,也有可能是3.

序列点其实很好把控,只要我们在平时编码的时候把握好副作用,尽量避免书写具有多个副作用的子表达式的复合语句,就可以很好地把控住序列点。

语句

语句是代码的基本构建块,其引导了代码的结构,划分了代码的层次,提供元信息供编译器或者解释器使用。其实我们的源代码都是由一条一条的语句所构成的,编译器或者解释器在进行解析的时候也是以语句为基本单位进行解析的。

和表达式一样,语句也可以分为简单语句和复合语句。

  • 简单语句

    我们上文提到,我们的代码其实是由一条一条的语句所构成的,每种语言都有自己的语句结束符,用来标识一条语句的结束,如下Java代码:

    int a = 1;
    a = 1+2;
    

    Java以;作为语句的结束符,以这个为线索,我们可以很容易分辨出上述的代码中有两条语句。

  • 复合语句

    我们的程序并不是从头到脚一路执行下去的,在程序的执行过程中我们会有流程控制,比如分支;而某一个分支中操作可能会非常复杂,这用一条语句肯定是无法解决的,而复合语句就是用来把多条语句组合成一个整体的。

    我们通常会在如下情况下把一系列的语句集合起来,形成一条复合语句

    • 流程控制

      在分支或者是循环结构中,某一个分支或者是循环体中的逻辑往往用一条语句是无法完成的,这个时候我们就需要一个复合语句来组织逻辑。

    • 函数体

      毫无疑问,一个函数也可能包含多条语句。

    • 出于控制作用域的目的

      我们讨论的语言都是基于词法作用域的,而词法作用域一般以复合语句(块)为单位进行组织,声明一个新的块就能创建一个新的词法作用域。

    • 各种声明等等

    每一门语言也都有自己组织复合语句的方式,比如Java、c、C++等语言中使用{}来组织复合语句,在Python中用缩进来组织复合语句。

    总的来说,复合语句的作用就是把一系列相关的代码组织成一个整体,是代码的结构和层次的体现。

表达式语句

看到这里我们可能也发现了,其实语句才是最高层的语法结构,高级语言的编译或者解释也是以语句为单位进行的,也就是说,表达式其实是语句下一级的语法结构,表达式是可以构成语句的。

我们不禁会产生这样一种感觉——似乎只有语句才能表达语义,而表达式只负责计算,然而,事实上却是,大多数时候一个表达式其实就具有完整的语义了。所以,这个时候就出现了这样一个概念——表达式语句,意思就是由一个表达式构成的语句。

表达式和表达式语句的区别就是表达式语句=表达式 + 语句分隔符。比如下面的Java代码:

int a;
a = 1 + 1;

上面的Java代码中,第二行的a=1+1是一个表达式,而a=1+1;则是一个语句了。

其实,我们的代码中80%的语句可能都是表达式语句,表达式语句的存在可能也是造成表达式和语句这两个术语使用混乱的原因

语句的合法性

在某些语言中,会对语句的合法性做出一些限制,比如:

  • 副作用

    我们上文提到,副作用是程序执行效果的体现,也就是说,如果一条语句执行完成之后不会产生任何的副作用,那么其实这条语句是没有任何意义的——如果把它从代码中移除,不会对程序的执行结果造成任何影响。

    比如Java中,如果一个表达式语句不会产生任何的副作用,Java编译器就会认为其不是一个合法的表达式语句,如下:

    image-20200830185653164

  • 可达性

    同样的,对于永远不可能执行的代码,也是没有任何意义的,这样的代码就是不可达的,而代码的解析是以语句为单位的,所以很多语言也会做一些语句的可达性分析,如果发行某些语句是不可达的,那么其就认为这些语句是不合法的,如下Java代码:

    image-20200830185906967

到现在为止,我们介绍了高级语言中的token、表达式和语句,这三者是高级语言流于表面的特性,所有的高级语言都是依靠这三者,来完成自身到机器代码的映射的。

在接下来的文章中,我会介绍其他的一些高级语言为我们提供的抽象能力,不同于token、表达式和语句,这些抽象特性在不同的语言中的表现形式可能是大相径庭的,但是它们的内核可能又殊途同归。

感谢你耐心读完。本人深知技术水平和表达能力有限,如果文中有什么地方不合理或者你有其他不同的思考和看法,欢迎随时和我进行讨论(laomst@163.com)。


  1. 对某个变量进行值查询,也就是使用某个变量的值,在后面介绍变量的文章中会详细解释这个概念 ↩︎

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

劳码识途

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值