当前位置: 首页 > article >正文

【c++入门系列】:引用以及内联函数详解

🔥 本文专栏:c++
🌸作者主页:努力努力再努力wz

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

💪 今日博客励志语录最陡的上坡路,往往是通往山顶的捷径;最猛的风暴,才能扬起最远的帆。咬牙坚持的‘再试一次’,就是改写结局的伏笔

那么在上一篇文章,学习了命名空间以及函数的重载以及缺省参数,那么这篇文章,我会继续来补充c++的语法,看看它在c语言的基础上又进行了怎样的改善,那么废话不多说,直接进入进入正文

引用

1.为什么要有引用

那么我们知道在c语言中,我们访问一个变量的时候,可以通过指针的方式来访问一个变量,那么指针它本身就是一个变量,只不过它存储的内容不是其指向变量的具体的值,而是其地址,所以我手上持有一个已经有指向的变量的指针,那么要通过它去访问其指向的变量,那么就是通过(*)解引用的方式来获取到指向的目标变量的值。

那么想必指针的用法以及如何去定义大家已经十分熟悉,那么我们再来重新审视评价一下指针的优劣,那么指针本身的作用无非就是保存其指向变量的所在空间的地址,那么我们可以通过指针保存的地址来访问变量所处的空间从而读取或者修改变量的内容,那么要达到这个目的,那么就需要我们在定义指针的时候,正确的初始化该指针,让其指向要目标变量比如变量a,而指针本身的内容是保存其指向的a变量的地址而不是a变量本身的值,所以要访问a变量就需要解引用该指针

int* ptr=&a;
(*ptr)++;

但是正是由于指针本身保存的是指向的目标变量的地址而不是数据,并且我们可以随意的修改指针的指向,那么如果我们不熟悉指针的操作的话,那么指针不正确的使用就会引发各种后果,比如我们没有正确的初始化指针,本来我们要让指针ptr指向的是a变量,结果指向的是b变量,那么访问以及修改的内容是b变量的值而对a变量没有任何影响,又比如我们对该指针压根就没有初始化,然后我们就对指针进行各种的操作比如访问以及解引用,而指针没有初始化,那么意味着该指针本身保存的内容就是随机值,那么如果编译器的检查不严格的话,允许编译通过,那么此时我们对该指针进行解引用以及访问就会引发一系列的后果,也就是大家熟悉的野指针问题

那么刚才的指针没有进行初始化或者指向错误都是非常低级的陷阱了,相信各位大佬还是不容易跳进这个坑里面去的,而由于指针保存的是变量的地址而不是变量的内容,那么有些人可能误以为指针保存的是变量的内容,那么他若想修改该指针指向变量的值,比如说要对变量加一,那么直接就是对该指针加一

//错误示范
ptr++;
//正确示范
(*ptr)++;

那么对指针进行加一以及对解引用的指针进行加一是两个不同的概念,那么指针进行加一的话,是让指针向后移动一个单位,那么这个单位的大小具体取决于指针指向的数据类型,比如指向的是int类型,那么指针往后移动,那么一次移动就是4个字节,而指向的是char类型,那么一次移动就是一个字节。

那么如果这个坑各位大佬还是不容易跳进去的话,那么如果我再给出一个二级指针以及三级甚至四级指针让你去使用,那么阁下该如何应对,那么使用二级指针以及三级指针来访问一个目标数据,多次的解引用一定让人觉得麻烦,其次如果你指针的使用稍有不慎,比如你用一个指针保存了用malloc函数在堆上申请开辟一大片连续空间的首元素的地址,然后你自己free掉了malloc申请的空间之后,没有对指针进行置空处理,还依然在用指针进行解引用访问,那么就会导致越界访问,所以总而言之,指针是具有风险并且使用起来较为麻烦的,当然被指针给坑过的肯定不只有你一个人,估计还有我们的c++祖师爷,所以人家给干脆直接在指针的基础上进行改善,得到了引用,那么这就是为什么要有引用,那么就是为了解决c语言指针的安全性以及操作麻烦的问题

2.引用是是什么以及如何用

引用的作用和指针是一样的,那么它也是指向一个数据,那么我们可以通过引用来访问该数据,可谓就是一个指针2.0版本,但是相比于指针来说,那么它可就不需要所谓的解引用了,比如我们该引用指向一个int类型的a变量,那么如果此时你用的是指针,你还要小心的使用提醒自己别跳进坑里了,比如一定要对解引用的指针加一而不是指针本身加一,但是如果你是用的引用,那么这些担忧以及顾虑全都消失了,那么你直接对该引用加一,那么其指向的a变量也同样进行了加一,虽然我们对的是引用进行加一,但是我们仿佛操作的就是a变量本身。

