本篇文章篇幅过长,但是希望读者能一点点边看边思考的看下去。我相信,一定会让你对于引用这一语法有所收获的。
引用
概念
在 C++ 中,引用是一个别名,是对一个已存在的变量提供另一个名称。引用可以被认为是变量的一个别名,通过这个别名可以访问和修改变量存储的数据。引用提供了一种简洁的方式来操作变量,同时避免了指针的一些复杂性。
精简:
1、引用是对已有的变量另外起一个新的名称;
2、新的名称有权限访问和修改被引用的变量。
引用特性
1、引用在定义时必须初始化, 如下:
#include <iostream>
int main()
{
int a = 10;
//int& b; // 编译器报错编译不通过,引用时没有初始化。
int& c = a; // 引用时初始化,编译成功
}
2、一个变量可以有多个引用,如以下,b、c、d都是变量a的引用
#include <iostream>
int main()
{
int a = 10;
int& b = a;
int& c = a;
int& d = a;
}
3、引用一旦引用一个实体,再也不能引用其他的实体。如:
#include <iostream>
int main()
{
int a = 10;
int& b = a;
int c = 6;
//int& b = c; // 编译报错,b已经是变量a的引用,不能再当任何其他的实体的引用
}
定义形式:
引用的使用形式:type& referenceName = originalVariable;
其中,
type: 被引用的变量的类型
referenceName : 被引用的变量的新的名称。
originalVariable: 被引用的变量。
(club:不要忘了类型后面的“ & ”,
C语言中是取地址的操作符。在C++中还是引用的操作符,区别是:
不带类型,在变量、对象前面的 “ & ”,是取地址
带类型的,在变量、对象的前面 “ & ”,是引用
ps:在C++中,定义出来,并且开空间的,都可以叫对象
)
如以下例子:
#include <iostream>
int main()
{
int a = 10; // 定义变量a
int& b = a; // 引用,给变量a起个别名b
return 0;
}
在程序中,定义了一个 int 类型的变量a,并且初始化为10。
然后,对变量a引用,给变量a起了个别名为b。我们可以说 “ b 是 a 的引用 ”,也可以说“ b 是 a 的别名 ”,两者说法本质上是一个意思,通常习惯性说第二种说法,b是a的别名。接着,我们进行以下实验:
(注意:在这个程序中,“b” 是一个引用变量,当它并不是一个独立的变量,而是“a” 的别名)
#include <iostream>
int main()
{
int a = 10; // 定义变量a
int& b = a; // 引用,给变量a起个别名 b
cout << "a: " << a << endl; // 输出打印变量a的值
cout << "b: " << b << endl; // 输出打印变量a的别名,b的值
b = 20; // 更改变量a的别名b的值
cout<<endl; // 换行
cout << "a: " << a << endl; // 输出打印变量a的值
cout << "b: " << b << endl; // 输出打印变量a的别名,b的值
return 0;
}
运行结果:
从控制台的输出打印结果中,我们可以发现以下两点:
① 声明定义了变量a的别名b后,打印 a 和 b 的值,发现变量a的别名b的值,和变量a的值相同。
② 对变量a的别名b的值进行修改后,打印 a 和 b 的值,发现 b 的值变为修改后的值,同时 a 的值也变为了 b 修改后的值。
对此,我们做一个大胆的假设,别名b指向的地址,就是变量a的地址。即 a 和 b 的地址是相同的。是不是呢?通过调试发现如下:
调试结果,证明了我们的假设,成立。
总结:
如图所示,我们在程序中,创建了变量a,同时初始化为10。编译器分配内存地址 0x00f9fafc 给变量a。
然后,我们又对变量a进行引用,起了个别名b。如图所示,a的别名b就是地址0x00f9fafc 的另一个名称。
这里,b对这块地址空间具有访问和更改的权限,因此当我们对别名b的值更改为20时,0x00f9fafc 这块地址上存储的10,也就被更改为了20。所以当我们打印a的值时,输出打印结果也为20。因为变量a就是这块地址空间起初的名称。
至此,对于引用的概念、使用的形式以及原理有了一定的了解。让我们想这么个问题,
引用有什么用处?C++中弄出这么个语法功能作用是什么?
抱着这么些个问题,进入下面的学习,引用能做什么、分别的作用以及效果。
引用做参数
概念
使用引用作为函数的参数。如:
#include <iostream>
void Swap(int& x1,int& x2)
{}
在函数Swap中,参数x1、x2都是引用,当有实参传过来时,x1、x2便是实参的引用。这种传递称作引用传递。
这里,在我学习时便有个疑问? 调用函数时,实参变量传过去给函数形参的是什么?我们一起来思考这个问题。在那之前,我们先回忆一下C语言中值传递的细节。
首先我们知道,在C语言中学习的函数参数传递为,值传递,如下:
#include <iostream>
void Swap1(int x1,int x2)
{}
int main()
{
int a = 10;
int b = 20;
Swap1(a,b);
return 0;
}
函数Swap1被调用时,变量a和b传过去的是变量a和b各自的拷贝副本。Swap1函数会给形参x1和x2分别分配一块空间,形参x1和x2分别接收到是来自实参变量a和变量b各自的拷贝副本。因此在函数中对形参x1和x2进行更改时,并不会对Swap1函数外的变量a和b产生影响( 因为形参和实参地址不同 )。
那么,引用作为函数的形参时呢?调用函数时,实参传过去的是什么?很明显不可能拷贝副本了。因为当函数形参为引用时,我们对形参更改,函数外的实参,也就是调用函数时传入的变量也会被更改,这跟上面的值传递是不一样的。我们对此进行实验:
#include <iostream>
using namespace std;
void Swap(int& x1,int& x2) // int& x1 = a, int& x2 = b;
{
int tmp = x1;
x1 = x2;
x2 = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a,b);
cout << a << " " << b << endl; // 输出打印变量a和b的值
return 0;
}
在上面程序中,我们调用Swap函数,在参数为引用的函数里面将x1和x2的值进行了交换。之后将实参,即变量a和b的结果输出打印。结果如下:
结果显示,变量a和b的数值也发生了交换,这与我们前面的学习不谋而合。这是必然的,因为函数Swap的参数x1和x2分别是传过来的实参变量a和b的引用。即a是x1的别名,b是x2的别名。
根据前面的学习,我们知道对x1和x2进行更改时,a和b也会被更改。
如若还想更进一步验证,不妨利用调试,查看变量a和b以及形参x1和x2。如我们上面的学习思路正确的话,变量a 和形参 x1 的地址一致,变量b和形参x2的地址一致。调试结果如下:
验证正确。
回到我一开始提到的问题,函数调用时,变量a和b传过去的是什么?形参x1、x2分别引用的是什么?
首先,排除值传递中的传递拷贝副本。经过我查找,我得到了以下的观点:
在C++中,当声明一个变量时,变量在被内存中被分配了一个地址。这个地址就是该变量的引用。当我们调用函数使用引用传递时,变量传递给函数的,实际上就是将变量的引用( 也就是地址 )传递给了函数参数。
所以在上面程序中,变量a和b是已经存在的变量,在内存中有着相应的地址,这个地址也可以叫做它们的引用。当调用Swap函数时,变量a和b传过去的就是各自的引用,也就是各自的地址。函数形参中接收到实参时,可以理解为int& x1 = a (传过来的是a的引用/地址) , int& x2 = b(传过来的是b的引用/地址)。这个过程叫做引用传递。
至此,我们知道了函数参数作为引用时,变量传递过程是怎样进行。下面继续学习引用的作用。
作用
一样的,拿上面举的例子:
#include <iostream>
using namespace std;
void Swap(int& x1,int& x2) // int& x1 = a, int& x2 = b;
{
int tmp = x1;
x1 = x2;
x2 = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a,b);
cout << a << " " << b << endl; // 输出打印变量a和b的值
return 0;
}
引用的作用:
①:可以作为输出型参数。所谓的输出型参数就是,一种在函数中用于返回结果的参数。这样的参数允许函数修改调用者提供的变量的值,以便将计算的结果传递回调用者。输出型参数通常通过引用或指针传递,使得函数能够直接修改调用者提供的变量。
我们发现,引用作为函数参数时,作用和指针有点类似。通过外部的函数Swap能够更改main函数内的变量。但是切勿将引用和指针等价了,二者是有所区别的,后面单独阐述二者的区别。
②:引用传递 比 值传递效率更高。因为引用传递比起值传递,少了拷贝副本。我们对此做个实验,见如下代码:
#include <iostream>
using namespace std;
#include "time.h"
struct A {
int a[10000];
};
// 结构体A的大小是40000 byte
void TestFunc1(A a) {} // 值作为参数
void TestFunc2(A& a) {} // 引用作为参数
int main()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; i++)
TestFunc1(a);
size_t end1 = clock();
//以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; i++)
TestFunc2(a);
size_t end2 = clock();
cout << "TestFunc1(A) - time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&) - time:" << end2 - begin2 << endl;
//以引用作为函数参数 比 以值作为函数参数 性能更高
return 0;
}
}
运行结果:
通过控制台输出打印可以看到,**引用传递** 比 **值传递** 的性能要高得多。下面简单分析一下程序流程。
程序分析:
1、我们在程序中,定义了一个结构体A,结构体A里面申请了一个int类型的数组,数组有10000个元素。整个的结构体A大小为40000字节。
2、定义了两个函数,一个使用变量参数,一个使用引用参数。
3、然后在main函数中,分别记录两个函数传入结构体变量a时,多次调用各自调用的时间。因为值传递时需要拷贝副本,而引用传递直接传的引用(也就是地址)。所以引用传递的耗费的时间要比值传递耗费的时间要少得很多。
至此,对于引用作为参数时的作用的学习大致完成,但是在结束之前,不知大家有没有和我一样有一个疑问?
问题:
那就是前面引用概念上说到,引用定义时必须初始化,那为什么作为函数参数时不需要呢?大家可以想一想,文章结尾给出答案。
效果
如上面所举的例子:
#include <iostream>
using namespace std;
void Swap(int& x1,int& x2) // int& x1 = a, int& x2 = b;
{
int tmp = x1;
x1 = x2;
x2 = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a,b);
cout << a << " " << b << endl; // 输出打印变量a和b的值
return 0;
}
运行效果:
我们可以通过引用传递,达到跟指针一样的效果。将变量a和b传参给外部函数Swap,在外部函数中实现变量的更改。
下面来看看,引用作为返回值的情况。
引用做返回值
概念
引用作为函数的返回值。如:
int& func(int i) // 返回值 int& 为引用
{
return i;
}
其中函数func的返回值类型为,int& ,也就是引用。
作用
首先,并不是所有的返回值都适合作为引用的。有两种情况适合引用作为返回值,
① 返回值是全局变量
② 返回值时静态变量
在讲述这两种情况前,我们先对了解一下其他情况的引用返回值的弊端,这样更有利于我们的理解。如:
#include <iostream>
using namespace std;
// 2、引用做返回值
int Count1()
{
int n = 0;
n++;
return n; // 传值返回 int tmp = n;
}
int& Count2()
{
int m = 0;
m++;
return m; // 传引用返回 int& tmp = n;
}
int main()
{
// 传值会产生一个拷贝变量(临时变量具有常性),传引用则不会
// int& r1 = Count1();//编译不通过,因为Count1返回的是拷贝了n的值的临时变量,
//临时变量具有常性,只能读,此处r1为int类型可读可写,属于权限放大
const int& r1 = Count1();
int& r2 = Count2();
return 0;
}
我们先来对以上程序进行分析:
1、在程序中,定义了两个函数Count1 和 Count2。两个函数各自都返回了函数内定义的局部变量。其中Count1的返回是值返回,Count2的返回是引用返回。
2、我们在main函数中有三行代码,我们逐行进行分析,第一行代码:
int& r1 = Count1();
这行代码中,会先调用函数Count1,函数Count1返回了int类型的变量,变量存储的是函数内局部变量n的值,然后给这个变量做引用,即起个别名为r1。结果显示编译器会报错,编译不通过。
原因:首先我们要清楚,函数Count1中,return n 时,返回的并不是局部变量n自身,而是编译器创建的一个临时变量,我们假如临时变量记为tmp。临时变量tmp的类型为函数返回值的类型int,然后编译器会将局部变量的值拷贝给临时变量tmp,即 int tmp = n。最终效果是函数想要返回局部变量n的值,但是实际上返回的是临时变量tmp,tmp中拷贝了n的值。
所以这行代码会演变为:int &r1 = tmp;r1是临时变量tmp的别名。这将出现两个问题:
①、临时变量具有常性( 只能读不能写 ),而这是引用r1的类型是int,是可读可写的。引用赋值时有权限大小的问题,现在我们将一个只能读不能写的类型 赋值给 可读可写的类型,这将会导致权限的放大。而权限可以缩小,但是却不能放大,因此编译器会报错不通过,这时问题一。
②、还有一个问题需要注意,那就是临时变量tmp作用完后是会被编译器销毁的,也就是空间地址使用权会被编译器收回。而这时引用r1依旧是“指着”这块地址。这时不管我们是利用r1去访问还是更改这块空间上的数值,都将导致程序崩溃,因为非法访问了属于编译器的空间地址。因此r1这时是一个危险引用,也叫做悬空引用(dangling reference),这是问题二。
如何解决这两个问题?
问题一,权限放大的问题,我们可以将引用的权限一开始便缩小,与右边的权限做匹配,也就是第二行代码:
const int& r1 = Count1;
此时引用r1的类型是 const int,和函数返回类型的权限一致,便能解决权限的问题。
问题二:危险引用的问题,解决方法就是,让函数内的局部变量n,变为静态变量( 在前面加个static );或者将局部变量n改为全局变量,或者接收返回值时不适用引用,这留到待会再讲。
我们接着来看第三行代码:
int& r2 = Count2();
函数Count2内返回的是局部变量m,一样的编译器会创建一个临时变量tmp,临时变量类型为int& ,即 int& tmp = m;看到这里我们知道,此时临时变量tmp是局部变量m的引用,是m的别名。因此第三行代码会转变为: int& r2 = tmp; 即给临时变量tmp起个别名r2,而临时变量tmp本就是局部变量m,因此相当于有给Count2内的局部变量m起了个新的别名r2。即局部变量m有两个别名,tmp和r2。这时会有权限的问题吗?不会,因为tmp是m的别名,相当于tmp就是m,r2是tmp的别名,就相当于又给m的起一个别名,而局部变量m的类型是 int 可读可写的类型,因此不存在权限放大的问题。但是 ,依旧会存在危险引用的问题。不管是创建的临时变量tmp( 但是是作为m的引用了 ),还是 r2 ,都是m这块地址上的名称。访问操作的都是这块空间地址,但是局部变量出了作用域后,便会被编译器销毁,即收回空间地址。因此和上面问题二一样的问题,危险引用的问题。
至此,对于函数返回值作为引用时,返回值不是静态变量或全局变量的情形做了一个大概的了解。下面学习一下,返回值适用当引用的情况,即返回的变量是静态变量或者全局变量。
返回全局变量时的,返回值引用
看以下示例:
#include <iostream>
using namespace std;
int global_val = 0; // 全局变量,初始化为0
int& func()
{
global_val = 10;
return global_val;
}
int main()
{
cout << global_val << endl;
func();
cout << global_val << endl;
int& f1 = func();
f1 = 20;
cout << global_val << endl;
return 0;
}
运行结果:
程序分析:
在程序中,声明定义了一个全局变量 global_val 并且初始化为0。func函数里将全局变量global_val 赋值为10。
在main函数中,一开始输出全局变量global_val 的值;然后调用函数func,再输出全局变量的值;再给func函数的返回值做个引用f1,对f1赋值为20,然后输出打印全局变量的值。
输出打印结果为0,10,20。
0 和 10 都很容易理解, 我们着重讲一下20的由来:
根据前面的学习,我们知道在func函数中,返回全局变量global_val 时,实际上为 int& tmp = gloval_val ,即返回的是 gloval_val的引用,一个别名。f1 又是tmp的引用,而tmp是全局变量的别名,再给tmp做引用起个别名,相当又全局变量global_val的起了一个新的别名。这时全局变量global_val这块地址上有三个名称,分别是global_val 、 tmp 、 f1。这时我们对f1进行更改,相当在更改global_val全局变量这块地址上的数值,因此输出打印global_val 变量地址上的数值时,为更改后的20。
这便是返回值是全局变量时,返回值做引用的效果。下面看看静态变量是怎么样的。
返回静态变量时,返回值引用
看以下示例:
#include <iostream>
using namespace std;
int& func1()
{
static int val = 0;
return val;
}
int main()
{
int& b = func1();
b = 20;
func1();
return 0;
}
为了方便观察,我们利用调试,查看函数内局部的静态变量val。
调试结果:
根据前面的知识,我们能知道,函数func1返回值是编译器创建的临时变量tmp,即int& tmp = val。tmp是静态局部变量val的别名,又对tmp进行引用,int& b = tmp。相当于又给静态局部变量val起了个新的别名b。对b进行更改时,其实改的就是静态局部变量val地址上的值。而静态局部变量是存储在静态区的,因此并不会因为出了func函数的作用域而被销毁。因此可以放心的引用。
总结:
当使用引用作为返回值时,需要注意,返回值的作用域问题,会不会又危险引用的情况,有则要避免。当返回值的变量不受函数作用域的影响时,能尽量用引用作返回值尽量使用。因为根据上面的学习,我们知道利用引用作返回值时,比起值返回而言少了拷贝副本的流程,这无疑是更好的选择。
下面来看看引用和指针的区别,或者说引用相对于指针的优势在哪。
引用比起指针的区别/优势
1、引用概念上定义一个变量的别名。指针存储一个变量地址
2、引用在定义时必须初始化( 作为函数时可以不用初始化 )。指针没有要求
3、引用在初始化时引用一个实体后,就不能再引用其他实体。而指针可以在任何时候指向任何一个同类型实体
4、没有NULL引用,但有NULL指针
5、在sizeof中含义不同:引用结果为引用类型的大小。但指针始终是地址空间所占字节个数( 32位平台下占4个字节 )
6、引用自加即引用的实体增加1。指针自加即指针向后偏移一个类型的大小
7、有多级指针,但是没有多级引用
8、访问实体方式不同,指针需要显式解引用。引用编译器自己处理
9、引用比指针使用起来相对更安全
(
语法概念上:b是a的别名,不开空间
底层实现上:(去汇编里查看,调试右键有个‘转汇编 ’选项),引用跟指针的实现是一样的,是存地址去实现的
所以,语法是语法,底层是底层
)
下面是我罗集到的信息,就不多加修改直接给大家作参照思考了:
在 C++ 中,使用引用和使用指针都允许函数修改调用者提供的变量。但是,引用和指针之间有几个关键的区别,这些区别可以影响代码的可读性、安全性和使用方式。
- 空指针问题:
①.引用不能为 null,它必须始终指向一个合法的对象。这有助于避免由空指针引起的错误。
②.指针可以为空(null),这意味着在使用指针之前必须检查指针是否为 null,以避免在尝试访问空指针时出现未定义的行为。
- 语法和使用:
③.使用引用时,不需要像指针一样使用解引用运算符(*)来访问所引用的对象。
④.指针需要通过解引用运算符来访问所指向的对象。
- 安全性:
⑤.引用通常被认为比指针更安全,因为它们在创建时必须被初始化,并且无法被重新指向其他对象。一旦引用被初始化,它将始终引用同一个对象。
⑥.指针可以在程序运行时重新指向不同的对象,这可能导致错误,例如在没有适当检查的情况下修改了不应该修改的对象。
- 传递参数:
⑦.传递参数时,引用更直观且更易读,因为函数参数的语法更简洁。
⑧.指针需要额外的解引用运算符和取址运算符,可能使得代码看起来更复杂。
综上所述,引用通常更安全且更易用,因为它们在语法上更简洁,不会为空,并且不会被重新指向其他对象。然而,在某些情况下,指针提供了更灵活的操作,例如动态分配内存或者需要在函数中传递空值的情况。选择使用引用还是指针取决于特定的情况和需求。
结局彩蛋:
回答一开始留的问题:引用定义时必须初始化,那为什么作为函数参数时不需要呢?
答:
当引用作为函数参数时,不要求立即初始化。这是因为函数参数的引用,会在函数调用时被绑定到传递给它的对象上。也就是说,函数参数的引用声明时定义时虽然没被初始化,但是在函数被调用时会被绑定到相应的实参。
我的理解是,编译器在编译时,检查语法的时候,对引用会强制要求一定要有绑定的对象。对于绑定到创建的变量的引用时,编译器编译到时发现引用没有初始化绑定对象就会立马报错。而对于函数来说,如果没有被调用,那么编译器便不会去编译检查函数的语法( 虽然函数写了,但是没用到程序没用到时,编译器编译的时候是不会白费功夫去检查的 ),也就不存在引用没对象的问题。
而当函数被调用时,编译器检查语法的时候,这时函数的参数的引用又自动绑到传过来的实参上了,同样的也就不会出现引用没绑定对象的情况
关键:编译(检查语法)时,引用必须绑定对象
以上便是我个人的见解,如有不对,可以点拨改正。