21天学会C++:Day5----引用

news/2024/12/5 9:51:01/

· CSDN的uu们,大家好。这里是C++入门的第五讲。
· 座右铭:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏:C++专题

 

目录

 1. 知识引入

 2. 引用的特性

 2.1 引用在定义时必须初始化

2.2 一个变量可以有多个引用

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

3. 常引用

​编辑

4. 引用的使用场景

4.1 做参数

4.2 做返回值

5. 引用的底层实现

6. 引用与指针的区别


 1. 知识引入

想必大家都还记得在用C语言实现单链表的时候的痛苦吧,什么二级指针,太难了!我们来看看当时的代码:

typedef struct ListNode
{struct ListNode* next;int val;
} ListNode;void ListPushHead(ListNode** pphead)
{//代码实现省略
}int main()
{struct ListNode* phead = NULL;ListPushHead(&phead);return 0;
}

因为形参只是实参的临时拷贝,形参的改变不影响实参。因此想要改变 phead 就必须要传 phead的地址过去。phead又是指针,所以就要传二级指针。真令人头大!

于是C++引入了新语法:引用。我们来看看引用的定义。

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。(这是在引用理解层面的定义,至于引用到底开不开空间,后面会讲解的)
 比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。

语法:

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

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

我们来看看有了C++的引用,上面的代码可以怎么写呢?

你可以将形参定义为这个结构体指针的引用,因为引用是变量的别名,结构体指针的引用那么就是这个结构体指针的别名,通过改变这个别名指向的内容就能改变结构体指针指向的内容。

typedef struct ListNode
{struct ListNode* next;int val;
} ListNode;void ListPushHead(ListNode*& pphead)
{//代码实现省略
}int main()
{struct ListNode* phead = NULL;ListPushHead(phead);return 0;
}

 或者你还可以这么写:

将结构体 typedef 为 *ListNode,那么 ListNode 就是结构体指针,这样在形参的书写上会更加简洁。但是理解起来就有一点困难了。

typedef struct ListNode
{struct ListNode* next;int val;
} *ListNode;void ListPushHead(ListNode& pphead)
{//代码实现省略
}int main()
{struct ListNode* phead = NULL;ListPushHead(phead);return 0;
}

 2. 引用的特性

我们再来看看引用的定义:

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

在理解引用时,你可以直接在脑海中呈现一张图:

有了这个图,我们来理解引用的特性。 

 2.1 引用在定义时必须初始化

我们可以看到下面的代码是编译失败的,原因就是引用在定义的时候没有初始化。至于为什么必须在定义的时候初始化,后面会讲解。

2.2 一个变量可以有多个引用

这个很好理解,一个变量当然可以有多个别名啦!就像一个人,他可能会有乳名,小名,大名等等。同样地,你可以在脑海中呈现那个图:

最后那条语句,我们让定义了一个变量 b 的引用,实际上 d 也指向 a 所指向的空间,因为 b 是 a 的引用嘛。既然多个变量指向可同一块空间那么,我们改变其中一个变量的值,其他的变量的值肯定也会发生相应的变化。上面的图可以很好地帮助大家理解。

int main()
{int a = 20;int& b = a;int& c = a;int& d = b;d = 30;cout << "a: " << a << endl;cout << "b: " << b << endl;cout << "c: " << c << endl;cout << "d: " << d << endl;return 0;
}

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

这里可以先尝试记忆,原因后面才会提到。

我们来看下面的代码,c = b 看上去好像是让 c 指向 b 这个变量,但实际上只是将 b 的值赋值给了 c ,并不是改变了引用的实体。我们可以通过打印值来证明。

int main()
{int a = 20;int b = 30;int& c = a;c = b;return 0;
}

 我们可以看到将 b 赋值给 c ,改变了 c 的值,也改变了 a 的值,这说明 c 还是变量 a 的引用,c = b 只是将 b 的值赋值给 c。

3. 常引用

我们来看上面的代码为什么会报错:const 修饰了变量 a,代表 a 指向的空间的内容不允许被改变。我们将变量 a 取别名为 ra,这个时候我们的操作权限就被放大了。因为引用变量没有加任何的限定符,表示通过引用变量 ra 能够修改 ra 指向的内容,而 ra 又是 a 的别名,则可推出 通过 ra能够改变 a 指向的内容。这就会与 const int a = 20;相冲突。

因此对于常量的引用,我们必须加上 const 限定符。不能让操作权限放大。

int main()
{const int a = 20;const int& ra = a; //加上 const 限定符,进制权限的放大return 0;
}

有人就可能会说了,既然不允许权限的放大,那允不允许权限的缩小呢?当然是可以的啦。

int main()
{int a = 20;const int& ra = a;return 0;
}

 通过 const 限定符使得 ra 的权限缩小,只能读 ,不能修改。

4. 引用的使用场景

4.1 做参数

在知识引用里面就是引用做参数的例子呀!

下面的知识点会涉及函数栈帧的相关知识,不懂的老铁可以先瞅瞅:详解函数栈帧的创建和销毁。

我们知道在函数栈帧创建的过程中,实参向形参的传递会拷贝实参,但是拷贝实参必然会有时间的消耗。但是引用就比较特殊啦,因为引用是变量的别名,我们在理解的层次上,可以认为引用并不会开辟空间,所以在传递参数的时候,效率就会比不传引用高。下面的代码会一定程度上证明只一点:

