【C++入门】引用(指针和引用的区别)

文章介绍了C++中的引用概念,作为对C语言指针的优化,引用是变量的别名,不额外占用内存。引用在定义时必须初始化并与其对象共享内存。引用常用于函数参数以避免拷贝,提高效率,但在返回值时需谨慎使用,因为可能造成悬空引用。此外,文章讨论了常引用const的用法以及引用与指针的区别。
摘要由CSDN通过智能技术生成

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


前言

本章是补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的。

一、问题引入

C语言的指针中,我们需要开辟一块新的空间来存在变量的地址;若要通过指针间接改变变量a,则需要对指针变量p解引用*p)进行间接访问

在这里插入图片描述

然而,C++创始人Bjarne Stroustrup大佬觉得这种方式用起来贼不舒服。因此,创建了一个新的语法 — 引用

二、引用的概念

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

  • 引用的表示方式
int main()
{
	int a = 0;

	//对a取别名为b
	//语法表示:引用类型& 别名 = 引用对象
	int& b = a;

	return 0;
}

注意:引用类型必须和引用对象是同种类型的

接下来我们对b赋值是否会改变变量a

在这里插入图片描述

当然我们可以通过打印地址的方式来验证引用是不会额外开辟内存空间

在这里插入图片描述

二、引用的特性

  1. 引用在定义时必须初始化

在这里插入图片描述

  1. 一个变量可以有多个引用

在这里插入图片描述

  1. 引用改指向不是对另一个对象取别名,而是赋值。

在这里插入图片描述

三、引用的使用场景

3.1 实现一个交换函数

【C语言版】

在这里插入图片描述

在C语言中,我们通常都是传变量地址给形参,这样才能对两个变量进行了交换。那么接下来看看引用版本:

在这里插入图片描述

3.2 单链表

【C语言版】

在这里插入图片描述

在单链表的插入中,当一开始链表为空时,需要改变头指针的指向,则要传地址。而如果用引用则是下面的代码

在这里插入图片描述

由于引用类型必须和引用对象是同种类型,所以引用类型和引用对象的类型是ListNode*,然后再加上引用符(&),说明pplistphead的别名。

3.3 引用做参数的意义

  1. 做输出型参数:形参的改变要影响实参。例如:写一个函数交换两个实值
  2. 提高效率。只有当调用自定义函数时,形参才会实例化(有自己的空间)。然后实参就会传递(拷贝)给形参,如果实参的数据量过大,拷贝的时间就长,效率就低。

以下是引用做参数和未用引用做参数的性能比较:

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

struct A 
{ 
	int a[100000];  // 10w数据
};

void TestFunc1(A a)  // 没有引用
{;}

void TestFunc2(A& a) // 引用传参
{;}

void TestRefAndValue()
{
	A a ;

	// 无引用传参
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
	{
		TestFunc1(a);
	}
		
	size_t end1 = clock();

	// 引用传参
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++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;
}

【程序结果】

在这里插入图片描述

四、引用做返回值问题

先来回顾传值返回

#include <iostream>

using namespace std;

int func()
{
	int a = 0;
	a++;

	return a;
}

int main()
{
	int ans = func();
	
	cout << ans << endl;

	return 0;
}
  • 注意:在func函数中,变量a并不是直接返回给main函数中的ans,中间会生成一个临时变量(也有可能是寄存器),变量a会先拷贝给临时变量,然后返回给ans
  • 为啥会生成临时变量呢?
    原因是:变量a出了作用域,其栈帧就销毁了。而这个临时变量就是为了存储a的值,然后临时变量再通过拷贝给ans(两次拷贝)
  • 知识回顾:函数栈帧的创建和销毁

有的人想可以用static修饰变量a,这样变量a就在静态区,栈帧销毁后就不会生成临时变量了。

在这里插入图片描述

但其实在编辑器认为:生不生成临时变量是由返回值的类型决定的。因此加了static后,还是会生成临时变量。

那么如果不想要生成这个临时变量,有没有办法呢?

