百分征集: 可以代替 i++ + ++i 的选择题

AlanWillaims 2009-11-17 12:19:31
本帖继续昨天的一个讨论:

http://topic.csdn.net/u/20091116/23/b2122ee7-b4d2-4584-8024-cda678698f39.html?29272

昨晚我又考虑了一下规范, 发现似乎涉及到对一个对象求值和副作用顺序的表达式, 如果它看起来怪怪的那基本上肯定是未定义的了. 不过我目前还不太确定这一点. 现在我希望在这里征集一些真正适合考人的题目. 要求:

1. 按规范不是未定义或未指定的;
2. 看起来有迷惑性, 容易答错;
3. 难点在于求值与副作用的顺序, 而不是算符的优先级, 或者函数参数求值之类的;
4. 最好是一些在 C 和 C++ 中定义不同的表达式, 例如在 C++ 中有定义而 C 中未定义.
5. 不是特别长.

希望能够有三种以上的思路. 放出100分求教. 谢谢!
...全文
399 38 打赏 收藏 转发到动态 举报
写回复
用AI写文章
38 条回复
切换为时间正序
请发表友善的回复…
发表回复
kxalpah 2009-12-08
  • 打赏
  • 举报
回复
lizzoe 2009-11-18
  • 打赏
  • 举报
回复
帮顶,UP
Taiyangchen 2009-11-18
  • 打赏
  • 举报
回复
[Quote=引用 3 楼 lgccaa 的回复:]
什么叫“真正适合考人的题目”,中国人的“考”,从出生到入土,都成严格的判定制度啦

要招人不是可以从几道题就看出来的,要教人更不应该给几条题就评定学的怎么样的
[/Quote]
顶!
Learninghappy 2009-11-17
  • 打赏
  • 举报
回复
不错啊
很好
我顶
ChRedfield 2009-11-17
  • 打赏
  • 举报
回复
[Quote=引用 13 楼 rlxtime 的回复:]
引用 2 楼 big_cucumber 的回复:
谁写(i++)+(i++)+(i++)类似这样的代码,就操他娘


对! 再拖出去打死
[/Quote]
jackyjkchen 2009-11-17
  • 打赏
  • 举报
回复
建议出代码安全(溢出、非法访问)这方面的题目,运算符优先级整个去掉
amossavez 2009-11-17
  • 打赏
  • 举报
回复
3. 序列点对副作用的限制

C99和C++2003都有类似的如下规定
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的新值是旧值+1)。特别的,标准
要求任意可能的执行顺序都必须满足该条件,否则代码将是undefined behavior

之所以序列点会对副作用有如此的限制,就是因为C/C++标准没有规定子表达式求
值以及副作用发生之间的顺序,例如
extern int i, a[];
extern int foo(int, int);
i = ++i + 1; // 该表达式对i所做的两次修改都需要写回对象,i的最终值取决
// 于到底哪次写回最后发生,如果赋值动作最后写回,则i的值
// 是i的旧值加2,如果++i动作最后写回,则i的值是旧值加1,
// 因此该表达式的行为是undefined
a[i++] = i; // 如果=左边的表达式先求值并且i++的副作用被完成,则右边的
// 值是i的旧值加1,如果i++的副作用最后完成,则右边的值是i
// 的旧值,这也导致了不确定的结果,因此该表达式的行为将是
// undefined
foo(foo(0, i++), i++); // 对于函数调用而言,标准没有规定函数参数的求值
// 顺序,但是标准规定所有参数求值完毕进入函数体
// 执行之前有一个序列点,因此这个表达式有两种执
// 行方式,一种是先求值外层foo调用的i++然后求值
// foo(0, i++),然后进入到foo(0, i++)执行,这之
// 前有个序列点,这种执行方式还是在两个相邻序列
// 点之间修改了i两次,undefined
// 另一种执行方式是先求值foo(0, i++),由于这里
// 有一个序列点,随后的第二个i++求值是在新序列
// 点之后,因此不算是两个相邻的序列点之间修改i
// 两次
// 但是,前面已经指出标准规定任意可能的执行路径
// 都必须满足条件才是定义好的行为,这种代码仍然
// 是undefined

