目录
前言
本节我们正式进入C++基础之引用的学习,下面小编将会带领大家由浅入深逐步解剖引用的语法及应用场景,话不多说,我们直接上干货!!
一、引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
例如:《西游记》中孙悟空,也称“齐天大圣”,“美猴王”等,但都表示同一个人。
引用的语法为: 类型 & 引用变量名(对象名) = 引用实体
下面,我们来举个例子
#include<iostream>
using namespace std;
int main()
{
int a = 1;
int& ra = a; //ra是a的引用,引用也就是别名,相当于a再取了一个名称ra.
int& b = a;
int& c = b;//在物理空间上,a、ra、b、c共用一块空间,所以a、ra、b、c的值相同,都等于a = 1.
c = 2;//这块空间上有四个名称,此时对任意一个名称操作,都改变同一块空间的值,所以此时a = ra = b = c = 2.
return 0;
}
注意 : 引用类型必须和引用实体是同种类型的。
二、引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
3.引用一旦引用一个实体,就再也不能引用其他实体
int main()
{
int a = 1;
//int& b; //这行编译时会报错,因为引用在定义时必须初始化
int& c = a;
int d = 2;
c = d; //这里不是c变成d的引用,而是将d赋值给c
return 0;
}
三、常引用
加了 const 的变量是不能修改的,即变成的一种常量,可以理解为只读型;而没加 const 的变量可以理解为可读可写型。
下面小编带大家理解一下“权限放大”、“权限缩小”以及“权限平移”
注意:权限放大和缩小只适用于指针和引用。
int main()
{
//1.权限放大
const int a = 0;
//int& b = a; //这里编译会报错,原因:a的类型是const int,是只读的;而b的类型是int,是可读可写的;权限放大了,报错。
const int& b = a;//改成这样就不会报错,因为此时a,b都是可读可写的,属于权限平移。
//2.权限缩小
int c = 1;
int& d = c;
const int& e = c;//这里是可以的。原因:c是可读可写的,e变成c的别名是只读的,属于权限缩小。
//3.指针的情况
const int* cp1 = &a;
//int* p1 = cp1; //这里不可以,属于权限的放大
int* p2 = &c;
const int& cp2 = p2; //这里可以,是权限的缩小
//总结:引用取别名时,变量访问的权限可以缩小,不能放大。
return 0;
}
最后的一种情况理解起来比较难,也是需要注意分辨的,小编总结如下:
有类型转换的情况时:
int main()
{
int i = 0;
double ii = i;//隐式类型转换,这里是可以的,语法允许的,注意这里不是引用
------------------------------------------------
//double& dd = i;
//这里是不行的,属于权限被放大了。
const double& dd = i;
//这样才是可以的,是权限的平移。
}
这里是为什么呢,很多同学会懵掉,下面我们要理解赋值和引用是怎么发生的,请看下面原因和图解 。
– 原因: 类型转换时,会产生一个临时变量,赋值时 i 会先赋给临时变量,再由临时变量赋值给 dd ; 所以这里 dd 引用的不是 i,而是临时变量。 而临时变量具有常属性。 是只读的, dd 引用时若不加 const ,则属于权限放大了。
–图解:
总结:
- 权限放大和缩小只针对引用和指针,而普通变量之间的赋值是没有权限缩小和放大的关系的(例如:int i = 1;const int b = i; 是可以的)。
- 权限只能缩小,不能放大。
- 临时变量具有常属性。
- 权限的放大,const 不能给非 const ; const 只能给 const。
- 权限的缩小,非 const 既可以给非 const ;也能给 const。
四、引用的使用场景
4.1 引用做参数
// 用C语言时的做法
//void Swap_c(int* p1,int* p2)
//{
// int temp = *p1;
// *p1 = *p2;
// *p2 = temp;
//}
void Swap_cpp(int& r1,int& r2)
{
int temp = r1;
r1 = r2;
r2 = temp;
}
int main()
{
int a = 0,b = 1;
//Swap_c(&a,&b);
Swap_cpp(a,b);
return 0;
}
引用做参数,避免了使用指针;改变 Swap_cpp 函数内的形参 r1,r2 就是改变了 main 函数传入的实参 a,b;因为 r1, r2 分别是a, b的别名。
我们来看一下上面的栈帧图,它们在同一个进程里面。
在这里我们多提一下函数传参的三种方式:传值,传地址(指针),传引用。
后两种上面已有了展示,下面我们来看看传值
这里传值并不能达到这个函数的交换效果,因为这块 r1,r2会分别产生一块空间,a,b 传值会传给这两块空间,而函数里面的交换的是两个空间 r1, r2的值,函数结束时空间也会销毁,并不会影响a,b 的值。也就是大家熟悉的形参的改变不会影响实参。
而传引用时 r1,r2 就不会产生空间,而是作为 a,b 的别名和a,b 共用一块空间,所以能改变 a, b 的值。看下图:
4.2 引用做返回值
- 传引用返回:意思是返回的是返回对象的别名。(同一个空间)
- 传值返回:意思是返回的是返回对象的拷贝。(不同空间)
上图就是传值返回和传引用返回的一个对比,我们来分析一下;
首先传值(无论是参数传值还是传值返回都会产生一个拷贝变量),如上图传值返回返回的不是n, 而是n 的一个拷贝变量(临时变量),我们说过临时变量是具有常属性的,所以在 main 函数中接收的时候用 int& r1 = Count1() 是编译错误的(权限放大);所以用 const int& r1 = Count1() 接收就没问题了;而传引用返回时返回的是 n 的一个别名,相当于返回的是 int& tmp = n 中的 tmp , 这里是不会产生额外的空间的,所以用 int& r2 = Count2() 接收是没问题的,此时相当于 r2 是 n 的别名。
这里解释一下static,这里的static修饰的是生命周期,不会影响权限问题,static int 的类型还是 int ,理解权限问题时大家可以把static int 看作 int 理解,至于这里static的作用是什么,我们下面会详细分析。
这里没有 static 的话,程序会出很大的问题。
我们看下面一段代码,下面代码会输出什么结果呢?大家可以思考后去VS试试,下面我会给出结果。
没有加 static 时
int& Add2(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add2(1, 2);
Add2(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
没有加 static 的结果:
这里是随机值,有些编译器会是 7;
加了 static 后
int& Add2(int a, int b)
{
static int c = a + b;
return c;
}
int main()
{
int& ret = Add2(1, 2);
Add2(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
加了 static 后的结果:
这里是为什么呢?
为什么没加 static 的结果是错误的,为什么没加 static 结果会是随机值或者是7 ?(7是受Add(3,4)影响,函数结束回收空间之后没有被置成随机值的结果,现在的编译器一般都是随机值了)。
我们来看一看没加 static 时的栈帧图:
解释:
- 第一次调用 Add(1,2) 的时候,传参的时候把1传给了a,2传给了b,加起来就是c=3,这时候传引用返回,ret接受了c这个返回值,所以ret就是c的别名,第一次调用完成之后,进行了返回,返回之后Add函数的栈帧就被销毁了(销毁了,其实这块空间还在,只是使用权不是你的了),紧接着再调用Add(3,4),这时候还是建立了同样大小的栈帧,这时候 c 的那块空间的值变成了 7;再返回,此时栈帧也被销毁了(结果为 7 是因为栈帧销毁后 c 这块空间还没有被置成随机值,出现随机值就是因为栈帧销毁后 c 这块空间被置成随机值了),所以会出现7和随机值这两种情况。
- 这里说明当返回变量 c 是局部变量时,引用返回是不安全的。因为出了函数的作用域,函数的栈帧已经销毁了,但是你还在访问销毁的那块空间。
这种不安全还体现在 Add(1,2)执行完之后,你再随机调用一个其他函数(printf都行),都会在 main 函数下面建立栈帧,这个栈帧有可能就覆盖了 ret ;把它置成随机值。- 如果你不是看得很明白的话,或许下面这篇文章能帮助你理解:
- 函数引用返回底层分析 – 栈帧创建和销毁
那怎么解决这个问题呢?加上 static 问题就迎刃而解了。
我们来看看加了 static 之后的栈帧图:
- 加了static 之后,c 的 进程地址 在堆区,此时Add 函数执行完之后,函数栈帧被销毁了,但是 c 的这块空间还在,并没有受影响。
- 注意:加了static 之后,static int c = a + b;只会在第一次调用定义时被执行一次,下次再调用这个函数则不会再执行,会直接跳过。
总结: 一个函数要使用引用返回,返回变量出了这个函数的作用域还存在,就可以用引用返回,否则就不安全。(全局变量,静态变量适用于引用返回)
函数使用引用返回的好处:
1.少创建拷贝一个临时对象,提高效率。
五、传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
5.1 值和引用的作为返回值类型的性能比较
#include<iostream>
#include <time.h>
struct A{
int a[10000];
};
A a;
//值返回
A TestFunc1{return a;}
//引用返回
A& TestFunc1{return a;}
void main()
{
// 以值作为函数参树
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
结果:
可以看出,传引用返回在实践中时间效率更优,且数据越大越明显。
5.2 值和引用作为参数传递之间的性能差别
#include <time.h>
struct A{
int a[10000];
};
A a;
//值返回
A TestFunc1(A a){}
//引用返回
A& TestFunc1(A& a){}
void main()
{
A a;
// 以值作为函数参树
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
结果:
可见,引用作为参数传递也能体高效率。
六、引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们来看看引用和指针的汇编代码对比:
引用和指针的不同点:
1.引用在定义时必须初始化,指针没有要求。
2.引用在初始化引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
3.没有 NULL 引用,但有 NULL 指针
4.在 sizeof 中含有不同:引用结果为未引用类型的大小,但指针始终是地址空间所占字节数(32为平台下占4个字节,64位平台下占8个字节)
5.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
6.有多级指针,但是没有多级引用
7.访问实体方式不同,指针需要显示解引用,引用是编译器自己去处理。
8.引用比指针使用起来更加安全。
9.引用概念上定义一个变量的别名,指针存储一个变量地址。
结束语
至此本节内容全部讲完,本节的内容较多且比较晦涩,着重讲了C++之引用篇的一些语法、应用场景及一些底层实现。另外,小编的能力有限,如果有什么地方描述有误或者知识缺陷,欢迎各位大佬指出,小编定当及时改正。下节内容将给大家带来更精彩的C++知识,欢迎大家捧场!
最后想说这是小编的第一篇文章,感谢各位友友的观看,你们的支持就是小编创作的最大动力,友友们动动手指点个赞,留个评论吧!谢谢大家!