【C++】踏上C++学习之旅(三):“我“ 与 “引用“ 的浪漫邂逅

在这里插入图片描述

文章目录

  • 前言
  • 1. "引用"的概念
    • 1.1 "引用"的语法
  • 2. "引用"的特性
  • 3. "引用"的使用场景
    • 3.1 "引用"做参数
    • 3. 2 "引用"做返回值
      • 3.2.1 "引用"做返回值时需要注意的点
  • 4. 常引用
  • 5. "引用"在底层的实现
  • 6. "引用"和"指针"的不同点(面试常考)

前言

本文会着重的讲解"引用"的各项用法以及使用时需要注意的一些规则,另外这部分是面试官比较喜欢与指针一起作为问题来提问我们的,所以我在文章的末尾,给大家也准备好了答案!

话不多说,让我们从现在开始与"引用"进行一场浪漫的邂逅的吧!!!

哈哈哈

1. "引用"的概念

引用不是一种新的数据类型,而是在C++中给已存在变量起一个别名。编译器不会给引用变量开辟内存空间,它和它引用的变量共用同一块空间

举个例子,在《水浒传》中,李逵在家中被宋江叫做"铁牛",在江湖上人称"黑旋风"。那么,我们说"铁牛"和"黑旋风"都是在说李逵这个人,所以说"铁牛"和"黑旋风"就是别名。 在代码的世界里,相信大家已经对别名有所使用,就是typedef这个关键字通常被有做对结构体起别名。在C++中,引用是对变量起别名!

讲解完引用是什么之后,那我们就来看看,引用是如何在代码中表示的。

1.1 "引用"的语法

数据类型& 引用变量名(对象名) = 引用实体;

下面我来写一段代码,带着大家感受一下"引用"的魅力:

#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	int& ra = a;  //类型& 引用变量名 = 引用实体;

	cout << "a = " << a << endl;
	cout << "ra = "  << ra << endl;

	return 0;
}

引用演示

大家在注意引用的定义时,引用变量与引用实体必须得是相同的数据类型。否则,程序会报错的!

2. "引用"的特性

1. 引用在定义时必须初始化;
2. 一个变量可以有多个引用;
3. 一旦有个实体被引用,那么这个引用变量就不能再引用其它实体。

int main()
{
	int a = 10;
	//int&ra; //该条语句在编译时会报错
	int& ra = a;
	int& rra = a;
	printf("%p  %p  %p",&a,&ra,&rra);
}

错误示范

tupian
以上的代码案例就能很好的体现出引用的特性!

哈哈哈


3. "引用"的使用场景

光讲引用的定义和特性,相信这一定不能让大家认识到"引用"有多强大,有多舒服。那么,接下来,我结合C语言的代码场景来对比在C++下,引用的强大之处。

3.1 "引用"做参数

"引用"做参数,主要是针对输出型参数。

什么是输出型参数?
输出型参数:通过形参的改变能影响实参的改变。 这类的形参,我们就把它称为输出型参数。当然与之对应的还有输入型参数,这个参数想必大家肯定用的不少,==输入型参数:形参的改变不会影响到实参。==这类的形参,我们称它为输入型参数。

好处:
1. 针对输出型参数
2. 减少拷贝,提高效率(特别是大对象/深拷贝对象)

为了让大家更好的感受到"引用"的"爽",我将用多个代码在C语言下和C++下做比较:

//场景一:交换两个数(用C语言)
void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

//交换两个数(用C++的"引用")
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
struct Stack{
	int* a;
	int top;
	int capacity;
};

