0%

C++ 中的引用

引用是 C++ 中一个比较神奇的东西。在这之前或者说 C 语言中,一般是使用指针来减少传参所带来的不必要的开销。如函数传递的参数是数组或结构体时,使用指针会省很多事,毕竟传递的是地址。而 C++ 中引用变量的主要用途也是函数传参,子函数直接操作原始数据,而不是其副本,这样处理大型数据结构也会佷便捷。

指针

一维数组

以一维数组为例。众所周知数组名是是数组首元素的地址。因此调用子函数时,主函数传递的是数组首元素的地址,所以子函数接收的是地址,无法预知数组的长度,需要增加额外的参数指明数组元素的数量。

对于函数,一般用 int arr[] 这样的形式指明 arr 接收的是数组,这样的可读性强;换一种方法,因为传递的是数组首元素的地址,而数组首元素为 int 类型,地址是 int* 类型,因此可以用 int* arr 来接收一个数组,但是这样表意不明确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
void sum(int arr[], int len) {
int t = 0;
for (int i = 0; i < len; i++) {
t += arr[i];
}
cout << t;
}
int main (){
int arr[4] = {1, 2, 3, 4};
sum(arr, 4);
return 0;
}

二维数组

升级到二维数组,二维数组的类型本质就是指向『多个 int 组成的数组』的指针,因此参数的形式为 int (*arr)[4],而不是 int* arr[4]

  • 前者是一个『由 4 个指向 int 的指针』组成的数组;即一个数组,数组元素是四个 int 指针;
  • 后者是一个指向『由 4 个 int 组成数组』的指针;即一个指针,指向 4 个 int 的数组。为了更好的可读性,一般声明如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
void sum(int arr[][4], int len) {
int t = 0;
for (int i = 0; i < len; i++) {
for (int j = 0; j < 4; j++) {
t += arr[i][j];
}
}
cout << t;
}
int main (){
// 数组有三个元素,每个元素是数组
int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
sum(arr, 3);
return 0;
}

引用

二维数组的指针是不是感觉有点晕?先来看一下简单的引用:

1
2
3
int a{11};
// 类型是 int&,指向 int 的引用
int& b = a;

这样 ba 就指向了相同的值和内存单元,只是名字不一样。此外,引用必须在声明的时候进行初始化,否则这个变量不知道指向哪个内存单元和值,但是指针可以先声明在赋值。

此外,声明一旦绑定,就无法在修改。可以通过初始化声明来设置引用,不能通过赋值来设置。如下所示的程序,只是对引用 b 进行了赋值,而不是修改引用。

1
2
3
4
int a{11};
int& b = a;
int c{32};
b = c;

引用传参

回到主题,一般将引用用做函数传参时。主函数中的变量名是被调用函数中对应变量的别名,在调用时用实参初始化形参,因此引用参数被初始化为:函数调用时传递过来的实参。如下所示:

1
2
3
4
5
6
7
8
void swap(int& a, int& b) {
int t;
t = a;
a = b;
b = t;
}
int n1{12}, n2{21};
swap(n1, n2);

此外,传递引用时对类型的限制更加严格,以求和函数为例:

1
2
3
4
5
6
7
8
9
void sum(double n) {
cout << n;
}
int a{12};
// 临时变量
sum(a);
sum(6.2);
// 临时变量
sum(a + 6.3);

换句话说,当实参和形参的类型不匹配时,将会生成临时变量传给形参。但是引用则不行,限制相对严格,sum(a + 6.3) 会报错,传递的实参是表达式不是变量,而引用不能绑定到表达式上,且此时不会生成临时变量。

但是当参数为 const 引用时,会创建一个临时的无名变量,临时变量的值初始化为 a + 6.3,而后再将无名变量赋给引用:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
void sum(const double& len) {
cout << len;
}
int main (){
int a{10};
sum(a + 6.3);
return 0;
}

也许你会有疑问,这个时候为什么会生成临时变量?const 为什么合理呢?如果引用参数是 const,两种情况会生成临时变量:

  • 实参类型正确,但不是左值,如 a + 6.3 这样的表达式
  • 实参类型不正确,但可以转为正确类型,如 int 隐式转换为 doubledoubleint 则错误

