首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++ 入门必看:引用怎么用?inline 和 nullptr 是什么?

C++ 入门必看:引用怎么用?inline 和 nullptr 是什么?

作者头像
云泽808
发布2025-12-30 17:49:35
发布2025-12-30 17:49:35
360
举报

在这篇文章开始之前,我想给大家推荐一个非常牛的人工智能学习网站。在近几年,大家也知道人工智能和 AI 技术的发展也是非常迅速,越来越多的高校也纷纷一引入了相关课程,将其纳入教学体系。所以面对这样的趋势,提前掌握一些 AI 知识,必定能为未来的职业发展增加竞争力。 人工智能学习网站

一、引用

1.1 引用的概念和定义

引用不是新定义的一个变量,而是给已存在变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。比如:水浒传中李逵,宋江叫“铁牛”,江湖人称“黑旋风”;林冲,外号豹子头; 类型&引用别名=引用对象 C++中为了避免引入太多运算符,会复用C语言的一些符号,比如前面的<<和>>,这里引用也和去地址使用同一个符号&,要注意区分,但是个人觉得用更多符号反而更好,不容易混淆

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

创建i这个变量的时候会开辟一块空间叫i,int& j = i,就是给这块空间又去了一个名字叫j,还可以再取一个名字叫k

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

引用可以给一个变量取多个别名,也可以给别名取别名

1.2 引用的特性

  • 引用在定义时必须初始化(在定义时必须清楚是谁的别名)
  • 一个变量可以有多个引用
  • 引用一旦引用一个实体,再不能引用其他实体
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

k已经是i,j的别名了,就不能是实体m的别名了,图中k=m就是赋值了

1.3 引用的使用

C++中引用就是来解决指针不足的问题,引用的作用就是在大部分场景去替代指针,但是部分场景还是离不开指针

1.3.1 引用传参的使用

之前完成x和y的交换,是使用指针来完成的,这也可以使用引用平替

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

rx和ry是x和y的别名,rx和ry的交换,就是x和y的交换。这里引用看似没有初始化,其实是有的,引用在函数调用的时候才定义,定义的时候x和y传过来了

并且指针交换和引用交换是能同时存在的,在C++之中二者构成了函数重载

在数据结构这里也可以直接使用引用,图中形参就是实参的别名

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

再比如说现在要交换p1和p2两个指针,使用二级指针的话比较绕(二级指针解引用就是一级指针),可以使用引用来简化一下,引用既然可以给普通变量定义别名,也可以给指针变量定义别名

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

这里报错是因为调用不明确,他们两个函数参数按道理是不同的两个类型,但是二者都可以接收指针,所以不支持重载。将1注释掉就好了。图中注释是rp1和rp2,我这里标错了

再举一个C语言单链表的例子

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

不使用引用的时候,链表的尾插是这样的,二级指针确实很绕人,学了这门就,博主今天再看还是会蒙一下

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

这样phead就是plist的别名了,插入第一个节点的时候newnode给phead,phead改变就是plist改变

学校的一些教材上是这样写的,让人很难理解

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

首先要理解,如果结构体前面没有typedef,这里的x就是定义的结构体变量,p就是结构体的指针变量,且二者都是全局变量

代码语言:javascript
复制
struct A
{
}x,*p

前面有typedef,就是对类型的别名

代码语言:javascript
复制
//等价于
typedef struct SListNode SLTNode;
typedef struct SListNode* PSLTNode;

所以* PSLTNode,其实就是SListNode *,就是结构体的指针。就可以这样就和前面没什么区别了。我感觉他的设计本意是为了避免二级指针让人绕进去,但是加了C++的一些语法,反而写的更史了

引用也是不能完全替代指针的,像在链表,树,这些节点定义的位置,只能使用指针,节点与节点之间的物理空间并不连续,一定是一块物理空间存下一块物理空间的地址,所以至少要有一个指针。 树和链表的操作都有一个要求,需要改变指向,如图使用引用的话,第一个节点存了第二个节点的别名,但是如果第二个节点删了之后,是不能变成第三个节点的别名的。指针核心不能被引用的点就是,C++的引用无法改变指向

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

但是Java没有指针,只有引用。Java的引用是可以改变指向的

