0.4. 引用参数¶
0.4.1. 引用参数¶
在最简单的 值传递 或 值参数 方案中,每个函数都有独立的局部内存,参数在函数调用时从调用者复制到被调用者。但另一个方向呢?被调用者如何向其调用者回传信息?在被调用者末尾使用 "return" 将结果复制回调用者适用于简单情况,但并不适用于所有场景。此外,有时来回复制值是不可取的。 引用传递 参数解决了所有这些问题。在接下来的讨论中,“感兴趣值”一词将指代调用者和被调用者希望彼此通信的值。引用参数传递的是指向感兴趣值的指针,而不是感兴趣值的副本。该技术利用了指针的共享特性,使调用者和被调用者能够共享感兴趣值。
0.4.1.1. 比尔·盖茨示例¶
假设函数 A() 和 B() 都执行涉及比尔·盖茨净资产(以十亿美元计)的计算,—是本问题关注的值。 A() 是主函数,它存储初始值(截至 1998 年约为 55)。 A() 调用 B() ,后者尝试将该关注值加 1。
0.4.1.2. 比尔·盖茨 一趟¶
以下是代码及内存示意图,展示了一个简单但不正确的实现,其中 A() 和 B() 采用一趟。代码中标记了三个时间点 T1、T2 和 T3,并展示了每个状态下的内存情况...
void B(int worth) {
worth = worth + 1;
// T2
}
void A() {
int netWorth;
netWorth = 55; // T1
B(netWorth);
// T3 -- B() did not change netWorth
}
B() 对其局部的 worth 副本加 1,但当 B() 退出时, worth 被释放,因此修改它是无用的。感兴趣的值 netWorth 在整个过程中始终未变地保存在 A() 的局部存储中。函数可以修改其感兴趣值的局部副本,但该修改不会反映回原始值。这实际上只是局部存储旧的“独立性”特性,但在这种情况下并非所需。
0.4.1.3. 按引用¶
比尔盖茨问题的参考解决方案是使用单个 netWorth 变量来存储感兴趣的值,且从不复制它。相反,每个函数可以接收一个指向 netWorth 的指针。每个函数都可以通过解引用其指针来查看 netWorth 的当前值。更重要的是,每个函数只需解引用指向中央 netWorth 的指针并直接修改它,即可改变净 worth—。由于 netWorth 仅存在于一个位置—且每个人都拥有指向该主副本的指针,因此大家对它的当前值达成一致。下面的内存图示展示了改为使用 引用参数 的 A() 和 B() 函数。与之前一样,T1、T2 和 T3 对应代码中的点(如下所示),但您可以在查看代码之前先研究内存结构。
引用参数策略: B() 接收指向目标值的指针,而非副本。
0.4.2. 一趟¶
以下是在代码中使用一趟策略的步骤:
只保留一份感兴趣值的副本,即唯一的“主”副本。
将指向该值的指针传递给任何需要查看或修改该值的函数。
函数可以解引用其指针以查看或修改感兴趣的值。
函数必须记住它们没有自己的本地副本。如果它们解引用指针并修改值,则实际上是在修改主值。如果函数需要一个本地副本来安全地修改,则该函数必须显式分配并初始化这样一个本地副本。
0.4.2.1. 语法¶
C 语言中引用参数的语法仅对参数使用指针操作:
假设一个函数想要传达关于某个感兴趣值的—
int或float或struct fraction。该函数将其参数作为指向感兴趣值的指针—一个
int*或float*或struct fraction*。有些程序员会在引用参数的名称中添加单词 "ref",以提醒它是一个指向感兴趣值的引用,而不是副本。在调用时,调用者计算指向目标值的指针并传递该指针。指针的类型(指向目标值的指针)将与上述 (2) 中的类型一致。如果目标值是调用者的局部变量,则这通常会涉及使用 & 运算符(第 1 节)。
当被调用函数运行时,如果它希望访问目标值,则必须解引用其指针以访问实际的目标值。通常,这相当于在函数中使用解引用运算符 (
*) 来查看目标值。
0.4.2.2. 比尔·盖茨:按引用传递¶
这是使用引用参数编写的比尔·盖茨示例。此代码现在与上方的按引用内存图相匹配。
// B() now uses a reference parameter -- a pointer to
// the value of interest. B() uses a dereference (*) on the
// reference parameter to get at the value of interest.
void B(int* worthRef) {
// reference parameter
*worthRef = *worthRef + 1; // use * to get at value of interest
// T2
}
void A() {
int netWorth;
netWorth = 55; // T1 -- the value of interest is local to A()
B(&netWorth); // Pass a pointer to the value of interest.
// In this case using &.
// T3 -- B() has used its pointer to change the value of interest
}
0.4.2.3. 不要制作副本¶
引用参数使得被调用函数能够与其调用者进行通信。使用引用参数的另一个原因是避免创建副本。出于效率考虑,如果关注的值很大(例如数组),则创建副本可能不可取。创建副本需要额外的空间来存储副本本身,还需要额外的时间来完成复制操作。从设计角度来看,创建副本也可能不可取,因为一旦存在两个副本,若其中任何一个被修改,就无法确定哪一个是“正确”的。谚语云:“拥有一块手表的人总是知道当前时间;拥有两块手表的人却永远无法确定。”应避免创建副本。
0.4.3. 简单引用参数示例:交换 ()¶
引用参数的标准示例是一个 Swap() 函数,用于交换两个 ints 的值。这是一个简单的函数,但它确实需要修改调用者的内存,这正是趟的关键特征。
0.4.3.1. 交换 () 函数¶
Swap() 相关的值是两个 int 。因此, Swap() 不以 ints 作为其参数。它接受指向 int—( int* )的指针。在 Swap() 的主体中,参数 a 和 b 通过 * 解引用,以获取实际的( int )关注值。
void Swap(int* a, int* b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
0.4.3.2. 交换 () 调用者¶
要调用 Swap(),调用者必须传递指向感兴趣值的指针。
void SwapCaller() {
int x = 1;
int y = 2;
Swap(&x, &y); // Use & to pass pointers to the int values of interest
// (x and y).
}
Swap() 的参数是指向调用者局部变量中感兴趣值的指针。 Swap() 代码可以解引用这些指针以访问调用者的内存并交换值。在这种情况下, Swap() 遵循指针来交换 SwapCaller() 中变量 x 和 y 的值。 Swap() 将交换任意两个整数,只要给出指向这两个整数的指针即可。
0.4.3.3. 使用数组的 Swap()¶
为了说明感兴趣的值不必是简单变量,这里调用 Swap() 来交换数组中第一个和最后一个 int 。 Swap() 接受 int* ,但 ints 可以位于任何位置。数组内的 int 仍然是 int 。
void SwapCaller2() {
int scores[10];
scores[0] = 1;
scores[9[ = 2;
Swap(&(scores[0]), &(scores[9]));// the ints of interest do not need to be
// simple variables -- they can be any int. The caller is responsible
// for computing a pointer to the int.
上述对 Swap() 的调用可以等价地写为 Swap(scores, scores+9) ,这是由于 C 语言中的数组语法。你可以
如果您不熟悉这种情况,请忽略—它并非该语言的重要部分,且两种形式最终都会编译为完全相同的内容。
0.4.4. 更多语法¶
0.4.4.1. & 符号总是必需的吗?¶
当采用一趟传递时,调用者并不总是需要使用 & 来计算指向目标值的新指针。有时调用者已经拥有指向目标值的指针,因此无需进行新的指针计算。指向目标值的指针可以直接原样传递。
例如,假设 B() 被修改为调用一个 C() 函数,该函数将目标值加 2...
// Takes the value of interest by reference and adds 2.
void C(int* worthRef) {
*worthRef = *worthRef + 2;
}
// Adds 1 to the value of interest, and calls C().
void B(int* worthRef) {
*worthRef = *worthRef + 1; // add 1 to value of interest as before
C(worthRef); // NOTE no & required. We already have
// a pointer to the value of interest, so
// it can be passed through directly.
}
0.4.4.2. 关于 & Bug 标签页呢?¶
所有这些对 & 的使用可能会让你感到紧张—我们是否犯了第 2 节中的 & 错误?不,事实证明上述对 & 的使用是没问题的。& 错误发生在当 & 将一个指向被调用方局部存储的指针传回给调用方时。当被调用方退出时,其局部内存会被释放,因此该指针不再指向任何对象。在上述正确的情况中,我们使用 & 将指针从调用方传递给被调用方。由于调用方的局部变量在被调用方运行期间仍然存在,该指针对于被调用方来说仍然是有效的。由于一个简单的约束——调用方只能在其被调用方退出之后的某个时刻退出——所指向的对象将保持有效。使用 & 将指向局部存储的指针从调用方传递给被调用方是没问题的。相反的情况,即从被调用方传递到调用方,则是 & 错误。
0.4.4.3. ** 情况¶
如果调用者和被调用者之间需要共享和修改的值本身已经是一个指针,例如 int* 或 struct fraction* ,那会怎样?这会改变设置引用参数的规则吗?不会。在这种情况下,规则没有任何变化。它们的操作方式与之前完全相同。引用参数仍然是指向目标值的指针,即使目标值本身也是一个指针。假设目标值是 int* 。这意味着存在一个 int* 值,调用者和被调用者希望共享并修改它。那么引用参数应该是一个 int** 。对于 struct fraction* 类型的目标值,引用参数是 struct fraction** 。对引用参数执行一次解引用( * )操作即可得到目标值,这与简单情况下的处理方式一致。双指针( ** )参数在链表或其他操作指针的代码中很常见,其中需要共享和修改的目标值本身就是一个指针,例如链表头指针。
0.4.5. 引用参数总结¶
传值(复制)不允许被调用者向调用者回传信息,并且也具有制作副本的常规缺点。传址使用指针来避免复制目标值,并允许被调用者向调用者回传信息。
对于引用传递,只存在一份目标值的副本,并传递指向该副本的指针。因此,如果目标值是 int,其引用参数为 int* 。如果目标值是 struct fraction* ,其引用参数为 struct fraction** 。函数对引用参数使用解引用运算符( * )来查看或修改目标值。
0.4.6. Java 中的引用参数¶
因为 Java 没有 * / & 运算符,所以无法直接在 Java 中实现引用参数。也许这在面向对象编程范式中是可以接受的—:你应该通过向对象发送消息来修改它们,这使得引用参数的概念变得多余。调用者将被关注的值(或被关注的对象?)的(浅)引用传递给被调用者,而被调用者可以向其发送消息以进行修改。由于所有对象本质上都是浅层的,任何更改都会自动回传给调用者,因为被关注的对象从未被复制。
0.4.7. C++ 中的引用参数¶
引用参数是如此常见的编程任务,以至于它们已被作为官方功能添加到 C++ 语言中。因此在 C++ 中编写引用参数比在 C 中更简单。程序员只需在语法上指明希望某个参数通过引用传递,编译器便会处理其余部分。其语法是在参数类型的右侧附加一个 & 。因此, int 参数按值传递整数,而 int& 参数则按引用传递整数值。关键在于由编译器负责处理。在源代码中,无需再对 & 或 * 进行额外操作。因此,用 C++ 编写的 Swap() 和 SwapCaller() 看起来比 C 版本更简洁,尽管它们实现的功能相同。
void Swap(int& a, int& b) {
// The & declares pass by reference
int temp;
temp = a;
// No *'s required -- the compiler takes care of it
a = b;
b = temp;
}
void SwapCaller() {
int x = 1;
int y = 2;
Swap(x, y);
// No &'s required -- the compiler takes care of it
}
各种变量和参数的类型仅按其声明方式运行(本例中为 int )。实现引用参数所需的指针复杂层被隐藏。编译器会处理此事,而不会让这种复杂性干扰源代码中的类型。
