阿巴阿巴,最近搭建好了腾讯云的Linux环境,所以本篇C++的博客就尝试在Linux环境下来测试代码吧!
阿巴阿巴,最近搭建好了腾讯云的Linux环境,所以本篇C++的博客就尝试在Linux环境下来测试代码吧!

今天学习了C++的引用和内联函数,一起来瞅瞅它们都是些啥……

感谢你关注慕雪,欢迎来我的寒舍坐坐❄慕雪的寒舍


[TOC]

前言

众所周周知,C语言之中,有一个叫指针的家伙,它的使用方式如下

1
2
3
4
5
6
7
int main()
{
int a=10;
int*p=&a;//p是一个指针变量,指向a

return 0;
}

这时候我们就可以通过*p对指针解引用访问变量a

所以C++之中也有一个类似的东西,叫做引用,不过它和指针完全不同哦


1.引用

1.1基本形式

引用的基本方式如下

1
2
3
4
5
int a=10;
int& b=a;
int& c=a;//同一个变量可以有多个别名
//可以用两个不同的变量名引用同一个
//但是引用了之后不可以更改对象

此时的b和c都是a的别名,注意是别名!打印的时候,三个变量的结果都是10;

image-20220510194732657

可以用两个不同的变量名引用同一个变量,而且引用了之后不可以更改对象

  • 一个变量可以有多个引用
  • 指针可以更改指向的对象,引用不可以
  • 引用必须在定义的时候就初始化,不可以int& b;

image-20220510194600682

比如你叫李华,有人叫你“小李”,还有人叫你“英语作文人”,这两个外号都是你的别名。

指针并不是别名,指针是通过地址访问某个变量。而引用是给a变量起另外的两个名字,实际上b和c都可以当作a来使用

编译运行代码,让编译器打印出这三者的地址,可以看到它们的地址是一样的,因为它们本来就是同一个变量的不同名字。

image-20220510193932279

指针变量的地址和指针变量所指向对象的地址是不同的

引用的类型必须和引用实体的类型相同,不能用int&引用double类型

image-20220510195818363


1.2引用的权限问题

①const常量

引用可以引用常量,但是必须加const修饰

image-20220510194957550

基本的思路就是“权限可以缩小,但不可以放大”。

  • 在上面的代码中,a是一个可以修改的变量,但是const int&d=a;中的d是不能修改,只可读取a的内容。
  • e是不可修改的常量,所以我们不能用int&来放大权限

②int和double相互引用

1.1中有提到,我们不能用int&来引用double类型的变量,编译器会报错

不过我们可以用const int&类型来引用double,此时引用就不是简单的一个别名了

先来了解一下把double复制给int类型,这时候会产生“隐式类型转换”,h保存的是z的整数部分

image-20220510200102067

image-20220510200239078

在这个过程中,编译器会产生一个临时变量存放z的整数部分,然后赋值给h

  • 临时变量具有“常性”,可读不可改

而当我们用const int&类型来引用double时,实际上引用的是编译器产生的临时变量,它是一个常量,所以我们需要用const int&来引用

1
2
3
4
5
6
7
8
const int& i=z;//这里的i是临时变量的别名
//在引用的时候,创建了一个临时变量存放d的整数部分
//i的地址和z不相同,且临时变量不会销毁,生命周期和i同步
//生成的这个临时变量是常量,所以i的本质是引用了一个int类型
cout <<"i= "<<i<<endl;
cout <<"&i= "<< &i <<endl;
cout <<"&z= "<< &z <<endl;
//在c++中函数主要使用引用传参,后面会进一步学习

一个非常直观的验证方法,就是打印一下,瞅瞅它们的地址是否相同。可以看到,i的值和h是相同的,因为它引用的就是那个存放了整数部分的临时变量,这个临时变量的地址和z不同

image-20220510200949843

1.3引用的使用场景

①函数传参

众所周知,在C语言中,如果我们想在函数中修改某一个main传过来的参数,就必须进行传址调用。而在C++中,我们可以通过引用来操作

image-20220510201822801

可以看到,我们通过引用实现了在函数中修改a的值

image-20220510201858396

更加充分的体现便是Swap函数,在C语言中必须两个都传地址来调用

在C++中,配合函数重载,我们可以很方便的写出多个交换函数

image-20220510202111483

image-20220510202414145

直接测试一下,交换成功!

image-20220510202458000


②函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
int& Count(){
int n=0;//现在没有加static,返回的变量n可能会覆盖
n++;
cout<<"&n:"<<&n<<endl;
return n;
}
int main()
{
int& ret = Count();
cout<< ret <<endl;
cout<<"&ret:"<<&ret<<endl;
cout<< ret <<endl;
}

