让我们像调试器一样单步调试您的代码。在这里,我们将在即将调用insertionSort(v,n); 的行处放置一个断点。我们假设用户已经输入了有效的未排序输入整数。我会随意使用向量{3,2,9,5,7},这在你的情况下并不重要。
我们将像您的调试器一样进入代码,现在这会因您的特定编译器和调试器工具而异,但为简单起见,我们可以根据 C++ 语言的规则概括此处发生的情况。
这里的问题不会在运行时发生。这里的问题发生在编译时。它仍然是有效的代码,因为没有生成编译错误,所以您认为它一定是运行时错误,因为您没有得到正确的输出。
在这里,您必须了解 C++ 编译器如何解释函数声明/定义和函数组合。编译时,它会查找您声明为 insertionSort() 的函数,该函数必须在首次使用之前定义。在你的情况下是。您在代码的第 3 行声明并定义了它。接下来它必须确定它的返回类型和参数类型是否匹配。
您在第 30 行的呼叫站点:insertionSort(v,n);
在第 3 行找到匹配的声明/定义:vector<int> insertionSort(vector<int> v, int n); 它返回一个 vector<int>,但在 C++ 中函数调用点的返回类型可以忽略。接下来是参数类型,因为它采用vector<int> 和int 类型的value。
当您的源代码被编译成目标代码,然后链接并翻译成程序集时。这里发生的情况是,如果您不使用指针或动态内存(堆),则此函数将拥有自己的堆栈帧,其中这些变量仅可见并且在此函数的范围内具有生命周期。
当此函数返回调用站点时,这些变量将被销毁。此外,按值传递通常会导致传入的值的副本。
下面是这个函数的堆栈帧的样子:
在这里,我将在文字算法图中使用伪汇编助记符。现在,由于编译器优化和具有多个缓存的现代硬件以及向量内在函数,这是现代硬件的简化,因为它们超出了这里发生的范围,所以可以忽略。
- rsp -> 寄存器堆栈指针
- ret -> 从过程中返回
- rpt -> 栈帧的过程返回指针
- r0 -> 零寄存器
- [r1 - rn] -> 任意多用途寄存器
- mov -> 操作码或指令,通过立即数或其他寄存器或内存位置将值移动、存储或加载到某个寄存器中。
//Stack Frame:
{
// Setup the stack and frame pointers... internal housekeeping
// Return Type Setup: vector<int>
rpt assigned to the first value in `vector<int>`
// this is reserved memory specifically for return values
// Parameter Type Setup: vector<int>, int
// n's initialized value stored in reg1
mov r1, n's value
// values to be stored {3,2,9,5,7}
mov r2, 3
mov r3, 2
mov r4, 9
mov r5, 5
mov r6, 7
/* Function Implementation. Not going to convert this to assembly
However, it will use the rpt pointer to construct the new
vector that is local to this function after the necessary
checks are performed...
for(int i=1;i<=n-1;i++)
{
int currentEle = v[i];
int prevEle = i-1;
// right index where current is inserted
while(prevEle>=0 and v[prevEle]>currentEle){
v[prevEle+ 1] = v[prevEle];
prevEle = prevEle-1;
}
v[prevEle+1] = currentEle;
}
*/
// Make sure that shared registers are reassigned or their original values
// are restored before returning from procedure.
// More internal housekeeping...
// Assuming the algorithm is correct and the local registers were assigned
// to the designated memory pointed to by rpt
// Then prt would look something like:
// {rpt[0] = r3, rpt[1] = r2, rpt[2] = r5, rpt[3] = r6, rpt[4] = r1}
ret rpt
}
这是您的堆栈框架在汇编中的样子的伪示例。现在,ret 就像 C/C++ 中的指针,但在汇编中它是一个特殊寄存器,用于保存 CPU 寄存器文件中另一个寄存器或寄存器段的内存地址,甚至可能指向现代系统中的缓存......
现在,在您的 C++ 代码中,我们已经到达了这个函数的末尾,它超出了范围。所有局部变量都被销毁,因为该内存被归还给系统和/或操作系统......
在调用这个函数的过程中,你永远不会assign它从它的返回值到任何变量。您通过值将v 传递给此函数,并将副本复制到堆栈中。您在此函数内部创建了一个临时向量并执行了排序算法。你从这个函数返回并且从不使用它的返回值。
在 C++ 代码的下一行部分中,您正在使用基于范围的 for 循环并遍历 main 的变量 v,该变量从用户输入中分配了值 {3,2,9,5,7}。
您通过值将其传递到您的函数中,该值将信息复制到临时文件中。并且 main 的 v 变量位于其自己的堆栈框架中,永远不会被修改。 insertionSort()' 堆栈帧中的临时返回变量是被更改的那个。
这不是编译时错误,也不是运行时错误。由于没有完全理解语言的规范以及编译器如何处理它,这只是算法的糟糕实现设计。
要修复此函数,您可以删除返回类型并通过引用传递。签名将如下所示:
void insertionSort(vector<int>& v, int n);
这里将变量的引用传递给函数。这一次,当您的编译器看到这一点并稍后生成目标代码以翻译成程序集时。它不会复制这些值,而是直接使用相同的寄存器或缓存行。
在这种情况下,传递给此函数的对象可以在对其执行任何计算后进行修改。
这是一个基本整数加法函数的简单示例,它完全复制了您所做的:
// Pass By Value:
int add(int a, int b) {
a = a+b;
return a;
}
int main() {
int a = 3;
int b = 5;
add(a, b);
std::cout << a;
return 0;
}
在这里您希望看到 8 被打印出来,但实际上来自 main 的 a 从未被修改过,3 将被打印到您的控制台并且您从未使用过该函数的返回值。
让我们检查一下通过引用的版本:
void add(int& a, int b) {
a = a+b;
return a;
}
int main() {
int a = 3;
int b = 5;
add(a, b);
std::cout << a;
return 0;
}
在这个版本中,因为在 add() 的堆栈帧中直接使用了对 main 的 a 的引用,所以 main 的 a 将被修改。这一次,当您打印到控制台时,您将看到一个8。
我希望这有助于理清在 C++ 中将参数类型传递给函数的不同方式。您可以按值、按引用和按指针传递,其中一些具有 const 版本,这超出了本主题的范围。此外,在实现算法时,不要依赖您的 parameter 或函数 argument 排序。在生成必要的程序集时,特定编译器将如何设置函数的参数或参数列表没有规范或单一使用约定或保证。有各种调用约定,您必须知道您正在使用哪个编译器以及正在使用什么调用约定。
我希望这有助于您了解您的代码发生了什么,不同抽象级别的底层发生了什么以及为什么您会得到您得到的结果,并帮助您了解这是什么类型的错误.
其他人似乎只是说,“传递价值而不是引用”,假设您知道它们是什么和意思……我对待您就像这是您第一次查看代码或新语言一样。