指针就好比我们现实生活中的秘书,那么指针指向的变量就是它的老板,那么该秘书有老板的电话,那么我们现在我们要找老板谈话,那么由于秘书的存在,那么我们不能与老板亲自见面来进行谈话,而是只能通过秘书,然后秘书给老板打电话,然后我们拿着秘书的电话给老板谈话,间接的来找到老板,那么这就是指针。

而引用就好比我们要找一个人,比如老师上课找班上的同学起来回答问题,那么就是在课堂上直接叫他的名字起来回答问题,那么假设该学生叫张三,那么老师可以叫张三起来回答问题,那么假设该学生张三在班上还有一个小名叫小三,那么老师也可以叫小三起来回答问题,因为张三和小三是同一个人,所以说从这例子我们就可以理解引用了,那么引用其实就是给一个变量取了一个别名,那么我们现在定义了一个引用b指向a,那么相当于这个变量既可以叫a也可以叫b,那么我们对a变量直接进行加一,那么我们也可以对引用b直接加一,本质上其实也是对a进行了加一,就好比老师叫张三或者小三起来回答问题,虽然叫的名字不同,但其实都是同一个人

那么怎么定义一个引用呢,那么我们引用首先要确定你指明的数据类型,比如你指向的是int类型还是double类型,还是char类型,那么确定完类型之后,后面加一个c语言的取地址符,那么就是引用

//引用指向char类型
char a='a';
char& b=a;
//引用指向int类型
int a=10;
int& b=a;
//引用指向double类型
double a=10.1;
double& b=a;

那么指向一个元素就是直接用赋值运算符连接该元素即可,那么引用的定义以及使用是很很EZ的,那么我们写一段简单的代码来动手实践熟悉一下引用,那么具体代码的内容就是定义一个变量a,然后再定义一个引用指向该变量a,让a的值加一:

#include<iostream>
using namespace std;
int main()
{
     int a=10;
    int& b=a;
    b++;
    cout<<"a="<<a;
    return 0;
}

运行截图:

在这里插入图片描述

那么既然我们学的是c++,那么肯定有很多的小伙伴好奇底层引用是怎么实现的,那么我这里想说的是,底层引用的实现肯定是通过指针来实现的,但是在语言层面上,我们去理解引用,那么可以认为它本身是不开辟空间的,因为引用相当于是和其指向的变量共享一个物理内存,所以我们操作引用就等于操作变量本身,所以不需要像c语言那么麻烦的解引用来间接访问变量,而至于底层的细节,那么c++将其给封装隐藏了起来,那么至于c++底层是怎么实现引用的,本文就不在赘述,感兴趣的小伙伴可以自行去了解,我们知道引用如何用以及后文所讲的引用的细节便足矣。

3.引用相关的细节补充以及引用场景

那么引用虽然和指针完成的是同样的工作,但是得注意对于c语言中的指针,我们可以随意的修改指针的指向,但是c++的引用我们是不能随意的修改指向,并且引用在定义的时候就得初始化,那么我们可以写一段简单的c++代码来实验一下:

#include<iostream>
using namespace std;
int main()
{
     int& a;
    return 0;
}

然后我们来运行跑一下这段代码,来看看运行结果:
在这里插入图片描述

那么根据运行结果我们知道,它编译阶段就过不去,因为引用必须要初始化,并且初始化的指向也有要求,不能指向空,也就是nullptr,那么可以写一段简单的代码来验证一下:

#include<iostream>
using namespace std;
int main()
{
     int& a=nullptr;
    return 0;
}

运行结果:
在这里插入图片描述

那么从引用的这些使用规则我们就能看到c++祖师爷的良苦用心了,并且还没完

那么引用一旦完成初始化之后,那么它就与该初始化所指向的变量给绑定在了一起,不能修改指向,所以一个变量可以有多个引用,但是一个引用只能指向一个变量

引用常量

那么我们再来看一下这样一个场景,我们定义一个引用指向一个常量,那么看看会发生怎么样的结果

#include<iostream>
using namespace std;
int main()
{
    const int a=10int& b=a;
    return 0;
}

在这里插入图片描述

那么它会在编译阶段就会报错,那么为什么会报错呢?

