C++引用

引用

引用不是新定义一个变量,而是给已存在的变量取一个别名,译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

在语法上,一定要认为引用没有开空间,目前先不用考虑底层上

引用的语法为:类型& 引用变量名(对象名) = 引用实体;

引用类型必须和引用实体是同种类型的

int main()
{
	int a = 10;
	int& b = a;//定义引用类型
}

ba的引用,ab本质上是一个变量,它们公用一个空间,改变任意一个另一个都会改变
在这里插入图片描述

  • 引用时必须进行初始化
int main()
{
	int a = 10;
	int& b;//引用时需要初始化,这样写是错误的
}
  • 一个变量可以有多个引用
int main()
{
	int a = 10;
	int& b = a;
	int& c = b;

}

ba的·引用,c又是b的引用,所以bc都是a的引用,它们三个共用一个空间
在这里插入图片描述

  • 引用一旦引用了一个实体,不能再引用另一个实体
int main()
{
	int a = 10;
	int x = 5;
	int& b = a;//b已经是a的引用了
	b = x;//这里不会改变b引用的实体,只是把x的值拷贝给了b,b还是a的引用
}

引用的使用

引用做参数

1.引用做参数,常用于输出型参数

输出型参数就是在函数内部改变,并且在函数外部还能拿到的参数,其实也可以成为“形参的改变可以影响实参”
例如交换两个变量值,这2个变量在后序程序中还会用到,所以是输出型变量,在一些关于数组的OJ题中,一般函数参数都有一个表示数组大小的变量numSize,这个形参也是输出型参数

下面通过引用实现一个swap函数

void swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

再对比一下原先在C语言中的写的swap

void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

可以看到用引用最直接的一个点就是不需要再传指针了
以往的swap函数中,为了在函数中能够改变要交换的2个数的值,只能传指针,否则就是形参的改变不会影响实参
在C++中,传引用,形参就是实参的别名,这样形参的改变就可以改变实参

2.引用做参数,可以提高效率,尤其是对于深拷贝的类对象和大对象

作为函数传参,在传参过程中,函数不会直接传递实参,而是传递实参的一份临时拷贝,以这个临时拷贝作为函数的形参,在值传递的过程中有拷贝操作,效率低下
在这个过程中,如果实参是深拷贝的类对象或者是大对象,那么它们的拷贝就会大大降低效率

所以,可以用引用做参数,在传参的过程中,没有拷贝的操作,形参就是实参的一个别名,这样能大大地提高效率

需要注意的一点:引用传参和指针传参效率一样,不存在较大的效率差异

下面我们看一下值传参和引用传参的效率差异:

struct A
{
	int a[10000];
};

void func1(A a){}//值传参
void func2(A&a){}//引用传参
int main()
{
	A a;//大对象
	int BeginTime1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		func1(a);
	int EndTime1 = clock();

	int BeginTime2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		func2(a);
	int EndTime2 = clock();

	cout << "值传参:" << EndTime1 - BeginTime1 << endl;
	cout << "引用传参:" << EndTime2 - BeginTime2 << endl;


}

可以看到,引用传参消耗的时间明显少于值传参
在这里插入图片描述


引用做返回值

在学习引用做返回值之前,要了解一下返回return的作用机制:
在函数中,return了一个值或变量后,并不会直接返回,在中间会生成临时变量,最后返回的值其实是临时变量

在这里插入图片描述
在这个过程中,会有拷贝操作,拷贝会导致效率低

为什么要设置临时变量? 如果返回值的作用域是这个函数,函数栈帧销毁后,返回值也被销毁了

所以为了解决这个问题,我们可以引用做返回值:

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

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

传引用返回,中间就不会生成临时变量了,可以减少拷贝,提高效率

从下图就可以看出,用引用返回可以减少一次拷贝在这里插入图片描述

下面是用一个引用去接受引用返回:

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

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

这里我们用一个引用接收了引用返回,ret其实就是n的别名

这样可以再减少一次拷贝,还能提高效率
在这里插入图片描述

这里需要注意的是,想要使用引用返回,那么要返回的值不应该随着函数栈帧的销毁而销毁,换而言之,返回的变量应该是储存在静态区或者是动态开辟出来的,从上面的代码可以看出,里面返回的n都是用static修饰过的,如果是局部对象的话,会有问题


下面是几个关于引用返回局部变量的代码,我们来进行分析:

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

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

这个函数用了引用返回,返回的是n的别名,但是n是局部变量,当函数调用结束后,n所在的空间会被销毁以及归还了它的使用权

所以 ret的值是不确定的
如果func函数结束,栈帧销毁,没有清理栈帧,ret的值是侥幸正确的
如果func函数结束,栈帧销毁,清理了栈帧,ret的值是随机的

下面再看一个错误案例:

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

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

这里ret就是n的别名,函数调用完毕后,函数被销毁,n的空间就被归还给操作系统了
而这里ret还指向原先n的空间,如果此时什么函数都不调用,输出的ret还是正确的值,因为原先的栈帧没被其他函数栈帧覆盖
如果调用了其他函数,原先的栈帧被其他的函数栈帧覆盖,此时的ret就是随机值了

结果如下图:
在这里插入图片描述


分析完了几个常见的错误,下面来看一下引用返回的效率

struct A { int a[10000]; };

A a;

