1 | int i = 0; |
i++ vs ++i
- i++ is called postfix increment. This means the value of i is passed on and then i is incremented.(后自增,先使用后自增,整个表达式的值为 i)
- ++i is called prefix increment. This means 1 is added to i and then that value is passed on.(前自增,先自增后使用,整个表达式的值为 i+1)
Side Effect(副作用)& Sequence Point(序列点)
以下概念主要针对 C++ 解释,但其它语言有着相似的涵义。
- Side Effect(副作用)
- C++ 标准中对副作用是这样定义的:
Accessing an object designated by a volatile glvalue, modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
- C++ (一个线程)在运行的时候有一个环境,这个环境只跟以下内容有关:
- 内存状态
- I/O
- 每当程序改变了以上两件东西,就被当做程序产生了副作用。在 C++ 中,以下行为被视作带有副作用:
- 访问一个被标注为
volatile
的对象 - 修改一个对象的值
- 调用函数库中的 I/O 函数
- 以及调用了任何包含上述行为的函数
- 访问一个被标注为
- 后面三个好理解,但是 为什么“访问一个被标注为
volatile
的对象”,也会被视作产生副作用呢?想进一步了解可以阅读C/C++常见修饰符(inline&static&const&extern&volatile)—— volatile
- C++ 标准中对副作用是这样定义的:
- Sequence Point(序列点)
- 序列点的出现,是为了防止编译器过度优化,使得程序产生错误的结果。有如下代码:
-
1
2
3
4int i = 1, j = 2;
cout << i++ << endl;
j = i;
cout << j << endl;- 这段程序的副作用就是:
- 使 i 的值加一
- 使 j 的值等于 i
- 输出 i
- 输出 j
- 为了效率,编译器可能会改变程序的执行顺序,变成如下的执行顺序:
- 这段程序的副作用就是:
-
1
2
3
4
5
6
7
8
9
10
11// 合并读取操作
load i to reg1
load j to reg2
// 合并I/O操作
output reg1, reg2
// 合并写入操作
reg2 = reg1 + 1
write reg2 to i
write reg2 to j- 很显然,这样做是不对的。序列点的出现就是为了解决这个,标准中如下定义:
At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.
序列点出现的时候所有在序列点之前的副作用都必须被完成,所有在序列点之后的副作用都还没有发生,序列点分隔了副作用。 - 标准中规定,序列点出现在下面几个地方:
- 分号处
- 未重载的逗号运算符,|| 运算符,&& 运算符中
- 三元 ?: 运算符
- 计算完函数所有参数之后,且在函数的第一条语句之前
- 函数的返回值已经被写入调用者之后,且在调用处之后的第一条语句被执行之前
- 在初始化完所有基类和成员对象之后
- 在 C++ 中,两个序列点之间的操作的顺序是 Unspecified Befavior,即标准中不指定,由编译器自己决定如何决策。这是因为 C/C++ 企图通过对语言进行较少(其实是适中)的约束,来使得编译器对语言有着更大的优化空间,以此来优化性能。
- 很显然,这样做是不对的。序列点的出现就是为了解决这个,标准中如下定义:
Well-defined meaning in Java
Stackoverflow: Why does this expression i+=i++ differs from Java and C?
Stackoverflow: Pre & post increment operator behavior in C, C++, Java, & C#
Java and C# evaluate expressions from left to right, and the side-effects are visible immediately.
Java evaluation order is from left to right and the operands are evaluated before the operation.
Java 语法对i = i++;
这样的表达式有着明确的执行定义 —— 从左到右先确定操作数,再按优先级顺序进行操作(主要是右操作数相关的操作)。
At run time, the expression is evaluated in one of two ways. If the left-hand operand expression is not an array access expression(即使左操作数存在数组访问,也是满足先左后右), then four steps are required:
率先按照从左到右的顺序对左右操作数进行 evaluation(计算),并保存下来。
- First, the left-hand operand is evaluated to produce a variable. If this evaluation completes abruptly(产生异常), then the assignment expression completes abruptly for the same reason; the right-hand operand is not evaluated and no assignment occurs.
- Otherwise, the value of the left-hand operand is saved and then the right-hand operand is evaluated. If this evaluation completes abruptly, then the assignment expression completes abruptly for the same reason and no assignment occurs.
- Otherwise, the saved value of the left-hand variable and the value of the right-hand operand are used to perform the binary operation indicated by the compound assignment operator. If this operation completes abruptly, then the assignment expression completes abruptly for the same reason and no assignment occurs.
- Otherwise, the result of the binary operation is converted to the type of the left-hand variable, subjected to value set conversion(数据类型转换) to the appropriate standard value set (not an extended-exponent value set), and the result of the conversion is stored into the variable.
- 基于上述的要求,整个代码的完整操作顺序如下:
-
1
2
3
4
5
6
7
8
9
10
11
12
13// 确定左操作数:此处 i=0
// 确定右操作数,(i++) 的值为 i:此处 i=0
load i to reg2
// 自增操作(++)是确定右操作数需要完成的操作
reg3 = reg2 + 1
write reg3 to i
// 赋值操作(可以被优化):此处 i=0
// reg4 = reg2
// write reg4 to i
write reg2 to i
// 此处 i=0
Undefined behavior in C/C++
In C/C++, the order of evaluation of subexpressions is unspecified, and modifying the same object twice without an intervening sequence point(序列点间) is undefined behavior.
C/C++ 语法对i = i++;
这样的表达式出于效率的考虑并没有给出明确的执行定义(只有保证操作满足优先级顺序,操作数没有确定的 evaluation 顺序),编译器无论怎么做都是合法的,但对程序语义来说会导致不确定的行为(Undefined Behavior)。
Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored. The requirements of this paragraph shall be met for each allowable ordering of the subexpressions of a full expression; otherwise the behavior is undefined.
- 因此,对于
i = i++;
这样的代码,i 的值被写入了两次,一次是 ++ 时,一次是被赋值时,这就出现了 Undefined Behavior,最终的执行顺序取决于编译器的优化,可能会像上面提到的在 Java 中按照 left-to-right 的顺序执行,也可能出现其它执行顺序(可以用=
是从右到左的结合性解释),比如: -
1
2
3
4
5
6
7
8
9
10
11
12
13// 确定右操作数,(i++) 的值为 i:此处 i=0
load i to reg2
// 自增操作(++)是确定右操作数需要完成的操作
reg3 = reg2 + 1
write reg3 to i
// 确定左操作数:此处 i=1
// 赋值操作(可以被优化):此处 i=1
// reg4 = reg2
// write reg4 to i
write reg2 to i
// 此处 i=0- 因为
=
操作符的这种结合性,编译器更多时候是生成上述执行顺序的代码,而不是 Java 语言那种。如下,是 x86-64 g++ 编译器对该部分代码生成的汇编指令:
- 因为
-
1
2
3
4
5
6
7
8# load i to reg2
400832: 8b 45 f8 mov -0x8(%rbp),%eax
# reg3 = reg2 + 1
400835: 8d 50 01 lea 0x1(%rax),%edx
# write reg3 to i, i=1 after
400838: 89 55 f8 mov %edx,-0x8(%rbp)
# write reg2 to i
40083b: 89 45 f8 mov %eax,-0x8(%rbp)
i += i++;
i += i++; $\Longleftrightarrow$ i = i + i++;
- Java
-
1
2
3
4
5
6
7
8
9
10
11
12
13// 确定左操作数:此处 i=0
load i to reg1
// 确定右操作数,(i++) 的值为 i:此处 i=0
load i to reg2
// 自增操作(++)是确定右操作数需要完成的操作
reg3 = reg2 + 1
write reg3 to i
// 赋值操作(可以被优化):此处 i=0
reg4 = reg1 + reg2
write reg4 to i
// 此处 i=0
- C/C++
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 确定右操作数,++ 优先级比 + 高
// right of +:(i++) 的值为 i,此处 i=0
load i to reg1
// 自增操作(++)是确定右操作数需要完成的操作
reg2 = reg1 + 1
write reg2 to i
// left of +:此处 i=1
load i to reg3
// 确定左操作数:此处 i=1
// 赋值操作(可以被优化):此处 i=1
reg4 = reg3 + reg1
write reg4 to i
// 此处 i=1- 注:上述这种执行顺序并不是唯一的执行顺序,但可以说是编译器最常采用的优化方案,在 g++ 上正是这种情形。