那么我们知道const修饰的变量是具有常性,所谓的常性就是我们对带有常性的数据只能读取但不能修改,一旦引用指向了一个const修饰的对象之后,那么意味着我们可以通过这个引用来修改const修饰的对象,所以这里编译器进行了严格的检查,即使我们定义了引用指向了该变量但是只是对引用做读取而没有做写入,那么在编译阶段依然无法通过,编译器直接一刀斩乱麻,阻止后果的发生

所以要想让引用指向一个常量,那么我们只能用常引用来指向该常量,那么const修饰的引用就意味着该引用只能用来读取而不能用来写入

const int& b=a;

常量引用

上文我们知道一个常量只能被const修饰的引用所指向,而对于const修饰的引用来说,那么它不仅可以指向一个const修饰的变量也同样也可以指向不被const修饰的变量
我们知道数据是具有读和写两个权限,那么const修饰的数据被设置为只读,而非const修饰的数据则是可读可写,所以常引用指向const修饰的数据,那么意味着权限的平移,因为他们都是只读,而常引用指向一个非const修饰的变量,那么我们通过该引用访问,就意味着权限的缩小,原本可读可写变为了只读,但是编译器允许权限的平移以及缩小,但是不允许权限的放大,也就是非const修饰的引用指向一个常量

那么认识了常引用之后,我们再来看这样一个场景,也就是我们定义一个int类型的引用,但是让其指向一个double类型的数据,那么看看会发生什么

#include<iostream>
using namesapce std;
int main()
{
    double a=10.1
    const int& b=a;
    cout<<"a="<<b;
    return 0;
}

那么我们来跑一段这个代码,看看结果:
在这里插入图片描述

那么我们发现其实结果很符合我们的预期,因为引用是定义的要指向的int类型的数据,但是我们让其指向的数据是doubl类型的,那么明显数据类型不一样,肯定会类型不匹配的错误,但是还没完,如果我此时用const修饰该引用,那么又会发生什么呢?

#include<iostream>
using namesapce std;
int main()
{
    double a=10.1
    const int& b=a;
    cout<<"a="<<b;
    return 0;
}

在这里插入图片描述

那么我们发现编译通过了,运行结果是10,那么其实通过这个运行结果我们就大概能知道这是怎么一回事了

因为我们用int类型的引用指向的是double类型的变量,那么编译器识别到了类型不一样后,此时会发生隐式类型转换,也就是将double类型转换为int类型,但是它不是修改double变量本身让其转化为int,而是用一个临时变量来保存double类型数据强制转换为int类型的值,而临时变量是具有常性的,所以我们只能通过从常引用来接收这个临时变量,并且此时临时变量由于被常引用所指向,那么它的生命周期就和该引用的生命周期一样

那么知道引用如何定义以及相关的注意事项之后,大家是否清晰的了解引用的一些应用场景呢,而我们知道引用就是指针2.0版本,那么指针所应用的场景,那么肯定同样是引用所应用的场景

1.输出型参数

那么我们知道当我们在代码中调用一个函数,那么会为调用的该函数创建一个函数栈帧,也就是在栈上开辟一段空间其中来存储该函数的局部变量以及形参等内容,所以当我们调用一个函数的时候,将实参传递给形参,那么形参只是实参的一份拷贝,所以形参的改变不影响实参,那么如果我们要让形参的改变影响实参,那么我们就得将形参设置为指针,该指针保存的就是实参的地址,然后在函数体内部就能够解引用指针来访问实参

同理,我们也可以将形参设置为用引用来进行接收,那么引用与指针不同的是,引用作为输出型参数使用起来更方便,因为指针还需要解引用来访问以及修改,而引用由于就是给其指向的变量取了一个别名,所以我们对形参操作就等于直接对实参操作

引用作为参数的好处不仅仅只有这一个方面,实参传递给形参需要值的拷贝,那么假设我们实参的大小特别大,那么拷贝的代价其实是挺大的,而引用作为参数就不需要经过拷贝,引用直接指向实参从而提升了效率,那么我们来看一下引用作为参数的例子:

交换两个数

#include<iostream>
using namespace std;
void swap(int& a, int& b)
{
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
}
int main()
{
    int a=10;
    int b=20;
    swap(a,b);
    cout << "a=" << a << " b=" << b;
    return 0;
}

在这里插入图片描述

2.引用做返回值

