文章目录
1.引用的概念
C++很珍惜关键字和操作符,对于引用C++没有定义新的关键字和操作符,而是共用了C语言的&(取地址操作符)。在C++中把&放在类型的后面,变量的前面就叫作引用。
类型& 引用变量名(对象名) = 引用实体;
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
注意:引用类型必须和引用实体是同种类型的。
2.引用的特性
2.1. 引用在定义时必须初始化
2.2. 一个变量可以有多个引用
我们说b是a的引用,也可以说b是a的别名。一个变量可以有多个别名,甚至可以对一个变量的别名取别名,但本质上是同一块内存空间,对任何一个引用进行修改,其他的引用也被修改,因为本质上是同一块内存空间的内容被修改了。
2.3. 引用一旦引用一个实体,再不能引用其他实体
大家想想,假设我现在又定义了一个变量e,e的值是20。那我把e给b在这个地方到底是什么意思呢?是把e的值赋值给b这块空间呢,还是说让b变成e的别名呢?
很显然,这里必然是把e的值赋值给b。
3.常引用
3.1.引用的权限问题
以前我们学习指针时,除了学习普通指针外,还有一个const指针。那么引用涉不涉及这个const引用呢?其实也涉及而且在很多场景都有着重要的作用。
假设有人定义了一个变量x,这个x是const int
的,那么我现在给x取别名我怎么取呢?像之前讲解的方法一样取别名可以吗?ok,请看下图:
我们发现按照之前的方法没办法去引用x,那怎么样才能引用呢?其实我们只要在引用时加一个const就行了。那我们能不能用加了const的引用来引用普通的没加const的int变量呢?
诶,我们发现这样也是可以的,为什么这里可以这样引用呢?那引用之间的关系到底是什么呢?
记住,取别名的原则:对引用实体变量,引用的权限(主要指读写权限)只能缩小,不能放大。
为什么说30行int& y = x;
是引用权限放大呢?因为变量x是const的,要求只能被读,不能被修改,而引用y是int型,既能读又能修改,所以引用权限被放大,这种写法是不可行的(有点喧宾夺主的意思),而权限缩小是可以的。这里和指针还有点不一样,因为指针既可以const指针变量本身,也可以const指针变量所指向的内容。
3.2.引用可以引用常量
那我们给一个常量取别名可不可以呢?我们发现给常量取别名也要加上const。
3.3.在类型转换中为什么可以用引用
**ok,重头戏来了。**我们知道引用类型必须和引用实体是同种类型的。像double d = 2.2;int& e = d;
这种写法肯定是不行的,那为什么引用时加一个const就可以了呢?如图:
我们发现只给了一个警告,没有报错。这是为什么呢?
我们知道C++是基于C语言发展而来的,C++就要沿袭C语言的一些特性。我们知道C语言大一点的类型赋值给小一点的类型会发生截断,反之则会提升。这个我们以前也讲过。
这里int f=d;
严格来说不是截断,因为浮点数的存储规则和整数不同,这里实际上是发生转换,把浮点数部分的数据丢掉。
但是大家要在另外一个方面理解一个过程:赋值不是直接赋值的,赋值过程会借助一个临时变量来完成。
因为d是8个字节,它无法直接赋值给f,而且d是浮点数,存储规则不一样,截断也不好直接截。从原理上来说,编译器会把d的整数部分取出来,赋值给一个临时变量(大小是4个字节),临时变量再赋值给f。也就是说隐式类型转换也会产生一个临时变量,临时变量具有常性(就像它被const修饰了一样,不能被修改)。
所以之前double d = 2.2; int& e = d;
e不能引用d并不是因为类型不同,而是因为e引用的是中间产生的临时变量,而临时变量具有常性,临时变量赋值给e是不是权限就放大了呀!加上const就是为了让权限不发生改变。
那有可爱的小伙伴就要问了:那为什么int f = d;
不用加const呢?
因为e其实是临时变量的别名,如果不加const是不是意味着要改变临时变量,但临时变量不可改。而int f = d;
是赋值,是把临时变量的值拷贝给f,f的改变不会影响临时变量。普通对象并不存在权限放大或者缩小的问题(因为赋值是拷贝),只有引用和指针才会涉及const这类问题。
我们发现e和d的地址并不相同,这也能验证我们之前所说的。
4.使用场景
4.1. 做参数
-
以后我们会学一个东西叫模板,传参是就要尽量避免传值传参,因为传值传参会进行拷贝,如果模板T是自定义类型,可能会存在深拷贝的问题,所以最好用引用传参,而引用加了const,不管参数是什么类型都可以传参,哪怕涉及类型转换。听不懂也没关系,我们后面会继续讲解模板,这里不再继续延伸了。
-
另一个例子:引用配合上函数重载,引用参数的交换实际就是实参的交换,且配合函数重载调用起来就像在调用同一个函数。
-
还可以做输出型参数:
-
不带头节点的链表要改变头节点时不用传二级指针,用引用:
4.2.做返回值
我们先复习一个知识点:
被static修饰的变量只会初始化一次,生命周期变为全局的,不改变其作用域。
4.2.1.传值返回
ok,接下来我们来讲一下返回值的具体过程。传值返回的时候会产生一个临时变量,它跟传参一样,如果小临时变量会用寄存器替代,如果大它就会在main函数的栈帧中提前开好空间。实际上返回值的类型其实就是临时变量的类型。把n拷贝给临时变量,临时变量再拷贝给ret。设计这个临时变量是不是因为没有被static修饰的局部变量出了函数作用域会被销毁呀!
怎么证明有没有产生临时变量呢?临时变量具有常性。
4.2.2.引用作为返回值
引用作为返回值,说明tmp是n的别名,tmp是int &型,没有多开辟空间且没有权限改变问题,tmp再赋值给ret。实际上就是直接把n赋值给了ret。
如何证明上述过程?看地址:
但是这个代码存在问题,因为n出了函数作用域会销毁,ret也没有了意义,无法正常使用。(引用间接搞出来的野指针问题)。
第一次调用函数栈帧没有被覆盖,所以n还是1,第二次调用函数栈帧被覆盖了,所以n是随机值。其实有些编译器第一次就会清理函数栈帧,所以第一次是随机值也不奇怪。
注意:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。否则可能会出现越界问题。
4.2.3.传值返回和引用返回的区别
传值返回:会有一个拷贝。
引用返回:没有拷贝,直接返回变量的别名。
5.效率比较
5.1.传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
5.2.值和引用的作为返回值类型的性能比较
通过上述代码的比较,发现传值和引用在作为传参以及返回值类型上效率相差很大。
6.引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
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 = &
*pa = 20;
return 0;
}
我们来看下引用和指针的汇编代码对比:
引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全