C 语言中,术语副作用(side effect)是指对数据对象或者文件的修改。例如,以下语句 var = 99;
的副作用是把 var 的值修改成 99。对表达式求值也可能产生副作用,例如:
se = 100
对这个表达式求值所产生的副作用就是 se 的值被修改成 100。
序列点(sequence point)是指程序运行中的一个特殊的时间点,在该点之前的所有副作用已经结束,并且后续的副作用还没发生。
C 语句结束标志——分号(;)是序列点。也就是说,C 语句中由赋值、自增或者自减等引起的副作用在分号之前必须结束。我们以后会说到一些包含序列点的运算符。任何完整表达式(full expression)运算结束的那个时间点也是序列点。所谓完整表达式,就是说这个表达式不是子表达式。而所谓的子表达式,则是指表达式中的表达式。例如:
f = ++e % 3
这整个表达式就是一个完整表达式。这个表达式中的 ++e、3 和 ++e % 3 都是它的子表达式。
有了序列点的概念,我们下面来分析一下一个很常见的错误:
int x = 1, y;
y = x++ + x++;
这里 y = x++ + x++ 是完整表达式,而 x++ 是它的子表达式。这个完整表达式运算结束的那一点是一个序列点,int x = 1, y; 中的 ; 也是一个序列点。也就是说,x++ + x++ 位于两个序列点之间。标准规定,在两个序列点之间,一个对象所保存的值最多只能被修改一次。但是我们清楚可以看到,上面这个例子中,x 的值在两个序列点之间被修改了两次。这显然是错误的!这段代码在不同的编译器上编译可能会导致 y 的值有所不同。比较常见的结果是 y 的值最后被修改为 2 或者 3。在此,我不打算就这个问题作更深入的分析,各位只要记住这是错误的,别这么用就可以了。有兴趣的话,可以看看以下列出的相关资料。
C 语言标准对副作用和序列点的定义如下:
Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression may produce side effects.
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.
翻译如下:
访问易变对象,修改对象或文件,或者调用包含这些操作的函数都是副作用,它们都会改变执行环境的状态。计算表达式也会引起副作用。执行序列中某些特定的点被称为序列点。在序列点上,该点之前所有运算的副作用都应该结束,并且后继运算的副作用还没发生。
----------------------------------------------------------------------------------------------
让我们来看看下面的代码:
int i=7; printf(”%d\n”, i++ * i++);
你认为会返回什么?56?no。正确答案是返回 49?很多人会问为什么?难道不该打印出56吗?在ccfaq中有非常详尽的解释,根本原因在于c中的序列点。
请注意,尽管后缀自加和后缀自减操作符 ++ 和 — 在输出其旧值之后才会执行运算,但这里的“之后”常常被误解。没有任何保证确保自增或自减会在输出变量原值之后和对表达式的其它部分进行计算之前立即进行。也不能保证变量的更新会在表达式 “完成” (按照 ANSI C 的术语, 在下一个”序列点”之前) 之前的某个时刻进行。本例中, 编译器选择使用变量的旧值相乘以后再对二者进行自增运算。只有到达一个序列点之后,自增运算才能保证真正被执行。
包含多个不确定的副作用的代码的行为总是被认为未定义。(简单而言, “多个不确定副作用”是指在同一个表达式中使用导致同一对象修改两次或修改以后又被引用的自增,自减和赋值操作符的任何组合。这是一个粗略的定义。) 甚至都不要试图探究这些东西在你的编译器中是如何实现的 (这与许多 C 教科书上的弱智练习正好相反);正如 K&R 明智地指出,”如果你不知道它们在不同的机器上如何实现, 这样的无知可能恰恰会有助于保护你”。
那么,所谓的序列点是什么意思呢?
序列点是一个时间点(在整个表达式全部计算完毕之后或在 ||、 &&、 ? : 或逗号 运算符处, 或在函数调用之前), 此刻尘埃落定,所有的副作用都已确保结束。 ANSI/ISO C 标准这样描述:
在上一个和下一个序列点之间,一个对象所保存的值至多只能被表达式的计算修改一次。而且前一个值只能用于决定将要保存的值。
第二句话比较费解。它说在一个表达式中如果某个对象需要写入, 则在同一表达式中对该对象的访问应该只局限于直接用于计算将要写入的值。这条规则有效地限制了只有能确保在修改之前才访问变量的表达式为合法。
例如 i = i+1 合法,而 a[i] = i++ 则非法。为什么这样的代码:a[i] = i++; 不能工作?子表达式 i++ 有一个副作用 — 它会改变 i 的值 — 由于 i 在同一表达式的其它地方被引用,这会导致无定义的结果,无从判断该引用(左边的 a[i] 中)是旧值还是新值。那么,对于 a[i] = i++; 我们不知道 a[] 的哪一个分量会被改写,但 i 的确会增加 1,对吗?
不一定!如果一个表达式和程序变得未定义,则它的所有方面都会变成未定义。
为什么&& 和 || 运算符可以产生序列点呢?这些运算符在此处有一个特殊的例外:如果左边的子表达式决定最终结果 (即,真对于 || 和假对于 && ) ,则右边的子表达式不会计算。因此,从左至右的计算可以确保,对逗号表达式也是如此。而且,所有这些运算符 (包括 ? : ) 都会引入一个额外的内部序列点。
分享到:
相关推荐
(sequence point)是指程序运行中的一个特殊的时间点,在该点之前的所有副作用已经结束,并且后续的副作用还没发生,而两个序列点之间所有的表达式或代码执行顺序是未定义的。
本文开创性地分析了序列点在C语言表达式求值中的作用:序列点左边的操作数要先于其右边的操作数求值。讨论了逗号操作符,、逻辑与操作符&&、逻辑或操作符||和条件操作符?:的问号处需要序列点的原因。举例分析了序列点...
给定一个序列,求和为k的最长子序列和最长子序列长度,c语言帮忙提供一个算法 输入一个序列,如: 1,5,6,4,2,3 输入一个整数,如:7 输出: 1 4 2 3 上述例子和为7的子序列有1,4,2 和 3,4 和 2,5 和 1,6 最长子...
序列点在C语言表达式求值中的作用.pdf
C语言中关键字volatile的作用,使用说明和例子
对于一个栈,已知元素的进栈序列,判断一个由栈中所有元素组成的排列是否是可能的出栈序列。
给定一个长度为N(不大于500)的正整数序列,请将其中的所有奇数取出,并按增序输出
C语言编写斐波那契序列(数据结构),用vc写的
C语言斐波那契序列三种方法 #include #include #define enoughsize 100 //最大队列长度 typedef struct { int *base; //初始化的动态分配存储空间 int front; //头指针,若队列不空,指向队列头元素 int rear; //...
C语言中return的作用.pdf
给定一个整数栈,已知进栈序列,判断某整数序列是否为合法的出栈序列。 【输入形式】 从标准输入读取一个整数N(3≤N≤10),表示进栈序列为1 2 3 …… N。再读取N个整数,表示待测整数序列。 【输出...
C语言中的5个段作用[定义].pdf
最长公共子序列.(C语言编写) 算法最长公共子序列.(C语言编写) 算法最长公共子序列.(C语言编写) 算法
单片机C语言中code的作用 单片机C语言中code的作用
伪随机序列的产生c语言和verilog实现,原理和特性里面都有所介绍,可以直接方便的参考,代码本人撰写完成;没任何问题!
C语言的最长公共总序列代码 C语言的最长公共总序列代码 C语言的最长公共总序列代码
简单的C语言点对点聊天程序.供初学者学习参考。
大数据场景下序列化和反序列化技术,谷歌提供技术 protobuf-jetbrains-plugin-0.13.0.zip
这是我这两天才完成的原创代码,就是比较经典的求一个随机序列的最长递增子序列问题。例如: n=5 随机序列为 5 1 4 2 3,正确输出为1 2 3,即长度为3的递增子序列。里面附带实验详细说明,感兴趣的可以下来参考。 ...
c语言实现64点FFT源代码,经上机检验,绝对可行。也可以计算IDFT。