// 值返回
A Func1() { return a; }
// 引用返回
A& Func2() { return a; }

void Test()
{
	
	int begin1 = clock();
	for (int i = 0; i < 100000; ++i)
		Func1();
	int end1 = clock();
	
	int begin2 = clock();
	for (int i = 0; i < 100000; ++i)
		Func2();
	int end2 = clock();
	
	cout << "值返回" << end1 - begin1 << endl;
	cout << "引用返回" << end2 - begin2 << endl;
}

int main()
{
	Test();
}

在这里插入图片描述


引用返回还有一个作用就是:修改返回值

在C语言实现的顺序表中,我们如果想修改某位置的值,是需要调用函数的,可能是先通过一个查找函数找到要修改值的位置,然后再调用修改函数,对值进行修改

大致的代码如下:

struct SeqList
{
	int* a;
	int size;
	int capacity;
};


int find(SeqList* plist, int x)
{
	for (int i = 0; i < plist->size; i++)
	{
		if (plist->a[i] == x)
			return i;
	}
}

void modify(SeqList* plist, int pos, int x)
{
	plist->a[pos] = x;
}
int main()
{
	SeqList list;
	//查找5所在的下标,然后进行修改
	int pos = find(&list, 5);
	modify(&list, pos, 1);
}

这样需要调用2次函数,不是很方便
而在C++中,可以通过引用返回解决这个问题
因为顺序表一般都是动态开辟在堆区的,所以使用引用返回完全没有问题

struct SeqList
{
	int* a;
	int size;
	int capacity;
};

int& AT(SeqList& list, int x)
{
	for (int i = 0; i < list.size; i++)
		{
			if (list.a[i] == x)
				return list.a[i];
		}
}


int main()
{
	SeqList list;
	AT(list, 5) += 8;//利用引用可以直接修改
}

总结:

引用传参使用于输出类参数,它也可以提高效率,尤其是深拷贝和大对象
引用返回适用于:1.提高效率(深拷贝和大对象) 2.修改返回值

并且
基本任何场景都可以使用引用传参
谨慎使用引用作为返回值。出了作用域,对象还在,可以使用引用返回,否则不可以使用引用返回。


常引用

const int & = a这种引用被const修饰的叫做常引用

const int a = 10;
int &b = a;

上面的语句编译时会出错,从2种角度可以进行解释:
1.b是a的别名,b的改变会改变a,不符合const的修饰,所以出错
2.引用的过程中,权限不能放大,可以缩小、平移
所以const int a = 10; const int &b = a;才是正确的

根据上面2种解释,我们再来看看接下来的语句:

int a = 10;
const int &b = a;//可以成功编译,这里是权限的缩小

const int a = 10;
const int &b = a;//权限平移

const int& a = 10;//10是常量,所以要用const接受
int a = 10;//错误,权限放大

我们来观看下面的语句:
const int a = 10;
int b = a;
这个是不是权限放大呢?
答案是:不是
这也可以用2种角度解释:
1.b的改变不会影响a
2.这只是一个拷贝,无权限问题

int main()
{
	double a = 1.11;
	int& ra = a;//错误
}

不同类型间转换会产生临时变量,临时变量具有常性,所以应该是:const int& ra = a;
在这里插入图片描述

前面也说过,值返回也会产生一个临时变量,这个临时变量也有常性
所以用引用接受传值值返回要加上const修饰

int test()
{
	static int n = 10;
	return n;
}

int main()
{
	const int& ret = test();
}

在这里插入图片描述


引用和指针

语法层面上,引用就是一个别名,它没有独立的空间,和其他引用公用一个空间
底层实现上实际是有空间的,引用是通过指针来实现的

int main()
{
	int a = 10;

	int& ra = a;
	ra = 20;

	int* pa = &a;
	*pa = 20;
}

我们看一下引用和指针的汇编代码对比:
在这里插入图片描述
对比可以看出:引用和指针在汇编上没有任何区别
这就说明引用是类似指针的方式实现的
这同时也说明了前面提到过的:传引用和传指针效率基本相同

引用和指针的不同:
1.引用概念上定义一个变量的别名,指针是存储一个变量地址
2.引用在定义是必须初始化,指针不必
3.存在空指针,不存在空引用
4.引用在初始化引用了一个实体后,不同再引用其他实体,指针可以在任何时候指向任何一个同类型实体
5.在sizeof的结果不同,引用结果是引用类型的大小,指针始终是4字节(32位)或8字节(64位)
6.引用自加即实体加1,指针自家即指针向后偏移一个类型大小
7.有多级指针,没有多级引用
8、访问实体方式不同:指针需要解引用,引用编译器自己处理

一道面试题:
关于引用以下说法错误的是( )。(阿里巴巴2015笔试题)
A.引用必须初始化,指针不必
B.引用初始化以后不能被改变,指针可以改变所指的对象
C.不存在指向空值的引用,但是存在指向空值的指针
D.一个引用可以看作是某个变量的一个“别名”
E.引用传值,指针传地址
F.函数参数可以声明为引用或指针类型

答案:A
A、B、C、D、F选项都很容易理解
E选项需要说一下:“引用传值,指针传地址”,第一遍读起来感觉没错误,但其实引用表面好像是传值,其本质也是传地址,只是这个工作有编译器来做,所以错误

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

疯癫了的狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值