1. 概述
上篇我们讨论了指针,这篇我们来了解了解C++中的引用。如果你还没有看上篇的指针,强烈推荐你先看一下,因为引用实际上只是指针的扩展。所以你需要了解,至少在基本层面上,了解指针是如何工作的,才能更好的理解这个篇章。
指针
和引用
在C++甚至其他语言中被大量提及的两个关键字。本篇中说的指针和引用,计算机现在用它们做的事情几乎是一样的。从语义上讲,我们如何使用和书写他们有一些细微的区别。但根本上,引用通常只是指针的伪装,引用只是在指针上的语法糖,让指针更容易阅读和理解。引用基本上就和听起来一样,是一种我们引用现有变量的方式。不像指针,你可以创建一个新的指针变量,然后设置他等于空指针或其他类型的指针,指针可以设置为0。但引用不行,因为引用必须"引用"已经存在的变量,引用本身并不是新的变量,它们不占用内存,它们没有真正的内存空间,它们不像典型的变量,因为它们是作为一个变量的引用。
2. 案例
下面开始案例
1. 准备项目
1. 引用
我们有一个简单的项目,项目中有一个main.cpp文件,文件内容如下

现在,我们创建一个int型的变量a,并给它赋值为5。如下

变量a是一个整数,如果我想给这个变量创建一个引用
,我可以输入变量的类型然后后面紧跟着&符号
,如int&
。
2. '类型+&' 与 '&+变量' 的区别
arduino
'类型+&':引用声明
'&+变量':取变量内存地址
注意这个&
符号实际上是变量声明的一部分
。在上篇C++指针中,我们了解,如果我们创建一个指针,我们可以使用一个&
运算符来实际获取现有变量的内存地址
,如int* ptr = &a;
,&a
是获取变量a的内存地址
。在int&
这里不同,因为这里的&
符号实际上是类型的一部分,它实际上不在现有变量的旁边。int&
中的&
号是类型的一部分,所以要注意这个区别。因为有很多人只要看到&
号,他们就认为都是引用
亦或者认为都是取地址
。所以&
号具体是什么取决于上下文。像这样int&
,&
在类型的旁边,所以它是一个引用。
好,我们继续,我们给这个引用取名ref,并赋值为a,int& ref = a;

就是这么简单,我们只是让引用ref等于一个现有变量a。
所以我们现在所做的就是创造了一个叫做别名的东西
,因为这个ref,它并不是一个真正的变量,只是一个引用
,ref"变量"实际上并不存在,它只存在于我们的源代码中。如果我们现在编译我们的代码,我们不会得到两个变量a和ref,我们只会得到变量a。
我们现在能做的是,可以使用ref就像是使用a一样,如果我们设ref等于2,然后我们来打印a,如下

F5运行程序

可以看到打印a的值是2,我们通过给ref赋值等于2,改变了a的值。因为不管出于什么目的,ref都是a,我们给a记了个别名,在这种情况下,我们的引用不是一个指针或类似的东西,编译器不需要实际创建一个新变量。如果你编译代码,这个代码ref = 2;
就相当于你设置了a=2;
。这只是我们可以在源代码中编写,以使我们的生活更简单的东西,如果我们现在想给一个变量起别名的话。
让我们尝试更复杂的东西。
3. 值传递、指针传递和引用传递
1. 值传递
假设我想写一个函数,void Increment(int value)
,函数体中递增value的值。如果我们这样写这个函数,接收到参数是一个值。然后修改main函数代码,我们在main函数中调用Increment函数,将a作为参数传递进去,只是通过值传递它,看看会发生什么

在函数Increment的接收参数int value
中,可以看到,我们并没有把接收的参数把它作为一个指针int* value
或者一个引用int& value
或者类似的东西传递。
这里会拷贝a变量的值5,复制到函数Increment中,复制会创建一个全新的变量,就像这样写一样int value = 5;

这就是将要发生的事情。我可以证明上面这点,恢复Increment函数。

F5运行程序

可以看到,控制台实际打印的是5,就是变量a的初始值。没有改变。因为Increment函数是按值传递,并没有真正的修改变量a的值,而是在函数Increment自己创建了一个变量来递增。
2. 指针传递
我需要做的是通过引用来传递变量,这样变量a才会递增。因为我们真正想做的是影响这个a变量。那么我们应该怎么做呢?如何通过将这个变量传递到函数中来修改它呢?
上次我们讨论了指针,还记得指针就是我们的内存地址吧,所以从理论上讲,我们不把将实际值5传递给函数,而是可以把变量a的内存地址传递过去。因为我们可以在这个函数中做的是,我们可以查找那个内存地址,看到值5,然后修改那个内存地址,我们可以写入该内存地址,因为我们已经将该内存地址传递给了函数。
下面来看下使用指针的方式
首先我们修改Increment函数的参数,修改为int* value
就收一个指针。

