12-5 按左值引用传递
在前面的课程中,我们介绍了左值引用(12.3 - 左值引用)和指向常量的左值引用(12.4 - 指向常量的左值引用)。单独来看,这些似乎没什么用——既然可以直接使用变量本身,为什么还要创建变量的别名呢?
在本课中,我们将深入探讨参考文献的用途。从本章后面开始,您将会看到参考文献的频繁使用。
首先,交代一些背景知识。在2.4 课——函数参数和实参简介中,我们讨论过pass by value,传递给函数的实参会被复制到函数的参数中:
#include <iostream>
void printValue(int y)
{
std::cout << y << '\n';
} // y is destroyed here
int main()
{
int x { 2 };
printValue(x); // x is passed by value (copied) into parameter y (inexpensive)
return 0;
}
在上述程序中,当调用函数printValue(x)时, x的值2会被复制到形数y中。然后,在函数结束时,对象y会被销毁。
这意味着当我们调用该函数时,我们复制了实参的值,但只是短暂地使用它,然后就将其销毁了!幸运的是,由于基本类型的复制成本很低,所以这并不是什么问题。
有些对象复制起来成本很高。
标准库提供的大多数类型(例如std::string)都是类类型。类类型的复制成本通常很高。我们应该尽可能避免对复制成本高的对象进行不必要的复制,尤其是在几乎立即销毁这些副本的情况下。
以下程序可以说明这一点:
#include <iostream>
#include <string>
void printValue(std::string y)
{
std::cout << y << '\n';
} // y is destroyed here
int main()
{
std::string x { "Hello, world!" }; // x is a std::string
printValue(x); // x is passed by value (copied) into parameter y (expensive)
return 0;
}
这打印:

虽然这个程序运行符合预期,但效率很低。与之前的例子一样,当printValue()调用时,实参数x会被复制到printValue()另一个形参y中。然而,在这个例子中,实参是一个std::string对象, 而不是int 对象, 而std::string对象是一个类,复制起来开销很大。而且每次调用printValue()时都会进行这种开销很大的复制!
我们还可以做得更好。
按引用传递
避免在调用函数时对参数进行昂贵的复制的一种方法是使用pass by reference替代pass by value。使用按引用传递时,我们将函数参数声明为引用类型(或常量引用类型),而不是普通类型。函数调用时,每个引用参数都会绑定到相应的实参。由于引用充当了实参的别名,因此不会复制实参。
以下是与上面相同的示例,但使用按引用传递而不是按值传递:
#include <iostream>
#include <string>
void printValue(std::string& y) // type changed to std::string&
{
std::cout << y << '\n';
} // y is destroyed here
int main()
{
std::string x { "Hello, world!" };
printValue(x); // x is now passed by reference into reference parameter y (inexpensive)
return 0;
}

这个程序与之前的程序完全相同,只是形参类型从按值传递std::string改为了左值引用 std::string&。现在,当调用 printValue() 时,左值引用参数 y会被绑定到实参(argument) x 。绑定引用总是开销很小的,不需要复制 x 。因为引用充当了被引用对象的别名,所以当 printValue() 使用引用 y 时,它访问的是实际的参数 x(而不是 x 的副本)。
关键见解:
按引用传递允许我们将参数传递给函数,而无需在每次调用函数时都复制这些参数。
以下程序演示了值形参与实参是不同的对象,而引用形参则被视为实参:
#include <iostream>
void printAddresses(int val, int& ref)
{
std::cout << "The address of the value parameter is: " << &val << '\n';
std::cout << "The address of the reference parameter is: " << &ref << '\n';
}
int main()
{
int x { 5 };
std::cout << "The address of x is: " << &x << '\n';
printAddresses(x, x);
return 0;
}
该程序运行一次后产生了以下输出

我们可以看到,实参的地址与值参数的地址不同,这意味着值参数是一个不同的对象。由于它们拥有不同的内存地址,为了使值参数的值与实参的值相同,必须将实参的值复制到值参数所占用的内存中。
另一方面,我们可以看到,获取引用参数的地址得到的地址与实参的地址相同。这意味着引用参数被视为与实参是同一个对象。
按引用传递允许我们更改实参的值
当按值传递对象时,函数参数接收到的是实参的副本。这意味着对形参值的任何更改都会作用于实参的副本,而不是实参本身:
#include <iostream>
void addOne(int y) // y is a copy of x
{
++y; // this modifies the copy of x, not the actual object x
}
int main()
{
int x { 5 };
std::cout << "value = " << x << '\n';
addOne(x);
std::cout << "value = " << x << '\n'; // x has not been modified
return 0;
}
在上述程序中,由于 值形参 y 是 x 的副本,因此当我们递增y 时,只会影响 y。该程序的输出结果为:

然而,由于引用与被引用的对象作用相同,因此在使用按引用传递时,对引用形参所做的任何更改都会影响实参:
#include <iostream>
void addOne(int& y) // y is bound to the actual object x
{
++y; // this modifies the actual object x
}
int main()
{
int x { 5 };
std::cout << "value = " << x << '\n';
addOne(x);
std::cout << "value = " << x << '\n'; // x has been modified
return 0;
}
该程序输出:

在上面的例子中,x 初始值为 5。当调用函数addOne(x)时,引用形参 y 绑定到实参 x。当 addOne() 函数递增引用 y 时,实际上是实参 x 从 5 递增到 6(而不是 x 的副本)。即使函数 addOne() 执行完毕,这个改变后的值仍然保留。
关键见解:
通过引用非常量值来传递值,我们可以编写修改传入参数值的函数。
函数能够修改传入参数的值非常有用。假设你编写了一个函数,用于判断怪物是否成功攻击了玩家。如果成功,怪物应该对玩家的生命值造成一定伤害。如果你按引用传递玩家对象,函数可以直接修改传入的玩家对象的生命值。如果你按值传递玩家对象,则只能修改玩家对象副本的生命值,这不太实用。
按引用传递只能接受可修改的左值参数
由于对非常量值的引用只能绑定到可修改的左值(本质上是一个非常量变量),这意味着按引用传递仅适用于可修改的左值参数。实际上,这极大地限制了按引用传递对非常量值的适用性,因为这意味着我们不能传递常量变量或字面量。例如:
#include <iostream>
void printValue(int& y) // y only accepts modifiable lvalues
{
std::cout << y << '\n';
}
int main()
{
int x { 5 };
printValue(x); // ok: x is a modifiable lvalue
const int z { 5 };
printValue(z); // error: z is a non-modifiable lvalue
printValue(5); // error: 5 is an rvalue
return 0;
}


幸运的是,这个问题有一个简单的解决方法,我们将在下一课讨论。我们还会探讨何时应该按值传递,何时应该按引用传递。

浙公网安备 33010602011771号