|
| 1 | +\section{(左值)引用与引用参数传递} |
| 2 | +我们在前文中介绍了指针参数传递。这种方法固然强大,但是每次传递参数都要使用指针或地址,这就显得有点麻烦了。如果读者了解C语言的话,应该就会知道,每次我们使用 \lstinline@scanf@ 来进行输入,都要用取地址符来传入相应变量的地址方可。 |
| 3 | +\begin{lstlisting} |
| 4 | + int num; |
| 5 | + scanf("%d", &num); //传递num的地址,这样才能改变num |
| 6 | +\end{lstlisting}\par |
| 7 | +而在C++中,每次我们使用 \lstinline@cin@ 来输入变量的值时,都不需要取其地址。但是按照我们之前的讲解,如果按值传递的话,我们只能修改副本中的数据,而不会影响原数据。这是怎么回事呢? |
| 8 | +\begin{lstlisting} |
| 9 | + int num; |
| 10 | + cin >> num; //为什么不用传递地址,也可以改变num的值? |
| 11 | +\end{lstlisting}\par |
| 12 | +要解决这个问题,我们就需要了解\textbf{引用\footnote{泛讲篇中不讲解右值引用,这里提及的所有引用都指左值引用。}(Reference)}及其在参数传递过程中的作用。\par |
| 13 | +\subsection*{什么是引用?} |
| 14 | +我们曾言,变量名不是变量的本质。如果两个不同的变量名绑定了同一个地址,那么它们表达的信息就是相同的\footnote{当然,这两个变量名必须是同一类型的,因为类型会影响内存信息的解释方式。}。我们可以用其中一个名字来读取或修改它们的值,当然也可以用另一个名字。\par |
| 15 | +引用的作用相当于变量的别名,定义一个引用的基本语法是 |
| 16 | +\begin{lstlisting} |
| 17 | + <类型> &<引用名> = {变量}; //必须在定义之时初始化 |
| 18 | +\end{lstlisting} |
| 19 | +这里的 \lstinline@&@ 不是取地址运算符的意思,而是表示我们在定义引用。一旦这个引用绑定了这个变量,它就充当了这个变量的别名,我们使用引用的效果与使用变量名等同。 |
| 20 | +\begin{lstlisting} |
| 21 | + int a {3}; |
| 22 | + int &ref {a}; //定义一个引用,它充当a的别名 |
| 23 | + ++ref; //更改ref相当于更改a |
| 24 | + cout << a; //输出a的值,观察结果 |
| 25 | +\end{lstlisting} |
| 26 | +这个程序的输出结果将会是 \texttt{4}。这说明我们用 \lstinline@ref@ 和用 \lstinline@a@ 的效果是相同的。\par |
| 27 | +如果你去输出一下它们的地址就会发现,\lstinline@ref@ 与 \lstinline@a@ 的地址是相同的。 |
| 28 | +\begin{lstlisting} |
| 29 | + cout << &ref << endl << &a; //输出ref和a的地址,它们应当相同 |
| 30 | +\end{lstlisting}\par |
| 31 | +我们还可以定义常量引用,它同样是一种引用,但 \lstinline@const@ 限制了我们通过这个引用来修改变量值的能力。 |
| 32 | +\begin{lstlisting} |
| 33 | + int a {3}; |
| 34 | + const int &ref {a}; //定义一个常量引用 |
| 35 | + ++a; //没问题,a是一个变量 |
| 36 | + ++ref; //错误! |
| 37 | +//error: increment of read-only reference 'ref' |
| 38 | +\end{lstlisting} |
| 39 | +在这种情况下,我们可以用 \lstinline@a@ 来修改相应的值,但不能用 \lstinline@ref@ 来修改相应的值。这说明,``变量''和``常量''并不是直接体现在数据中的信息,而只是``变量名''对数据内容是否拥有修改权限的体现。\par |
| 40 | +至于本来就定义成常量的数据,如果我们用普通引用来作为它的别名,编译器就不会允许。 |
| 41 | +\begin{lstlisting} |
| 42 | + const double std_gravity {9.80665}; //这是一个常量 |
| 43 | + double &refgravity {std_gravity}; //错误! |
| 44 | +//error: binding reference of type 'double&' |
| 45 | +//to 'const double' discards qualifiers |
| 46 | + const double &ref_gravity {std_gravity}; //正确 |
| 47 | +\end{lstlisting} |
| 48 | +总而言之,我们只能用常量引用来绑定常量。而我们可以使用普通引用或常量引用来绑定变量。如果使用常量引用来绑定变量,我们可以用变量名来修改变量的值,但不能使用引用。\par |
| 49 | +\subsection*{引用参数传递} |
| 50 | +还记得我们在讲指针时介绍的 \lstinline@exchange@ 函数吗?如果要使用引用来接收实参,那么我们就相当于在 \lstinline@exchange@ 函数中创造了这个实参的别名,它和 \lstinline@main@ 函数中的变量共享了相同的内存地址。于是我们可以直接用这个``别名''来修改实参的值了。这种方式又被称为\textbf{按引用传递(Passing by reference)}。 |
| 51 | +\begin{lstlisting} |
| 52 | +void exchange(int &a, int &b) { //接收两个引用参数 |
| 53 | + int tmp {a}; //临时变量 |
| 54 | + a = b; |
| 55 | + b = tmp; |
| 56 | +} |
| 57 | +int main() { |
| 58 | + int a {3}, b {4}; |
| 59 | + exchange(a, b); //传入实参 |
| 60 | + cout << a << ' ' << b; //输出,检验结果 |
| 61 | + return 0; |
| 62 | +} |
| 63 | +\end{lstlisting} |
| 64 | +最后的输出结果也合乎我们的预期,正是\\\noindent\rule{\linewidth}{0.2pt}\texttt{ |
| 65 | +4 3 |
| 66 | +}\\\noindent\rule{\linewidth}{0.2pt} |
| 67 | +这说明我们的确可以用它来完成数值交换的操作。\par |
| 68 | +另外你可能还记得,我们在前面介绍了一个设置了默认参数的 \lstinline@input_clear@ 函数,用以清除错误输入。它的定义中就使用了引用参数: |
| 69 | +\begin{lstlisting} |
| 70 | +void input_clear(istream &in = {cin}) { //不提供参数时使用默认参数cin |
| 71 | + in.clear(); //清除错误状态 |
| 72 | + while (in.get() != '\n') //清除本行输入 |
| 73 | + continue; |
| 74 | +} |
| 75 | +\end{lstlisting} |
| 76 | +为什么要使用引用参数呢?这是因为,我为了清除 \lstinline@cin@(或其它 \lstinline@istream@ 对象)的错误状态,必须要对实参作出修改。如果按值传递的话\footnote{实际上这是不可能的,\lstinline@istream@ 类已经删除了拷贝构造函数,所以这个语法根本不能通过编译。},我修改的只能是 \lstinline@in@ 这个副本的状态。\par |
| 77 | +引用参数传递还有一些其它的功用。很多时候我们希望把某个类型的对象(变量)传入函数中,我们不需要修改它的值,所以按值传递也可以。但是这个对象可能占用非常大的内存空间(通常是某种数据结构或类),如果在函数调用时要为它建立一个副本的话,那就既浪费内存空间,又浪费算力和时间。这时我们会选择这样定义函数: |
| 78 | +\begin{lstlisting} |
| 79 | +void func(const Type &obj) {接收Type类型的常量引用 |
| 80 | + //... |
| 81 | +} |
| 82 | +\end{lstlisting} |
| 83 | +这样只是为传入的实参创建了一个别名而已,并不需要浪费大量的时间和空间来作复制工作。如果要防止误修改实参,我们也可以把它定义成常量引用,这样就不会出问题啦。\par |
| 84 | +\subsection*{引用作为返回值} |
| 85 | +来看一看这个语句,它初看上去可能有点费解: |
| 86 | +\begin{lstlisting} |
| 87 | + (num *= num) %= 11; //这是在做什么? |
| 88 | +\end{lstlisting} |
| 89 | +让我们用前面学过的运算符的有关知识来分析一下吧:\par |
| 90 | +首先,\lstinline@%=@ 运算符把 \lstinline@(num*=num)@ 与 \lstinline@11@ 隔开。而在 \lstinline@num*=num@ 表达式中,\lstinline@*=@的作用是乘赋值,于是这个表达式的作用是把 \lstinline@num@ 变成 \lstinline@num@ 的平方。\par |
| 91 | +下一步,因为赋值语句返回的是左操作数本身,所以 \lstinline@(num*=num)%=11@ 的作用相当于 \lstinline@num%=11@。\par |
| 92 | +我们可以试着写一两个函数来模拟这个语句的行为,比如说,一个叫乘赋值 \lstinline@mul_ass@。它接收的第一个参数是引用,这是为了能够改变它的值;第二个参数是常量引用,这是因为我们无需用 \lstinline@b@ 来修改它的值。 |
| 93 | +\begin{lstlisting} |
| 94 | +int mul_ass(int &a, const int &b) { |
| 95 | + a = a * b; |
| 96 | + return a; |
| 97 | +} |
| 98 | +\end{lstlisting} |
| 99 | +这样一来我们就可以用 \lstinline@mul_ass(num,num)@ 来实现和 \lstinline@num*=num@ 一样的功能了。\par |
| 100 | +接下来我们应该把它的返回值扔到 \lstinline@rem_ass@ 当中了。 |
| 101 | +\begin{lstlisting} |
| 102 | +int rem_ass(int &a, const int &b) { |
| 103 | + a = a % b; |
| 104 | + return a; |
| 105 | +} |
| 106 | +\end{lstlisting} |
| 107 | +这样我们就可以用 \lstinline@rem_ass(num,11)@ 来实现和 \lstinline@num%=11@ 一样的功能了。\par |
| 108 | +但是如果我们这么写,编译就会出现问题: |
| 109 | +\begin{lstlisting} |
| 110 | + int num {5}; |
| 111 | + rem_ass(mul_ass(num, num), 11); |
| 112 | +//error: cannot bind non-const lvalue reference of type 'int&' |
| 113 | +//to an rvalue of type 'int' |
| 114 | + cout << num; |
| 115 | +\end{lstlisting} |
| 116 | +编译器报错信息的意思是:``不能把 \lstinline@int&@ 型的左值引用绑定到 \lstinline@int@ 型的右值上。''关于左值右值的问题,我们暂不讨论;但是这个问题,我们需要解决。\par |
| 117 | +问题的关键在于,\textbf{函数返回的返回值,其实是一个``副本'',而不是 \lstinline@return@ 所跟的变量本身!}。因此我们是在对着一个不应该取引用的内容\footnote{我们在精讲篇中会更详细地谈讨此类问题。简而言之,不是所有数据都可以取地址的,也不是所有数据都可以被引用的。}按引用传递参数,那当然就会产生问题了。\par |
| 118 | +这和我们在按值传递参数的过程中面临的窘境如出一辙。而解决方法也很相似,就是使用引用来返回值。\par |
| 119 | +\begin{lstlisting} |
| 120 | +int& mul_ass(int &a, const int &b) { |
| 121 | + a = a * b; |
| 122 | + return a; //返回值按引用传递,就不会再创建副本了。 |
| 123 | +} |
| 124 | +int& rem_ass(int &a, const int &b) { |
| 125 | + a = a % b; |
| 126 | + return a; //同上 |
| 127 | +} |
| 128 | +int main() { |
| 129 | + int num {5}; |
| 130 | + rem_ass(mul_ass(num, num), 11); //现在它能正常运行了 |
| 131 | + cout << num; |
| 132 | +} |
| 133 | +\end{lstlisting} |
| 134 | +\subsection*{引用的类型与 \lstinline@is_same@} |
| 135 | +我们发现,引用类型与基本数据类型有太多相同之处。比如说,我们可以把引用完全当作变量名来看待,从而我们可以读取或修改内容。还有,我们可以为引用再取一个别名,其效果相当于为原来的变量取一个别名: |
| 136 | +\begin{lstlisting} |
| 137 | + int num, &ref {num}, &rref {ref}; //效果等同于&rref{num} |
| 138 | +\end{lstlisting} |
| 139 | +再看地址,\lstinline@num@ 和 \lstinline@ref@ 的地址也永远相同,它们的内存大小可以用 \lstinline@sizeof@ 求得,这也是相同的。\par |
| 140 | +看了这么多,我们发现,引用好像是一个分身,或者是真假美猴王那样的关系,我们根本分辨不清谁是本体,谁是别名。\par |
| 141 | +那么,变量与引用的类型一样吗?其实是不一样的。\lstinline@num@ 是 \lstinline@int@ 类型无疑,而 \lstinline@ref@ 和 \lstinline@rref@ 都是 \lstinline@int&@ 类型的。这几个名字看似一模一样,但正主还是原变量,六耳也终究不是孙悟空。\par |
| 142 | +那么如何检验类型呢?我们可以用 \lstinline@type_traits@ 库的 \lstinline@is_same@ 来检验之。它是一个类模版,可以接收两个模版参数,并检验它们是否是同一类型。如果相同话,其静态成员 \lstinline@value@ 的值就是 \lstinline@true@;如果不同的话,其静态成员 \lstinline@value@ 的值就是 \lstinline@false@。\footnote{这里出现了很多新概念,比如类模版,模版参数、静态成员等。读者无须知道细节,我们会在后面慢慢道来。} |
| 143 | +\begin{lstlisting} |
| 144 | + //需要包含头文件type_traits |
| 145 | + cout << is_same<int, int>::value << endl; //int与int相同,故输出1 |
| 146 | + cout << is_same<double, long double>::value; //不同,故输出0 |
| 147 | +\end{lstlisting} |
| 148 | +提醒读者,\lstinline@cout@ 输出 \lstinline@bool@ 类型的值时,会默认以整数的形式输出。我们也可以用 \lstinline@cout.setf(ios_base::boolalpha)@ 来让它以布尔值的方式输出。\par |
| 149 | +但是这里有另一个问题:尖括号中只能接收类型信息,我们不能直接把一个变量,或者引用,或者指针塞进去。这时我们就要用到 \lstinline@decltype@ 了。\lstinline@decltype@ 是一个编译时操作,它会解释出一个表达式的类型。 |
| 150 | +\begin{lstlisting} |
| 151 | + int num; |
| 152 | + cout << is_same<decltype(num), int>::value; //将输出1 |
| 153 | +\end{lstlisting} |
| 154 | +因此我们可以用 \lstinline@decltype@ 配合 \lstinline@is_same@ 来判断类型的差异了。\par |
| 155 | +先来看一下 \lstinline@num@, \lstinline@ref@ 和 \lstinline@rref@ 是不是同一个类型。 |
| 156 | +\begin{lstlisting} |
| 157 | + int num, &ref {num}, &rref {ref}; |
| 158 | + cout << is_same<decltype(num), decltype(ref)>::value << endl; |
| 159 | + cout << is_same<decltype(ref), decltype(rref)>::value; |
| 160 | +\end{lstlisting} |
| 161 | +这两句的输出分别是 \lstinline@0@ 和 \lstinline@1@,说明 \lstinline@ref@ 和 \lstinline@rref@ 都是同一个类型的,它们都和 \lstinline@num@ 不是同一个类型的。\par |
| 162 | +接下来我们具体看一下它们三个分别是什么类型。 |
| 163 | +\begin{lstlisting} |
| 164 | + cout << is_same<decltype(num), int>::value << endl; |
| 165 | + cout << is_same<decltype(ref), int&>::value << endl; |
| 166 | + cout << is_same<decltype(rref), int&>::value << endl; |
| 167 | +\end{lstlisting} |
| 168 | +这三个输出的结果全部为 \lstinline@1@,说明 \lstinline@num@ 是 \lstinline@int@ 类型的,而 \lstinline@ref@ 和 \lstinline@rref@ 是 \lstinline@int&@ 类型 的,它们并不相同。\par |
| 169 | +\lstinline@is_same@ 是一个很实用的类型判断工具,我们在后面也会用到它的。\par |
0 commit comments