我试图使用递归来乘以两个整数,并且意外地编写了这段代码:
//the original version
int multiply(int a, int b)
{
if ( !b )
return 0;
else
return a + multiply(a, b ? --b : ++b ); //accident
}
我说,我意外地写了这个,因为我打算写:
b > 0 ? --b : ++b
代替b ? --b : ++b
我意识到我打算写wouldn't work。但令我惊讶的是,我所做的不打算写does work。
现在,我注意到b ?--b : ++b
基本上等同于--b
,因为else-block中的b
保证不为零。因此,我修改了上述代码,将b?--b:++b
替换为--b
,如下所示:
//the modified version
int multiply(int a, int b)
{
if ( !b )
return 0;
else
return a + multiply(a, --b); //modification here
}
由于原始版本已经出现,我希望修改后的版本也能正常运行。但令我惊讶的是,doesn't work!
--b
不等同b ?--b : ++b
如果b
非零?如果它是等价的,那么为什么第一个代码可以工作但第二个代码没有呢? 注意:这里,通过“工作”,我的意思是它给出了正确的输出。也就是说,它给出了传递给函数的整数的乘法。
答案 0 :(得分:6)
它不起作用。我不知道吸烟是什么意思,代码会溢出堆栈。
修改强>
尝试使用gcc 4.6.0 - segfault(由于堆栈溢出)。但是,如果您启用-O2
优化,那么确实“它可以正常工作”。总之:它是偶然的,取决于编译器在幕后的作用。
g++ -g -Wall -o f f.cpp # stack overflow
g++ -O2 -g -Wall -o f f.cpp # "works"
答案 1 :(得分:5)
TL; DR版本: b? --b: ++b
打印结果而--b
失败且SIGXCPU
的原因是ideone设置了提交代码的时间限制。一个版本得到更好的优化,并在允许的时间内完成。另一个版本给出完全相同的答案,但你不会看到与ideone,因为它运行得太慢并且在时间限制内被中止。
至于为什么没有发生堆栈溢出,我想在一种情况下编译器必须将递归转换为迭代(这不是尾调用,但它可以简单地转换)。
转换的结果类似于http://ideone.com/AeBYI,它确实给出了正确的结果。通过更高的优化设置,编译器可以在编译时计算结果并在结果代码中存储常量。
答案 2 :(得分:5)
以下代码的输出应该给出一个非常强大的线索;)
#include <iostream>
using namespace std;
int multiply(int a, int b)
{
cout << "a = " << a << "\tb = " << b << std::endl;
if ( !b )
return 0;
else
return a + multiply(a, b ? --b : ++b );
}
int main() {
cout << multiply(12, 7) << endl;
cout << multiply(12, -7) << endl;
cout << multiply(-12, -7) << endl;
cout << multiply(-12, 7) << endl;
return 0;
}
在我看来,通过长途跋涉得到答案。
编辑:为了回应下面Nawaz的评论,第一种方法起作用,因为两个补码有符号整数运算的变幻莫测。就像我说的那样,它需要很长的路要走。正如其他人已经指出的那样,由于某些编译器优化或其他原因而启用了它。您可以在下面的代码中看到以前提供的测试输入:
int mul2(int a, int b)
{
int rv = 0;
while (b--) rv += a;
return rv;
}
它实际上应该适用于任何一对整数但在某些情况下需要一些时间才能运行(但我希望你对任何情况下的效率都不感兴趣)。
第二种情况不起作用,因为条件b > 0 ? --b : ++b
实际上会将b
转换为abs(b)
。也就是说,即使b = -7,您只需在测试用例中添加7次。如果你希望它的行为方式与我认为你希望它表现的方式相反,你需要做的事情如下:
int multiply(int a, int b)
{
if ( !b )
return 0;
else if (b < 0)
return -a + multiply(-a, -b-1);
else
return a + multiply(a, --b);
}
编辑2:请改为尝试:
short multiply(short a, short b)
{
if ( !b )
return 0;
else
return a + multiply(a, b ? --b : ++b );
}
和
short multiply(short a, short b)
{
if ( !b )
return 0;
else
return a + multiply(a, --b);
}
这两个编译,运行并返回相同(正确)结果,有或没有优化。正如其他人所指出的那样,您所看到的执行时间差异的原因只能归因于编译器优化代码的方式。您需要分析这两种方法的汇编代码,以确定时间差异的根本原因。
答案 3 :(得分:1)
实际上,它与--b无关,而是与你的算法无关。
如果b < 0,你有什么感受?您将无限循环并最终导致堆栈溢出。
这就是为什么你在第一次乘法(12,7)时得到了正确的结果,但是当你调用乘法(12,-7)时你的程序就会失败。
答案 4 :(得分:1)
由于2的补码数的工作方式,你的代码对于b的正负值都是“正确的”。对于负面的b来说,任何递归版本都需要一个很大的堆栈来工作。因此,只要编译器发出非递归版本,就会有工作代码。所以它归结为:我的编译器在内部使用什么规则来确定何时发出非递归代码。这取决于编译器的编写方式。
答案 5 :(得分:0)
但令我惊讶的是,我不打算写的东西确实有用。
显然正在发生的事情是,在g ++的优化级别2,编译器正确地看到,如果b为正,则代码等同于* b。它显然也看到如果b为负,则代码会调用未定义的行为。在所有情况下,编译器通过泛化为* b来优化未定义的行为。这是g ++ -O2的汇编程序(i686-apple-darwin10-g ++ - 4.2.1):
.globl __Z8multiplyii
__Z8multiplyii:
LFB1477:
pushq %rbp
LCFI0:
movq %rsp, %rbp
LCFI1:
xorl %eax, %eax
testl %esi, %esi
je L5
movl %esi, %eax
imull %edi, %eax
L5:
leave
ret
我不相信优化者。 IMO编译器对识别未定义行为的响应应该是编译失败而不是优化未定义的行为。
修改强>
我不会将另一个答案添加为另一个答案,而是添加另一个答案作为编辑。
向任何物理学家询问无限和1 + 2 + 3 +的价值......他们会告诉你它是-1/12。 (例如,见http://math.ucr.edu/home/baez/qg-winter2004/zeta.pdf)。通过两个补码算法的类似技巧,这需要很长的路要走。一个解决方案,不涉及长期的负数:
int multiply (int a, int b) {
if (b == 0) {
return 0;
}
else if (b < 0) {
return -multiply (a, -b);
}
else {
return a + multiply(a, b-1);
}
}
即使在高级别的优化中,我的编译器也不再足够聪明,无法将上述内容识别为乘法。将上面的内容拆分为两个函数,编译器再一次确认正在做的是整数乘法:
int multiplyu(int a, unsigned int b) {
return (b == 0) ? 0 : a + multiplyu(a, b-1);
}
int multiply(int a, int b) {
return (b < 0) ? -multiplyu (a, -b) : multiplyu (a, b);
}
答案 6 :(得分:0)
当multiply
为负数时,两种形式的b
在Visual Studio中崩溃,堆栈溢出。
所以,答案是,两种形式都不正确。可能gcc
中发生的事情是,由于某些怪癖(不是错误!),编译器正在优化第一个示例中的尾递归而不是第二个示例。
作为旁注,即使您将其更改为b > 0 ? --b : ++b
,您仍然没有乘以b
的符号(例如multiply(-1, -1) == -1
)