那么我们知道引用可以作为函数的参数,那么它同样可以作为返回值,但是需要谨慎使用,因为我们知道调用一个函数会在栈上为该函数创建一个栈帧,那么一旦函数调用结束,那么栈帧就被销毁了,此时该函数的栈帧所在的空间就会被回收给下一个调用的函数使用,所以我们在函数中返回的局部变量也会随着栈帧一起被销毁

而如果是传值拷贝,那么从函数返回的局部变量到外部主函数接收的变量就会发生一次拷贝,将值带回给该函数调用处,然后栈帧被销毁,所以我们引用指向的返回值是在函数中定义的局部变量,那么此时一旦函数调用结束,那么栈帧被销毁了,那么此时该空间被回收,但是引用却依然指向那片空间,而如果函数栈帧被销毁后,操作系统没有对那片空间的数据做清理的话,那么此时返回的值还是正确的,但是如果我们在调用一次函数,那么此时那片空间就会给新的函数所使用,原来的空间会被覆盖,那么此时引用指向的就是一个随机值

那么我们也可以写代码来实验一下:

#include<iostream>
using namespace std;
int count(int x)
{
       x++;
       return x;
}
int main()
{
      int x=10;
     int& a=count(x);
     cout<<"x="<<a<<endl;
     int& b=count(x);
     cout<<"x="<<b<<endl;
     cout<<"&a"<<a<<endl;
     return 0;
}

那么我们来跑一下这段代码,看看运行结果:
在这里插入图片描述

发现第一次调用count函数之后,打印引用的值就是随机值,说明在调用count函数结束之后就对count函数的栈帧进行了清理

所以我们知道用引用作返回但是返回值是局部变量的的后果,那么我们希望返回的变量不会随着函数栈帧一起被销毁,那么只能是该变量不在栈上存在而是在其他位置上比如堆或者静态区中存在,所以如果我们用malloc申请了在堆上分配了一片空间,那么返回该空间的首元素的地址,那么我们就可以用引用返回,因为该在堆上申请开辟的空间不会随着函数调用结束而被销毁,而是可以由用户控制其生命周期,如果用户没有显示的调用free函数,那么它的生命周期和程序一样长,同理我们在函数中用static修饰的变量,那么它会存在于静态区中,返回该静态变量,也可以用引用。

那么引用作为返回值是一把双刃剑,那么它虽然可以避免值的拷贝,但是我们一定要注意返回值是否会和函数栈帧一起被销毁

内联函数

1.为什么要有内联函数

那么我们知道调用一个函数会为函数创建一个栈帧,其中函数栈帧中就保存了该函数定义的局部变量以及形参,那么如果我们反复调用同一个函数,那么函数调用涉及到创建函数的栈帧,函数调用结束又要清理栈帧,那么这个过程必然是要付出时间代价,所以为了优化这部分的时间代价,便有了内联函数的出现

2.内联函数是什么以及怎么用

所谓的内联函数,那么当我们调用一个内联函数的时候,那么此时不会为该函数建立栈帧,而是将其定义也就是代码段直接嵌套在函数的调用处,那么直接在调用处执行函数代码段的内容,从而避免建立函数的栈帧,而c语言则是可以通过宏函数,但是宏函数的编写比较复杂并且还不便于调试,所以相比之下内联函数就十分优秀,那么它能够便于调试并且定义简单

定义一个内联函数则是通过inline关键字来声明该函数是内联函数

inline swap(int& a,int& b)

至于函数的调用就和我们正常调用普通函数一样即可,而这中间的转换则是交给了编译器来进行处理

3.内联函数的注意事项

那么这里我们要首先要理解的就是内联函数它具体优化的点是什么,很多人经常误以为内联函数是优化的空间消耗,因为按照他们的理解,内联函数避免了在栈上开辟函数的栈帧,而是在调用处展开函数的代码,但是实际上内联函数并不是优化函数栈帧开辟的消耗,因为函数被调用的时候,那么它确实会在栈上开辟函数栈帧,那么一旦调用结束,该栈帧就会被销毁了,那么该栈帧的空间就会被回收来给下一次调用的函数所利用,所以不存在函数栈帧的消耗,那么它优化的其实是建立栈帧以及销毁栈帧的时间开销

所以如果是在这样的场景下,比如我们在不同位置被反复调用了该函数并且该函数的内容不大只有几行代码,那么我们可以尝试将其设置为内联函数