再说一下返回的问题,传值返回传值传参类似,都会产生临时变量,这里并不是用ret作为一个返回值(func函数调用结束之后才会有返回值,此时ret都销毁了),返回的是ret的拷贝的临时变量。一般返回的对象比较小,这种情况下临时变量会存到寄存器里面,如果比较大,就会在两个栈帧中开一片空间来存(在函数调用之前就会开好),func销毁,中间的空间再拷贝给x,再把中间的栈帧(寄存器存的值)销毁

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

要看懂这个图首先要理解函数调用会在栈这个区域建立一个栈帧

代码语言:javascript
复制
func() += 1;

所以也不能对func的返回值直接进行加等,func传值返回返回的是ret拷贝的临时对象,临时对象又有一个特点,临时对象是不能被修改的

1.3.2 传引用返回的错误使用

再说传引用返回,这里返回的是ret的别名

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

但是这样的传引用返回的行为,本质上是非常危险的,func函数结束之后,func栈帧就销毁了,但是tmp依旧是ret这块空间的别名,tmp依旧在访问ret,就相当于野指针的访问。此时返回的就不一定是0了,也有可能是随机值 VS上返回的是0,但是报警告了,越界读是不一定报错的,越界写才会报错

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

补充一下,一块空间被销毁了之后,还是可以访问这块空间的,无论free还是栈帧结束,是把这块空间的使用权还给操作系统,但是这块空间没有清空,也就是上上张图还能输出0的原因,而且空间是可以反复使用的,这块空间操作系统还可以分配给别人用 ,类似于租房。房子退了之后,我还可以进入这块房间的原因是,又悄悄地配了一把钥匙。

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

再改一下,func1返回的是ret的别名(tmp),x又相当于是tmp的别名,相当于x就是ret的别名。所以这里虽然func1被销毁了,但是还能通过x别名,访问到这一块空间。

再补充一下,这里返回了ret的别名,ret是个局部变量,为什么ret销毁了,x还能是ret的引用呢?

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

这里转到反汇编,这里定义个i的变量初始化为0

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

反汇编lea就是取地址,把i的地址取出来放到eax这个寄存器之中,然后再把eax的值给p。这两句汇编代码的意思就是把i的地址给p,这里开了空间,

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

这里取i的地址给eax,再把eax的值给一个叫r1的变量,这r1就是一个指针,这就可以发现二者是一样的

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

并且可以看到解引用对指针指向的变量++,同引用(r1是i的别名,r1的改变就是i的改变),二者的汇编指令也是一样的

所以说引用是个语法层的概念,语法层上是不开空间,但是语法最终要被编译成指令,在汇编指令这一层是没有引用的,只有指针,引用的底层也是转换成指针实现的

再回头看就懂了,func1函数结束,栈帧销毁,ret也跟着销毁了,虽然销毁了,但是空间还在,取了别名,x就已经存了地址

所以C++引用这里是分上层和下层的,这两层要分开理解,语法层是表达层,底层是实现层 就比如像老婆饼里没有老婆一样,表达的意思是这个饼做出来和老婆做得一样好,都不一定见的这个饼是一个女性做的,也有可能是个扣脚大汉做的呢

虽然语法上说引用是别名,没有拷贝,但底层毕竟是指针,是开了空间的(4个或8个,开个指针)。看似没有提高效率,但是在传值传参的时候,如果传了一个大对象(几百个字节),传引用传参的高效率就体现出来了,无论传多大的对象,引用的开销从真正底层的角度来说,只需要一个指针的开销

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

栈帧是向下生长的,func1销毁之后,调用func2函数,func2和func1栈帧是一样大的,因为他们都定义了一个变量。func2建立栈帧的位置和大小是和之前的func1重叠的。func2没有操作直接销毁了,但是VS下的栈帧销毁并没有清空空间,所以再次访问x,x是这块空间的别名,就输出456了

1.3.3 传引用返回的正确使用

如图出了作用域,func1结束,ret还在,此时被static修饰的ret就不存在func1的栈帧之中了

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

再举一个例子,依旧是顺序表,这里随便搭了一个架子,细节不要在意

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

下面使用引用做返回值,就不需要再使用SLModify

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

此时SLat就变成了一个既可以读,又可以写的函数,return pls->a[i];此时返回第i个位置值的别名,既可以读到这个值,也可以修改这个值,而且不会返回类似野引用的东西,pls->a[i]不是一个局部变量,而是外面的结构体指向的数组上的内容,返回的是a指向数组的第i个位置值的别名

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