前面我提到在一个序列点上程序的状态不一定是确定的,原因就在于相邻的两个序
列点之间可能会发生多个副作用,这些副作用的发生顺序是未指定的,如果多于一
个的副作用用于修改同一个对象,例如示例代码i = ++i + 1;,则程序的结果是依
赖于副作用发生顺序的;另外,如果某个表达式既修改了某个对象又需要读取该对
象的值,且读取对象的值并不用于确定对象新值,则读取和修改两个动作的先后顺
序也会导致程序的状态不能唯一确定
所幸的是,“在相邻的两个序列点之间,一个对象只允许被修改一次,而且如果一
个对象被修改则在这两个序列点之间只能为了确定该对象的新值而读一次”这一强
制规定保证了符合要求的程序在任何一个序列点位置上其状态都可以确定下来

注,由于对于UDT类型存在operator重载,函数语义会提供新的序列点,因此某些
对于built-in类型是undefined behavior的表达式对于UDT确可能是良好定义的,
例如
i = i++; // 如果i是built-in类型对象,则该表达式在两个相邻的序列点之间对
// i修改了两次,undefined
// 如果i是UDT类型该表达式也许是i.operator=(i.operator++(int)),
// 函数参数求值完毕后会有一个序列点,因此该表达式并没有在两个
// 相邻的序列点之间修改i两次,OK

由此可见,常见的问题如printf("%d, %d", i++, i++)这种写法是错误的,这类问
题作为笔试题或者面试题是没有任何意义的
类似的问题同样发生在cout << i++ << i++这种写法上,如果overload resolution
选择成员函数operator<<,则等价于(cout.operator<<(i++)).operator<<(i++),
否则等价于operator<<(operator<<(cout, i++), i++),如果i是built-in类型对
象,这种写法跟foo(foo(0, i++), i++)的问题一致,都是未定义行为,因为存在
某条执行路径使得i会在两个相邻的序列点之间被修改两次;如果i是UDT则该写法
是良好定义的,跟i = i++一样,但是这种写法也是不推荐的,因为标准对于函数
参数的求值顺序是unspecified,因此哪个i++先计算是不能预计的,这仍旧会带来
移植性的问题,这种写法应该避免
rlxtime 2009-11-17
  • 打赏
  • 举报
回复
[Quote=引用 2 楼 big_cucumber 的回复:]
谁写(i++)+(i++)+(i++)类似这样的代码,就操他娘
[/Quote]

对! 再拖出去打死
xiaopoy 2009-11-17
  • 打赏
  • 举报
回复
i+ ((i+= 1)+= 1)
amossavez 2009-11-17
  • 打赏
  • 举报
回复
2. 表达式求值(evaluation of expressions)与副作用发生的相互顺序

C99和C++2003都规定
Except where noted, the order of evaluation of operands of individual
operators and subexpressions of individual expressions, and the order
in which side effects take place, is unspecified.

也就是说,C/C++都指出一般情况下在表达式求值过程中的操作数求值顺序以及副
作用发生顺序是未说明的(unspecified)。为什么C/C++不详细定义这些顺序呢?
原因是因为C/C++都是极端追求效率的语言,不规定这些顺序,是为了允许编译器
有更大的优化余地,例如
extern int *p;
extern int i;
*p = i++; // (1)
根据前述规定,在表达式(1)中到底是*p先被求值还是i++先被求值是由编译器决定
的;两次副作用(对*p赋值以及i++)发生的顺序是由编译器决定的;甚至连子表
达式i++的求值(就是初始时i的值)以及副作用(将i增加1)都不需要同步发生,
编译器可以先用初始时i的值(即子表达式i++的值)对*p赋值,然后再将i增加1,
这样就把子表达式i++的整个计算过程分成了两个不相邻的步骤。而且通常编译器
都是这么实现的,原因在于i++的求值过程同*p = i++是有区别的,对于单独的表
达式i++,执行顺序一般是(假设不考虑inc指令):先将i加载到某个寄存器A(如
果i是寄存器变量则此步骤可以跳过)、将寄存器A的值加1、将寄存器A的新值写回
i的地址;对于*p = i++,如果要先完整的计算子表达式i++,由于i++表达式的值
是i的旧值,因此还需要一个额外的寄存器B以及一条额外的指令来辅助*p = i++的
执行,但是如果我们先将加载到A的值写回到*p,然后再执行对i增加1的指令,则
只需要一个寄存器即可,这种做法在很多平台都有重要意义,因为寄存器的数目往
往是有限的,特别是假如有人写出如下的语句
extern int i, j, k, x;
x = (i++) + (j++) + (k++);
编译器可以先计算(i++) + (j++) + (k++)的值,然后再对i、j、k各自加1,最后
将i、j、k、x写回内存,这比每次完整的执行完++语义效率要高
amossavez 2009-11-17
  • 打赏
  • 举报
回复
. 什么是序列点(sequence points)

