作为参数传递时如何复制值

How are values copied when passed as arguments?

本文关键字:复制 何复制 参数传递      更新时间:2023-10-16

因此,当按值传递参数时,值被复制到函数的参数中,但它是如何工作的呢?参数是否只是声明为常规变量并分配作为参数传递的值?喜欢这个:

int getx(int z, int x)
{
int a = z+x;
return a;
}
int main()
{
int q = 2;
int w = 55;
int xx = getx(w, 2);
return 0;
}

如果是这样,为什么要调用它来复制值?参数变量不是刚刚分配了变量 x 的值吗?那么复制了什么呢?

简短有趣的答案:你应该把变量想象成装玩具的盒子。 如果一个函数通过 vaule 获取参数并调用该函数,你只是告诉函数你的盒子里有什么玩具。它去得到自己的玩具,和你拥有的玩具(但不是你的玩具)一模一样,然后玩它。所以你不在乎它对功能里面的玩具做了什么,因为它不是你的实际玩具。当你通过引用传递时,你实际上是在给函数提供你自己的盒子里的玩具,它和你的玩具一起玩,而不是得到自己的玩具。

更长更深入的答案:这意味着当你在 main 函数中调用int xx = getx(w, 2);在 getX 函数中,您将使用与您传递的数据块具有相同位的数据块。但它们不是同一个数据块。这意味着z只是调用函数时w中的信息的副本。

假设你这样写getx(其中z是按值传入的)

int getx(int z, int x) {
int a = z + x;
z = z + 1;
return a;
}

在这种情况下,在 main (getx(w, 2)) 内部调用此函数后w的副本以 55 的形式出现,w以 55 的形式出现

相反,如果你有这个:

int getx(int& z, int x) {
int a = z + x;
z = z + 1;
return a;
}

然后像你在主中一样调用它

int main()
{
int q = 2;
int w = 55;
int xx = getx(w, 2);
return 0;
}

在这种情况下,z是通过引用传入的(请注意使用int&而不是int)。这意味着,您不是在w使用数据的副本,而是实际上将在getx中使用真实w的数据。 所以在这种情况下,在你的主函数中,在你调用getx(w, 2)w(不是副本)作为 55 进入,所以w显示为 56

附言在我看来,通过引用通常是不好的做法。你不能仅仅通过阅读getx(w, 2)就知道w会和它进来时不同。

为了了解函数如何使用其参数列表,无论是按值传递还是通过引用传递,您应该尝试做的一件事是创建或模拟一个表,该表将代表执行的每一行代码的堆栈帧,对于每个新的代码块或范围,您将需要一个新的堆栈帧表。

看看这个简短的程序和图表:

主.cpp

#include <iostream>
int someCalculationByValue( int a, int b ) {
a *= 2 + b;
return a;         
}
int someCalculationByReference( int& a, int& b ) {
a *= 2 + b;
return a;
}
int main() {
int x = 3;
int y = 4;
std::cout << "Starting with x = " << x << " and y = " << y << std::endl;
int ansValue = someCalculationByValue( x, y );
std::cout << "After Calculation" << std::endl;
std::cout << "x = " << x << " y = " << y << std::endl;
std::cout << "answersValue = " << ansValue << std::endl << std::endl;
std::cout << "Starting with x = " << x << " and y = " << y << std::endl;
int ansReference = someCalculationByReference( x, y );
std::cout << "After Calculation" << std::endl;
std::cout << "x = " << x << " y = " << y << std::endl;
std::cout << "answersReference = " << ansReference << std::endl << std::endl;
return 0;
}

- 为简单起见,我将跳过仅显示局部变量和用户定义函数的std::cout调用。

