【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 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 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 #include 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 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 #include 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=10printf("%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 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++中引用和内联函数的内容啦~