【 C++入门 】引用
目录
1、引用概念
2、引用特性
引用在定义时必须初识化
一个变量可以有多个引用
引用一旦引用一个实体,再不能引用其它实体
3、常引用
3.1、取别名的规则
权限放大error
权限不变
权限缩小
3.2、拓展1:如何给常量取别名
拓展2:临时变量具有常性
3.3、对权限控制的用处
4、引用的使用场景
4.1、做参数
4.2、做返回值
传值返回
传引用返回
5、传值、传引用效率比较
6、引用和指针的区别
1、引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;看代码:
int main() {//把b叫做a的引用,也叫做b是a的别名int a = 10;int& b = a;return 0; }
这里有一个变量a,a这块空间占4个字节,现在又给a取了一个名字叫b,也就是说a和b同时可以访问且修改这块空间,并且这里a和b的地址均是一样的。
引用的实质就是在取别名,就好比西游记里的孙悟空,你叫他弼马温、齐天大圣、孙行者都是一个道理
既然引用是在取别名,那我对别名进行修改,就相当于对自己本身进行修改:
2、引用特性
引用具有三大特性:
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其它实体
接下来,我将做具体演示:
引用在定义时必须初识化
我能否写出如下的引用呢?
int& d;
答案很明显,还没等你编译,就已经肉眼看见的错误了,综上,引用在定义时必须初始化。
一个变量可以有多个引用
比如我有个变量a,你可以给其取个别名b,也可以取个别名c,甚至给别名c再取别名d都可以,并且这些别名和a的地址均是一样的,我改变其中一个,其它的也会随之改变。
引用一旦引用一个实体,再不能引用其它实体
看代码:
在这段代码中,我们已经给a取别名b,随后把e的值赋给b,这里可不是对e取别名了,通过编译即可看出来,b的地址同引用的a的地址,而不同于e的地址。
3、常引用
3.1、取别名的规则
我们在取别名的时候,不是在所有情况下都可以随便取的,要在一定范围内。
对原引用变量,权限只能缩小,不能放大。
权限放大error
我们都清楚C语言有个const,在C++的引用这一块也是有const引用的
假如我们现在有const修饰过的变量x,现在想对x取别名,还能像如下的方式进行吗?
//权限放大 err const int x = 20; int& y = x;
此时的y还是x的别名吗?编译起来一看全是错误
这就是典型的权限放大,学过C语言我们都清楚,const是只读的,对于变量x,我们只可以进行读,不能进行修改。而此时我们对x引用成y,且是int型的,此时y是可读可写的,不满足x的只读条件。
那怎么样才能对x进行引用呢?只需要确保权限不变即可,见下文:
权限不变
想要控制权限不变非常简单,只需要对x引用的同时加上const修饰即可,让变量y也是只读的
//权限不变 const int x = 20; const int& y = x;
那如果变量没有加const修饰,但是在引用时加了const可以吗?这就是权限缩小,看下文:
权限缩小
//权限缩小 int c = 30; const int& d = c;
我们针对上述代码进行编译,发现编译器没有任何报错,答案是可以的。
这里的c是可读可写的,我对c进行const引用,顶多就是把c改变为只读的,只是权限缩小,不违反要求,当然是可以的。
3.2、拓展1:如何给常量取别名
可以给常量取别名吗?
int& c = 20; // err
其实是不可以直接进行取别名的,但是我们加上const就可以了:
const int& c = 20; // right
拓展2:临时变量具有常性
看如下代码:
double d = 2.2; int& e = d;
现在e能成为d的别名吗?
很明显不可以,编译器发生错误。但是我加上const,发现它竟然就不会出错了:
double d = 2.2; const int& e = d;
怎么解释上述代码呢?这就需要我们先回顾下C语言的类型转换
C++本身是在C语言的基础上走的,C语言在相似类型是允许隐式类型转换的。大给小会截断,小给大会提升。看如下代码:
double d = 2.2; int f = d;
编译器运行后:
这里的会丢失数据其实就是会丢失精度
- 注意:
这里在把d的值赋给f时并不是直接赋值的,会把d的整数部分取出来,赋值给一个临时变量,该临时变量大小4个字节,随后再把这个临时变量给给f
临时变量具有常性,就像被const修饰了一样,不能被修改
- 谈到这,你就应该能够理解上文的这段代码为什么要加上const才能编译通过:
double d = 2.2; const int& e = d;
答案很简单,这里e引用的是临时变量,临时变量具有常性,不能直接引用,否则就是放大了权限,加上const才能保证其权限不变
- 可能又会有人提问了,那为什么这段代码在赋值的时候不加上const呢?
double d = 2.2; int f = d;
其实很简单,上述加const是在我引用的基础上加的,如若不加const,那么就是放大权限,让e变为可读可写的同时临时变量也如此,而此段代码中,对f的改变并不会影响到我临时变量,更不会影响到d,主要就是普通的变量不存在权限放大或缩小。
- 此时又有人提问了,那么此时的e还是对d的引用吗?
double d = 2.2; const int& e = d;
这当然不是,此时的e是对临时变量的引用,是临时变量的别名。可以通过编译来验证:
3.3、对权限控制的用处
这里简单提下,例如这个传参的问题。
如若函数写出普通的引用,那么很多参数可能会传不过来:
仔细看这段代码,只有a能正常传过去,后面的均传不过去,因为后面传的参数均涉及权限放大,固然编译器会出错
但是当我们在函数的形参那加上const呢?
加了const后编译器就会报错了
4、引用的使用场景
引用的使用场景分为两个:
- 做参数
- 做返回值
接下来,我将会详细讲解下:
4.1、做参数
就比如说我现在要写一个Swap函数,以前是用指针写的:
//指针版 void Swap(int* pa, int* pb) {int tmp = *pa;*pa = *pb;*pb = tmp; }
而现在,我们就可以巧用引用来完成Swap函数
//引用版 void Swap(int& x, int& y) {int tmp = x;x = y; y = tmp; } //支持函数重载 void Swap(double& x, double& y) {double tmp = x;x = y;y = tmp; }
现在,引用就可以做我的形参,就不用再像以前C语言那样总是取地址&,并且在调用的时候也会非常方便,因为有函数重载的加持。
int main() {//交换整数int a = 0, b = 1;Swap(a, b);//交换浮点数double c = 1.1, d = 2.2;Swap(c, d);return 0; }
- 引用还有一个好处在输出型参数会得到体现:
int* preorderTraversal(struct TreeNode* root, int* returnSize) {//…… }
这里给一个*returnSize多少有点奇怪,其实这样写就非常方便:
int* preorderTraversal(struct TreeNode* root, int& returnSize) {//…… } int main() {preorderTraversal(tree, size); }
加上引用会在调用时省去写&,也更方便理解,减少对指针的使用。
综上,引用做参数的好处如下:
- 输出型参数
- 减少拷贝、提高效率
引用还有一个使用场景是做返回值,具体看下文:
4.2、做返回值
先看一段代码:
int Count() {static int n = 0;n++;return n; } int main() {cout << Count() << endl; //1cout << Count() << endl; //2cout << Count() << endl; //3return 0; }
针对此段代码,我们运行的结果是1、2、3。
- 这里可能有人会提问为什么不是1、1、1呢?注意这里使用了静态区的变量只会初始化一次,也就是说我static int n = 0这行代码在编译时只有第一次会跳到这,其余两次均不会走这一行代码,你每次进去的n都是同一个n。通过调试我们就可以看出,这里n的地址始终都是一样的。
传值返回
传值返回是有讲究的。正如这段代码:
int Count() {int n = 0;n++;return n; } int main() {int ret = Count();return 0; }
在传值返回的过程中会产生一个临时变量(类型是int),如果这个临时变量小它会用寄存器替代,如果大就不会用寄存器替代。
具体返回的过程是先把函数的n拷贝给临时变量,再把临时变量拷贝给ret。
为什么要设计这个临时变量呢?
上述代码不可以直接把n返回给ret,这里我们简要画个栈帧图即可看出:
main函数里有个变量ret,汇编时会call一个指令跳到函数Count,Count里有一个变量n。这里不能把n直接传给ret,因为在函数Count调用完成后要拿一个值赋给ret,且函数调用完后函数栈帧就销毁了,所以赋给ret的这个值就是设计出的临时变量
如何证明我这中间会产生临时变量呢?
只需要加个引用即可。
这里很明显编译发生错误。为什么呢?这里其实答案就很明显了,这里ret之所以出错不就是因为其引用的是临时变量呢,因为临时变量具有常性,只读不可修改,直接引用则会出现上文所述的权限放大问题。 所以这不很巧合的验证了此函数调用中途会产生临时变量。
解决方法也很简单,保持权限不变即可,即加上const修饰:
传引用返回
我们对上述代码进行微调:
int& Count() {int n = 0;n++;return n; } int main() {int ret = Count();return 0; }
这里加上了引用&后,中间也会产生一个临时变量,只是这个临时变量的类型是int&。我们把这个临时变量假定为tmp,那么此时tmp就是n的别名,再把tmp赋值给ret。这个过程不就是直接把n赋给ret吗。这里区分于传值返回的核心就在于传引用的返回就是n的别名。
如何证明传引用返回的是n的别名?
只需要在函数调用时加个引用即可:
我们也可以通过打印法来验证:
这里ret和n的地址一样,也就意味着ret其实就是n的别名。综上,传值返回和传引用的返回的区别如下:
- 传值返回:会有一个拷贝
- 传引用返回:没有这个拷贝了,返回的直接就是返回变量的别名
现在又存在一个问题了:我传引用的代码对不对?
我传引用返回后,ret就是n的别名,但是有没有想过,出了函数出了这个作用域我n不是都销毁了吗,怎么还会有别名呢?
空间的销毁不是说空间就不在了。空间的归还就好比你退房,虽然你退房了,但是这个房间还是在的,只是说使用权不是你的了。但是假说你在不小心的情况下留了一把钥匙,你依旧是可以进入这个房间,不过你这个行为是非法的。这个例子也就足矣说明了上述的代码是有问题的。是一个间接的非法访问。
仔细看我这段截图:
这里第一次打印ret的值为1,第二次打印的ret为随机值,这就是因为发生了覆盖。这里你第一次打印是正常的,随后打印完后,函数栈帧销毁,此时又打印了其它东西,又会函数调用覆盖了原来函数的位置,当你第二次打印ret的值时自然就是随机值了。
综上这种情况是不能进行引用返回的。
- 若我非要引用返回呢?如何正确使用?
加上static即可:
int& Count() {static int n = 0;n++;cout << "&n: " << &n << endl;return n; } int main() {int& ret = Count();cout << ret << endl;cout << "&ret: " << &ret << endl;cout << ret << endl;return 0; }
加上了static后就把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; //7return 0; }
这段代码执行的结果ret的值为7,首先我Add(1,2),调用完后,返回c的别名给ret,随即调用完Add栈帧销毁,当我第二次调用时c的值就被修改为7了,这里想表达的是这里是不安全的。
正常情况下我们应该加上static:
加上static后这里ret的值就是3了,因为加上了static初始化只有一次。此时c在静态区了,销毁栈帧它还在。
- 这里再演示下其被覆盖的情形:
正常情况:
不加static发生覆盖:
5、传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include <time.h> 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(); }
- 值和引用的作为返回值类型的性能比较
#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; } int main() {TestReturnByRefOrValue(); }
6、引用和指针的区别
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
接下来就上述指针与引用不同点做详细解析:
- 引用在定义时必须初始化,指针没有要求
int& r; //err 引用没有初始化 int* p; //right 指针可以不初始化
- 在sizeof中含义不同:引用结果为引用类型的大小,但直至始终时地址空间所占字节个数(32位平台下占4个字节)
double d = 2.2;double& r = d;cout << sizeof(r) << endl; //8
- 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main() {int a = 10;//语法角度而言:ra是a的别名,没有额外开空间//底层的角度:它们是一样的方式实现的int& ra = a;ra = 20;//语法角度而言:pa存储a的空间地址,pa开了4/8字节的空间//底层的角度:它们是一样的方式实现的int* pa = &a;*pa = 20;return 0; }
我们来看下引用和指针的汇编代码对比:
通过反汇编我们可以看出:引用是按照指针方式来实现的。
【 C++入门 】引用相关推荐
- Android Studio开发入门-引用jar及so文件
最近初学安卓开发,因为以前从未用过JAVA,连基本的语法都要从头开始,所以不太顺利.在尝试使用百度语音识别引擎时遇到了如何引用jar及so文件的问题.在GOOGLE加多次尝试之后,找到了一个比较简单的 ...
- C++入门 “引用”,“内联函数” 详解
目录 一.引用 1.引用的概念: 2.引用的格式: 3.引用的特性 4.取别名原则: 难点:隐式类型转换的引用 5.引用的使用场景: [1]做参数: [2]做返回值 (1)int& Count ...
- Ant Design 入门-引用自己命名的组件
微信小程序开发交流qq群 173683895 承接微信小程序开发.扫码加微信. 自己创建的组件:代码 import { Table, Divider, Tag } from 'antd'; ...
- android 入门-引用库项目
http://blog.csdn.net/arui319/article/details/6831164 转载于:https://www.cnblogs.com/luquanmingren/p/463 ...
- 百度地图API调用实现获取经纬度以及标注
一.申请AK 百度搜索百度地图API,进入官网文档,按照官网文档提示注册百度账号并申请AK.申请AK 填写相关信息,应用名称随意.应用类型根据协议来选择. 二.编写代码 根据官网的代码(入门引用和获取 ...
- 存储过程-浅尝辄止(游标使用)
.先进行一下存储过程的入门介绍: 1.语法 也无非是一些枯燥的语法,智商120以上的你们相比随便找本书就可以看看 2.入门 引用一位大佬的文章,大家有兴趣可以看看文章里面对语法介绍的很全面,写的例子也 ...
- 仿链家地图找房的简单实现 1
本篇目录: 使用入门 简单使用流程 链家地图找房效果 区域点位气泡 数据结构 实现 addOverlay方法 区域边界 获取区域点位经纬度 获取区域边界 小结 最近由于项目需要,开始调研如何使用百度地 ...
- jieba分词流程及算法学习
目录 jieba 特点 算法 jieba分词流程图 Trie 树 建立 DAG 词图 分词 DAG 代码实现 计算全局概率Route ,基于词频最大切分组合 隐马尔可夫HMM 算法 引用 jieba ...
- html弹幕播放器源码,高性能HTML5弹幕播放器 Moe2_player
软件介绍 Moe2_player是一款高性能HTML5弹幕播放器. 特点:完全基于html5,移动设备友好,(iphone需要添加到桌面作为webapp方可观看弹幕) 性能强大,在多倍于B站最大弹幕覆 ...
- CSS入门之引用、选择器、属性(六分之三)
CSS 入门教程六分之三篇 没办法,我直播教小伙伴CSS入门,属性讲完,准备说定位的时候,他们就喊停,hold不住了...所以先写到六分之三,23333333 要点 解释 引用 如何使用定义的CSS样 ...
最新文章
- 机器学习笔试题精选(一)
- ApacheTomcat解析请求参数的过程
- 虚拟桌面模拟查找点击自绘控件
- UGUI 帧动画插件
- 异常检测算法之IForest
- 基于OOS批量修改资源标签值
- Java中List的contains方法,你用对了吗?
- 【机器人学与计算机视觉基础】(一)位置与姿态描述 1 位姿的抽象符号表示
- sass编译css(转自阮一峰)
- jquery双击一行跳转页面_Word软件使用小技巧,鼠标双击在不同操作对象中有不同的功能...
- 几种开源许可协议(转载)
- 制作简单启动型 U 盘_附制作工具下载
- VRay5.0 for 3dsMax2016-2021及素材库
- 网络视频传输的服务质量(QoS)
- Web前端助手-功能丰富的Chrome插件
- 3dmax 2023安装教程
- 想要一款iOS矢量绘图编程软件?推荐来了
- HTML5 标签大全
- android,繁体+代码,2013.08.08——— android 汉语言简繁体转换
- 【电脑讲解】电脑常用快捷键,10个常用快捷键提高电脑工作效率