// Stack Frame Staring With First line of execution in the main function
// Stack Frame Start - Visible Scope Resolution of Main
{x} - Type int : 4bytes on 32bit - Scope Visibility - main()
+--+--+--+--+    
|  |  |  |  |    // On 32bit system 4bytes of memory - each block is 1 byte
+--+--+--+--+
{y} - Type int : 4bytes on 32bit - Scope Visibility - main()
+--+--+--+--+
|  |  |  |  |
+--+--+--+--+
{ansValue} - Type int : 4bytes on 32bit - Scope Visibility - main()
+--+--+--+--+
|  |  |  |  |
+--+--+--+--+
{=} assignment or evaluation
{someCalculationByValue} - Function Call - New Scope - New Stack Frame
{return value} - Type int 4bytes on 32bit - Returns back to calling function
In this case it returns back to main and flows into assignment which then
gets stored into {ansValue}
+--+--+--+--+
|  |  |  |  |  // Normally Has no name look up but is expected to be
+--+--+--+--+  // returned by this function when it goes out of scope
{a} - Type int - 4bytes on 32bit system - Parameter List - Visibility
+--+--+--+--+    is local to this function only - it copies the value
|  |  |  |  |    that is already stored in the variable that was passed
+--+--+--+--+    to it or by direct value
{b} - Type int - 4bytes on 32bit system - Parameter List - Visibility
+--+--+--+--+    is local to this function only - it copies the value
|  |  |  |  |    that is already stored in the variable that was passed
+--+--+--+--+    to it or by direct value
{a} an L-Value followed by compound assignment 
{*=} a compound assignment followed by arithmetic operation or expression
R-Value {a} = {a} * (2 + {b})
{return} Return statement - return back to caller in this case main() which
flows into the previous assignment in main that stores this return 
value in {ansValue}
// Scope Resolution is now back in main()
{ansReference} - Type int : 4bytes on 32bit - Scope Visilbity - main()
+--+--+--+--+
|  |  |  |  |
+--+--+--+--+
{=} assignment or evaluation
{someCalculationByReference} - Function Call - New Scope - New Stack Frame
{return value} - Type int 4bytes on 32bit - Returns back to calling function
In this case it returns back to main and flows into assignment which then
gets stored into {ansReference}
+--+--+--+--+
|  |  |  |  |  // Normally Has no name look up but is expected to be
+--+--+--+--+  // returned by this function when it goes out of scope
// No Local Variables Declared - Uses the actual variables that are passed 
// in by the caller as this does substitution from its declarative variables
{a} - the actual variable passed in  followed by compound assignment 
{*=} followed by arithmetic operation or expression {a} = {a} * (2 + {b})
However since this is by reference the direct use of main's variables 
are used so this then becomes: {x} = {x} * (2 + {y})
{return} - back to caller in this case main() which flows into the previous
assignment in main that stores this return value in {ansReference}
// Scope Resolution is now back in main()

现在,让我们执行实际的函数调用,以了解编译器在后台为每个函数调用执行的操作。


someComputingByValue()

x & y are passed by value from main's local scope variables
x has value of 3
y has value of 4
// Since passing by value
a is assigned a value of what x has which is 3
b is assigned a value of what y has which is 4
The arithmetic compound assignment and expression with substitution
{a(3)} *= 2 + {b(4)}
{a(3)}  = {a(3)} * (2 + {b(4)})
{a}     = (18)
return {a(18)} -> 18 is returned back and saved into main's {ansValue}

在计算后的 main 函数中,我们将 x 和 y 打印到控制台 x 的值仍然为 3,y 的值仍为 4;主的没有任何变化 x 和 y 值。


someComputingByReference()

x & y are passed by reference from main's local scope variables
x has value of 3
y has value of 4
// Since passing by reference
a is replaced with x that has a value of 3
b is replaced with y that has a value of 4
The arithmetic compound assignment and expression with substitution
Since by reference this function has no local variables of (a & b) it 
uses direct substitution of the variables that are passed in by its caller:
in this case; the main function.
{x(3)} *= 2 + {y(4)}
{x(3)}  = {x(3)} * (2 + {y(4)})
{x}     = (18)
return {x(18)} -> 18 is returned back and saved into main's {ansReference}

这次我们打印main的局部堆栈变量x&y,但这次 x 不再是 3,它现在是 18 与返回值相同,因为它在此函数中作为引用进行了修改,y 也会发生同样的情况,因为它也是一个引用,但我们没有在第二个函数中修改它,因此它的值保持不变。你有它;按值传递(复制)或按引用传递(直接替换)的工作原理差异。

