Skip to content

Commit 0c92308

Browse files
committed
Updated to Chapter 5, Section 3
1 parent e002e85 commit 0c92308

File tree

8 files changed

+220
-43
lines changed

8 files changed

+220
-43
lines changed

Structure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@
312312

313313
不过要强调下,怎么理解它们的定义语法。这对后面指针和数组的关系来说很重要。
314314

315-
### (左值)引用参数传递
315+
### (左值)引用与引用参数传递
316316

317317
> 泛讲篇不讲右值引用。
318318
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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

main.aux

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,20 @@
113113
\@writefile{toc}{\contentsline {section}{\numberline {5.2}``常量指针''``指向常量的指针''}{92}{section.5.2}\protected@file@percent }
114114
\@writefile{lof}{\contentsline {figure}{\numberline {5.8}{\ignorespaces 修改指针的指向/修改指针的内容}}{92}{figure.5.8}\protected@file@percent }
115115
\@writefile{lof}{\contentsline {figure}{\numberline {5.9}{\ignorespaces \lstinline @const@ 限制了什么?}}{93}{figure.5.9}\protected@file@percent }
116+
\@writefile{toc}{\contentsline {section}{\numberline {5.3}(左值)引用与引用参数传递}{94}{section.5.3}\protected@file@percent }
116117
\gdef \LT@ii {\LT@entry
117118
{1}{42.00002pt}\LT@entry
118119
{1}{136.19997pt}\LT@entry
119120
{1}{117.00002pt}\LT@entry
120121
{1}{52.00002pt}\LT@entry
121122
{1}{82.00002pt}}
122-
\@writefile{toc}{\contentsline {part}{精讲篇}{97}{part*.68}\protected@file@percent }
123-
\@writefile{toc}{\contentsline {chapter}{\numberline {附录 A\hspace {.3em}}C++运算符基本属性}{97}{appendix.A}\protected@file@percent }
123+
\@writefile{toc}{\contentsline {part}{精讲篇}{101}{part*.72}\protected@file@percent }
124+
\@writefile{toc}{\contentsline {chapter}{\numberline {附录 A\hspace {.3em}}C++运算符基本属性}{101}{appendix.A}\protected@file@percent }
124125
\@writefile{lof}{\addvspace {10.0pt}}
125126
\@writefile{lot}{\addvspace {10.0pt}}
126-
\newlabel{ch:appendix_A}{{A}{97}{C++运算符基本属性}{appendix.A}{}}
127-
\@writefile{lot}{\contentsline {table}{\numberline {A.1}{截至C++17的所有运算符}}{97}{table.A.1}\protected@file@percent }
128-
\newlabel{tab:A-1}{{A.1}{97}{截至C++17的所有运算符}{table.A.1}{}}
127+
\newlabel{ch:appendix_A}{{A}{101}{C++运算符基本属性}{appendix.A}{}}
128+
\@writefile{lot}{\contentsline {table}{\numberline {A.1}{截至C++17的所有运算符}}{101}{table.A.1}\protected@file@percent }
129+
\newlabel{tab:A-1}{{A.1}{101}{截至C++17的所有运算符}{table.A.1}{}}
129130
\gdef \LT@iii {\LT@entry
130131
{1}{33.67001pt}\LT@entry
131132
{1}{33.81001pt}\LT@entry
@@ -137,11 +138,11 @@
137138
{1}{26.72002pt}\LT@entry
138139
{1}{27.283pt}\LT@entry
139140
{1}{86.44002pt}}
140-
\@writefile{toc}{\contentsline {chapter}{\numberline {附录 B\hspace {.3em}}ASCII码表(0到127)}{99}{appendix.B}\protected@file@percent }
141+
\@writefile{toc}{\contentsline {chapter}{\numberline {附录 B\hspace {.3em}}ASCII码表(0到127)}{103}{appendix.B}\protected@file@percent }
141142
\@writefile{lof}{\addvspace {10.0pt}}
142143
\@writefile{lot}{\addvspace {10.0pt}}
143-
\@writefile{lot}{\contentsline {table}{\numberline {B.1}{33个ASCII控制字符}}{99}{table.B.1}\protected@file@percent }
144-
\newlabel{tab:B-1}{{B.1}{99}{33个ASCII控制字符}{table.B.1}{}}
144+
\@writefile{lot}{\contentsline {table}{\numberline {B.1}{33个ASCII控制字符}}{103}{table.B.1}\protected@file@percent }
145+
\newlabel{tab:B-1}{{B.1}{103}{33个ASCII控制字符}{table.B.1}{}}
145146
\gdef \LT@iv {\LT@entry
146147
{1}{33.67001pt}\LT@entry
147148
{1}{33.81001pt}\LT@entry
@@ -155,11 +156,11 @@
155156
{1}{33.67001pt}\LT@entry
156157
{1}{33.81001pt}\LT@entry
157158
{1}{26.72002pt}}
158-
\@writefile{lot}{\contentsline {table}{\numberline {B.2}{95个ASCII可打印字符}}{100}{table.B.2}\protected@file@percent }
159-
\newlabel{tab:B-2}{{B.2}{100}{95个ASCII可打印字符}{table.B.2}{}}
160-
\@writefile{toc}{\contentsline {chapter}{\numberline {附录 C\hspace {.3em}}相关数学知识}{101}{appendix.C}\protected@file@percent }
159+
\@writefile{lot}{\contentsline {table}{\numberline {B.2}{95个ASCII可打印字符}}{104}{table.B.2}\protected@file@percent }
160+
\newlabel{tab:B-2}{{B.2}{104}{95个ASCII可打印字符}{table.B.2}{}}
161+
\@writefile{toc}{\contentsline {chapter}{\numberline {附录 C\hspace {.3em}}相关数学知识}{105}{appendix.C}\protected@file@percent }
161162
\@writefile{lof}{\addvspace {10.0pt}}
162163
\@writefile{lot}{\addvspace {10.0pt}}
163-
\@writefile{toc}{\contentsline {section}{\numberline {C.1}进制与进制转换}{101}{section.C.1}\protected@file@percent }
164-
\@writefile{toc}{\contentsline {chapter}{跋}{103}{appendix*.69}\protected@file@percent }
165-
\gdef \@abspage@last{110}
164+
\@writefile{toc}{\contentsline {section}{\numberline {C.1}进制与进制转换}{105}{section.C.1}\protected@file@percent }
165+
\@writefile{toc}{\contentsline {chapter}{跋}{107}{appendix*.73}\protected@file@percent }
166+
\gdef \@abspage@last{114}

0 commit comments

Comments
 (0)