C中的sequence point和相关的undefined behavior

序列点(sequence point)是C中的一个重要概念,这篇文章是关于我所理解的序列点以及一些相关的未定义行为(undefined behavior),在这几天查阅相关资料的过程中也加深了自己的理解,所以打算将这些都写在博客,不过必须说明的是我所讨论的范围只限于C99

什么是序列点

简单的说就是程序的执行序列中一个特定的点,在该点的之前所有的求值(evolution)的副作用(side effect)都已完成,且该点之后所有的求值副作用都尚未发生,原文请参考ISOIEC 9899-1999的5.1.2.3的第2条

何处产生序列点

这个在C99的标准的Annex C列出来了所有产生序列点的地方,我简单翻译一下:

  • 调用一个函数并在它的所有参数被求值之后

要注意的是参数分割的逗号不是逗号运算符,所以这些分隔符并不会产生序列点,在调用一个函数的时候,在求值它的所有实参之后,真正执行函数之前存在一个序列点,参考6.5.2.3的第10条

  • &&、||、?、,四个操作符的第一个操作数(operand)被求值之后

在以上这些操作符的第一个参数被求值完毕后将产生一个序列点

  • 一个完整的声明符(declarator)后面

  • 一个初始化表达式,表达式语句中的表达式,if,switch,while和do中的控制表达式,for语句中的三条表达式,return语句中的表达式

  • 库函数返回之前

  • 在格式化输入输出函数的转换说明符和其对应的行为关联之后

  • 在调用比较函数的前后,以及在任何调用比较函数和向其传递参数的行为之间也会产生一个序列点

序列点相关的未定义行为

在C99的AnnexJ.2 undefined behavior列表里面我找到两个显式提及序列点的未定义行为,关于第二个我的理解也还是相当模糊,本文仅讨论第一个,大意如下:
在两个序列点之间不能多次(多于1次)修改同一个对象,如果对象被修改那么在修改前的值仅可以读取用于决定最后的储存值
由此可以列出一堆导致未定义行为的代码
例如对以下例子都有

int foo[10],i=0;

那么

i=i++;

这个完整的表达式产生一个序列点,距离上一个序列点,i的值被多次修改故导致未定义行为

foo[i++]=i;

我们知道这个等式等价于foo+i++=i,因为C的标准没有规定等号两边的求值顺序(order of evaluation),所以并不能确定a[i++]中的读写和右边的读先后发生顺序故此导致未定义行为,foo[i]=i++同理
另外要说明的一个例子是

foo[i]=i;

这个并不会产生未定义行为的,虽然左边(right hand side)a[i]的i的读取和最后的储存值无关,只是决定了储存位置,但是注意到这个表达式中i并未被改变,第二句所说的是必须(1)对象被改变(2)对象被读取用于和最后储存值无关的操作,同时满足这两个才会导致未定义行为
再举两个很常见的典型未定义行为例子:

printf("%d %d",i++,i++);//两个序列点内多次修改i
printf("%d %d",i,i++);//求值顺序未定义导致未定义行为

结语

我认为国内高校的大多数编程入门课实在做的太差了,本来我就并不看好用C作为计算机科学的入门教学的语言,姑且不谈用C这个事,但我发现无论是老师那是教材,关于序列点,副作用(side effect),求值顺序这些非常重要的概念都没提及,另外关于运算符优先级(operator precedence)和结合性(associativity)都讲不清楚或者根本讲错了的书籍也是比比皆是,由此国内计算机教育环境可见一斑

查阅过程在SOF上收获绝大,真是个好东西啊。如果你对本博文有任何疑问,欢迎邮箱联系我进行讨论:)

references:

[1]ISO/IEC 9899-1999(E)
[2]Defined behaviour for expressions
[3]What does 'prior value shall be accessed only to determine the value to be stored' mean?
[4]Sequence point
[5]Order of evaluation
[6]C Operator Precedence