要弄清楚你的"示例"代码应该显示什么有点困难。部分原因是因为参数传递的基础语义并没有真正很好地映射到可以显示的代码中。这些都是幕后的细节,不能真正用比平时更明确的语言表达。

从你的简短解释中,我也不太确定你的论证传递的心理模型到底是什么,所以我不确定从哪里开始澄清它。因此,让我们从头开始。

当函数"按值"接受参数时,该函数的调用方会创建传递对象的新副本,并将其传递给函数。然后,该函数使用该副本,对它执行任何它想要的操作。当该函数结束时,该副本实际上将被丢弃。这会让调用方保留其原始对象。

当函数"通过引用"接受参数时,该函数的调用方实际上将其自己的对象副本传递给函数。然后,该函数使用该副本,对它执行任何它想要的操作。当该函数结束时,它对对象所做的任何更改都是永久性的,因为它是同一对象,并且这些更改将反映在调用方站点上。换句话说,没有复制

。按值传递实际上是 C 中一切工作的方式。当您执行以下操作时:

void Function(int foo);

参数foo按值传递。同样,当您执行以下操作时:

void Function(int * foo);

参数foo仍然是按值传递的;只是按值传递的参数实际上是一个指针,因此这模拟了"按引用"传递,因为您通过指针间接传递对内存中原始值的引用。

在C++中,您实际上具有真正的按引用传递语义,因为该语言具有一流的引用类型。因此,当您这样做时:

void Function(int & foo);

参数foo实际上是通过引用传递的 —Function获取对调用方具有的原始对象的引用。现在,在幕后,C++将通过指针实现引用,所以真的没有任何新的事情发生。您只需从语言中获得保证,永远不会创建"空"引用,这使您免于整个类别的错误。

我相信,通过查看编译器如何在后台实际实现这一点,可以增强对这些细节的理解。实现细节因实现和体系结构而异,但通常,有两种基本方式可以将参数传递给函数:在堆栈上或在处理器的内部寄存器中。

如果在堆栈上传递参数,则调用方会将值"推送"到堆栈上。然后调用该函数,并从堆栈读取/使用数据。函数完成后,参数将从堆栈中"弹出"。在伪汇编语言中:

PROCEDURE getx                    // int getx(int one, int two)
LOAD    reg1, [stack_slot1]   // load parameter from stack slot #1 into reg1
LOAD    reg2, [stack_slot2]   // load parameter from stack slot #2 into reg2
ADD     reg1, reg2            // add parameters (reg1 += reg2)
RETURN  reg1                  // return result, in reg1
END

PROCEDURE main                    // int main()
PUSH   2                      // push parameter 1 onto stack
PUSH   55                     // push parameter 2 onto stack
CALL   getx                   // call function 'getx'
// The function has returned its result in reg1, so we can use it
// if we want, or ignore it.
POP    stack_slot1            // pop parameters from stack to clean up stack
POP    stack_slot2
RETURN 0
END

在这里,我们将常量值"推送"到堆栈上。但是,我们可以很容易地在寄存器中推送值的副本。

请注意,"push"会复制值,因此通过堆栈传递总是按值传递,但正如我们所说,可以传递指针的副本以提供按引用传递语义。通过指针对对象所做的任何更改都将反映在被调用方中。

如果在寄存器中传递参数,则调用方必须确保将该值加载到相应的寄存器中。然后调用该函数,并从该寄存器读取/使用数据。函数完成后,它对寄存器中的值所做的任何更改仍然可见。例如:

PROCEDURE getx                    // int getx(int one, int two)
ADD     reg1, reg2            // add parameters (reg1 += reg2)
RETURN                        // result is left in reg1
END

PROCEDURE main                    // int main()
MOVE   reg1, 2                // put '2' in reg1
MOVE   reg2, 55               // put '55' in reg2
CALL   getx                   // call function 'getx'
// The function has modified one or both registers, so we can use
// those values here, or ignore them.
RETURN 0
END

如果main在函数调用之前或之后对值执行其他操作,则它可以在getx用于其参数的完全相同的寄存器中执行此操作。这基本上是按引用传递语义。或者,它可以通过先将值复制到新寄存器中,调用getx,然后将结果复制回来获取按值传递语义。