在最早的通用编程语言中,如FORTRAN,这种区别是非常清楚的。在FORTRAN中,一个语句是一个执行单位,是你做的一件事。它不被称为 "行 "的唯一原因是有时它跨越了很多行。一个表达式本身不能做任何事情,你必须把它分配给一个变量。在FORTRAN中表达式是一个错误,因为它不做任何事情。你必须对这个表达式做一些事情。
FORTRAN并没有我们今天所知的语法,这个语法是和Backus-Naur Form(BNF)一起发明的,是Algol-60定义的一部分。当时,语义上的区别("有一个值 "与 "做某事")被载入了语法中:一种是表达式,另一种是语句,解析器可以将它们区分开来。
后来语言的设计者模糊了这种区别:他们允许表达式做事情,也允许语句有一个值。C语言的设计者意识到,如果允许你评估一个表达式并丢弃其结果,就不会造成任何伤害。在C语言中,每一个语法表达式都可以通过在结尾处加一个分号而成为一个语句。
表达式和语句的这种模糊性出现在所有的C语言衍生语言中(C、C++、C#和JAVA),它们仍然有一些语句(如while),但几乎允许任何表达式作为语句使用。
这两个 "语法类别" 会导致重复劳动。例如,C语言有两种形式的条件,if语句和?:表达式。
有时,人们希望有这样的重复:例如,在标准的C语言中,只有语句可以声明一个新的局部变量,但这种能力非常有用,GNU C编译器提供了一个GNU扩展(语句表达式),使表达式也可以声明一个局部变量。
而其他语言的设计者不喜欢这种重复,他们很早就看到,如果表达式可以有副作用,也可以有值,那么语句和表达式之间的语法区别就不是那么有用了,所以他们把它去掉了。Haskell、Icon、Lisp和ML都是没有语句的语言,它们只有表达式。即使是类似结构的循环和条件形式也被认为是表达式,而且它们有值。
#include <IOStream>
int main() {
int x;
x = 2, 3;
std::cout << x << std::endl;
return 0;
}
结果:2
#include <iostream>
int f(){ return 2, 3; }
int main() {
int x;
x = f();
std::cout << x << std::endl;
return 0;
}
结果:3
分析如下:
x=2,3是一个表达式,表达式中逗号操作符“,”的优先级低于赋值运算符“=”。所以这个表达式语句相当于(x=2),3;
return 2, 3;是返回语句,return不属于表达式,只有2,3为表达式。逗号操作符是从左到右计算,所以2,3表达式结算结果是3。
FP,是John Backus创立的支持函数级编程范式的编程语言。它允许消去命名变量。这种语言是在Backus的1977年图灵奖获奖演讲论文《编程可以从冯诺依曼风格中解放出来吗?程序的函数式风格及其代数》中提出的。这篇论文点燃了对函数式语言研究的兴趣,最终导致了现代函数式语言,但不是Backus曾希望的函数级范式。
在他的这篇图灵奖论文中,Backus描述了FP风格与基于lambda演算的语言有着如何不同:
FP系统基于了对叫做泛函形式的一组固定的组合形式的利用。它们加上简单的定义,就是从现存函数建造新函数的唯一方式;它们不使用变量或替代规则,并且它们成为程序相关的代数的运算操作。FP系统的所有函数都是一种类型的:它们映射对象到对象之上并总是接受一个单一实际参数。
FP自身在学术界之外从未被大量使用。在1980年代,Backus创建了后继语言FL,它也保持为研究项目。
冯诺依曼风格是一种语句与表达式的编程风格,而函数式编程风格是建立在使用组合形式创建程序的基础上。
表达式(expression)是运算符和它们的操作数的序列,它指定一项计算。表达式的求值可以产生一个结果(比如 2+2 的求值产生结果 4),也可能产生副作用(比如i = 5; i++;的副作用i变成6)。
常见运算符 |
||||||
赋值 |
自增 |
算术 |
逻辑 |
比较 |
成员访问 |
其他 |
a = b |
++a |
+a |
!a |
a == b |
a[b] |
a(...) |
特殊运算符 |
||||||
static_cast 转换一个类型为另一相关类型 |
初等表达式:任何运算符的操作数都可以是其他的表达式或初等表达式。
初等表达式包括以下各项:
常量表达式:定义能在编译时求值的表达式。这种表达式能用做非类型模板实参、数组大小,并用于其他要求常量表达式的语境(上下文)。
不求值表达式:运算符 typeid、sizeof、noexcept 、 decltype (C++11 起)和requires 表达式 的操作数是不求值表达式,因为这些运算符仅查询其操作数的编译期性质。因此,std::size_t n = sizeof(std::cout << 42); 不进行控制台输出。不求值的运算数被当做完整表达式 (C++14 起),所以要符合表达式的一些要求。
弃值表达式:弃值表达式是仅用来实施其副作用的表达式。从这种表达式计算的值被舍弃。这样的表达式包括任何表达式语句的完整表达式,内建逗号运算符的左边的实参,以及转型到类型 void 的转型表达式的实参。运算符delete是弃值表达式,因其返回值是void类型。
求值顺序:求值任何表达式的任何部分,包括求值函数参数的顺序都是未说明的(除了一些例外)。编译器能以任何顺序求值任何操作数和其他子表达式,并且可以在再次求值同一表达式时选择另一顺序。
#include <cstdio>
int a() { return std::puts("a"); }
int b() { return std::puts("b"); }
int c() { return std::puts("c"); }
void z(int, int, int) {}
int main() {
z(a(), b(), c()); // 允许全部 6 种输出排列
return a() + b() + c(); // 允许全部 6 种输出排列
}
可能的输出:
b
c
a
c
a
b
优先级和结合性:优先级 指定包含多个运算符的表达式中的运算顺序。 结合性指定是否在包含多个具有相同优先级的运算符的表达式中,将操作数分组在其左侧或右侧的一个(a=b=c)。
结合性规定对于一元运算符是冗余的,只为完备而给出:一元前缀运算符始终从右到左结合(delete ++*p 为 delete(++(*p)))而一元后缀运算符始终从左到右结合(a[1][2]++ 为 ((a[1])[2])++)。
优先级和结合性是编译时概念,与求值顺序无关,后者是运行时概念。
优先级 |
运算符 |
描述 |
结合性 |
1 |
:: |
作用域解析 |
从左到右 |
2 |
a++ a-- |
后缀自增与自减 |
|
type() type{} |
函数风格转型 |
||
a() |
函数调用 |
||
a[] |
下标 |
||
. -> |
成员访问 |
||
3 |
++a --a |
前缀自增与自减 |
从右到左 |
+a -a |
一元加与减 |
||
! ~ |
逻辑非和逐位非 |
||
(type) |
C 风格转型 |
||
*a |
间接(解引用) |
||
&a |
取址 |
||
sizeof |
取大小[注 1] |
||
co_await |
await 表达式 (C++20) |
||
new new[] |
动态内存分配 |
||
delete delete[] |
动态内存分配 |
||
4 |
.* ->* |
成员指针 |
从左到右 |
5 |
a*b a/b a%b |
乘法、除法与余数 |
|
6 |
a+b a-b |
加法与减法 |
|
7 |
<< >> |
逐位左移与右移 |
|
8 |
<=> |
三路比较运算符(C++20 起) |
|
9 |
< <= |
分别为 < 与 ≤ 的关系运算符 |
|
> >= |
分别为 > 与 ≥ 的关系运算符 |
||
10 |
== != |
分别为 = 与 ≠ 的相等性运算符 |
|
11 |
a&b |
逐位与 |
|
12 |
^ |
逐位异或(互斥或) |
|
13 |
| |
逐位或(可兼或) |
|
14 |
&& |
逻辑与 |
|
15 |
|| |
逻辑或 |
|
16 |
a?b:c |
三元条件[注 2] |
从右到左 |
throw |
throw 运算符 |
||
co_yield |
yield 表达式 (C++20) |
||
= |
直接赋值(C++ 类默认提供) |
||
+= -= |
以和及差复合赋值 |
||
*= /= %= |
以积、商及余数复合赋值 |
||
<<= >>= |
以逐位左移及右移复合赋值 |
||
&= ^= |= |
以逐位与、异或及或复合赋值 |
||
17 |
, |
逗号 |
从左到右 |
语句(statement)是依序执行的 C++ 程序片段。任何函数体都是语句的序列。
类型: