【C++】一篇带你搞懂引用 C++入门(3)

目录

问题引入

1. 引用的概念

1.1. 引用的表示方法

1.2. 引用的特性

1.2.1. 引用在定义时必须进行初始化

1.2.2. 变量可以有多个引用

1.2.3. 引用一旦引用一个实体,再不能引用其他实体

1.3. 常引用、引用权限

1.4. 引用的使用场景

1.4.1 做参数

1.4.2. 做返回值

传值返回:

传引用返回:

1.5. 引用导致野指针

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

1.7. 引用和指针的区别


问题引入

生活中我们或多或少都有一些外号,比如《水浒传》中,及时雨宋江、九纹龙史进……而C++中也可以给同一个变量起外号,这就是引用。

1. 引用的概念

引用不是新定义一个变量,而是为已经有变量起一个别名。系统并不会为其开辟一个新的空间,而是与原来的变量共用一块内存。

1.1. 引用的表示方法

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

这里熟悉C语言的同学就会发现,&是C语言中的取值符,同时也是按位运算符,这其实是运算符重载例子。类似于函数重载,运算符重载也是的同一个运算符有着不同的功能,编译器能够根据上下文来理解运算符到底是什么意思。除了这里提到的,C++中还有一些运算符重载的例子:<<可以是插入运算符,也可以是左移运算符。

代码示例:

#include <iostream>
using namespace std;

int main() {
	int a = 10;
	int& b = a;
	int& c = b;
}

由本段代码可知,有一个是实体变量a,有两个别名b、c,我们可以通过调试来看一下这几个值的内存地址:

 通过调取内存不难发现,a、b、c指向的是同一块内存空间。

【注意】:引用类型必须和引用实体是同种类型的。


1.2. 引用的特性

引用的三个特性:

  • 引用必须在定义时进行初始化
  • 一个变量可以有多个引用
  • 引用一旦引用了一个实体,就不能再引用别的实体

1.2.1. 引用在定义时必须进行初始化

引用时对已经存在的变量起别名,所以必须为引用指定变量(初始化)

int& b;//错误,未初始化

 


1.2.2. 变量可以有多个引用

就像人可以有多个别名:

int main() {
	int a = 10;
	int& b = a;
	int& c = b;
}

1.2.3. 引用一旦引用一个实体,再不能引用其他实体

这个也比较好理解,因为引用一旦引用了一个已经存在的实体,就是这个实体的别名,当然不能再成为其他实体的别名。


1.3. 常引用、引用权限

大家看一下这段代码能不能编译成功?

int main() {
	const int x = 1;
	int& y = x;
     
    return 0;
}

会出现错误警告:

【注意】

引用原则:对原变量的引用,权限不能放大。


再来看一下这段代码能否编译成功?

int main() {
	const int x = 1;
	//int& y = x;

	const int& z = x;    //权限不变

	int a = 2;
	const int& b = a;    //权限变小

    return 0;
}

是可以成功通过编译的,很明显这段代码是符合引用原则的,也就是说权限不变或者权限缩小都是符合引用原则的,唯一注意的就是:权限不能放大


1.4. 引用的使用场景

1.4.1 做参数

void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main() {
	int a = 1, b = 2;
	Swap(a, b);
	cout << a << " " << b << endl;
}

将引用作为函数的参数,也就是说x是a的别名、y是b的别名,使用引用比较方便理解。

那么既然值可以作为函数参数,引用也可以作为函数参数,那么为什么要使用引用来作为参数呢?或者说使用引用作为函数参数的好处是什么?

这里使用这样一个测试代码,能够更加直接地看出使用引用作为函数参数的高效性:

#include <time.h>
#include <iostream>
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;
}

运行结果:

可以很明显的看出,引用作为参数的效率是更加高效的。因为当值作为函数参数或者返回值类型时,函数并不会直接传递实参或者返回变量本身,而是传递参数或者返回变量的一份临时拷贝,这个效率是十分低效的。

所以使用引用作函数参数的意义:

  • 输出型参数(允许修改外部变量)

  • 减少拷贝,提高效率


1.4.2. 做返回值

先来看一下这段代码的运行结果:

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

int main()
{
	int ret1 = Count();
	int ret2 = Count();
	int ret3 = Count();

	cout << ret1 << endl;
	cout << ret2 << endl;
	cout << ret3 << endl;

	return 0;
}

运行结果:

n虽然是定义在函数内部的静态局部变量,但是生命周期是全局的,所以我们能够看见结果的输出。但是实际生活中,静态的变量是少见的,大多还应该是局部变量。局部变量就会存在一个问题,我们学过函数栈帧的知识:返回值会储存在寄存器当中,当退出了函数(函数栈帧被销毁),这个返回值就会随之销毁ret就接收不到返回值,但是我们这里确实接收到了返回值的结果,这是为什么?

这个涉及到编译器的底层运作机制,为了解决这个问题,会自动产生一个临时变量,将寄存器中的返回值拷贝一份,赋值给临时变量,然后将临时变量返回(临时变量不会受函数栈帧销毁的影响)


传值返回:

也就是上面提到的,会产生一个临时变量,然后将返回值拷贝给它,最后返回临时变量,然后ret接收(也就是拷贝给ret)。

怎么证明确实产生了一个临时变量?

这个地方报错了,我们思考一下,如果没有产生临时变量,那么我使用一个int&来接收一个返回类型int,这是肯定没有问题的。但是出现了报错,就可以证明中间确实产生了一个临时变量,因为临时变量是有常性的,而这样接收的话,相当于是权限的放大。而我只要加上一个const就可以了。


传引用返回:

//在返回值类型上面加上&
int& Count()
{
	int n = 0;
	n++;
	cout << "n:" << &n << endl;
	return n;
}

int main()
{
	int& ret = Count();
	cout << "ret:" << &ret << endl;
	return 0;
}

PS:这里把const去掉了(并不影响我们得出的结论)

这段代码我们怎么理解?其实我们同样可以这样理解:同样产生了一个临时变量,只不过这个临时临时变量类型是int&,也就是n的别名,所以返回的是n的别名,所以接收的ret也应该是int&类型。

我们可以打印一下n和ret的地址:

可以发现是一样的,也就说明ret就是n的一个别名。


1.5. 引用导致野指针

回到上面提到的代码片段:

int& Count()
{
	int n = 0;
	n++;
	cout << "n:" << &n << endl;
	return n;
}

int main()
{
	int& ret = Count();
	cout << "ret:" << &ret << endl;
	return 0;
}

大家思考一下,这段代码有没有问题?似乎我们看结果来说,代码是没有问题的,但是我们加上一句代码试试:

int& Count()
{
	int n = 0;
	n++;
	cout << "&n:" << &n << endl;
	return n;
}

int main()
{
	const int& ret = Count();
	cout << ret << endl;                //第一次打印ret
	cout << "&ret:" << &ret << endl;
	cout << ret << endl;                //第二次打印ret
	return 0;
}

运行结果:

oh~mygod!!!会惊奇地发现,ret的值居然在变化,所以毫无疑问这段代码肯定是有问题的。但是问题出在哪?

是因为出了函数的作用域,Count函数就被销毁了,所以再去访问时就会造成非法访问,也就是引用搞出来的野指针。

我们可以在举一个更直观例子说明:

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):" << ret << endl;

	return 0;
}

运行结果:


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

#include <time.h>
#include <iostream>
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;
}

 


1.7. 引用和指针的区别

引用的底层还是使用指针实现的,可以使用反汇编查看一下定义引用和指针的区别:

引用和指针的不同点:

  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、付费专栏及课程。

余额充值