//场景二:给栈初始化(用C语言)
void StackInit(struct Stack* pst,int STDefault = 4)
{
	pst->a = (int*)malloc(sizeof(int)*STDefault);
	if(pst->a == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	pst->top = 0;
	pst->capacity = STDefault;
}

//给栈初始化(用C++的"引用")
void StackInit(struct Stack& st,int STDefault = 4)
{
	st.a = (int*)malloc(sizeof(int)*STDefault);
	if(st.a == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	st.top = 0;
	st.capacity = STDefault;
}

int main()
{
	struct Stack st;
	//用C语言的版本
	StackInit(&st);
	//用C++的版本
	StackInit(st);
}

大家可以仔细对比一下,是C语言指针的写法好用,还是C++的"引用"好用。

StackInit函数struct Stack& st这个形参就相当于输出型参数,当然这个想象在Swap函数更加明显。

好了,"引用"作为形参的第一个好处我理解了,那第二个好处又怎么解读呢?

我说引用作为参数,可以减少拷贝,提高效率 ,这个点就体现在函数栈帧的创建和销毁中。如果对这方面不了解的读者,可以看一下往期我写的文章:【C语言】函数栈帧的创建和销毁(启航——迎接崭新的自己)。

回到主线上,我们在调用一个函数时,会在栈区给函数的调用开辟一块空间,这块空间就是函数的栈帧,编译器开会就会往栈中压入以西寄存器之类的东西。重点来了,随后,它就会把我们的形参从右往左依次压入栈中,在这个过程中是通过寄存器将形参先拷贝下来,而这段拷贝是要花时间的。 而我们使用"引用"的话,就可以掠过拷贝的过程,这将这个变量给放到栈中,减少了拷贝的花销。

大家可以拷贝一下程序,在你自己的电脑检测一下:

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

//减少拷贝,提高效率
struct test //创建一个大对象
{
	int a[10000];
};

test a;
void Func1(test a) {};
void Func2(test& a) {};

int main()
{
	int begin1 = clock();
	for (int i = 1; i <= 10000; i++)
	{
		Func1(a);
	}
	int end1 = clock();

	int begin2 = clock();
	for (int i = 1; i <= 10000; i++)
	{
		Func2(a);
	}
	int end2 = clock();


	cout << "time void Func1(test a) : " << end1 - begin1 << endl;
	cout << "time void Func2(test& a) : " << end2 - begin2 << endl;

	return 0;
}

结果
可以看到,引用确实是提高了程序的效率!

当然,引用不仅在参数中能大放异彩,它在做函数返回值时也同样优秀。

3. 2 "引用"做返回值

好处:
1. 减少拷贝,提高效率(特别是大对象/深拷贝对象)
2. 修改返回值 + 获取返回值

请大家看下面的代码:

#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;
}

结果

3.2.1 "引用"做返回值时需要注意的点

请大家想看看下面这个代码的结果:

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;
}

引用做返回值
可以看到引用作为返回值时,及其容易出错,那错误的原因就是"非法访问"。

有的读者此时就会说,程序也没有崩溃,何来的"非法访问"一说?
有时候并不是编译器不报错就不代表你这个代码没有问题,就像数组越界一样。

🍉那我们该怎么理解"非法访问"这一说呢?
这就又要牵扯到函数栈帧的知识了。函数栈帧在被销毁时,编译器做了一个这样的策略,它会将这个返回值用一个寄存器给保存起来。如果我们用"引用"的话,就相当于直接拿着这个返回值的地址了,所以函数栈帧销毁时,会把这块返回值空间的使用权归还给操作系统,此时我们还要用的话,就相当于"非法访问"了。

那有眼尖的读者就会看到,就算是这样,结果也是没有问题的啊。
这是因为编译器在函数栈帧销毁时的处理方式不同:

两种处理方式:

  1. 函数栈帧销毁时,编译器不清空栈帧里面的内容
  2. 函数栈帧销毁时,编译器会清空栈帧里面的内容

显然,我们的编译器是选择前者的方案。

可以看看一下这个代码,让你感觉到用"引用"作为返回值是否非法访问了:

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

int& count(int x)
{
	int n = x;
	n++;
	//...
	return n;
}

int main()
{

	int& ret = count(10);
	cout << ret << endl;
	printf("ssssssssssssssssssssssssss\n");
	rand();
	
	cout << ret << endl;

	return 0;
}

jieguo
结果分析:可以看到第二次打印变量ret的值时,发现是一个随机值,这也就是说明了我i们正在非法的访问者我们的内存。

总结:所以我们在用"引用"作为返回值时,一定不要用局部变量作为函数的返回值,或者和说不要使用在栈区上创建的变量,可以使用静态区或者堆区上的变量!

4. 常引用

“引用"还有一种场景,那就是"常引用”。

常引用通常搭配着一个关键字const使用。

int main()
{
	int& ret1 = func1();  // 权限放大,编译器会报错
	const int& ret1 = func1(); // 权限平移
	int ret1 = func1();  // 拷贝

	int& ret2 = func2();		// 权限平移
	const int& rret2 = func2();  // 权限缩小

	return 0;
}