在main函数中,调用Increment函数传入的是变量a的内存地址,而不仅仅是值5,可以用&a
取到变量a的内存地址Increment(&a);

接下来,就是逆向引用*
Increment函数接收的指针value,*value
,这样我们就可以直接写入内存。

这里是先逆向引用*
指针value,而不是直接对指针value自增。如果不加逆向引用*
,那就是直接对地址进行递增,然而,指针只是一个整数,如果我们不再value前面加一个*
号,没有dereference操作符*
,那么内存地址就会递增,而不是内存中实际的值。(*value)++;
这里加括号,因为不加括号,因为操作的顺序,它会先做value++
增量运算,然后再做逆向引用,和我们想要的相反。我们想要的是先逆向引用获取到值然后再递增。
F5运行程序

可以看到打印了6。现在符合了我们的预期。我们成功通过指针将变量传递到一个函数中。
3. 引用传递
然后本篇是关于引用的。如果使用引用,我们可以做的比刚刚更加简单,用更少代码和更少的修饰语法。
所以我们能做的就是重写这个Increment,用引用int& value
来代替指针int* value
,这意味着我们不需要用到逆向引用了。在main函数中,我们不需要传递a的内存地址,只需要传递a即可

由于它是通过引用传递的,我们基本上重写了代码,但做了完全相同的事情。当它编译时,它和我们之前写的完全一样,然而,这一次,我们的代码看起来更漂亮,这也是唯一的区别了。
我们F5运行代码

这就是引用的全部真相了。它们只是语法糖,对于引用,没有什么是指针不能做的。指针就像引用,但指针更有用,更强大。然而如果你可以使用引用,就像上面例子那样,那么就一定要使用引用,因为引用会让代码更加简洁和简单。而不是像指针,那么麻烦,而引用使得你的代码干净得多。
4. 引用一旦声明赋值,就无法引用其他变量
关于引用,我想提到的另一件重要的事情是,一旦你声明了一个引用,你不能改变它引用的东西。假设我有两个整数a和b,a设为5,b设为8,int a = 5;
int b = 8;
,然后我要做的是声明一个引用,被设为a的引用 int& ref = a;
,然后可能稍后在我的代码中,用我们的引用ref去引用b,我们可以这样做吗?

答案是否定的,我们不能这样做。在这个例子中,我们创建了一个对变量a的引用ref,当我们决定把这个引用ref设为b时,意思是a的值被赋值为b的值,也就是8,也就是ref = b;
,执行完后,a等于8,b等于8。这就是我们会得到的。

F5运行程序

可以看到,如果我们给变量a的引用ref赋值变量b,这会导致变量a变为8,然后Increment(a);
函数变量a自增1,所以控制台输出9。也就是说,我们并没有改变引用实际引用的变量,ref一直引用的都是a。
这也意味着当我们声明一个引用时,我们需要把它进行赋值,且不能不赋值

如果不赋值,编译器会给出提示,引用需要一个初始化声明,当我们声明一个引用时,我们必须马上给它赋值。因为引用必须引用一些东西,记住引用不是一个真正的变量,只是一个引用。

在这个例子中,如果我真的需要实现更改引用的功能,我们该怎么做?
5. 指针变量可更改指向内存地址
如果我真的想改变ref引用的东西,正如我之前说的,引用不是一个真的变量,所以我们需要创建变量。我们可以首先设置这个变量来指向
a,然后,我们更改为指向
b,注意,我在这里说的是指向
,所以这里指的是指针。
所以我们来改下代码

我们把int& ref
引用改为int* ref
指针,我们一开始可以把指针ref设置为变量的内存地址(&a
),也就是int* ref = &a;
。当我想把指针变量ref的指向变成b时,ref = &b;
,指针变量ref指向了b。
还记得吧,要想改变指针指向的值,我们需要先逆向引用 *
这个指针*ref
,然后给它赋值。在这个例子中,我们让a等于2,让b等于1,然后打印变量a和变量b,如下

F5运行程序

可以看到打印出来的是2和1。