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

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





文章目录

  • ​​前言​​
  • ​​1.引用​​
  • ​​1.1基本形式​​
  • ​​1.2引用的权限问题​​
  • ​​①const常量​​
  • ​​②int和double相互引用​​
  • ​​1.3引用的使用场景​​
  • ​​①函数传参​​
  • ​​②函数返回值​​
  • ​​③优化函数调用时间​​
  • ​​1.4引用和指针的汇编代码​​
  • ​​1.5引用和指针的区别​​
  • ​​2.内联函数​​
  • ​​2.1基本形式​​
  • ​​2.2查看预处理文件​​
  • ​​2.3查看汇编代码​​
  • ​​①Linux环境​​
  • ​​②VS2019​​
  • ​​2.4内联函数的特性​​
  • ​​结语​​

前言

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

int main()
{
int a=10;
int*p=&a;//p是一个指针变量,指向a

return 0;
}

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

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


1.引用

1.1基本形式

引用的基本方式如下

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

此时的b和c都是a的别名,注意是别名!

【C++】引用和内联函数_内联函数

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

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

【C++】引用和内联函数_c++_02

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


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


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

【C++】引用和内联函数_编译器_03


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


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

【C++】引用和内联函数_编译器_04


1.2引用的权限问题

①const常量

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

【C++】引用和内联函数_编译器_05

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

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

②int和double相互引用

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

【C++】引用和内联函数_开发语言_06

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

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

【C++】引用和内联函数_开发语言_07

【C++】引用和内联函数_编译器_08

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

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

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

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不同

【C++】引用和内联函数_内联函数_09

1.3引用的使用场景

①函数传参

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

【C++】引用和内联函数_编译器_10

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

【C++】引用和内联函数_内联函数_11

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

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

【C++】引用和内联函数_内联函数_12

【C++】引用和内联函数_编译器_13

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

【C++】引用和内联函数_编译器_14


②函数返回值

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的内容会被编译器覆写)

【C++】引用和内联函数_c++_15

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

【C++】引用和内联函数_c++_16

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

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

【C++】引用和内联函数_c++_17

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

【C++】引用和内联函数_c++_18

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

【C++】引用和内联函数_编译器_19

③优化函数调用时间

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

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

【C++】引用和内联函数_开发语言_20

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

【C++】引用和内联函数_编译器_21

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

【C++】引用和内联函数_内联函数_22

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

【C++】引用和内联函数_c++_23

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

【C++】引用和内联函数_内联函数_24


1.4引用和指针的汇编代码

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

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

return 0;
}

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

【C++】引用和内联函数_内联函数_25

用​​objdump -S​​语句,查看Linux环境下的汇编????

【C++】引用和内联函数_编译器_26

1.5引用和指针的区别

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

2.内联函数

2.1基本形式

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


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


#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语句可以把源文件生成为预处理后的文件


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


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

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

【C++】引用和内联函数_c++_27

2.3查看汇编代码

①Linux环境

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

g++ test.cpp

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

objdump -S a.out

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

【C++】引用和内联函数_内联函数_28

实际上,我们在编译的时候需要调整编译器的优化操作

g++ -O2 test.cpp

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

【C++】引用和内联函数_开发语言_29

②VS2019

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

【C++】引用和内联函数_内联函数_30

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

【C++】引用和内联函数_开发语言_31

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

【C++】引用和内联函数_编译器_32


2.4内联函数的特性

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

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

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

【C++】引用和内联函数_开发语言_33

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


结语

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


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