以上的写法都是被允许的。可以看到常引用的权限是小于引用的权限的。

🍉总结:在使用常引用时,我们要注意权限之间的问题,只能进行权限的平移或者权限的缩小。权限的大小关系:"引用"权限 > "常引用"权限。

5. "引用"在底层的实现

大家可以看一下下面代码的反汇编代码:

int main()
{
	int a = 10;
	int& ra = a;
	ra = 20;
	
	int* pa = &a;
	*pa = 20;
	return 0;
}

以下是对应代码的反汇编代码:
反汇编代码
可以看到的是,"引用"的底层也是用指针来实现的!

重点说明:在底层实现上"引用"的确是消耗了空间,但是在语法概念上,我们是认为"引用"是不会单独开辟空间的。我们会通常使用后者的说法。

6. "引用"和"指针"的不同点(面试常考)

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

以上就是本文的全部内容了,如果觉得本文对你有帮助的话,麻烦给偶点个赞吧!!!

哈哈哈

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/897328.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【设计模式系列】命令模式

目录 一、什么是命令模式 二、命令模式的角色 三、命令模式的典型应用场景 四、命令模式在Runnable中的应用 一、什么是命令模式 命令模式&#xff08;Command Pattern&#xff09;是一种行为设计模式&#xff0c;它将一个请求或简单操作封装为一个对象。这个模式提供了一种…

在使用new Date()生成时间戳时,发现数据库中 的时间总是多出一秒钟。

项目汇报的时候&#xff0c;进一步研究问题 insert into t_tax_file(task_id, task_no, business_type, file_name, file_url, creator_id, created_time, modifier_id,modified_time)value (10, taskNo测试, 1, 文件名称, 文件地址, 1, 2024-10-21 10:25:21.889, 1, 2024-10-…

CCF-BDCI大数据与计算智能大赛TOP4-京东生鲜

2023 CCF 大数据与计算智能大赛《线上线下全场景生鲜超市库存履约一体化决策》top4南山论剑 摘要1 数据预处理1.1 数据整合1.2 数据划分 2 特征工程2.1 静态特征2.2 动态特征 3 方案设计3.1 数据构造3.2 模型训练3.3 模型融合3.4库存分配3.5 方案对比 链接: CCFBDCI-线上线下全…

对BSV区块链下一代节点Teranode的答疑解惑(上篇)

​​发表时间&#xff1a;2024年8月7日 2024年初BSV区块链研发团队揭晓了即将到来的Teranode更新的突破性特性&#xff0c;这些特性将显著提升网络的效率和处理速度&#xff0c;使BSV区块链能够达到百万级TPS。 Teranode的项目主管Siggi Oskarsson强调&#xff1a;“当你阅读这…

uniapp项目结构基本了解

基本结构的解释 App.vue&#xff1a;应用的根组件&#xff0c;定义全局布局和逻辑。pages/&#xff1a;存放各个页面的 .vue 文件&#xff0c;定义应用的具体页面和功能模块。main.js&#xff1a;应用入口文件&#xff0c;初始化应用&#xff0c;挂载 App.vue。manifest.json&…

[Linux进程概念]命令行参数|环境变量

目录 一、命令行参数 1.什么是命令行参数 2.为什么要有命令行参数 &#xff08;1&#xff09;书写的代码段 &#xff08;2&#xff09;实际的代码段 3.Linux中的命令行参数 二、环境变量 1.什么是环境变量&#xff1f; 2.获取环境变量 &#xff08;1&#xff09;指令…

基于Multisim电子配料秤电路设计(含仿真和报告)

【全套资料.zip】电子配料秤电路设计Multisim仿真设计数字电子技术 文章目录 功能一、Multisim仿真源文件二、原理文档报告资料下载【Multisim仿真报告讲解视频.zip】 功能 电子配料秤仿真功能: 准确测量物体重量&#xff0c;精确度0.1Kg使用两位数码管显示重量信息 使用拨码…

深度学习 基本函数01

np.dot 是 NumPy 库中的一个函数&#xff0c;用于计算两个数组的点积&#xff08;也称为内积或数量积&#xff09;。点积是两个向量的对应元素乘积之和。 np.random.normal 是 NumPy 库中的一个函数&#xff0c;用于生成符合正态分布&#xff08;也称为高斯分布&#xff09;的…

