【C++】引用

前言

关于引用

C++是C语言的继承,它可进行过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。引用(reference)就是C++对C语言的重要扩充。引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。引用的声明方法:类型标识符 &引用名=目标变量名;

引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了*。

(节选自百度百科)

Tips

在讲引用之前,我想补充一点小知识,个人认为这会对引用和之后的学习有所帮助。

先看一段代码:

void fuc(int i, double d)
{
	cout << "void fuc(int i double d)" << endl;
}

void fuc(double d, int i)
{
	cout << "void fuc(double d, int i)" << endl;
}

int main()
{
	fuc(1, 1.1);
	fuc(1.1, 1);
	return 0;
}

通过这段代码我们不难发现,fuc函数进行了重载,但是,我们在调用的时候,他似乎能够自动匹配到对应的fuc函数中去,是因为编译器足够智能能够判断吗?又或者是因为现在的技术十分发达,已经可以自动判断匹配了?都不是,想要理解这个问题,我们要简单知道编译器编译代码的过程:
编译器编译分为几个过程:
1.预处理:头文件展开(把头文件的内容拷贝过来放到这个文件中)/宏替换/去掉注释/条件编译
条件编译:#ifdef等
预处理后,test.cpp变成test.i
2.编译:检查语法,无错误生成汇编代码(指令级代码)
编译后生成test.s
3.进行汇编(与汇编代码不同):将汇编代码生成二进制机器码
生成test.o
4.进行链接:合并链接,生成可执行程序
a.out/xxx.exe

编译器编译时会生成符号表,注意,到这一步后,我们要知道,在C语言和C++中,有一个修饰规则叫做函数名修饰规则,C语言的修饰规则简单来说是链接时会用函数名到符号表中找地址,而如果函数名相同符号表就乱了,这也是为什么C语言不支持函数重载的原因。

而C++的修饰规则则更复杂,但也更充分,他会通过函数的调用方式,返回值类型,参数个数甚至参数类型来在符号表中查找函数,但是注意,这里并没有提及利用返回值查找函数,因此,对比C++和C可以发现,C是拿函数名字去找,C++是用修饰后的函数名去找,同时在这里也证明了返回值不同不能构成函数重载。那把函数返回值带入函数名修饰规则,能不能构成重载?答案是不能,因为函数调用的时候不带返回值,会造成调用指向不明确,也会出错。

讲了这么多,对于我们最开始的问题有所帮助吗?当然是有的,总的来说,因为C++的函数名修饰规则,即他查找函数的方式不同,使他能够在调用函数名相同的不同函数时,能够准确地找到对应的函数。

这里再多提一嘴,为什么函数只有声明没有定义会找不到?因为函数编译后才会生成地址,一个函数没有指令就不能生成地址,那拿符号表里的名字去找就找不到

正文

同样的,咱们先从一段代码开始理解

int main()
{
	int a = 0;
	int& b = a;
	cout << &a << endl;
	cout << &b << endl;
	b++;
	a++;
	return 0;
}

引用的符号是&,或许刚学完C语言的同学会觉得奇怪,这会不会与取地址的那个&冲突?当然不会,使用场景和使用方法不同,并不会造成冲突,只有在定义类型和变量中间才叫引用,否则就是取地址。

在上面的代码中,我们看到引用最基础的用法是int& b = a;这代表了b是a的别名,这不难理解,我们在日常生活中,不论是朋友还是老师,或是家长,有时候总会给你自己,或别人取一些外号,举个例子:小刚的酒量很好,因此他被朋友们取了个外号叫酒鬼,而小刚在家里有一个小名,叫做小小刚,而小刚的表哥,又喜欢叫他刚子,这“小刚”,“酒鬼”,“小小刚”,“刚子”是不是都指的是小刚?那些名字不过是给小刚取的一个外号,一个别名罢了,他们都可以指代小刚,而这,就是引用。我们创建了一个变量a并给他初始化,现在我不想用a直接进行操作,我想用b来代替a,或者说叫代表a,我想用b来改变a,这个时候我们就可以用引用,让b成为a的别名,让b去代表他进行相关的操作,同时能够将发生的变化转移到a上。所以,a b操控的是同一个变量a。

同时,我们也可以给指针类型变量取别名,引用也有限制:引用必须在定义时初始化,也就是说,我们不能直接用int& b; 当然,一个变量可以有多个引用:

int a = 0;
int& b = a;
int& c = a;
int& d = b;

这与刚刚举的例子相同,一个变量可以有多个别名

在理解了他的基础用法后,我们再进一步,引用是否可以用在函数当中?我们来看下面的代码

引用传参

void swap(int& x1, int& x2)
{
	int tmp = x1;
	x1 = x2;
	x2 = tmp;
}

int main()
{
	int x = 0, y = 0;
	swap(x, y);
	return 0;
}

可以看到,swap函数没有使用指针,但是传进去的x和y依然发生了交换,这是因为x1,x2是x,y的别名,改变x1,x2自然就会改变x,y,就好比让小小刚去做饭就等于让小刚去做饭。