C99和C++2003对序列点的定义相同
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.

中文表述为,序列点是一些被特别规定的位置,要求在该位置前的evaluations所
包含的一切副作用在此处均已完成,而在该位置之后的evaluations所包含的任何
副作用都还没有开始

例如C/C++都规定完整表达式(full-expression)后有一个序列点
extern int i, j;
i = 0;
j = i;
上面的代码中i = 0以及j = i都是一个完整表达式,;说明了表达式的结束,因此
在;处有一个序列点,按照序列点的定义,要求在i = 0之后j = i之前的那个序列
点上对i = 0的求值以及副作用全部结束(0被写入i中),而j = i的任何副作用都
还没有开始。由于j = i的副作用是把i的值赋给j,而i = 0的副作用是把i赋值为
0,如果i = 0的副作用发生在j = i之后,就会导致赋值后j的值是i的旧值,这显
然是不对的

由序列点以及副作用的定义很容易看出,在一个序列点上,所有可能影响程序状态
的动作均已完成,那这样能否推断出在一个序列点上一个程序的状态应该是确定的
呢?!答案是不一定,这取决于我们代码的写法。但是,如果在一个序列点上程序
的状态不能被确定,那么标准规定这样的程序是undefined behavior,稍后会解释
这个问题
amossavez 2009-11-17
  • 打赏
  • 举报
回复
什么是副作用(side effects)

C99定义如下
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.

C++2003定义如下
Accessing an object designated by a volatile lvalue, 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.

可以看出C99和C++2003对副作用的定义基本类似,一个程序可以看作一个状态机,在
任意一个时刻程序的状态包含了它的所有对象内容以及它的所有文件内容(标准输入
输出也是文件),副作用会导致状态的跳转

一个变量一旦被声明为volatile-qualified类型,则表示该变量的值可能会被程序之
外的事件改变,每次读取出来的值只在读取那一刻有效,之后如果再用到该变量的值
必须重新读取,不能沿用上一次的值,因此读取volatile-qualified类型的变量也被
认为是有副作用,而不仅仅是改写
注,一般不认为程序的状态包含了CPU寄存器的内容,除非该寄存器代表了一个变量,
例如
void foo() {
register int i = 0; // 变量i被直接放入寄存器中,本文中被称为寄存器变量
// 注,register只是一个建议,不一定确实放入寄存器中
// 而且没有register关键字的auto变量也可能放入寄存器
// 这里只是用来示例,假设i确实放入了寄存器中
i = 1; // 寄存器内容改变,对应了程序状态的改变,该语句有副作用
i + 1; // 编译时该语句一般有警告:“warning: expression has no effect”
// CPU如果执行这个语句,也肯定会改变某个寄存器的值,但是程序状态
// 并未改变,除了代表i的寄存器,程序状态不包含其他寄存器的内容,
// 因此该语句没有任何副作用
}
特别的,C99和C++2003都指出,no effect的expression允许不被执行
An actual implementation need not evaluate part of an expression if it
can deduce that its value is not used and that no needed side effects
are produced (including any caused by calling a function or accessing
a volatile object).
honghu069 2009-11-17
  • 打赏
  • 举报
回复
考试和实践完全是两码事....
平时根本不会这么做的,不用太抠这些东西
npuhuxl 2009-11-17
  • 打赏
  • 举报
回复
帮顶
forster 2009-11-17
  • 打赏
  • 举报
回复
帮腚
fengsha1986923 2009-11-17
  • 打赏
  • 举报
回复
帮着顶下吧``
AlanWillaims 2009-11-17
  • 打赏
  • 举报
回复
3楼说的很对. 但是学校和招人的人不那么想. 他们似乎以为这种题能考验考生的经验综合能力. 我怀疑他们是不是真的知道 C 和 C++ 都是有规范的.

lgccaa 2009-11-17
  • 打赏
  • 举报
回复
什么叫“真正适合考人的题目”,中国人的“考”,从出生到入土,都成严格的判定制度啦

要招人不是可以从几道题就看出来的,要教人更不应该给几条题就评定学的怎么样的
big_cucumber 2009-11-17
  • 打赏
  • 举报
回复
谁写(i++)+(i++)+(i++)类似这样的代码,就操他娘
big_cucumber 2009-11-17
  • 打赏
  • 举报
回复
帮顶
加载更多回复(18)

69,335

社区成员

发帖
与我相关
我的任务
社区描述
C语言相关问题讨论
社区管理员
  • C语言
  • 花神庙码农
  • 架构师李肯
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