9.11 - 按引用返回和按地址返回
Key Takeaway
- 引用返回的对象必须存在于返回引用的函数的作用域之外,否则将导致悬空引用。永远不要通过引用返回局部变量
- 不要按引用返回非const的局部静态变量
- 如果函数返回一个引用,并且该引用用于初始化或赋值给一个非引用变量,则返回值将被复制(就像它是通过value返回的一样)。
- 如果参数通过引用传递给函数,则通过引用返回该参数是安全的
在上节课中,我们介绍过,当实参按值传递时,实参的值会被拷贝一份到形参。 对于基本类型来说(拷贝开销小),这没有什么问题。但是对于类类型来说,拷贝开销通常会很大(例如 std::string
)。通过按引用传递(通常为const
)或按按地址传递可以避免这种开销。
在按值返回时,我们会遇到类似的情况:返回值的副本被传递回调用者。如果函数的返回类型是类类型,则代价可能很高。
1 |
|
按引用返回
在将类类型传递回调用者的情况下,我们可能(也可能不)希望按引用返回。通过引用返回返回一个绑定到被返回对象的引用,这样就避免了对返回值进行复制。要通过引用返回,只需将函数的返回值定义为引用类型:
1 2 |
|
下面例程展示了其原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
程序打印:
1 |
|
因为 getProgramName()
返回 const 引用,所以 return s_programName
执行时,getProgramName()
会返回 const 引用到 s_programName
(避免了拷贝对象)。调用者可以使用该引用来访问 s_programName
的值,从而将其打印出来。
按引用返回的对象其持续时间必须超过函数调用本身
使用按引用返回最需要注意的事是:程序员必须确保被引用的对象比返回引用的函数寿命长。否则,返回的引用将称为悬垂引用(引用一个已被销毁的对象),使用该引用将导致未定义行为。
在上面的 程序中,因为 s_programName
具有静态存储持续时间,所以它的知道程序结束才会被销毁。当main
函数访问该返回的引用时,它实际访问的是 s_programName
,这时是没有问题的,在这个时间点上它还没有被销毁。
接下来,修改上面的程序,看看函数返回悬垂引用的情况下会发生什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
程序打印:
1 |
|
之所以这样是因为 id1
和 id2
引用的是同一个对象(即静态变量 s_x
),所以任何对该变量的修改(例如 getNextId()
),都会影响到所有引用。通过const引用返回静态局部值的程序经常出现的另一个问题是,没有标准化的方法将s_x
重置回默认状态。这样的程序必须使用非惯用的解决方案(例如重置参数),或者只能通过退出和重新启动程序来重置。
虽然上面的例子有点傻,但程序员有时会为了优化目的而尝试上面的做法,然后程序就不能按预期工作了。
最佳实践
避免返回对非const局部静态变量的引用。
如果通过引用返回的局部变量的创建成本很高(因此不必每次函数调用都重新创建该变量),则有时会返回对 const 局部静态变量的const引用。但这是罕见的。
返回一个指向const全局变量的const引用有时也是一种用于封装对全局变量访问的方式。我们在课程6.8 - 为什么非 const 全局变量是魔鬼中讨论这个问题。如果有意且谨慎地使用,这也是可以的。
使用返回的引用来访问/初始化普通变量时会创建拷贝
如果函数返回一个引用,并且该引用用于初始化或赋值给一个非引用变量,则返回值将被复制(就像它是通过value返回的一样)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
在上面的例子中,getNextId()
返回的是一个引用,但是 id1
和 id2
都是非引用的普通变量。这种情况下,返回的引用绑定的值会被拷贝到这个普通变量,因此程序打印:
1 |
|
当然,这也违背了通过引用返回值的目的。
还需要注意的是,如果程序返回一个悬垂引用,则该引用在复制之前一直悬空,这将导致未定义行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
按引用返回按引用传递的参数没有问题
在很多情况下,通过引用返回对象是有意义的,我们将在未来的课程中遇到许多这样的情况。不过,我们现在可以举一个有用的例子。
如果参数通过引用传递给函数,则通过引用返回该参数是安全的。这是有意义的:为了将参数传递给函数,参数必须存在于调用者的作用域中。当被调用的函数返回时,该对象必须仍然存在于调用者的作用域中。
下面是这样一个函数的简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
输出:
1 |
|
在这个例子中,调用者按引用传递了两个 std::string
对象,然后经过比较,两个字符串中按照字母表比较排在前面的对象被按引用(const)返回给主调函数。如果我们是按照值传递,则会导致创建std::string
的三个拷贝(每个形参拷贝一次、返回值拷贝一次)。而使用引用传递则可以避免这些拷贝。
调用者可以通过引用修改值 caller can modify values through the reference
当实参按非const引用传递时,函数可以通过引用修改实参的值。
类似的,当按非const引用返回给主调函数时,调用者可以使用该引用修改返回值。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
在这个例子中, max(a, b)
在调用 max()
函数时传入a
和 b
作为实参。引用形参 x
绑定到实参 a
,而引用形参 y
绑定到实参 b
。随后,函数会判断 x
(5
) 和 y
(6
) 哪个比较大。本例中显然 y
更大,因此 y
(仍然绑定到 b
) 被按引用返回给主调函数。调用者随后通过返回的引用将 b
赋值为 7。
因此,表达式 max(a, b) = 7
最终解析为 b = 7
。
程序打印:
1 |
|
按地址返回
按地址返回与按引用返回的工作原理几乎相同,只不过返回的是指向对象的指针而不是对对象的引用。按地址返回有与按引用返回相同的注意事项——按地址返回的对象必须比返回地址的函数的作用域更长久,否则调用者将收到一个悬垂指针。
按地址返回比按引用返回的主要优点是,如果没有要返回的有效对象,则可以使用函数返回 nullptr
。例如,假设我们有一个想要搜索的学生列表。如果在列表中找到了要查找的学生,则可以返回一个指向表示匹配学生的对象的指针。如果我们没有找到任何匹配的学生对象,我们可以返回nullptr
来表示没有找到匹配的学生对象。
其主要缺点则是调用者必须记得在解引用返回值之前进行指针判空操作,则可能会发生空指针解引用并导致未定义行为。由于这种危险,除非需要返回“无对象”的能力,否则通过引用返回应优先于通过地址返回。
最佳实践
首选通过引用返回而不是通过地址返回,除非返回“无对象”(使用nullptr
)的能力很重要。