当我们把n作为int&类型来返回时,ret此时是对n的引用。但是函数中的变量n在出了函数后销毁了,所以在main函数中打印ret的时候,可能会打印出随机值(这个要看什么时候n的内容会被编译器覆写)

image-20220510203242420

而当我们带上static后,多次打印n的值都不会出现问题,因为此时n的空间并没有被销毁

image-20220510203041011

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

下面用一个简单的Add函数来演示一下上面提到的两种情况

image-20220510211528967

如果去掉static修饰,编译器会报警告,而且打印的值会在第二次cout调用的时候被覆写

image-20220510211612628

当Add函数的c变量加了static修饰后,打印的值都是稳定的,不会被覆写(因为c的空间没有被销毁)

image-20220510211654828

③优化函数调用时间

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

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

image-20220510204613625

而使用引用作为返回值的时候,就不需要用寄存器来接收临时变量,这时候就优化了函数返回的时间

image-20220510210208928

可以看到,用引用返回的时间消耗很小!

image-20220510210250448

再来试试把引用作为参数传参的消耗,和传址、传值进行对比,代码和上面的类似,稍微修改一下测试函数就行了

image-20220510212448528

可以看到,传地址和引用作为参数的传参消耗都是很小的。因为传值的时候需要拷贝数据!

image-20220510212531872


1.4引用和指针的汇编代码

用下面的代码来查看引用和指针的汇编区别

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;

return 0;
}

你可以看到,指针和引用的汇编代码是相同的。因为C++的引用,本质上是用指针实现的!

image-20220510221020032

objdump -S语句,查看Linux环境下的汇编👇

image-20220510221106736

1.5引用和指针的区别

  • 引用是别名;指针是指向地址
  • 引用必须在定义的时候初始化;指针无要求
  • 引用的sizeof大小和引用对象相同;指针无论指向的谁,大小都是4/8
  • 引用不能为NULL;指针可以为NULL
  • 引用++即对象数值+1;指针++是指向的地址向后偏移
  • 引用无多级;指针存在二级、三级……
  • 引用比指针使用起来更加安全(不会出现野指针)
  • 引用是编译器处理的;指针需要手动解引用
  • ……

1.6 数组指针引用

笔试的时候遇到了这个纠错题,我感觉这里是错的,但还是没选出来;

1
2
3
int arr[20];
int (&ref1)[20] = arr;// 正确的数组引用
int& ref2[20] = arr; // 错误的引用

2.内联函数

2.1基本形式

在函数名前用inline修饰的函数是内联函数,编译器在处理此类函数的时候,会将函数在调用它的地方打开。此时内联函数就没有函数压栈的开销,提高了程序运行的效率

这部分和C语言学习过的#define类似,但define是直接替换,内联函数不是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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;
}

2.2查看预处理文件

使用下面的Linux语句可以把源文件生成为预处理后的文件

这部分可以看看我之前用树莓派操作的博客哦!【传送门】

1
g++ -E test.cpp -o test.i

可以看到define被替换了,但是内联函数并没有

image-20220510223314503

2.3查看汇编代码

①Linux环境

这时候我们先编译这个文件

1
g++ test.cpp

然后使用下面的语句查看汇编代码

1
objdump -S a.out

然后你就发现,这不还是有call函数调用嘛?这哪里没有调用呢?

image-20220510223619655

实际上,我们在编译的时候需要调整编译器的优化操作👉【参考博客】

1
g++ -O2 test.cpp

这时候的汇编代码就没有call了

image-20220510223824967

②VS2019

要想调整VS2019的优化等价,需要在项目属性中C/C++ -常规中修改调试信息格式为“程序数据库”

image-20220510224242163

然后在优化-内联函数扩展修改成只适用于inline(/Ob1)

image-20220510224327261

然后调试,右键转到反汇编,可以看到,no call🕵️‍♂️

image-20220510224122808


2.4内联函数的特性

define没有传参检查,且不能debug+可读性不高,内联函数解决了这一缺点

  • 内联函数是用空间换时间的做法,省去函数调用的开销
  • 函数代码很长的时候不适合用内联函数(define同理)
  • 在代码行数很长的时候,编译器会自己判断是否使用inline。如果函数体内有循环/递归等,编译器优化的时候会取消内联
  • inline不可以声明和定义分离,会导致链接错误

对最后一点展开介绍一下,当我们把内联函数的声明和定义放在不同的源文件和头文件中,编译器会报错找不到函数

image-20220510225427502

这是因为内联函数在调用的时候已经展开了,对应的函数地址也没了,所以无法正常链接


结语

引用和内联函数的博客到这就结束啦,如果对你有帮助,还请点个赞再走哦!

笔记难免有错,还请大佬们无情指出!