java未定义行为,为什么这些构造使用前后递增的未定义行为?

这篇博客深入探讨了C语言中关于'i++', 'i+=i++'等运算符的未定义行为和潜在问题。它解释了编译器可能如何处理这类表达式,以及为何在多线程或并发环境中可能导致意外结果。作者通过实例和标准引用展示了标准规定的不确定性,并提供了反汇编代码分析来揭示实际执行过程。
摘要由CSDN通过智能技术生成

#include

int main(void)

{

int i = 0;

i = i++ + ++i;

printf("%d\n", i); // 3

i = 1;

i = (i++);

printf("%d\n", i); // 2 Should be 1, no ?

volatile int u = 0;

u = u++ + ++u;

printf("%d\n", u); // 1

u = 1;

u = (u++);

printf("%d\n", u); // 2 Should also be one, no ?

register int v = 0;

v = v++ + ++v;

printf("%d\n", v); // 3 (Should be the same as u ?)

int w = 0;

printf("%d %d\n", ++w, w); // shouldn't this print 1 1

int x[2] = { 5, 8 }, y = 0;

x[y] = y ++;

printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?

}

#1楼

尽管不太可能有任何编译器和处理器实际执行此操作,但是在C标准下,对于编译器而言,使用以下序列实现“ i ++”是合法的:

In a single operation, read `i` and lock it to prevent access until further notice

Compute (1+read_value)

In a single operation, unlock `i` and store the computed value

虽然我不认为任何处理器都支持硬件来有效地完成这样的事情,但人们可以轻松想象这种行为会使多线程代码更容易的情况(例如,如果两个线程尝试执行上述操作,则可以保证这种情况)序列同时, i将增加两个),并且将来的处理器可能会提供类似的功能并不是完全不可想象的。

如果编译器按照上述指示编写i++ (根据标准合法)并在整个表达式求值过程中散布以上指令(也是合法的),并且没有注意到其他指令之一碰巧访问了i ,编译器可能会(并且合法)生成一系列死锁的指令。 可以肯定的是,在两个地方都使用相同变量i的情况下,但是如果例程接受对两个指针p和q引用,并使用(*p)和(*q) ,则编译器几乎可以检测到问题。在上面的表达式中(而不是使用i两次),不需要编译器识别或避免如果为p和q传递了相同对象的地址时将发生死锁。

#2楼

该行为无法真正解释,因为它同时调用了未指定的行为和未定义的行为 ,因此我们无法对此代码做出任何一般性的预测,尽管如果您阅读Olve Maudal的著作(例如Deep C和Unspecified and Undefined),有时可以在特定情况下使用特定的编译器和环境进行猜测,但是请不要在生产环境附近这样做。

因此,继续进行未指定的行为 ,在c99标准草案第6.5节第3段中说( 强调我的 ):

语法表示操作符和操作数的分组。74)除非稍后指定(对于函数调用(),&&,||,?:和逗号运算符), 子表达式的求值顺序和哪种副作用都未指定。

因此,当我们有这样的一行时:

i = i++ + ++i;

我们不知道会先评估i++还是++i 。 这主要是为编译器提供更好的优化选项 。

由于程序在序列点之间多次修改变量( i , u等),因此我们在这里也具有未定义的行为 。 从标准草案第6.5节第2段( 重点是我的 ):

在上一个序列点与下一个序列点之间,对象的存储值最多只能通过对表达式的求值来修改 。 此外, 在先值仅应被读取以确定要存储的值 。

它引用了以下未定义的代码示例:

i = ++i + 1;

a[i++] = i;

在所有这些示例中,代码都试图在同一序列点中多次修改对象,这些对象将以;结尾; 在以下每种情况下:

i = i++ + ++i;

^ ^ ^

i = (i++);

^ ^

u = u++ + ++u;

^ ^ ^

u = (u++);

^ ^

v = v++ + ++v;

^ ^ ^

未指定的行为在c99标准草案的 3.4.4节中定义为:

使用未指定的值,或本国际标准提供两种或两种以上可能性且在任何情况下均不对所选内容施加任何其他要求的其他行为

未定义的行为在3.4.3节中定义为:

在使用非便携式或错误程序构造或错误数据时的行为,对此国际标准不施加任何要求

并指出:

可能的不确定行为范围从完全忽略具有无法预测结果的情况到在翻译或程序执行过程中以环境特征记录的方式表现(带有或不带有诊断消息)到终止翻译或执行(带有发布)诊断消息)。

#3楼

C标准说,一个变量最多只能在两个序列点之间分配一次。 例如,分号是一个序列点。

因此,每个形式的语句:

i = i++;

i = i++ + ++i;

以此类推。 该标准还规定行为是不确定的,不是不确定的。 一些编译器确实会检测到这些并产生一些结果,但这不是每个标准的结果。

但是,可以在两个序列点之间增加两个不同的变量。

while(*src++ = *dst++);

上面是复制/分析字符串时的常见编码实践。

#4楼

如果您很想知道代码的确切程度,那么只需编译和反汇编您的代码即可。

这是我在机器上得到的,以及我认为正在发生的事情:

$ cat evil.c

void evil(){

int i = 0;

i+= i++ + ++i;

}

$ gcc evil.c -c -o evil.bin

$ gdb evil.bin

(gdb) disassemble evil

Dump of assembler code for function evil:

0x00000000 : push %ebp

0x00000001 : mov %esp,%ebp

0x00000003 : sub $0x10,%esp

0x00000006 : movl $0x0,-0x4(%ebp) // i = 0 i = 0

0x0000000d : addl $0x1,-0x4(%ebp) // i++ i = 1

0x00000011 : mov -0x4(%ebp),%eax // j = i i = 1 j = 1

0x00000014 : add %eax,%eax // j += j i = 1 j = 2

0x00000016 : add %eax,-0x4(%ebp) // i += j i = 3

0x00000019 : addl $0x1,-0x4(%ebp) // i++ i = 4

0x0000001d : leave

0x0000001e : ret

End of assembler dump.

(我...假设0x00000014指令是某种编译器优化?)

#5楼

int k[] = {0,1,2,3,4,5,6,7,8,9,10};

int i = 0;

int num;

num = k[++i+k[++i]] + k[++i];

printf("%d", num);

它将打印7 ... OP希望它打印6。

不能保证++i增量在其余计算之前全部完成。 实际上,不同的编译器在这里会得到不同的结果。 在您提供的示例中,首先执行2个++i ,然后读取k[]的值,然后读取最后一个++i ,然后读取k[] 。

num = k[i+1]+k[i+2] + k[i+3];

i += 3

现代编译器将对此进行很好的优化。 实际上,它可能比您最初编写的代码更好(假设它按照您希望的方式工作)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值