序列点
c语言中的关于序列点的知识点
副作用
一般来说,副作用是改变程序执行环境状态的操作。副作用包括修改对象、修改文件、访问易失性对象或调用执行任何这些操作的函数。
和副作用不同的一个作用就是产生值,而不是修改状态,比如说 加减 运算符是只产生值,而不会产生副作用的。
比较常见的产生副作用的符号:
- 一元运算符中只有
++
和--
这两个运算符会产生副作用。 -x和!b都没有副作用 - 赋值运算符
=
会产生副作用。 - 函数调用会产生副作用。
- 算术赋值运算符(例如+=和-=)既产生值又产生副作用,但其他二元运算符(例如+和–)仅产生值。它们没有副作用。
序列点
在 C/C++ 中,需要有序列点。这个序列点的作用就是用来帮助编译器确定各个表达式的求值顺序的。
在 C/C++ 中,一下的地方会被添加一个序列点:
&&(逻辑与)、||(逻辑或)、逗号运算符的左操作数与右操作数求值之间(前两者是短路求值的一部分)。例如,表达式*p++ != 0 && q++ != 0,子表达式p++ != 0的副作用都会在试图访问q之前完成。
三元条件运算符的第一个操作数之后,第二或第三操作数之前。例如,表达式a = (p++) ? (p++) : 0在第一个p++之后存在顺序点,因而在第二个p++求值之前已经做完一次自增。
完整表达式结束处。包括表达式语句(如赋值a = b;),返回语句,if、switch、while、do-while语句的控制表达式,for语句的3个表达式。
函数调用时的函数入口点。函数实参的求值顺序未指定,但顺序点意味着这些实参求值的副作用在进入函数时都已经完成。表达式f(i++) + g(j++) + h(k++),调用f(), g(), h()的顺序未指定,i, j, k的自增顺序也未指定。函数调用f(a,b,c)的实参列表不是逗号运算符,a, b, c的求值顺序未指定。
函数返回时,在返回值已经复制到调用上下文。(仅C++标准指出这一顺序点[6])
初始化的结束。例如,声明int a = 5;中的对5求值之后。
初始化列表的以逗号分割的各个初始化值,严格遵照从左至右求值。例如:int a[3] = {i++,j–,foo(101)};注意,此处不是逗号运算符。(从C++11标准指出这一顺序点)
在声明序列的每个声明(declarator)之间。例如,int x = a++, y = a++的两次a++求值之间。[7]注意,此例不是逗号运算符。
在上面提到的序列点中,在两个序列点之间的对象不可以出现不确定的顺序。其实就是同一个对象的求值的顺序必须要是确定的。
在两个序列点之间对于同一个对象,只可以有一次副作用
示例
1 |
|
这个例子是一个未定义的表达式,这个例子的序列点在 a=41
后面有一个,在 printf
后面有一个。所以就导致了 a++
和 printf
的执行顺序未定义。
1 |
|
上面这个例子是正确的,而不是未定义的表达式。因为 if
语句结束的后面是有一个序列点的,所以 a++
的自增操作在 if
语句的条件判断之前完成。然后在 printf
后面又有一个序列点,所以这个 a
的求值的顺序是确定的。
1 |
|
上面这个例子是错误的,是一个具有未定义行为的表达式。在 a = a++
这里只有一个序列点,所以到底是先进行 =
还是先 ++
这是不确定的。所以导致了这个表达式的行为未定义。