目录
前言:
hello,大家好,今天我们来分享一些关于C++的知识点——引用。
引用说白了,其实就是取一个别名。比如像下面这样:
好的,闲话少叙,让我们来步入正题。
1、引用的概念
1.概念:
<mark>引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量公用一块内存空间。</mark>
2.引用类型的定义
在C和C++中,我们用&符号来指示变量的地址,C++在此给&符号赋予了另一个含义,将其用来声明引用。
#include<iostream>
using namespace std;
int main()
{
int a=10;
int &ra = a;
}
<mark>注意:引用类型必须和引用实体类型保持一致。</mark>
我们将上面的程序加两行代码来打印两个变量的地址:
#include<iostream>
using namespace std;
int main()
{
int a=10;
int &ra = a;
cout << &a << endl;
cout << &ra << endl;
}
我们来看运行结果:
我们发现引用变量和实体类型的地址是一样的,这也可以在某种程度上证明引用变量只是实体的一个别名,而不是自立门户。
2.引用的特性
通过上面的介绍,我们已经大概了解了引用的概念,下面我们来介绍一下关于引用的特性
1.初始化
<mark>必须在声明引用变量时进行初始化</mark>
#include<iostream>
using namespace std;
int main()
{
int a=10;
int &ra;
ra = a;
}
这段代码没有在声明引用时对其进行初始化,我们来看一下它的运行结果:
这段代码是报错的。
2.可以一对多
<mark>一个变量可以有多个引用</mark>
#include<iostream>
using namespace std;
int main()
{
int a=10;
int &ra = a;
int &ra1 = a;
int &ra2 = a;
}
这样的代码是合理的。
这就像一个人可以给自己起好多别名一样,比如鲁迅先生,原名周树人,笔名鲁迅,还有其他笔名比如自树,讯行等等,共计181个。
3.关联的唯一性:直教人生死相许
<mark>引用一旦引用一个实体,再不能引用其他实体</mark>
引用一旦与某个变量相关联,就将一直忠于它,不可以再与别的变量关联。
话虽如此,但真的是这样吗?有人拿出了下面的这段代码:
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int b = 20;
int &ra = a;
ra = b;
cout << ra << endl;
return 0;
}
运行一看,结果好像有点没法接受呢:
着实吓了一跳,说好的不能与其他变量关联了呢?
我们产生了疑惑,是不是ra又引用了b呢?
那我们将上面的代码写地更细致一些,我们再来看一下:
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int b = 20;
cout << a << " " << b << endl;
int &ra = a;
cout << a << " " << b << " " << ra<<endl;
ra = b;
cout << a << " " << b << " " << ra<<endl;
return 0;
}
我们来观察一下结果:
我们发现,a的值由10变为了20.这说明什么呢?说明ra与a仍是相关联的,我们所谓的ra=b的操作,相当于将b赋值给a。
看到这里我们就明白了,引用一旦与某个变量相关联,就将一直忠于它,不可以再与别的变量关联这句话是成立的,引用一旦与变量相关联,就将生死相依,命运与共。
3.常引用
1.可不可以只在实体或引用前加const?
我们来看这样一段代码:
#include<iostream>
using namespace std;
int main()
{
const int a = 10;
int &ra = a;
return 0;
}
我们将实体定义为常量,那么还可以成功引用吗?
我们来看一下运行结果:
我们发现这是报错的,那么为什么会这样呢?
我们举一个例子,假设从前有一个人他的名字叫张三,它不吃韭菜,后来由于张三总是被违法,它很不愿意,于是它自己又起了一个名字叫张四,那么请问:张三叫张四之后就会吃韭菜了吗?
答案当然是否定的。他还是不吃韭菜。
那么我们回到上述代码,a本身是常量const型的,难道给a起了一个别名ra之后,a的值就可以修改了吗?
当然不成。所以,我们需要将引用也定义为const
顺利通过。
那么我们再来想,虽然上面的代码编不过,那么下面这段代码可以编过吗?
#include<iostream>
using namespace std;
int main()
{
int b = 10;
int &rb = b;
const int &crb = b;
return 0;
}
先不去揭晓答案,我们再来用一个例子分析一下:
还是张三,假设张三给自己改名为张四后,张三先前很胖,BIM超标,张三改名后决定要管理自己的身材,于是决定要控制BIM在健康的范围内,请问可以吗?当然可以啊。
那我们再来回到这个代码,之前我对自己没有限定,但是之后,我换了一个名字之后,我觉得要对自己有所约束了,所以我们猜测这个代码是可以的。事实的真相到底是什么呢?我们来揭晓:
代码是可以的。
于是,我们可以得出这样的结论:
<mark>引用放大本身的权限不可以,但缩小本身的权限可以。</mark>
2.可不可以类型不一样?
我们来看这样一个例子:
#include<iostream>
using namespace std;
int main()
{
int b = 10;
double &rb = b;
return 0;
}
这个代码中,我们定义的实体类型和引用类型是不同的,根据我们前面讲过的,这绝对是编译不过的。
但是,当我们将程序略作修改,把引用改为常引用:
#include<iostream>
using namespace std;
int main()
{
int b = 10;
const double &rb = b;
return 0;
}
我们惊奇地发现竟然编过了:
这又是为什么呢?
其实是这样的,当我们将int类型的值赋予double时,会发生隐式类型转换,在这个过程中,并不是直接把int值赋给double,而是会产生一个double类型的临时变量,这个临时变量再赋值给double。
<mark>临时变量具有常性</mark>,所以加const之后就可以编过。<mark>引用实际上是临时变量的引用。</mark>
4.引用场景
1.做参数
引用经常被用作函数参数,使得函数中的变量名成为调用程序中变量的别名,这种传递参数的方法成为按引用传递。
void Swap(int &left,int right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a=1,b=2;
Swap(a,b);
return 0;
}
在Swap中,left,和right分别是a和b的别名,所以交换left和right就相当于交换a和b。所以这与按值传递对变量的复制是不同的。
2.做返回值
int& Count()
{
static int n = 0;
n++;
return n;
}
这是一个传引用返回。返回的是返回对象的引用。
我们来看这样一个代码:
#include<iostream>
using namespace std;
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
这段代码的输出结果应该是什么呢?
我们发现最后输出结果是3和4的值,这又是为什么呢?
我们来分析一下:
原谅博主的图看起来有些凌乱。其实简而言之就是,在这个程序里,ret是c的引用,ret的值与c的最后一次调用的值相同。
5.传值传引用效率比较
1.引用作为参数
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
我们来用一个程序看一下:
#include<iostream>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
void TestFunc1(A a){
}
void TestFunc2(A& a){
}
void TestRefAndValue(){
}
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;
}
我们来看一下运行结果:
通过这个例子我们发现,以引用作为参数,可以极大提高效率
2.引用作为返回值
#include<iostream>
#include <time.h>
using namespace std;
struct A{
int a[10000]; };
A a;
// 值返回
A TestFunc1() {
return a; }
// 引用返回
A& TestFunc2(){
return a; }
void TestReturnByRefOrValue(){
}
int main()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
我们来观察一下运行结果:
通过上述代码的比较,发现返回值是引用与否类型上效率相差很大。返回引用可以提高效率。
6.引用与指针的关系
1.语法概念与底层实现
虽然在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。但是,在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
我们来看这段代码:
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& ra = a;
return 0;
}
其实,他和这段代码的是相同的:
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int* pa = &a;
return 0;
}
我们将两段代码略作修改然后放在一起:
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
然后我们观察它的汇编代码:
我们发现他们的汇编代码是一样的。
2.引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空 间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
7.总结
好了,今天我们关于引用的分享就到这里,欢迎大家批评指正。所以抓捕周树人到底与鲁迅有没有关系呢?答案在上文中已经揭晓。
最后,我们来分享一首关于名字的诗;
我也许会记得你的名字
我也许会记得你的名字
就像记得一座童话里的山
记得一条无人问津的河流
或者,那个再也没有人找到过的渡口
我也许会记得你的名字
记得那几个字从我的心里逃跑后
在风里流浪
记得我为追寻时
不得不三缄其口
我也许会记得你的名字
所以当秋风吹来时
我不再苦苦追问
我缓缓拾起江郊的枯草
记得你回来时
那已经光秃秃的屋顶