> 文档中心 > 【C++】引用以及关联函数(详解)

【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同属一块地址。

【C++】引用以及关联函数(详解)

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

1.3引用的特性

  1. 引用在定义时必须初始化
int  &d;//错误
  1. 一个变量可以有多个引用
int  a=10;int& b=a;int& c=b;int& d=c;
  1. 引用一旦引用一个实体,再不能引用其他实体
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;}

【C++】引用以及关联函数(详解)

我们可以看到e的地址和其他引用是不同的,只是赋值给了其他引用变量。

1.4常引用

引例

【C++】引用以及关联函数(详解)

当我们看到这个代码,如果加上const关键字,然后进行引用时,我们会发现编译错误,

【C++】引用以及关联函数(详解)

而当我们在引用前面也加上const的时候,我们发现程序就可以正常运行了

【C++】引用以及关联函数(详解)

那如果我们原先不用const修饰,最后用const引用呢

【C++】引用以及关联函数(详解)

【C++】引用以及关联函数(详解)

答案也是可以的,这是为什么呢,这涉及到了取别名的原则

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 。

临时变量具有常性

【C++】引用以及关联函数(详解)

所谓临时变量就是临时创建,必须是指向特定的内容不可更改

但是 f 的改变不会改变d,只是一种拷贝,所以我们没有改变他的读写权限,不需要加上const。

所以我们在用const int&类型来引用double时,实际上引用的是编译器产生的临时变量,也会创建一个临时变量。

【C++】引用以及关联函数(详解)

所以这里我们引用的是这个临时变量,而临时变量具有常性(不可修改),不加const的话,我们就扩大了权限。

int &e=d;  //放大权限const int&e=d;//缩小权限

所以其实这里**&e是临时变量的地址**,且临时变量不会销毁,生命周期和i同步

double赋值给int 给整数部分,
引用就相当于创建了一个整数部分的常数变量
引用的本质还是一个int类型、

我们可以调试验证一下:

【C++】引用以及关联函数(详解)

这里的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);//看起来很像一个函数,其实是俩个函数,用起来很舒服}

注意的是当我们使用引用为参数的时候,

【C++】引用以及关联函数(详解)

这里的参数是传不过去的,因为涉及到了权限的放大,这些参数都是只读,直接引用会扩大权限

所以这里我们只需要在函数传参加上const就行了

void func(const int& x)

const引用传参的好处:

  • 减少拷贝,提高效率
  • 任何类型都可以传,包括类型转换

做输出型参数

​ 我们在leetcode做oj题的时候,往往会出现输出型参数, 如果在C++中采用引用代替会更加方便。

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

首先我们来分析一下没有引用的传值输入

【C++】引用以及关联函数(详解)

普通的传值返回需要把返回值n给一个函数类型int的临时变量(函数类型就是返回值类型),再把临时变量给ret。

这里设计一个临时变量的原因:

为当函数Count里执行完各种代码后,返回n,等出了Count函数的作用域后n就会销毁,所以不能直接把n给ret,需要一个临时变量。

如何证明返回时存在临时变量呢?:

如果你用int& ret 接收,写成int& ret = Count(); 发现无法运行,因为临时变量有常性,所以需要写成const int& ret = Count(); 才能通过。

我们再来看看传引用返回

【C++】引用以及关联函数(详解)

当用引用接收引用返回时:这里ret和n的地址一样,也就意味着ret其实就是n的别名。但是因为n出作用域不会立即被覆盖,所以第一次通过ret可以打印是1,当打印第二次时,因为前面已经调用过一次打印函数,已 “销毁” 的Count函数栈帧在此时被打印函数覆盖,再打印ret就会是随机数了!
【C++】引用以及关联函数(详解)

即:

  • 如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,
  • 如果已 经还给系统了,则必须使用传值返回

用static修饰n后:用static静态变量使n只初始化一次且改变其生命周期,把n放进了静态区,这样n就一直存在,就可以通过ret找到n了,再怎么打印ret都是1.

【C++】引用以及关联函数(详解)

1.6传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是

传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是

当参数或者返回值类型非常大时,效率就更低。

因为在函数返回传参的时候,其实是先把返回值存放到寄存器中,而不是直接返回给main函数的变量

  • 当返回值很小(指占用空间)的时候,会用寄存器存放它的值
  • 当返回值很大的时候,部分编译器会先在main函数中预先开辟栈帧用来存放返回值

【C++】引用以及关联函数(详解)

我们可以通过代码测试一下效率:

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

【C++】引用以及关联函数(详解)

我们再来加上传址的函数,与传值和引用对比

【C++】引用以及关联函数(详解)

我们可以看到传址和引用的传参销毁都是差不多的,都比传值效率好,因为传值需要拷贝数据。

1.7引用和指针的区别

#include int main(){int a = 10;int& ra = a;ra = 20;int* pa = &a;*pa = 20;  return 0;}

我们可以通过查看他们的汇编代码,了解他们的底层实现

【C++】引用以及关联函数(详解)

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间

但是在底层实现上实际是有空间的,因为指针和引用的汇编代码是相同的

引用是按照指针方式来实现的。

引用和指针的不同点:

  • 引用是别名;指针是指向地址
  • 引用必须在定义的时候初始化;指针无要求
  • 引用的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是直接替换,内联函数不是。

【C++】引用以及关联函数(详解)

如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用

查看方式:

  • 在release模式下,查看编译器生成的汇编代码中是否存在call Add

  • 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2022的设置方式)

右击点击项目,点击属性-》

【C++】引用以及关联函数(详解)

【C++】引用以及关联函数(详解)

然后打断点,进行调试,右击转到反汇编,

【C++】引用以及关联函数(详解)

我们可以看到没有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++中引用和内联函数的内容啦~

局座张召忠