引用做返回值

int count()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = count();
	return 0;
}

这里我们是传值返回,并没有用到引用,我们知道,一个函数在调用完毕,即出了他的作用域后,就会销毁,而当中定义的局部变量也不例外,因此,一般的传值返回并不是将n返回给ret,出了函数作用域n就没有了,靠的是临时变量拷贝。

(Tips)关于传值返回:在这个例子中,n比较小,将n拷贝到临时变量,用寄存器充当临时变量返回,n比较大,会提前在main的栈帧中,main和函数栈帧的中间开辟一块空间为临时变量进行拷贝。

那我们用上引用,稍微做一下修改

int& count1()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = count1();
	cout << ret << endl;
	return 0;
}

打印后我们会发现,这里的值可能是1,可能是随机值,这是为什么?在这里我们将返回类型改为了传引用返回,相当于返回的是n的别名即引用,但是,n在函数调用完毕后就销毁了,那返回的n的引用就相当于野指针:空间销毁后仍然存在,但是指针指向这块空间,这个引用找不到n了,因为空间被销毁后去访问了这个空间,但是为什么值会不同?因为有些空间销毁后会置随机值,有些不会,所以这是编译环境以及编译器的问题

那我们再做测试,在ret处再加一个&试试?

int& count2()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	int& ret = count2();
	cout << ret << endl;
	cout << ret << endl;
	return 0;
}

这次我们发现结果又有所不同,两次打印下来的结果是1和随机值,为什么两次打印的结果会不一样?因为在C++中,cout是函数,需要调用,而调用函数要先传参,参数就传给了ostream流插入,又因为int& ret相当于是n的引用,所以第一个打印出1,但是调用后函数创建的栈帧会覆盖掉count函数的栈帧。第二次调用ret仍是n的引用,但是调用的范围是第一次覆盖后的空间,n不见了,自然就是随机数

注意:如果count函数栈帧够大,n在栈帧的下面,就会覆盖不了

因此,如果函数返回时,出了函数作用域,返回对象还在(没有返回给系统)就可以使用引用返回,否则就要用传值返回

讲到这里,我们可以稍微总结一下引用传参和引用返回:

传引用传参:(任何时候都可以):提高效率,输出型参数(形参的修改,影响的实参)
传引用返回:(出了函数作用域对象还在才可以用):提高效率,可以对参数进行修改

权限问题

int main()
{
	const int a = 0;
	//int& b = a; error
	//int b = a;
	const int& c = a;
	int x = 0;
	const int& y = x;
	int i = 0;
	//double& d = i;
	//error
	const double& d = i;
	return 0;
}

当引用遇上了const,就会有权限的问题,当变量a加上const后,我们不能对这个变量取别名,因为别名可以修改本来的变量,而const限制了a不能进行修改,所以这里叫做权限的放大,我们要知道,权限是不能放大的,那int b = a可以吗?是可以的,因为这是赋值,而不是取别名,不会对a造成影响,而后面的加上了const的c也可以成为a的别名,这是权限的平移,因为二者都不可更改,权限相同,所以可以取别名。下面的x和y则是权限的缩小,我们将x给了加上const的y,这里就会导致x可以改变,而是x别名的y不能改变。

最后关于const double& d = i;

c c++中规定:发生类型转换或表达式转换等,会产生临时变量,这里不是把i直接给d,而是给临时变量,这个临时变量时double类型,隐式显式都会产生,而临时变量具有常性,因此这里不可以是因为权限的放大,这里用const引用时,就没有权限的放大了,而是权限的平移,同时const会延长变量的生命周期,出了平移后变量的作用域才会销毁

所以,在引用过程中:权限可以平移,可以缩小,但是权限不能放大

实例化分析

了解了引用的用法,我们来举个例子看看他在实际使用时的效果

//C接口设计:
struct SeqList
{
	int* a;
	int size;
	int capacity;
};

int SLAT(struct SeqList* ps, int i)
{
	assert(i < ps->size);
	return ps->a[i];
}

void SLModify(struct SeqList* ps, int i, int x)
{
	assert(i < ps->size);
	ps->a[i] = x;
}
//CPP接口设计:
struct SeqList
{
	int* a;
	int size;
	int capacity;
};

int& SLAT(struct SeqList* ps, int i)
{
	assert(i < ps->size);
	return ps->a[i];
}

int main()
{
	struct SeqList s;
	SLAT(s, 1) = 0;
	cout << SLAT(s, 2) << endl;
	return 0;
}

这是顺序表的读取和修改函数,我们可以看到,加上了引用的函数显然要比C语言的函数便捷不少,可读性也增加了不少

总结

在底层代码中,引用相当于空间的拷贝,其实跟本来的方法没有什么区别,但是,底层代码并不是我们考虑并学习的重点,在语法上,引用就是取别名,并没有开空间,请大家多多学习引用,以便于以后的使用,同时希望各位观众姥爷多多点赞支持~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值