struct A { int a[10000]; }; void Func1(A a) {}void Func2(A& a) {}int main()
{A a;int begin1 = clock();for (int i = 0; i < 100000; i++)Func1(a);int end1 = clock();int begin2 = clock();for (int i = 0; i < 100000; i++)Func2(a);int end2 = clock();cout << "传值花费的时间:" << end1 - begin1 << endl;cout << "传引用花费的时间:" << end2 - begin2 << endl;return 0;
}

4.2 做返回值

做返回值就和普通的变量没什么大的区别,但有一点值得注意:因为引用是变量的别名,将引用作为函数的返回值,我们必须确保该变量不能在这个函数调用完毕后就被销毁了,即是:不可返回局部变量的引用。

大家能猜到下面的代码的输出结果吗?(IDE:VS2019)

int Func(int a, int b)
{int d = 0;d = a + b;return d;
}int& Add(int a, int b)
{int c = 0;c = a + b;return c;
}int main()
{int& a = Add(10, 20);cout << a << endl;Func(20, 30);cout << a << endl;return 0;
}

 下面来解释原因:

 我们来看看Linux下的g++编辑器的结果:segmentation fault,段错误,非法访问内存。

 切记:不可返回局部变量的引用。

还有一点就是:在返回值上引用也是不需要拷贝的,效率也是比直接返回值高好吧!这里就不再写代码演示了,代码和上面测试传参的相差不大。

5. 引用的底层实现

我们可以在调试的过程中查看汇编代码来观察引用的底层实现:

 我们看到引用的底层实现还是C语言的指针啊!那么我们就可以解释为什么引用定义出来就必须初始化,引用的实体不能改变了!原因就是这个指针经过了 const 修饰,类似于这样:

int* const reference

因为 cosnt 修饰所以我们必须在引用定义的时候初始化。因为 const 修饰所以我们不能更改引用的实体。

但是,我们在平时的应用时,将引用理解为别名,不开辟空间即可,当有人问你引用的底层实现时,你知道就行了!这样能简化理解的难度。

6. 引用与指针的区别

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

 上面的这些东东在理解的基础上记忆即可!切不可死记硬背。

好了,引用就差不多讲完了,有什么不正确的地方欢迎大家的指正。


http://www.ppmy.cn/news/94281.html

相关文章

NoSQL之Redis高可用与优化

目录 一、Redis高可用二、Redis 持久化2.1 Redis 提供两种方式进行持久化2.2 RDB持久化2.2-1 触发条件2.2-2 执行流程2.2-3 启动时加载 2.3 AOF 持久化2.3.1 开启AOF2.3.2 执行流程2.3.3 执行流程启动时加载 三、RDB和AOF的优缺点四、Redis 性能管理4.1 查看Redis内存使用4.2 内…

ONNX模型修改为自定义节点

参考一 首先&#xff0c;需要将ONNX模型中的节点修改为自定义节点。要实现这一点&#xff0c;您需要了解自定义节点的定义和如何在ONNX中使用它们。ONNX定义了一个自定义运算符的接口&#xff0c;您可以使用该接口定义自己的运算符&#xff0c;并将其编译为ONNX模型可以识别的…

java基础入门-13-【集合(List集合)】

Java基础入门-13-【集合(List集合)】 22、集合(List集合)1.Collection集合1.1 数组和集合的区别【理解】1.2 集合类体系结构【理解】1.3 Collection 集合概述和使用【应用】Collection集合概述Collection集合常用方法代码书写1.4 Collection集合的遍历【应用】迭代器介绍Co…

设置linux的时间

目录 一、什么是时间 &#xff08;1&#xff09;例子1 &#xff08;2&#xff09;例子2 二、什么是本地时间 三、linux设置本地时间的方法 &#xff08;1&#xff09;方式一&#xff1a;通过互联网自动同步 1.修改时间同步服务器 2.查看时间同步情况 &#xff08;2&…

Spring的作用域和生命周期

目录 1.Bean的作用域 2.Bean的作用域的分类 3.设置作用域 4.Spring的执行流程&#xff08;生命周期&#xff09; 5.Bean的生命周期 1.Bean的作用域 lombok &#xff08;dependency依赖&#xff09; 是为了解决代码的冗余&#xff08;比如说get和set方法&#xff09;那些构造…

二、服务网关-Gateway

文章目录 一、服务网关1、网关介绍2、Spring Cloud Gateway介绍3、搭建server-gateway模块3.1 搭建server-gateway3.2 修改配置pom.xml3.3 在resources下添加配置文件3.4添加启动类3.5 跨域处理3.5.1 为什么有跨域问题&#xff1f;3.5.2解决跨域问题 3.6服务调整3.7测试 一、服…

Kubernetes ElasticSearch 高级实践归纳和注意点

注意方面: 集群规划和节点配置:需要根据数据规模和性能需求来规划集群的大小和节点的配置,例如节点的 CPU、内存、存储等。高可用性和容错:ElasticSearch 支持主从复制和副本分片等机制,可以提供高可用性和容错能力,需要根据业务需求来配置。节点调度和亲和性:为了避免数…

ACL 2019 - AMR Parsing as Sequence-to-Graph Transduction

AMR Parsing as Sequence-to-Graph Transduction 论文&#xff1a;https://arxiv.org/pdf/1905.08704.pdf 代码&#xff1a;https://github.com/sheng-z/stog 期刊/会议&#xff1a;ACL 2019 摘要 我们提出了一个基于注意力的模型&#xff0c;将AMR解析视为序列到图的转导。…