我在C ++中编写了一个简单的二进制搜索函数。代码如下:
int binary_search(int arr[], int key, int imin, int imax)
{
if (imin > imax)
{
return -1;
}
else
{
int imid = imin + (imax - imin) / 2;
if (arr[imid] > key) binary_search(arr, key, imin, imid - 1);
else if (arr[imid] < key) binary_search(arr, key, imid + 1, imax);
else return imid;
}
}
但我发现如果我在第10行和第11行添加return
,代码似乎也会以同样的方式工作。代码如下:
int binary_search(int arr[], int key, int imin, int imax)
{
if (imin > imax)
{
return -1;
}
else
{
int imid = imin + (imax - imin) / 2;
if (arr[imid] > key) return binary_search(arr, key, imin, imid - 1);
else if (arr[imid] < key) return binary_search(arr, key, imid + 1, imax);
else return imid;
}
}
所以我的问题是这两种情况有什么区别?
答案 0 :(得分:4)
任何不返回任何内容的函数(void
)必须遇到return
语句,然后才能完成任务。这很简单,因为没有魔法“返回X,如果我不返回某些东西”命令,而使用该函数的人可能依赖于你所承诺的返回(但未能交付)。 / p>
如果您现在遵循导致原始函数中的递归调用的路径,您将看到首先发起递归调用的调用现在必须返回一些内容。相反,它只是忽略了递归调用的结果,并且用完了要做的事情。
这会导致一些名为 undefined behavior 的东西,因为C ++不知道你期望它做什么。事实上,"it is legal for it to make demons fly out of your nose.",虽然它通常 - 出于其灵魂的纯粹仁慈 - 限制自己崩溃可怕和不可预测。
存在两个主要选项,为什么您没有看到差异:
代码的编译方式使其未定义的内容可以按预期工作。你必须不依赖于此。在实践中,您的代码将以一种方式编译,即单个寄存器保存您的返回值(RAX)。由于递归调用是代码执行的最后一项操作,因此可能
您的测试用例实际上从未进行过递归调用。这在技术上是合法的,因为程序的正确性取决于它在运行时的行为。
。如果您有兴趣,该标准的相关部分是[stmt.return] / 2,其中说:
[...]离开函数末尾相当于没有值的返回;这会导致值返回函数中的未定义行为。
“Flowing”是指控制流,“值返回函数”是指具有void
以外的返回类型的任何函数。
答案 1 :(得分:1)
如果函数返回非void
“最终结束”(即没有返回语句)并且调用者使用返回值,则结果是未定义的行为。这种情况发生在你的第一个版本中(无论是你的递归调用还是调用者),除非 - 偶然 - 你的测试用例只会导致执行return imid
语句。
不幸的是,未定义行为的一个可能结果是看起来对所有测试用例都能正常工作的代码。什么都没有阻止。程序崩溃(或其他异常程序终止)是人们在未定义行为发生时通常更熟悉的内容,但是未定义行为的更隐蔽的结果是似乎表现得好像没有任何错误。< / p>
在实践中,你的第一个案例似乎正常工作的原因是盲目的运气。由于历史原因,我不会扩展,有一些公平的编译器将计算的工作数据(即函数中的语句)和结果放入机器寄存器,不打算清除这些寄存器,并且(当函数返回时{ {1}}或其他非int
类型)使用相同的机器寄存器来保存函数的返回值。所以你可以幸运的是,第一个例子中的代码就像第二个代码一样。但问题是,标准不能保证这种行为,可能在编译器之间发生变化,可能会随着编译器设置(例如优化选项)而改变,甚至可能在将来某个时候升级编译器时发生变化。
答案 2 :(得分:0)
递归最终会通过实际执行return
语句而结束。此时,编译器会将返回的值放在用于返回值的位置。
当你回到调用函数而不执行任何其他return语句时,它恰好仍然存在。只是偶然。