jmeter用csv data set config做参数化1

在jmeter中&#xff0c;csv data set config的作用非常强大&#xff0c;用它来做批量测试和参数化非常好用。 csv data set config的常用配置项如下&#xff1a; Variable Names处&#xff0c;写上源文件中的参数名&#xff0c;用于后续接口发送请求时引用 Ignore first line…

Mybatis多对一查询的配置及两种方法的使用示例对比以及Mybatis一对多查询两种方法使用示例及对比

一、Mybatis多对一查询的配置及两种方法的使用示例对比 为了试验Mybatis多对一的查询&#xff0c;我们先在数据库中建两个表&#xff0c;一个城市表&#xff0c;一个市区表&#xff0c;一个城市有多个区是一个一对多的关系&#xff1b;多个区对应一个城市是一个多对一的关系。建…

spring源码拓展点3之addBeanPostProcesser

概述 在refresh方法中的prepareBeanFactory方法中&#xff0c;有一个拓展点&#xff1a;addBeanPostProcessor。即通过注入Aware对象从而将容器中的某些值设置到某个bean中。 beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));aware接口调用 …

ThinkPad T480拆机屏幕改装:便携式显示器DIY指南

ThinkPad T480拆机屏幕改装&#xff1a;便携式显示器DIY指南 本文记录了将旧笔记本电脑 T480 拆机屏幕改装为便携式显示器的全过程。作者在决定升级设备后&#xff0c;选择通过 DIY 方式利用原有的屏幕资源。文章详细介绍了屏幕驱动板的安装、螺丝孔的剪裁、排线连接及固定的步…

[DB] NSM

Database Workloads&#xff08;数据库工作负载&#xff09; 数据库工作负载指的是数据库在执行不同类型任务时所需的资源和计算方式&#xff0c;主要包括以下几种类型&#xff1a; 1. On-Line Transaction Processing (OLTP) 中文&#xff1a;联机事务处理解释&#xff1a;…

hive初体验

1.首先&#xff0c;确保启动了Metastore服务。 runjar就是metastore进程 2.进入hive客户端: 命令:hive 3.操作:没有指定数据库时默认在default 一:创建表:CREATE TABLE test(id INT, name STRING, gender STRING); 完成,show tables看一下 也可以通过hdfs文件系统查看,默认路径…

go多线程

1.仅加go 在一个golang编写的程序&#xff0c;主函数运行完毕后&#xff0c;程序就结束了 package mainimport ("fmt""time" )func main() {// 如果这样写go 要加在上面的函数&#xff0c;因为如果只单独加在下面的函数或者都加上&#xff0c;程序就会直接…

Leetcode 柱状图中最大的矩形

h 是右边界&#xff0c;连续多个高度递增的柱子&#xff0c;如果遇到下一个 h < 栈顶元素(是最大的元素&#xff0c;单调递增栈)&#xff0c;那么会不断出栈来更新计算最大面积。 并非是一次性计算出最大面积的&#xff0c;很重要的一点是while (!stack.isEmpty()这一部分的…

Vivado自定义IP修改顶层后Port and Interface不更新解决方案

问题描述 在整个项目工程中&#xff0c;对自定义IP进行一个比较大的改动&#xff0c;新增了不少端口(这里具体的就是bram的读写端口)&#xff0c;修改是在block design中右击IP编辑在IP编辑工程中进行的。 在修改完所有代码后&#xff08;顶层新增了需要新加的输入输出端口&…

【计算机网络 - 基础问题】每日 3 题(四十九)

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?typeblog &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/fYaBd &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 C 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞…

字节流写入文件

一、创建输出流对象表示的文件三种方式 方法一&#xff1a; FileOutputStream fos new FileOutputStream("fos.txt",true);//最简便方法二&#xff1a; FileOutputStream fos new FileOutputStream(new File("fos.txt"));方法三&#xff1b; File f ne…

HCIP-HarmonyOS Application Developer 习题(十四)

&#xff08;多选&#xff09;1、HarmonyOs为应用提供丰富的Al(Artificial Intelligence)能力&#xff0c;支持开箱即用。下列哪些是它拥有的AI能力? A、通用文字识别 B、词性标注 C、实体识别 D、语音播报 答案&#xff1a;ABCD 分析&#xff1a; AI能力简介二维码生成根据开…