目录

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、引用特性

引用具有三大特性:

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

接下来,我将做具体演示:

引用在定义时必须初识化

我能否写出如下的引用呢?

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、引用的使用场景

引用的使用场景分为两个:

  1. 做参数
  2. 做返回值

接下来,我将会详细讲解下:

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

加上引用会在调用时省去写&,也更方便理解,减少对指针的使用。

综上,引用做参数的好处如下:

  1. 输出型参数
  2. 减少拷贝、提高效率

引用还有一个使用场景是做返回值,具体看下文:

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、引用和指针的区别

引用和指针的不同点:

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

接下来就上述指针与引用不同点做详细解析:

  • 引用在定义时必须初始化,指针没有要求
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++入门 】引用相关推荐

  1. Android Studio开发入门-引用jar及so文件

    最近初学安卓开发,因为以前从未用过JAVA,连基本的语法都要从头开始,所以不太顺利.在尝试使用百度语音识别引擎时遇到了如何引用jar及so文件的问题.在GOOGLE加多次尝试之后,找到了一个比较简单的 ...

  2. C++入门 “引用”,“内联函数” 详解

    目录 一.引用 1.引用的概念: 2.引用的格式: 3.引用的特性 4.取别名原则: 难点:隐式类型转换的引用 5.引用的使用场景: [1]做参数: [2]做返回值 (1)int& Count ...

  3. Ant Design 入门-引用自己命名的组件

    微信小程序开发交流qq群   173683895    承接微信小程序开发.扫码加微信. 自己创建的组件:代码 import { Table, Divider, Tag } from 'antd'; ...

  4. android 入门-引用库项目

    http://blog.csdn.net/arui319/article/details/6831164 转载于:https://www.cnblogs.com/luquanmingren/p/463 ...

  5. 百度地图API调用实现获取经纬度以及标注

    一.申请AK 百度搜索百度地图API,进入官网文档,按照官网文档提示注册百度账号并申请AK.申请AK 填写相关信息,应用名称随意.应用类型根据协议来选择. 二.编写代码 根据官网的代码(入门引用和获取 ...

  6. 存储过程-浅尝辄止(游标使用)

    .先进行一下存储过程的入门介绍: 1.语法 也无非是一些枯燥的语法,智商120以上的你们相比随便找本书就可以看看 2.入门 引用一位大佬的文章,大家有兴趣可以看看文章里面对语法介绍的很全面,写的例子也 ...

  7. 仿链家地图找房的简单实现 1

    本篇目录: 使用入门 简单使用流程 链家地图找房效果 区域点位气泡 数据结构 实现 addOverlay方法 区域边界 获取区域点位经纬度 获取区域边界 小结 最近由于项目需要,开始调研如何使用百度地 ...

  8. jieba分词流程及算法学习

    目录 jieba 特点 算法 jieba分词流程图 Trie 树 建立 DAG 词图 分词 DAG 代码实现 计算全局概率Route ,基于词频最大切分组合 隐马尔可夫HMM 算法 引用 jieba ...

  9. html弹幕播放器源码,高性能HTML5弹幕播放器 Moe2_player

    软件介绍 Moe2_player是一款高性能HTML5弹幕播放器. 特点:完全基于html5,移动设备友好,(iphone需要添加到桌面作为webapp方可观看弹幕) 性能强大,在多倍于B站最大弹幕覆 ...

  10. CSS入门之引用、选择器、属性(六分之三)

    CSS 入门教程六分之三篇 没办法,我直播教小伙伴CSS入门,属性讲完,准备说定位的时候,他们就喊停,hold不住了...所以先写到六分之三,23333333 要点 解释 引用 如何使用定义的CSS样 ...

最新文章

  1. 机器学习笔试题精选(一)
  2. ApacheTomcat解析请求参数的过程
  3. 虚拟桌面模拟查找点击自绘控件
  4. UGUI 帧动画插件
  5. 异常检测算法之IForest
  6. 基于OOS批量修改资源标签值
  7. Java中List的contains方法,你用对了吗?
  8. 【机器人学与计算机视觉基础】(一)位置与姿态描述 1 位姿的抽象符号表示
  9. sass编译css(转自阮一峰)
  10. jquery双击一行跳转页面_Word软件使用小技巧,鼠标双击在不同操作对象中有不同的功能...
  11. 几种开源许可协议(转载)
  12. 制作简单启动型 U 盘_附制作工具下载
  13. VRay5.0 for 3dsMax2016-2021及素材库
  14. 网络视频传输的服务质量(QoS)
  15. Web前端助手-功能丰富的Chrome插件
  16. 3dmax 2023安装教程
  17. 想要一款iOS矢量绘图编程软件?推荐来了
  18. HTML5 标签大全
  19. android,繁体+代码,2013.08.08——— android 汉语言简繁体转换
  20. 【电脑讲解】电脑常用快捷键,10个常用快捷键提高电脑工作效率

热门文章

  1. MySQL explain 例子_MySQL EXPLAIN结果集分析 - 附带大量案例
  2. JavaScript-学习笔记一
  3. 4. Python脚本学习实战笔记四 新闻聚合
  4. k8s在华为openeuler搭建
  5. 工程师排查故障三要诀
  6. Jasper问题总结与解决方法~
  7. 整数的上下取整和浮点数的上下取整 java
  8. Win11中双显示器快捷键 Win + Shift + 左/右失效问题
  9. vue3时间戳格式转换
  10. echarts双饼图