但是如果函数内容量大,假设该函数有100行代码,并且被调用了10000次在main函数的不同位置当中,那么如果我们将该函数设置为内联,那么这100号代码会在这不同的调用的位置处展开,那么此时该main函数的代码量会增加100*10000条,那么这部分代码最终会变转换为汇编指令以及机器码,那么转换成汇编指令需要一定的时间,并且还会导致代码膨胀的问题从而让可执行文件的体积变大,所以这就是为什么建议将那些函数内容量小的,并且在不同位置多次调用的设置为内联函数

并且我们设置了内联函数,那么编译器也不一定听我们的,也就是说我们用inline关键字声明的函数,编译器可以拒绝将其内联,那么原因也很简单,万一我们用户将所以定义的函数都用inline关键字去声明,甚至将递归函数都设置为inline,那还得了,所以不要以为你觉得我这个函数内容量很小,就10多行代码,那么编译器就一定将其转换内联,那么具体是否转换为内联,还得有编译器自己来决定

其次内联函数的声明和定义不能分开,如果我们像普通的函数那样将一个内联函数的声明和定义放在不同的头文件中,那么会引发什么样的后果呢?那么我们还是写一份代码来实验一下:

//a.h
#pragma once
inline int add(int x, int y);
//a.cpp
#include"a.h"
inline int add(int x, int y)
{
	return x + y;
}
//main.cpp
#include<iostream>
#include"a.h"
using namespace std;
int main()
{
    int x = 10;
    int y = 20;
    cout << add(x, y) << endl;
    return 0;
}

在这里插入图片描述

那么为什么会出现链接错误呢,那么其实就是因为在编译阶段会为每一个文件生成一个符号表,其中符号表就是记录全局的函数的声明以及对应的定义的地址,那么其中对于main.o文件来说,那么它调用了add函数,那么此时编译器就需要将add函数该展开到调用处并且将其记录到符号表中,但是由于编译是一个文件一个文件作为独立的编译单元,那么此时它看不到该函数的定义,所以该文件中对应的符号表中没有该函数对应的定义的地址,而对于a.cpp文件来说,由于该文件编译的时候,那么识别到add函数是inline关键字定义的,那么不会将其记录到符号表中,因为内联函数实在调用处直接展开的,所以在链接阶段的时候,生成一个全局的符号表时,那么此时要为add函数填入一个唯一的定义的地址,但是链接器找不到该add函数的定义,那么便无法完成链接,所以会报一个链接阶段的错误,

所以对于这种情况,函数调用处的所在文件一定得看到完整的内联函数的定义,所以:

//a.h
#pragma once
inline int add(int x, int y)
{
    return x+y;
}
//main.cpp
#include<iostream>
#include"a.h"
using namespace std;
int main()
{
    int x = 10;
    int y = 20;
    cout << add(x, y) << endl;
    return 0;
}

在这里插入图片描述

结语

那么这就是本篇文章关于c++的语法的讲解,那么本文就是c++入门系列的最后一篇,那么恭喜看到这的你,你已经成功的踏入了c++学习的大门,那么打开了c++的大门之后,接下来就是封装,继承,多态在等待着你,那么相信我们最终都一定能够战胜掌握它们,那么我的下一期c++系列的文章是类和对象,我会持续更新,希望大家能够多多关注,如果本文有帮组到你的话,还请三连加关注哦,你的支持,就是我创作最大的动力!
在这里插入图片描述


http://www.kler.cn/a/600713.html

相关文章:

  • javaweb自用笔记:Mybatis
  • Java 线程池全面解析
  • 【Pandas】pandas Series to_csv
  • vue3中watch 函数参数说明
  • 小蓝的括号串(栈,dfs)
  • PHP在2025年的新趋势与应用
  • xilinx约束中set_property -dict表示什么意思
  • Nuxt出现Error: Failed to download template from registry
  • C语言复习笔记--函数递归
  • Hugging Face Spaces 介绍与使用指南
  • 4.milvus索引FLAT
  • 黄土高原风蚀区解析多源数据融合与机器学习增强路径-RWEQ+集成技术在风蚀模数估算中的全流程增强策略—从数据融合到模型耦合的精细化操作指南
  • Linux云计算SRE-第二十一周
  • 国产开发板—米尔全志T113-i如何实现ARM+RISC-V+DSP协同计算?
  • 深入理解JavaScript中的同步和异步编程模型及应用场景
  • 2025年DeepSeek行业应用实践报告
  • Elasticsearch Windows 环境安装
  • Transformers快速入门-学习笔记(二)
  • Android设计模式之单例模式
  • Windows 10 系统下配置Flutter开发环境,保姆级教程冢铖2023-02-17 09:56广东