引用的基本原理
引用的实现原理其实通过下面5句话,1副图,1段代码就可以说明白了
1 | int a=1; |
1.引用变量b和被引用变量a并没有共用一块内存,b是另外开辟了一块内存的
2.引用变量b开辟的内存中存放的是a的地址
3.任何对变量b的操作,都将转换为对(*b)的操作,比如b=b+1实际上是(*b)=(*b)+1 而(*b)代表的就是a
4.基于上面3点我们可以总结出 引用变量b可以理解为被引用变量a的别名
5.引用必须在声明引用时将其初始化,而不能先声明,再赋值。也不能在使用过程中途对其赋值企图更改被引用的值,那样是无效的
比如:
int rats = 101;
int & rodents = rats;
int bunnies = 50;
rodents = bunnies;
在上面一通操作以后rodent引用的还是rats
上面的两句代码 对应的内存分布图就如下
再看一个实际的例子
1 | #include<iostream> |
运行结果:
1 | a:address->0031FD54 |
引用的基本使用
还没有测试函数的引用实参和右值引用
引用的基本类型
左值和右值
- 在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。
1
2
3
4举个例子,int a = b+c, a 就是左值,
其有变量名为a,通过&a可以获取该变量的地址;
表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,
我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
- 在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。
- 左值一定在内存中,右值有可能在内存中也有可能在寄存器中
1 | int a=5; |
左值引用与右值引用
- 在内存中的变量才是可以取地址的,而在寄存器中的变量是不可以取地址的。对于一个不能取地址的表达式或者值是无法直接引用的。
1 | void main() |
1 | void main() |
从以上两个例子可以看出int *pnum(&num1); int * &rnum = pnum;
通过一个指针在进行取别名是可以的,因为此时指针在内存中,而直接int * &rnum = &num1;取别名是不行的,&num1在寄存器中。
在内存中的值是可以直接取别名的也就是引用。但是在寄存器中的值在不可以直接被引用的。其实这就是所谓的左值引用和右值引用。
2.左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型。右值引用和左值引用都是属于引用类型。
3.右值引用的方法就是int * &&rnum = &num1; 。
4.下面来说一下为什么要右值引用,右值引用在你需要使用寄存器中的值的时候可以进行右值引用。寄存器的刷新速度很快,没有右值引用的话就需要将寄存器中的值拷贝到内存中,在进行使用,这是很浪费时间的。
1 | int getdata(int &&num) |
如上 int getdata(int &&num)就是对右值进行引用。 getdata(a + 1) 中a+1是右值在寄存器中,我们是不可以直接对他进行操作的,如果要操作得将其拷贝到内存中,如果是一个非常大的数据这种拷贝就会很占用内存,如果直接用右值引用就可以直接对其进行操作。从而节约内存。
- 无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
引用与const
- 左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值引用只能接受非常量左值对其进行初始化。
1
2
3
4
5
6
7int &a = 2; # 左值引用绑定到右值,编译失败
int b = 2; # 非常量左值
const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
const int d = 2; # 常量左值
const int &e = c; # 常量左值引用绑定到常量左值,编译通过
const int &b =2; # 常量左值引用绑定到右值,编程通过
- 左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值引用只能接受非常量左值对其进行初始化。
常量左值引用内部实现的原理是这样的:对于不可寻址的值,如文字常量,以及不同类型的对象,编译器为了实现引用,必须生成一个临时对象,将该对象的值置入临时对象中,引用实际上指向该对象(对该引用的操作就是对该临时对象的操作),但用户不能访问它。
1 | 例如: |
- 右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值,例如:
1 | int a; |
- 3.const引用与非const引用的对比
[0].const引用表示,试图通过此引用去(间接)改变其引用的对象的值时,编译器会报错!我们仍然可以直接改变其指向对象的值,只是不能通过引用改变。总结来说就是const引用只是表明:保证不会通过此引用间接的改变被引用的对象!下面是一个简单的例子:
1 | 1 #include <iostream> |
另外,const既可以放到类型前又可以放到类型后面,放类型后比较容易理解:
1 | string const *t1; |
[1]、const引用可读不可改,与绑定对象是否为const无关;非const引用可读可改,只可与非const对象绑定
例如:
1 | const int ival = 1024; |
[2]、非const引用只能绑定到与该引用同类型的对象,const引用则可以绑定到不同但相关的类型的对象或绑定到左值,同时const引用可以初始化为不同类型的对象或者初始化为右值,如字面值常量。例如:
1 | int i = 42; |
[3].const引用和非const引用在内存中的对比
例如const引用:
1 | const int t = 9; |
1 | 例如非const引用: |
引用与函数
1.类型匹配问题
- 如果函数形参,用到了引用类型
则如果不注意实参和形参的类型匹配,就会出现问题
比如如果形参是非常量左值引用 void func(int &a)
则如果你传进去的是常量左值 比如const int b = 1;
或者右值5
那么就会调用func(b) func(5)报错
根据这个表格做好类型匹配
具体原理看上面的讲解
https://blog.csdn.net/qq_40888863/article/details/119078245
- 比如如果形参是非常量左值引用 void swap(int &a,int &b)
但是你传进去的实参是long a= 1; long b = 2;
那么你传进去时 在赋值给形参的时候
实际上会生成一个临时变量
这样swap函数交换的实际上时临时变量
不会对实参进行交换
2. 什么时候函数形参用引用类型?
运行效率
引用类型形参不用像值传递那样开辟内存空间进行大量数据的赋值
对于类对象 不用调用拷贝构造函数
程序员能够修改调用函数中的数据对象
如果使用值传递, 修改的只是副本
3. 什么时候函数形参用const类型?
尽可能使用const
将引用参数声明为常量数据的引用的理由有三个:
1)、防修改:
使用const可以避免无意中修改数据的编程错误。2)、接受const和非const实参:
使用const使函数能够处理const和非const实参,否则将只能接受非const数据。3)、和引用配合,能同时接受左值和右值:
使用const引用使函数能够正确生成并使用临时变量。
4. 函数参数值传递、指针传递、引用传递区别
https://www.cnblogs.com/yanlingyin/archive/2011/12/07/2278961.html
5. 函数形参类型的使用准则
- 对于使用传递值而不做修改的函数:
如果数据对象较小,如内置数据类型或者小型结构,则按值传递。
如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
如果数据对象是较大的结构,则使用const指针或const引用,以提高运行效率。这样可以节省复制结构所需的时间和空间。
如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++增加引用特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
- 对于修改调用函数中数据的函数:
如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int型),则很明显,该函数将修改x。
如果数据对象是数组,则只能使用指针。
如果数据对象是结构,则使用引用或指针。
如果数据对象是类对象,则使用引用。