【C++】引用以及关联函数(详解)
文章目录
- 【C++】引用以及关联函数(详解)
- 1.引用
- 1.1引用概念
- 1.2引用的使用
- 1.3引用的特性
- 1.4常引用
- 1.4.1取别名的权限问题:
- const常量:
- double和int相互引用:
- 1.5引用的使用场景
- 1.做参数
- 传参
- 做输出型参数
- 2.函数返回值
- 1.6传值、传引用效率比较
- 1.7引用和指针的区别
- 引用和指针的不同点:
- 2.关联函数
- 2.1概念
- 2.2特性
- 结语
【C++】引用以及关联函数(详解)
1.引用
1.1引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
例如:我们知道一位伟大的球星科比-布莱恩特,我们通常叫他为科比,在NBA上他也有一个称号,叫黑曼巴。
1.2引用的使用
类型& **引用变量名(对象名) =**引用实体;
int a = 10;int& b = a;//定义引用类型int* p = &b;//取地址
这里我们就称b是a的引用,虽然引用和取地址符都是用的同个字符,但是用法是不同的。
通过调试,我们可以看到a和b同属一块地址。
注意:引用类型必须和引用实体是同种类型的
1.3引用的特性
- 引用在定义时必须初始化
int &d;//错误
- 一个变量可以有多个引用
int a=10;
int& b=a;
int& c=b;
int& d=c;
- 引用一旦引用一个实体,再不能引用其他实体
int e =20;
b=e;//e赋值给b,b的地址还是a
我们可以通过调试来验证一下。
#include <iostream>
using namespace std;
int main()
{int a = 10;int& b = a;int& c = b;int e = 20;b = e;return 0;
}
我们可以看到e的地址和其他引用是不同的,只是赋值给了其他引用变量。
1.4常引用
引例:
当我们看到这个代码,如果加上const关键字,然后进行引用时,我们会发现编译错误,
而当我们在引用前面也加上const的时候,我们发现程序就可以正常运行了
那如果我们原先不用const修饰,最后用const引用呢
答案也是可以的,这是为什么呢,这涉及到了取别名的原则
1.4.1取别名的权限问题:
- 对原引用变量,权限(读写权限)只能缩小,不能放大
以上面的例子来解读:
const int x=20;//可读不可写
int &y=x; //可读可写 //放大了权限,错误const int &y=x;//不变int c=30; //可读可写
const int &d=c; //可读 //缩小了权限
因为const关键字限制了我们读写权限,只能阅读,不能修改。
我们只能缩小读写权限,而不能放大读写权限。
const常量:
那么问题来了,如果我们对一个常量进行引用呢,
const int& c = 20;
则必须在引用前面加上const,因为常量具有常性(不能被修改),如果我们不加上const相当于赋予c可读可写的权限,就放大了权限,是不行的
double和int相互引用:
double d=2.2;
int f=d;
const int& e=d;
上面的例子为什么这里赋值不需要加上const,而引用需要加上const呢,我们来分析一下。
首先,我们将一个double类型的变量赋值给了int类型的变量,由于隐氏类型转换,double类型字节为8,int类型为4,所以赋值给 f 只有整数部分。然后其实赋值的时候并不是直接赋值的,而是会先创健一个临时变量,先赋值给临时变量,最后才赋值给 f 。
而临时变量具有常性
所谓临时变量就是临时创建,必须是指向特定的内容不可更改
但是 f 的改变不会改变d,只是一种拷贝,所以我们没有改变他的读写权限,不需要加上const。
所以我们在用const int&
类型来引用double时,实际上引用的是编译器产生的临时变量,也会创建一个临时变量。
所以这里我们引用的是这个临时变量,而临时变量具有常性(不可修改),不加const的话,我们就扩大了权限。
int &e=d; //放大权限
const int&e=d;//缩小权限
所以其实这里**&e是临时变量的地址**,且临时变量不会销毁,生命周期和i同步
double赋值给int 给整数部分,
引用就相当于创建了一个整数部分的常数变量
引用的本质还是一个int类型、
我们可以调试验证一下:
这里的e的地址和d的地址是不一样的,且e的值为2(验证了隐氏类型转换)
其实总结出来就是,引用和指针都是,一个改变就会影响原先的变量,就容易发生扩大权限的情况。
1.5引用的使用场景
1.做参数
传参
之前我们在学C语言的时候,如果修改某一个main传过来的参数,就必须进行`传址调用,
然而在C++中我们就可以使用引用来操作
void f(int& a)
因为实参给形参传值和传地址都需要传一份值/地址的拷贝,引用传参可以减少拷贝,提高效率
#include <iostream>
using namespace std;
int add(int& a,int& b)
{return a+b;
}
int main()
{add(1,2);
}
而且我们也可以配合函数重载,写出多个交换函数
void Swap(int& x,int& y)
{
int tmp=x;
x=y;
y=tmp;
}
void Swap(double& x,double& y)
{
int tmp=x;
x=y;
y=tmp;
}
int main()
{int a=1,b=2;Swap(a,b);int c=1.1,d=2.2;Swap(c,d);//看起来很像一个函数,其实是俩个函数,用起来很舒服
}
注意的是当我们使用引用为参数的时候,
这里的参数是传不过去的,因为涉及到了权限的放大,这些参数都是只读,直接引用会扩大权限
所以这里我们只需要在函数传参加上const就行了
void func(const int& x)
const引用传参的好处:
- 减少拷贝,提高效率
- 任何类型都可以传,包括类型转换
做输出型参数
我们在leetcode做oj题的时候,往往会出现输出型参数, 如果在C++中采用引用代替会更加方便。
2.函数返回值
int& Count()
{int n = 0;//变量n没有加static,返回的变量n可能会被覆盖n++;cout << " & n:" << endl;return n;
}
int main()
{int& ret = Count();cout << ret << endl;cout << "&ret:" << ret << endl;cout << ret << endl;
}
首先我们来分析一下没有引用的传值输入
普通的传值返回需要把返回值n给一个函数类型int的临时变量(函数类型就是返回值类型),再把临时变量给ret。
这里设计一个临时变量的原因:
为当函数Count里执行完各种代码后,返回n,等出了Count函数的作用域后n就会销毁,所以不能直接把n给ret,需要一个临时变量。
如何证明返回时存在临时变量呢?:
如果你用int& ret 接收,写成int& ret = Count(); 发现无法运行,因为临时变量有常性,所以需要写成const int& ret = Count(); 才能通过。
我们再来看看传引用返回
当用引用接收引用返回时:这里ret和n的地址一样,也就意味着ret其实就是n的别名。但是因为n出作用域不会立即被覆盖,所以第一次通过ret可以打印是1,当打印第二次时,因为前面已经调用过一次打印函数,已 “销毁” 的Count函数栈帧在此时被打印函数覆盖,再打印ret就会是随机数了!
即:
- 如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,
- 如果已 经还给系统了,则必须使用传值返回
用static修饰n后:用static静态变量使n只初始化一次且改变其生命周期,把n放进了静态区,这样n就一直存在,就可以通过ret找到n了,再怎么打印ret都是1.
1.6传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是
传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是
当参数或者返回值类型非常大时,效率就更低。
因为在函数返回传参的时候,其实是先把返回值存放到寄存器中,而不是直接返回给main函数的变量
- 当返回值很小(指占用空间)的时候,会用寄存器存放它的值
- 当返回值很大的时候,部分编译器会先在main函数中预先开辟栈帧用来存放返回值
我们可以通过代码测试一下效率:
#include <iostream>
#include <time.h>
using namespace std;
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();return 0;
}
我们再来加上传址的函数,与传值和引用对比
我们可以看到传址和引用的传参销毁都是差不多的,都比传值效率好,因为传值需要拷贝数据。
1.7引用和指针的区别
#include <stdio.h>
int main()
{int a = 10;int& ra = a;ra = 20;int* pa = &a;*pa = 20;return 0;
}
我们可以通过查看他们的汇编代码,了解他们的底层实现
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
但是在底层实现上实际是有空间的,因为指针和引用的汇编代码是相同的
引用是按照指针方式来实现的。
引用和指针的不同点:
- 引用是别名;指针是指向地址
- 引用必须在定义的时候初始化;指针无要求
- 引用的sizeof大小和引用对象相同;指针无论指向的谁,大小都是4/8
- 引用不能为NULL;指针可以为NULL
- 引用++即对象数值+1;指针++是指向的地址向后偏移
- 引用无多级;指针存在二级、三级……
- 引用比指针使用起来更加安全(不会出现野指针)
- 引用是编译器处理的;指针需要手动解引用
- ……
2.关联函数
2.1概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销, 内联函数提升程序运行的效率。
#include <stdio.h>
#include <iostream>
using namespace std;#define ADD(a,b) ((a)+(b))inline int Add(int a,int b){return a+b;
}int main ()
{int sum=ADD(1+3,2+4);//4+6=10 printf("%d\n", sum); int ret = 0;ret=Add(3,4);return 0;
}
这里会觉得之前C语言学的#define类似,但是define是直接替换,内联函数不是。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用
查看方式:
在release模式下,查看编译器生成的汇编代码中是否存在call Add
在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2022的设置方式)
右击点击项目,点击属性-》
然后打断点,进行调试,右击转到反汇编,
我们可以看到没有call !
2.2特性
inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环****/递归的函数不适宜使用作为内联函数。
inline对于编译器而言只是一个建议**,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
比如我们进行多文件操作,把内联函数的声明和定义放在不同的源文件和头文件中,编译器会报错找不到函数
// F.h
#include <iostream>
using namespace std;
inline void f(int i);// F.cpp
#include "F.h"
void f(int i) {cout << i << endl; }// main.cpp
#include "F.h"
int main()
{f(10);return 0; }// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?
f@@YAXH@Z),该符号在函数 _main 中被引用
结语
以上就是C++中引用和内联函数的内容啦~
【C++】引用以及关联函数(详解)相关推荐
- PHP的传值与引用,php 传值与传引用的区别详解
摘要 腾兴网为您分享:php 传值与传引用的区别详解,掌上公交,信用管家,天翼阅读,平安知鸟等软件知识,以及k歌达人,ml2010打印机驱动,维也纳大学app,建玛特,网盘快搜,中国禁毒数字展览馆,爱 ...
- java 引用传递_详解java的值传递、地址传递、引用传递
详解java的值传递.地址传递.引用传递 一直来觉得对值传递和地址传递了解的很清楚,刚才在开源中国上看到一篇帖子介绍了java中的值传递和地址传递,看完后感受颇深.下边总结下以便更容易理解. 按照以前 ...
- Unity文件、文件引用、Meta详解
原文链接:https://blog.uwa4d.com/archives/USparkle_inf_UnityEngine.html 这是侑虎科技第381篇原创文章,感谢作者陈广忠供稿.欢迎转发分享, ...
- 一个普通handler会持有activity引用吗_详解handler机制
Android中有很多机制,打开源码最先遇到的应该就是handler机制了,handler主要为了解决线程间通讯的问题,首先看一下handler该怎么用. ##handler的用法 1.在主线程中用h ...
- python自己创建模块引用失败_详解Python import方法引入模块的实例 Python怎么import自己写的模块...
python中 import导入模块失败的问题? python中的import引用不了模块我傻,为你傻;我痛,为你痛;深夜里,你是我一种惯性的回忆. 为什么我用from lianxi import*就 ...
- 引用java8里的方法_Java8中方法引用的使用详解
1. 引言 Java8中最受广大开发中喜欢的变化之一是因为引入了 lambda 表达式,因为这些表达式允许我们放弃匿名类,从而大大减少了样板代码,并提高了可读性. 方法引用是lambda表达式的一种特 ...
- php中unset函数是在哪一章_PHP引用(amp;)使用详解
php的引用(就是在变量或者函数.对象等前面加上&符号) 在PHP 中引用的意思是:不同的名字访问同一个变量内容. 与C语言中的指针是有差别的.C语言中的指针里面存储的是变量的内容,在内存中存 ...
- c++中引用及指针详解
这里写目录标题 1.指针 1.1.什么是指针 指针的本质 指针与地址 程序中如何声明指针以及如何使用运算符&和* 1.2.指针有什么作用 指针与函数参数 2.引用 2.1.什么是引用 2.2. ...
- Java引用数据类型String详解
Java引用数据类型(String) 引用数据类型概述 引用数据类型与基本数据类型的差别(举例说明) 引用数据类型概述 说到引用数据类型,那么何为引用数据类型? 引用类型,都可以用null值作为值,也 ...
- java对象创建与引用变量的详解
创建对象与引用变量 创建对象 基本类型变量和引用类型变量的区别 存储的值 赋值 引用数据和NULL 创建对象 ClassName objectRefVar = new ClassName(); 这条语 ...
最新文章
- Arrays.binarySearch 面试的坑
- BugKuCTF 杂项 多种方法解决
- Hibernate 系列 02 - Hibernate介绍及其环境搭建
- 我的github网址链接
- [概念学习] Virtualization的几个概念
- Javascript浏览器事件(上)
- 翻译: swift5 iOS中的自动布局教程:Auto Layout入门
- 关于redis HSCAN count参数不生效的问题
- 大学物理计算机仿真实验报告,大学物理实验实验报告模板.doc
- 蘑菇战争2显示没有连接服务器,蘑菇战争2新手怎么玩?四步带你解决新手难关[多图]...
- 小天鹅全自动洗衣机的PLC控制
- Goole Tag Manager 介绍
- BYD Mes系统接入示例图源码
- ossfs挂载到本地磁盘
- 【日语】动词的九种变形
- 更便捷的二维码报修系统
- linux设备模型十二(热拔插hotplug)
- Ubuntu上git的简单使用,拉取远程分支,修改并提交
- 数字电路课程设计 密码锁
- Java的运算符——取整、取绝对值、取余数、求模