大多数程序员对规则的第一部分感到愉快:对于序列点之间的一片内存,只能写入一次。 很多人不理解第二条限制。那条限制确保了这一点:如果在序列点之间内存既被读取又被写入, 那么读取将在写入开始之前完成。在C++(www.cppentry.com)未指定子表达式求值顺序的上下文中,这是确保安全 的唯一准则。 现在你应该明白为何“知道源代码中的序列点在哪里”很重要了。下面是一个完整的清单: 完全表达式(full expression):在完全表达式求值的结尾存在一个序列点。完全表达式的 值不被直接作为求解某个其他表达式的一部分使用。举个例子,在上面的函数bar()中,lhs * rhs是一个完全表达式。然而,在函数foo()中,lhs * rhs不是一个完全表达式,因为在计算对 result的赋值中使用了它的结果。 注意,一条语句可以包含不止一个完全表达式。例如,语句
包含两个完全表达式:a < b和i++。 函数调用(function call):两个序列点保护一个函数调用。在所有实参赋值之后立即有一 个序列点,从而函数体可以在“所有初始化形参的副作用已经完成”的假定之上继续执行。第 二个序列点位于返回点,它确保任何提供返回值的副作用在调用该函数的代码恢复执行前已经 完成。 很少有程序员编写的代码存在返回序列点的问题,不过入口的序列点有时会被误解。比 方说,
可能直到你更仔细地检查对bar()的调用之前,这段代码看上去都很健康。为了调用bar(), 必须计算两个表达式i和i++。它们不是完全表达式,因为结果将被作为实参用于初始化bar()的 形参。这意味着在i和i++的计算之间不存在序列点。然而,对第一个实参的求解要求读取存储 于由i指定的对象中的值,但我们无法确定什么东西将被写入i(考虑求解第二个实参期间递增i 的副作用),换句话说,我们破坏了关于在序列点之间读写同一个对象的规则。这就意味着我 们身处未定义行为的境地,任何事情都有可能发生。在实践中,这个问题通常表现为两种求值 顺序导致第一个实参具有不同的值。这种行为不应该麻痹你的警惕心,因为这并不是未指定的 行为,而是未定义的行为。请弄清楚这两种行为的区别,早晚有一天会派上用场。 逗号操作符(comma operator):在某些上下文中,逗号(,)仅仅是分隔一串项目(items) 的标点。在其他上下文中,则是C++(www.cppentry.com)序列操作符(sequence operator)。知道它到底在发挥什么作用,很大程度上是经验问题。不幸的是,这对你的程序可能有“深远的影响”。当逗号是序 列操作符时,它会向代码中注入一个序列点,意味着逗号左边的表达式是完全求值的(fully eva luated),在触及右边的表达式之前所有副作用均已完成。 更糟糕的是,如果逗号操作符的至少一个操作数是用户自定义类型,则C++(www.cppentry.com)允许程序员重 新定义它。在那些环境中,它不再是序列操作符,左右操作数(表达式)可以按任意顺序求解。 因此,或许最好假定逗号不是序列操作符,除非你确切地知道它确实是。 条件操作符(conditional operator):在条件操作符的左操作数的求值,和其他两个操作数 中被选中的那一个的求值之间,存在一个序列点。因此
就第一条语句来说,这段代码没问题。我无法想像怎么可能写出这样的语句,但其中不存 在未定义行为。由于在 处的序列点的保护,第一次对value的读取不会受到稍后对同一存储区 的写入的影响。 || 和&& 操作符:对于这些操作符的内建版本而言,左操作数(表达式)的求值后面都存 在一个序列点。这意味着左操作数是完全求解的,所有结果的副作用在计算右操作数之前已经 完成。注意,只在有必要确定其结果的时候才会计算右操作数。这意味着求解右操作数的任何 副作用依赖于左操作数的值。
【责任编辑: 雪花 TEL:(010)68476606-8007】
|