0
点赞
收藏
分享

微信扫一扫

C++打怪升级(二)- 引用详解

晒大太阳了 2022-10-05 阅读 148

BlogPicture1.jpg

~~~

前言

引用,是C++中重要的概念,它贯穿着C++的学习。不好好理解引用,接下来的路会不太好走哦!
不过别担心,看完这一篇问题就不大了。


引用是什么

概念

引用reference是为已经存在的变量取另外一个名字,是该变量的别名
C++语法角度:编译器不会为引用变量开辟内存空间,它和所引用的变量共用同一块内存空间
引用类型是**复合类型,**格式数据类型& 引用变量名(对象名) = 引用实体;
与指针类型类似数据类型* 指针变量名 = 对象的地址


简单举例

#include <iostream>
//只是为了说明引用在干嘛
int main(){
	int a = 10;
    int &ra = a;//ra对a的引用
    int &rra = ra;//rra对ra的引用,而ra是a的别名,故rra是a的引用
    
    a++;
    ra++;
    rra++;
    return 0;
}

image.png

image.png

image.png

image.png

image.png


引用特性

一般变量在初始化时是把初始值拷贝到变量(对象)中,而引用变量在初始化时是把引用变量和它的初始值绑定在了一起,并且无法重新绑定到另外一个对象。
image.png
image.png

image.png

image.png

image.png


引用的真正用途

引用做函数参数

减少拷贝,提高传参效率

#include <iostream>
using namespace std;
//交换两个整数
void Swap(int& a, int& b) {
	int tmp = a;
	a = b;
	b = tmp;
}

int main() {

	int x = 10;
	int y = 20;
	cout << "前x: " << x;
	cout << " y: " << y << endl;
	Swap(x, y);
	cout << "后x: " << x;
	cout << " y: " << y << endl;
	return 0;
}
void Swap(int* pa, int* pb) {
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

image.png

做输出型参数,直接修改实参

对于某些参数传入的目的不只是为了本函数使用,更是为了在本函数调用结束后能够反映到外界(主调函数等),函数调用结束返回时又只能返回一个变量,一个解决办法是使用引用做输出型参数,不作为返回值返回。
关于做输出型参数这一点指针类型也可以实现,引用则使得输出型参数更简化和方便。
举个例子:比如说在我们写oj题时就可能会遇到输出型参数
image.png


引用做返回值

#include <iostream>
using namespace std;

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

int main() {
	int& ret = Count();
	return 0;
}

关于函数返回时的一些讨论

我们先来看看**非引用返回(传值返回)**时,函数返回发生了什么:

#include <iostream>
using namespace std;

int Count(){
	static int n = 0;
	n++;

	return n;
}
int main() {
	int ret = Count();
	return 0;
}

总结来说,函数传值返回,返回的是待返回变量的拷贝;而待返回变量如果在待返回的函数栈帧里就会作为局部变量被销毁,尽管本例中待返回变量n不在待销毁栈帧里,而是在静态区,生命周期一直到程序结束,在函数Count销毁后,静态变量也不能够使用了因为其作用域在Count函数内部

image.png


再来看看传引用返回时发生了什么:

#include <iostream>
using namespace std;

int& Count(){
	static int n = 0;
	n++;
	
	return n;
}
int main() {
	int& ret = Count();
	return 0;
}

image.png


再来看一个非引用返回的局部变量的例子:

#include <iostream>
using namespace std;

int Count(){
	int n = 0;
	n++;
	return n;
}
int main() {
    
	int& ret = Count();
	return 0;
}

一个不恰当的的引用使用

#include <iostream>
using namespace std;

int& Count(){
	int n = 0;
	n++;
	return n;
}
int main() {
    
	int& ret = Count();
	return 0;
}

image.png

结论

传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直
接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
率较低,尤其是当参数或者返回值类型非常大时,效率就很低

#include <iostream>
#include <time.h>
using namespace std;

#include <time.h>
struct A { 
	int a[10000];
};
//传值
void TestFunc1(A a) {
}
//传引用
void TestFunc2(A& a) {
}

void Test(){
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 1000000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 1000000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

int main() {
	Test();
	return 0;
}

image.png


#include <iostream>
#include <time.h>
using namespace std;

//引用返回 与 传值返回效率
struct A {
	int a[10000];
};
A a;

// 值返回
A TestFunc1() {
	return a;
}

// 引用返回
A& TestFunc2() {
	return a;
}

void Test()
{
	// 以值作为函数的返回值类型
	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;
}

int main() {
	Test();
	return 0;
}

image.png


引用和指针的联系与区别

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
**在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。 **
见visual stdio 2019反汇编
image.png
联系:

image.png
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址;
2. 引用在定义时必须初始化,指针没有要求;
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体;

4. 没有NULL引用,但有NULL指针;
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节);

6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
7. 有多级指针,但是没有多级引用;
image.png
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
9. 引用比指针使用起来相对更安全 。


常引用

是使用const限定符修饰的引用,我们应该对const不会陌生,指针常量、常量指针、常变量等等我们都遇到过。

这里涉及到读写访问权限的问题:

#include <iostream>
using namespcae std;

int main() {
	//对于指针和引用:在赋值时需要考虑权限
	//a定义为可读可写变量
	int a = 0;
	//权限平移 - ra完全获得a的权限
	int& ra = a;
	//权限缩小 - rra只获得a的读权限
	const int& rra = a;

	//b定义为只读变量
	const int b = 10;
	//权限放大,出错 - rb企图获得超过b本身的权限
	//int& rb = b;
	//权限平移 - rrb获得读权限
	const int& rrb = b;
    
	return 0;
}

作为函数参数

问题:

#include <iostream>
using namespace std;
void function(int& x) {
	;
}

int main() {
	//a定义为可读可写变量
	int a = 0;
	int& ra = a;
	const int& rra = a;
    function(a);//true
    function(ra);//true
    
    function(rra);//error
    function(10);//error
	return 0;
}

可以看到function引用参数x只能接收实参ara,不能接收rra10。即x可以作为ara的引用,但是不能作为rra10的引用。原因就是x是可读可写的,ara也是可读可写的,x权限没有放大。而rra10是只读的,导致x是权限放大的,所以出错。

改进:

#include <iostream>
using namespace std;
void function(const int& x) {
	;
}

int main() {
	//a定义为可读可写变量
	int a = 0;
	int& ra = a;
	const int& rra = a;
    function(a);//true
    function(ra);//true
    
    function(rra);//true
    function(10);//true
	return 0;
}

x是只读的,ara是可读可写的,x权限没有放大。而rra10也是只读的,x的权限也没有放大的,正确。


对引用不能引用不同类型变量的进一步探究

我们知道一种类型的引用不能引用另一种类型的变量。

double a = 3.14;
double& ra = a;//true
int& rra = a;//error

image.png
那么我们再来看看这种形式:

double a = 3.14;
const int& rra = a;//true

为什么加上const修饰后就不报错了呢?
接下来揭示原因:

double a = 3.14;
const int tmp = (int)a;
const int& rra = tmp;

看一看arra的地址即可知道:
image.png


类似的临时变量的产生也是非常常见的,只是我们可能没有注意:
比如说:隐式类型转换、强制类型转换、算数转换等都会产生中间临时变量,改变的并不是变量本身。


结语

本节主要介绍引用的概念和一些用法,并对引用的用途进行了比较详细的分析。下次再见!


举报

相关推荐

0 条评论