且顺序表中数组的空间在堆上(因为堆内存可以在程序运行时动态分配,数组在栈上分配内存,其大小必须在编译时确定,无法动态修改),堆上的内存由程序员手动管理(或通过智能指针等机制自动管理),可以长期存在,直到显式释放,也就是出了作用域,SLat结束了,返回的值也还在

引用在出了作用域的那个对象不是局部对象的情况下,既可以减少拷贝,又可以修改引用对象

  • 引用在实践中主要是于引用传参引用做返回值中减少**拷贝提升效率(形参是实参的别名,没有额外开空间)**和改变引用对象同时改变被引用对象
  • 引用传参跟指针传参功能是类似的,引用传参相对更方便一些
  • 引用返回值的场景相对比较复杂
  • 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向
  • 一些主要用C代码实现版本数据结构教材中,使用C++引用替代指针传参,目的是简化程序,避开复杂的指针。

1.4 const引用

  • 可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。 如图,如果a被const修饰了,引用就不能使用了,此时a不能改变,但是b是a的别名,可以改变a,这就是一个权限放大的场景,权限是不能放大的,想要正常引用就要权限平移,b也要被const修饰
在这里插入图片描述
在这里插入图片描述

下图c自身可以修改,可读可写,但是d作为c的别名的时候权限缩小了,d作为c的别名,只能读不能写,不是c的权限缩小了

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
// 权限缩小放大,只存在于const指针&引用
const int* p1 = &a;
// 不能权限放大
// int* p2 = p1;
// p1指向的内容只能读,不能写
// p2对指向的内容还能写
const int* p2 = p1;//正确写法

// 可以权限缩小
int* p3 = &e;
const int* p4 = p3;

再补充一下,const引用是可以引用常量的

代码语言:javascript
复制
const int& a = 10;

如果不是改变形参影响实参的场景,引用传参在形参位置尽量加上const,不这样写有很多限制,如图y可以传,但是z和常量就传不过去

在这里插入图片描述
在这里插入图片描述
  • 不需要注意的是类似int& rb = a * 3; double d = 12.34; int& rd = d; 这样一些场景下a * 3的和结果保存在一个临时对象中,int& rd = d也是类似,在类型转换中会产生临时对象存储中间值,也就是此时,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发权限放大,必须要用常引用才可以
在这里插入图片描述
在这里插入图片描述

这里也会产生临时变量。double d = i;这里i不是直接赋值给d的,i会先放到临时变量之中,临时变量再赋值给d,这期间i的结构会重新改变,会按浮点数的存储规则存储在临时变量之中,这就是隐式类型转换的结果 显式类型转换的结果也是如此,int p = (int)&i;强制类型转换本质上也是产生一个临时变量。 临时变量具有另外一个特点,由于是开的临时空间,也会使用寄存器之类的来存,所以具有常性,就像被const修饰,所以说这里没加const(double& rd = i;)语法检测不能通过的原因是rd引用的不是i,rd引用的是临时变量,直接引用造成了权限的放大,临时变量就像被const修饰一样

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

到这里,前面传参一个类型转换的值也可以传过去了

在这里插入图片描述
在这里插入图片描述
  • 所以临时对象就是编译器需要一个空间暂存表达式的求职结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。

1.5 指针和引用的关系

C++中指针和引用就像是两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代

  • 语法概念上引用是 一个变量的取别名不开空间,指针式存储一个变量地址,要开空间
  • 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的
  • 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以再不断地改变指向对象
  • 引用可以直接访问指向对象,指针需要解引用才是访问指向对象
  • sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下式8个字节)
  • 指针很容易出现空指针和野指针的问题,引用很少出现(比如返回了一个局部对象的引用,类似野指针),引用使用起来相对更安全一些

二、inline

先看一下宏常见的问题

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

连续两个分号没有问题,但在下面的场景下就很坑了

代码语言:javascript
复制
int ret2 = ADD(1, 2) * 3;//有分号没办法乘
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

看似没有问题,但这个宏在另一种场景下就又不对了

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

所以可以看出宏函数很容易出现问题,很复杂,还不能调试

所以C++之父就搞了个内联出来

写个函数是不容易写错的,但写个宏函数很容易写错 宏函数的优点就是可以提高效率,适用于高频调用的小函数,预处理阶段宏会替换,不建立栈帧,而且宏甚至可以传类型

代码语言:javascript
复制
#define ADD(T, a, b)((a) + (b))
int ret1 = ADD(int, 1, 2);

