引用是C++中一个非常重要的一个概念,首先要告诉大家的是C++中的引用类比的是C语言中的指针。我们知道C++在语法上完善了许多C语言的不足之处,而引用就是为了完善指针而提出的一个新概念。有了引用的概念之后,除非在一些特定的情况下会使用指针外,其它大多数情况下都可以使用引用来解决。
本篇文章博主将详细的为大家讲解引用的用法,并会在最后为大家总结指针和引用的区别(这是面试中出现频率极高的考点)。
下面开始介绍。
一开始我们先来看何为引用?
引用有一个特别叫法叫“取别名”,用生活中通俗一点的话来说就是取外号。
举一个例子:水浒传中的李逵,在家称为"铁牛",江湖上人称"黑旋风"。
李逵,铁牛,黑旋风这三个名字表示的都是同一个人。
引用的概念:
在C++中引用不是新定义一个变量,而是
给已存在变量取了一个别名
,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
。
引用的定义方法:
类型& 引用变量名(对象名) = 引用实体。
下面我们来看引用是如何来定义的:
void test()
{
int a = 10;
int& b = a; //<=== = 定义引用类型
printf("a = %d, b = %d\n", a, b);
a = 20;
printf("a = %d, b = %d\n", a, b);
b = 30;
printf("a = %d, b = %d\n", a, b);
printf("a的地址:%p\n", &a);
printf("b的地址:%p\n", &b);
}
上述代码就是一个简单的引用过程,首先定义一块整型空间的变量名为a, 然后又给这块空间取别名叫b,这样a和b就同时引用这一块引用空间。
'&'符号我们在C语言部分也曾经接触过,为取地址符。这里注意,如果写成"&b"就是取变量b的地址,写成"int& b"则是对变量b进行引用。
下面我们来看代码的运行结果:
可以看到a和b的地址是一样的,它们指向的是同一块空间。改变a的值b也会跟着变,改变b值a的值也会发生改变。
注意:引用类型和引用实体必须是同种类型的
下面我们来看几个引用的特性
void test()
{
int a = 10;
int& b;
int b = a;
}
这段代码中,我可以不可以先定义一个引用,然后再把它初始化为变量a?
来看运行结果:
可以看到编译报错,提示必须对引用对象进行初始化。所以记住,引用在定义的时候必须要初始化。
void test()
{
int a = 10;
int& b = a;
int& c = a;
int& d = a;
printf("&a == %p\n", &a);
printf("&b == %p\n", &b);
printf("&c == %p\n", &c);
printf("&d == %p\n", &d);
}
我们可以对一个变量进行多次引用,这些引用相互之间是不会发生冲突的,他们都指向同一块空间。
void test()
{
int a = 10;
int& b = a;
int c = 10;
int& b = c;
}
这里我能不能先让变量b引用变量a,然后再用变量b转而引用变量c呢?
所以记住,引用一旦引用一个实体之后,就不能再引用其它实体了。
常引用部分我将会通过几个场景来为大家讲解。
void test()
{
const int a = 10;
int& b = a;
}
问题:这段代码中的引用有没有错误?
简单观察本段代码,我们发现这段代码中的初始变量a是用const修饰的常变量。
其实也不难理解,我原来变量a是被const修饰的常变量,这也就意味着这块空间里的值是不能被修改的。而你所用的引用变量b仅仅是int类型的,这也意味着该引用空间的值是可以被修改的。这就是我们常说的权限被放大从而导致错误。
改正:
void test()
{
const int a = 10;
const int& b = a;
}
void test()
{
int a = 10;
const int& b = a;
}
问题:这段代码的引用有没有问题?
这段代码和上段代码的区别就是,它的原变量a是int类型的,而引用变量b是被const修饰的。
这里我们也就可以得出一个结论:
引用在初始化的时候,权限可以缩小,但是不能被放大。
void test()
{
const int& a = 10;
}
首先这段代码是正确的,这里想告诉大家,引用也可以引用常量,只要在前面加上const修饰即可。
void test()
{
double a = 3.14;
int b = a;
}
问题:这段代码有没有语法错误?
这是一段C语言代码,代码中把浮点型变量a的值赋值给了整型b。
运行结果:
可以看到编译结果并没有出错,仅仅是报了一个警告说会有精度的缺失,那说明这样的赋值在语法上是完全可行的。
再来看这样一段代码:
void test()
{
double a = 3.14;
int& b = a;
}
这段代码是让整型b来引用浮点型a,前面我们看到整型变量可以接手浮点型变量赋值,那么整型变量可不可以引用浮点型变量呢?
运行结果:
可以看到这里输出报错,提示的错误信息是类型不匹配。那么为什么赋值可以而引用确不可以呢?下面我就来介绍此处存在的一些“细节”。
下面我来向大家展示浮点型赋值给整型的实际过程。
上图为浮点型与整型赋值过程示意图,可以看到将浮点型变量a赋值给整型变脸b的时候,并不是直接将a内存里的数据拷贝过去。这也不难理解,我们知道整型和浮点型的二进制存储规则是不同的,如果直接拷贝数据肯定会出错。
这里实际的拷贝过程是,计算机先通过算法把浮点型变量的整型部分读取出来存放在一块临时空间中,然后再把临时空间里的数据赋值给整型b。
所以实际赋值过程中,变量b和变量a并没有直接接触,实际是通过临时空间来充当中间变量进行数据转换的。
同理,在引用的时候,整型变量b实际引用的不是浮点型变量a所在的空间,而是引用临时空间的内存。所以为什么不能用整型变量来引用?这是因为临时变量是具有常性的,因此临时变量的值是不能被改变的。如果整型变量引用成功了这块具有常性的临时空间,就犯了上面提到的权限放大错误。
如果真的要引用,可以这样写:
void test()
{
double a = 3.14;
const int& b = a;
}
将整型变量b用const修饰称为常变量,这样就不会出现权限放大了。
这里我们还可以通过观察一下a,b变量的地址来验证我上面的说法。前面我曾介绍过,变量和变量的引用是指向同一块空间的,那么这里是否依旧如此呢?
可以看到虽然我们用b引用了a,但是两个变量指向的并不是同一块空间。这里b所指向的空间就是我刚刚提到的临时空间。这个细节确实是c++语法中一个比较复杂的地方,希望我的讲解大家可以理解。
引用做参数的效果和指针做参数有些类似。
先看下面这段代码
void Swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
如果要交换两个变量的值,这样传参能不能达到效果?
显然是不能的,C语言部分我们肯定学过,在函数内部改变形参的值,实参是不会发生任何改变的。
所以C语言阶段要交换两个变量的值我们通常是这样来写的:
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
把变量的地址作为参数传递过去,然后通过对指针的解引用来改变实参的值。
这里要告诉大家的是,用引用来做参数可以达到同样的效果:
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
这里的x和y是实参的引用,我们知道一个变量的引用和原变量是指向同一块空间的,所以在函数内改变引用的值就相当于改变了实参的值,从而达到交换的效果。
相信这里大家也感受到了引用做参数的第一个好处,就是可以作为输出性参数。
这里我再来回答一个问题,可以有人会有这样的疑惑:你前面不是说了吗,引用在定义的时候必须进行初始化,你这里直接定义一个形参做引用却并未对它进行初始化,这里会不会出现语法上的错误?
答案是不会的。因为这里的引用做形参它实际上并不是一个定义引用的过程,这里的形参仅仅是起到了声明的作用。引用真正意义上的定义初始化是在传参的时候进行的,当实参传给形参之后该引用才算真正的定义。
说起返回值大家肯定是不会陌生了,来看下面这段代码:
int Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
这段代码的作用是每调用一次函数返回一个比上次返回值大1的值,返回值从0开始。
如果是要求写这样一个函数带一个返回值,我想大家肯定是都没有问题的。不过这里我想问,这个返回值是如何返回的,main函数中变量是如何拿到这个返回值的?这个问题不知道大家有没有认真思考过。
下面我来说一下吧,如下图所示:
可以看到传值返回时首先需要创建一个临时空间来拷贝返回值变量的值,然后再把临时空间的值拷贝给变量ret。这是因为函数的返回变量一般出了函数作用域就会销毁(我这里所举的返回值是用static修饰的,不会销毁),所以不能直接拷贝函数的返回变量。因此需要创建一个临时空间来充当临时变量。这里需要注意,这个临时空间一定要开辟在main函数作用域,
也就是说在传值返回的过程中一共进行了两次拷贝,并且创建了一块临时空间。
下面我们再来分析传引用返回是如何进行的。
拿上图中的代码来举例,引用传参的时候,返回值是返回变量的n的引用,所以这里不需要再创建临时空间,直接把引用的值拷贝被变量ret,相当于直接把返回变量n的值拷贝给了ret。
可以看到,引用传参只进行了一次拷贝,并且没有创建临时空间。
但是这里大家肯定也感受到了,引用传参的时候,返回参数的声明周期不能仅限于当前函数。如果仅限于当前函数,那么出了函数作用域该变量的空间就会销毁,那么返回的引用就变成了类似于野指针,这样就不能把引用空间的值带给main函数中的变量。
所以得出结论:引用传参的时候,返回参数出了作用域不能被销毁。
为了再加深一下大家对引用做返回值的影响,下面我再来为大家举一个例子。
代码1:
int Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = Add(1, 2);
printf("hello\n");
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
这段代码的作用是用Add函数返回两个变量的和,main函数中的ret变量来接收这个返回值,最后再将变量ret打印到屏幕上。本段代码中函数的返回值采用的是传值返回,同时我还在打印ret之前打印了一个hello,这条语句的作用我下面会讲。
来看运行结果:
可以看到成功打印了hello,并且求得的和为3,没有任何问题。
代码2:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
printf("hello\n");
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
还是实现上面的功能,不过这次Add函数的返回参数采用的是引用返回,来看运行结果:
我们看到,这次打印完hello之后,求得的函数返回值是一个随机值,很显然出现了错误。
原因就是上面我提到的结论。
main函数中的变量ret引用的是Add中的返回变量c,而变量c出了函数作用域之后就会销毁,所以这块的变量ret实际上引用的是一块不属于当前程序的随机空间。当printf函数调用之后,原来Add函数栈帧里的内容就会被重置掉,这样ret引用空间里的内容就变成了一个随机值。
所以还是上面提到的结论:引用传参的时候,返回参数出了作用域不能被销毁!!!
首先我要说的是,引用和指针在底层的实现是没有任何区别的。
看这样一段代码:
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们来看下引用和指针的汇编代码对比:
可以看到引用和指针底层汇编代码的实现是完全相同的!
引用和指针的不同点总结:
对于这些区别,我的建议是不要死记,这里提到的点我基本上都在上面讲到过了。希望大家能够理解起来记忆,尽量用自己的理解将它们记下来。
本篇文章到这里就全部结束了,针对引用部分我也算是尽可能的将我所知的每一处细节都写给了大家,不得不说的是C++中的语法细节实在是太繁琐了。
最后希望这篇文章能够为大家带来帮助!