【C++入门】引用详解

1.引用概念

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

例如,你小时候家里人叫你的小名“二宝”,上学了同学们叫你的绰号“菠萝头”,“二宝”和“菠萝头”都是你的别名

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

void TestRef()
{
	int a = 10;
	int& ra = a;//定义引用类型

	printf("%p\n", &a);
	printf("%p\n", &ra);
}

在VS2013下的运行结果表明,a与ra共用同一块内存空间。
在这里插入图片描述

2.引用特性

  1. 引用在定义时必须初始化。引用变量在定义时就必须要确定是给谁取的别名,否则其所在的内存空间是无法确定的,也就无法创建出来。
  2. 一个变量可以有多个引用。例如人也可以有多个别名(绰号)。
  3. 引用一旦引用了一个实体,再不能引用其他实体。
void TestRef()
{
	int a = 10;
	// int& ra; // 该条语句编译时会出错
	int& ra = a;
	int& rra = a;
	printf("%p %p %p\n", &a, &ra, &rra); 
}

运行结果表明a的引用ra和rra与a共用同一块内存空间
在这里插入图片描述

3.常引用

在C++中,const修饰的变量具有宏替换的特性,在编译时会替换成对应的常量。

例1:

void TestConstRef()
{
	const int a = 10;
	int& ra = a; // 该语句编译时会出错,a为常量
	const int& ra = a;//通过
}

例2:

void TestConstRef()
{
	int& b = 10; // 该语句编译时会出错,b为常量
	const int& b = 10;//通过
}

结论1:在引用常量时,必须在类型前用const修饰**

例3:

void TestConstRef()
{
	double d = 12.34;
	int& rd = d; // 该语句编译时会出错,类型不同
	const int& rd = d;//而这样却可以通过?
}

在例3中,当使用const int类型引用double的实体时,编译器能通过,并且提示:从“double”转换到“const int”,可能丢失数据。说明这样的做法是支持的,只是不推荐。
于是我想,如果rd既然是d的引用,那么他们应该共用同一块内存空间,打印的地址应该是相同的,而结果却大失所望。
在这里插入图片描述
rd是const int类型的,其只取了d类型的整数部分12,并存储在另一个空间内,而不是指向d所在的空间,那不就与我们之前所接触到的概念相违背了吗,要理解这点我们就要知道C++的编译器在底层是如何处理引用的。参考本文第7点(引用和指针的区别)
结论2:引用类型必须与实体类型相同

4.使用场景

4.1 引用做函数参数

可以起到传指针的作用,对外部实参进行操作。注意在不需要修改实参时可以传值或在参数类型前加上const修饰
调用如下函数可以对外部实参起到交换作用。

void Swap(int& left, int& right) 
{
	int temp = left;
	left = right;
	right = temp; 
 }

4.2 引用做函数返回值

例一:

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

例二:下面的代码输出结果是什么?

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,而是最后一次调用Add(3,4)的返回值。
这是因为函数在调用时,系统会给每个正在调用的函数分配一个栈环境:栈帧,栈帧内部保存函数调用所需的参数和返回值等信息。
在反汇编中可以看到,在执行语句 int& ret = Add(1, 2); 时,引用类型的ret已经指向了调用Add(1, 2)的那块栈帧空间的返回值部分,在完成这条语句后,ret的值毋庸置疑是3.
但在第一次调用结束后,系统会自动回收第一次函数调用所使用过的栈帧,此时由于调用约定是__cdecl,该栈帧内部的数据并不会被清除,而是由下一次被调用的函数来清除之前的数据,这称为手动清栈。再执行语句 Add(3, 4),系统又在上次调用Add函数的位置给本次调用申请了栈环境,此时要注意,ret之前指向的那块栈空间正好是Add函数存储返回值的部分,因此会被Add(3, 4)修改成7。因此ret最后输出的结果会是7。

在这里插入图片描述
需要注意的是,在这里我们重复调用的是同一函数,因此在栈空间上申请出的栈帧大小一般是相同的,所以第一次调用时ret指向的栈帧中的返回值的部分,第二次指向的还是返回值,因此能得到返回值的结果。若调用不同的函数,该位置可能存储着不知道是什么类型的数据,因此可能会是随机值。
结论: 如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

5.传值、传引用的效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝。当实参很大时,如含有10000个int元素的结构体,用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

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

struct A
{ 
	int a[10000]; 
};

void TestFunc1(A a)
{}

void TestFunc2(A& a)
{}

void TestRefAndValue()
{
	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;
}

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

结论:传递的实参如果很小时,传值和传引用的效率很接近,但如果实参很大,传值的效率是远低于传引用的。
在这里插入图片描述

6.值和引用作为返回值类型的性能比较

#include <time.h>

struct A
{ 
	int a[10000]; 
};

A a;

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

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

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

通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。

7.引用和指针的区别

语法层面上,引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

int main()
{
	int a = 10;
	int& ra = a;
 
	cout<<"&a = "<<&a<<endl;
	cout<<"&ra = "<<&ra<<endl;
	return 0;
}

由前面的结论,输出的结果地址应该是相同的。
在这里插入图片描述
底层实现上实际是有空间的,因为在底层下,引用是按照指针方式来实现的。

int main()
{
	int a = 10;

	int& ra = a;
	ra = 20;

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

	return 0;
}

引用与指针的汇编代码对比如下:可见在底层,引用与指针的实现方式是一样的,只是编译器在背后帮我们取到了实体的地址,并在使用时帮我们解了引用,为我们使用引用提供了便利。
在这里插入图片描述
引用和指针的不同点:

  1. 引用在定义时必须初始化,指针没有要求
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  3. 没有NULL引用,但有NULL指针
  4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  6. 有多级指针,但是没有多级引用
  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  8. 引用比指针使用起来相对更安全
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值