左值:左值是可以被引用的数据对象,变量、数组、元素等,非左值有字面常量,多项的表达式等。或者说,可以放在赋值语句左侧 and 能访问地址的就是左值,也就是说,赋值语句左侧是可修改的内存块,const 变量也是左值,只是不可修改。

回到原问题,如果形参加上 const 修饰,意思是函数只使用这个值,不修改这个值。即使因类型不匹配生成了临时变量,引用参数引用这个临时变量,都不会造成任何不好的副作用。但此时就是值传递而不是地址传递,因为要用临时变量来存储数值。所以也推荐尽可能使用 const

  • 避免无意修改数据造成结果错误
  • 能更好的接收实参,生成并使用临时变量

返回值为引用

对于传统的调用函数而言,返回结果的这个值被复制到临时位置,也就是产生值的副本,调用程序将使用这个值。如:

1
2
3
4
5
6
7
8
int sum(int len) {
len += 10;
// len 复制到临时位置
return len;
}
int a{10};
// 从临时位置获取值
a = sum(a);

而返回引用的函数实际上是返回被引用变量的别名。返回引用值时,并不产生值的副本。而是将返回值直接复制给接收函数的变量或对象,言简意赅,当函数返回引用类型时,没有复制返回值创建临时变量,相反,返回的是对象本身,并复制到接收变量那里。

对于一个大型的数据结构如结构体,将结构体复制到额外的地址的开销会很大;如果返回引用,将返回的引用的结构体直接赋值给接收值,避免额外的开销。

但是,避免返回指向临时变量的引用,临时变量在执行完毕后会消失,引用会指向乱七八糟的地址,就跟避免指向临时变量的指针一样。有两种解决方法:

  • 使用 new,将数据放到堆区,不过内存模型的坑准备后续开
  • 传递一个额外的参数,传递给函数的引用,将该参数返回。因此返回引用时,要求在函数的参数中,包含有以引用方式需要被返回的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
struct node {
int arr[10];
double value{0};
};
// 返回变量 t 的引用
node& sum(int len, node& n) {
for (int i = 0; i < len; i++) {
n.value += i;
}
return n;
}
int main (){
node t;
// a 被 t 的引用给赋值
node a = sum(5, t);
// 修改 a 不会修改 t
a.value = 13.2;
// a 是 t 的引用,修改 a 也会修改 b
// node& a = sum(5, t);
cout << a.value << " " << t.value;
return 0;
}

此外,非引用函数的返回值类型是右值,这种语句位于表达式的右侧,也无法通过地址访问这个值,也无法放到复制语句的左侧。因为返回值的地址在执行完毕后就消失了,也就是说无法引用。如果一定要引用返回值,将返回值类型声明为引用,这样返回的就是左值,就可以引用。

总结一下:当返回结果需要做为左值时,就要用引用返回。即重载函数的返回结果需要出现在赋值语句左边时,必须用引用返回。如果不用引用返回,那么重载函数的返回结果会是一个临时变量,临时变量是不能放在赋值语句左边的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 错误,右值不能在赋值语句左侧
node sum(int len, node& n) {
for (int i = 0; i < len; i++) {
n.value += i;
}
return n;
}
sum(5, t).value = 12.3;
cout << t.value;

// 正确,返回的引用是左值
node& sum(int len, node& n) {
for (int i = 0; i < len; i++) {
n.value += i;
}
return n;
}
sum(5, t).value = 12.3;
cout << t.value;

如果不想返回的引用被修改,就加 const 修饰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
struct node {
int arr[10];
double value{0};
};
// 返回变量 t 的引用
const node& sum(int len, node& n) {
for (int i = 0; i < len; i++) {
n.value += i;
}
return n;
}
int main (){
node t;
// 将 t 引用的值赋值给 a,所以可以修改 a
node a = sum(5, t);
a.value = 13;
// b 引用 t,不可修改
node& b = sum(5, t);
// 错误
b.value = 14;
return 0;
}
感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章