当然有的。使用引用作为返回值,就不会产生临时变量。这也是传引用返回好处:减少拷贝,提高效率。

在这里插入图片描述

以上就是引用作为返回值的代码。但代码还是存在问题:类似野指针的访问。原因是:引用返回的是变量ans的别名,当调用完函数func时,由于变量a所在的空间就已经被销毁了(注意:销毁不是把空间整没了,而是把其使用权限还给了操作系统),这时由于是引用返回,返回时没有生成临时变量,这时ans相当于间接访问func函数(已被销毁),这导致数据没有保障,也就是说其实ans的值其实是不确定的。

总之,如果func函数结束,栈帧销毁,但没有清理栈帧(取决于编辑器,很明显VS是没有清理栈帧的),那么ans的结果是侥幸正确的;如果func函数结束,栈帧销毁,清理栈帧,那么ans就是随机值。

那么如何解决以上问题呢?还是static修饰变量a就行了

在这里插入图片描述

总结:

  1. 基本任何场景都可以用引用传参
  2. 但要谨慎用引用做返回值。出了函数作用域,对象不在了,就不能用引用返回,还在就可以用引用返回。

最后,我们可以测试值返回和引用返回的效率:

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

struct A
{
	int a[10000];
};

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

A& TestFunc2()
{
	return a;
}
void TestReturnByRefOrValue()
{
	A a;
	// 传值返回测试
	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()
{
	TestReturnByRefOrValue();
	
	return 0;
}

【程序结果】

在这里插入图片描述

可见使用传引用返回确实可以减少拷贝,提高效率。

五、 常引用const问题

  • const修饰引用对象

在这里插入图片描述

以上代码是错误的。原因是 const修饰变量是常变量不能被修改自身都无法修改,再对其取别名,也就自然不能被修改了。这里也说明了一点:引用过程中,权限不可以放大

  • const修饰别名

在这里插入图片描述

以上的代码是正确的。const修饰的是变量x的别名,也就是缩小的别名y的权限,不能对其进行修改,但对变量x修改还是可以的。这里也说明了一点:引用过程中,权限可以缩小。

在这里插入图片描述

  • const修饰不同类型

首先来看看一下代码:

在这里插入图片描述

我们在开头说过:引用的类型必须和对象是同种类型。但其实只说对了一半,那如果我对别名变量前加上const是否还会报错呢?

在这里插入图片描述

编译器居然没报错。这是为什么呢?原因是:它们之间发生类型转换

发生类型转换时,会产生临时变量,而这个临时变量具有 常属性,并且这个临时变量会对应转变的目标类型。

在这里插入图片描述

再举2个例子:

在这里插入图片描述

错误原因:首先函数func传值返回,这种返回不是直接返回x的值,而是先将x的值拷贝给临时变量再返回,而临时变量具有常性,所以应该用const修饰。

最后一个例子:

在这里插入图片描述

  1. 410行代码解释:这是引用返回,返回x的别名,对别名再取别名是ok的。(权限平移)
  2. 412行代码解释:首先res2x的别名,const修饰别名,缩小了res2的权限。(权限缩小)

六、引用和指针的区别(面试常考点)

  1. 在语法层面上:引用和引用对象共用同一块空间。

在这里插入图片描述

  1. 但底层实现上实际是有开空间的,引用是按照指针方式来实现的

在这里插入图片描述
我们来看下引用和指针的汇编代码对比

在这里插入图片描述

  1. 引用从概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化;指针没有要求,但可能会有野指针问题

在这里插入图片描述

  1. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

  2. sizeof中含义不同:引用类型的大小就是其类型本身;但指针始终是地址空间所占字节个数(32位平台下占4个字节/64位是8个字节)

  3. 引用自增即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

  4. 有多级指针(一级指针、二级指针…),但是没有多级引用

  5. 访问实体方式不同,指针需要显式解引用,引用的变量是直接使用

  6. 引用比指针使用起来相对更安全。因为指针更容易出现空指针、野指针等问题

  7. 不存在指向NULL的引用,但是存在指向NULL的指针

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值