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

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

1.引用

1.1引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

例如:我们知道一位伟大的球星科比-布莱恩特,我们通常叫他为科比,在NBA上他也有一个称号,叫黑曼巴。

1.2引用的使用

类型& **引用变量名(对象名) =**引用实体;

int a = 10;
	int& b = a;//定义引用类型
	int* p = &b;//取地址

这里我们就称b是a的引用,虽然引用和取地址符都是用的同个字符,但是用法是不同的。

通过调试,我们可以看到a和b同属一块地址。

image-20220511111752982

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

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 <iostream>
using namespace std;
int main()
{
	int a = 10;
	int& b = a;
	int& c = b;
	int e = 20;
	b = e;

	return 0;
}

image-20220511113225542

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

1.4常引用

引例

image-20220511114024233

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

image-20220511114131755

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

image-20220511114202457

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

image-20220511141950908

image-20220511142001077

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

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 。

临时变量具有常性

image-20220511150358191

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

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

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

image-20220511151054723

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

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

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

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

我们可以调试验证一下:

image-20220511152140994

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

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

image-20220512160034551

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

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

void func(const int& x)

const引用传参的好处:

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

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

image-20220512132942992

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

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

image-20220512185817690

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

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

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

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

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

我们再来看看传引用返回

image-20220512204121561

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

即:

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

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

image-20220513012817798

1.6传值、传引用效率比较

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

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

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

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

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

image-20220513132545329

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

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

image-20220513133043117

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

image-20220513133449882

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

1.7引用和指针的区别

#include <stdio.h>
int main()
{
	int a = 10;
	int& ra = a;
	ra = 20;
	int* pa = &a;
	*pa = 20;
 
 	return 0;
}

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

image-20220513134523673

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

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

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

引用和指针的不同点:

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

image-20220513142514879

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

查看方式:

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

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

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

image-20220513141925935

image-20220513141944443

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

image-20220513142228892

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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Poolblue7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值