C++中内联就是用来替换宏的

  • 用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的时候展开(不是把函数部分的代码直接放到主函数处,而是把函数地方的逻辑经过一定的处理,经过编译器自己分析,用于主函数处)内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率
  • inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。
在这里插入图片描述
在这里插入图片描述

这里转到反汇编来理解

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

从调用Add函数来看,push就是压栈,栈是向下生长的,上面是高地址,下面是低地址,栈顶插了一个2后,esp会向下走一点点

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

call是调用一个函数,后面跟的是函数的地址(函数被编译完是一串指令,函数的地址就是第一句指令的地址),汇编也是可以调试的,f11就进入Add函数

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

call就是jmp指令的地址,jmp也是一个跳转指令,jmp到1830这个地址,h是16进制的后缀

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

此处就是函数真实的地址了,push ebp之后,再把esp给ebp

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

然后让esp向下减,开栈帧

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

这里传值返回是传到一个临时的寄存器当中,a mov到eax之中,add就是+=,把b+=到eax中,相当把a和b加和的结果放到eax寄存器当中

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

最后ret就是return,退出栈帧

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

imul是乘的指令,把eax乘3的结果放到eax之中,最后一条指令把eax的结果mov到ret2这个变量当中

当前的内联就 没有发挥作用

  • C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数
  • vs编译器debug版本下面默认是不展开inline的,这样方便调试(调试是要保留栈帧建立的过程的),在release模式下就可以展开了 ,但是release就不能调试了,想看汇编进入调试才可以,debug版本想展开需要设置一下以下两个地方
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

设置好后再进入汇编

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

这里就没有call Add这一指令了,图中就是展开的逻辑,1先给eax,add+=把1和2加起来放到eax之中,再让eax乘3放到ecx这个寄存器当中,然后再将ecx这个寄存器的值放到ret2变量里,和调用函数实现的逻辑是一摸一样的

这是函数中又加入了几行代码,展开就可以看到没有call Add,call Add变成了这些指令,一行变20行

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

我这里自己试了一下,在VS中一个函数大概10句代码量的时候,在设置下加了inline也不会展开了

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

这样由编译器决定是否展开的原因就是,随着代码的增多,展开的指令就变多了,如果交给程序员就会有一个恶性膨胀的问题,如图,如果

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

不展开的话,有1w个调用的地方,就有1w个call指令。1w个call指令都是执行的func函数的指令,所以内联也是有缺陷的,它会导致代码指令膨胀,最后转换成二进制的可执行程序就变大了

  • inline不建议声明和定义分离到两个文件,内联函数建议直接在.h文件里定义
在这里插入图片描述
在这里插入图片描述

分离会导致链接错误(预处理阶段.h文件展开,就得到了函数的声明,调用函数的时候,此时内联是无法展开的,此时函数还没有实现,此时内联是一定废掉了)。就要call函数的地址,但此时也没有地址,此时只有声明,函数的地址是要由定义来生成的,定义编译成很多句指令,第一句指令的地址才是函数的地址。之后链接时会出现报错


三、nullptr

nullptr是C++11的一个关键字,是用来替代C语言的NULL的

NULL实际上是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

代码语言:javascript
复制
#ifndef NULL
    #ifdef __cplusplus
        //C++中的NULL被替换为0,默认为整型
        #define NULL    0
    #else
        //C语言中的NULL也被替换为0,但是被强制类型转换为(void*)
        #define NULL    ((void *)0)
    #endif
#endif
  • C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void * )的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过f(NULL)调用指针版本的f(int * )函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。f((void* )NULL);调用会报错
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在C++之中,void* 给int* 是需要强制类型转换的

  • C++11中引用nullptr,nullptr是一个特殊的关键字(就相当于语法层在编译的时候就特殊解决该问题了),nullptr是一种特殊类型的字面量,它可以转换成任意其它类型的指针类型(值还为0)。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式转换为指针类型,而不能被转换为整数类型。

总结

以上就是C++语法入门的全部内容了,主播花了两篇终于写完了,真的好肝,C++知识点整理起来,比C多了很多

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、引用
    • 1.1 引用的概念和定义
    • 1.2 引用的特性
    • 1.3 引用的使用
      • 1.3.1 引用传参的使用
      • 1.3.2 传引用返回的错误使用
      • 1.3.3 传引用返回的正确使用
    • 1.4 const引用
    • 1.5 指针和引用的关系
  • 二、inline
  • 三、nullptr
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档