跳到主要内容
C++ 核心语法解析:引用、内联函数与 nullptr | 极客日志
C++
C++ 核心语法解析:引用、内联函数与 nullptr 深入解析 C++ 三大核心基础概念。引用作为变量别名,无需额外内存开销,常用于传参避免拷贝及返回值修改,但需注意生命周期问题。内联函数在编译期展开调用代码,消除栈帧开销提升效率,但可能导致代码膨胀,建议短小函数使用。nullptr 关键字替代 NULL 宏,解决类型转换歧义问题,是 C++11 标准推荐的空指针表示方式,确保类型安全。
CryptoLab 发布于 2026/3/30 更新于 2026/4/25 1 浏览C++ 核心语法解析:引用、内联函数与 nullptr
引用
引用的概念和定义
引用不是新定义的变量,而是给已存在变量取一个别名。编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型&引用别名=引用对象
C++ 中为了避免引入太多运算符,会复用 C 语言的一些符号,比如前面的 << 和 >>,这里引用也和去地址使用同一个符号 &,要注意区分。
创建 i 这个变量的时候会开辟一块空间叫 i,int& j = i,就是给这块空间又去了一个名字叫 j,还可以再取一个名字叫 k。
引用可以给一个变量取多个别名,也可以给别名取别名。
引用的特性
引用在定义时必须初始化(在定义时必须清楚是谁的别名)
一个变量可以有多个引用
引用一旦引用一个实体,再不能引用其他实体
注意,k 已经是 i、j 的别名了,就不能是实体 m 的别名了,图中 k=m 就是赋值操作。
引用的使用
C++ 中引用是为了解决指针不足的问题,引用的作用就是在大部分场景去替代指针,但是部分场景还是离不开指针。
引用传参的使用
之前完成 x 和 y 的交换,是使用指针来完成的,这也可以使用引用平替。
rx 和 ry 是 x 和 y 的别名,rx 和 ry 的交换,就是 x 和 y 的交换。这里引用看似没有初始化,其实是有的,引用在函数调用的时候才定义,定义的时候 x 和 y 传过来了。
并且指针交换和引用交换是能同时存在的,在 C++ 之中二者构成了函数重载 。
在数据结构这里也可以直接使用引用,图中形参就是实参的别名。
再比如说现在要交换 p1 和 p2 两个指针,使用二级指针的话比较绕(二级指针解引用就是一级指针),可以使用引用来简化一下,引用既然可以给普通变量定义别名,也可以给指针变量定义别名。
这里报错是因为调用不明确,他们两个函数参数按道理是不同的两个类型,但是二者都可以接收指针,所以不支持重载。将其中一个注释掉就好了。
再举一个 C 语言单链表的例子。
不使用引用的时候,链表的尾插是这样的,二级指针确实很绕人,即使是资深开发者有时也会觉得蒙。
这样 phead 就是 plist 的别名了,插入第一个节点的时候 newnode 给 phead,phead 改变就是 plist 改变。
学校的一些教材上是这样写的,让人很难理解。
首先要理解,如果结构体前面没有 typedef,这里的 x 就是定义的结构体变量,p 就是结构体的指针变量,且二者都是全局变量。
struct A {} x, *p;
前面有 typedef,就是对类型的别名。
所以 *PSLTNode,其实就是 SListNode*,就是结构体的指针。就可以这样就和前面没什么区别了。我感觉他的设计本意是为了避免二级指针让人绕进去,但是加了 C++ 的一些语法,反而写得更复杂了。
引用也是不能完全替代指针的,像在链表、树,这些节点定义的位置,只能使用指针,节点与节点之间的物理空间并不连续,一定是一块物理空间存下一块物理空间的地址,所以至少要有一个指针。
树和链表的操作都有一个要求,需要改变指向,如图使用引用的话,第一个节点存了第二个节点的别名,但是如果第二个节点删了之后,是不能变成第三个节点的别名的。指针核心不能被引用的点就是,C++ 的引用无法改变指向 。
但是 Java 没有指针,只有引用。Java 的引用是可以改变指向的。
再说一下返回的问题,传值返回 和传值传参 类似,都会产生临时变量,这里并不是用 ret 作为一个返回值(func 函数调用结束之后才会有返回值,此时 ret 都销毁了),返回的是 ret 的拷贝的临时变量。一般返回的对象比较小,这种情况下临时变量会存到寄存器里面,如果比较大,就会在两个栈帧中开一片空间来存(在函数调用之前就会开好),func 销毁,中间的空间再拷贝给 x,再把中间的栈帧(寄存器存的值)销毁。
要看懂这个图首先要理解函数调用会在栈这个区域建立一个栈帧。
所以也不能对 func 的返回值直接进行加等,func 传值返回返回的是 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 了。
传引用返回的正确使用 如图出了作用域,func1 结束,ret 还在,此时被 static 修饰的 ret 就不存在 func1 的栈帧之中了。
再举一个例子,依旧是顺序表,这里随便搭了一个架子,细节不要在意。
下面使用引用做返回值,就不需要再使用 SLModify。
此时 SLat 就变成了一个既可以读,又可以写的函数,return pls->a[i]; 此时返回第 i 个位置值的别名,既可以读到这个值,也可以修改这个值,而且不会返回类似野引用的东西,pls->a[i] 不是一个局部变量,而是外面的结构体指向的数组上的内容,返回的是 a 指向数组的第 i 个位置值的别名。
且顺序表中数组的空间在堆上(因为堆内存可以在程序运行时动态分配,数组在栈上分配内存,其大小必须在编译时确定,无法动态修改),堆上的内存由程序员手动管理(或通过智能指针等机制自动管理),可以长期存在,直到显式释放,也就是出了作用域,SLat 结束了,返回的值也还在。
引用在出了作用域的那个对象不是局部对象的情况下,既可以减少拷贝,又可以修改引用对象。
引用在实践中主要是于引用传参 和引用做返回值 中减少拷贝提升效率 (形参是实参的别名,没有额外开空间)和改变引用对象同时改变被引用对象
引用传参跟指针传参功能是类似的,引用传参相对更方便一些
引用返回值的场景相对比较复杂
引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++ 的引用跟其他语言的引用(如 Java)是有很大的区别的,除了用法,最大的点,C++ 引用定义后不能改变指向,Java 的引用可以改变指向
一些主要用 C 代码实现版本数据结构教材中,使用 C++ 引用替代指针传参,目的是简化程序,避开复杂的指针。
const 引用 可以引用一个 const 对象,但是必须用 const 引用。const 引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
如图,如果 a 被 const 修饰了,引用就不能使用了,此时 a 不能改变,但是 b 是 a 的别名,可以改变 a,这就是一个权限放大 的场景,权限是不能放大的,想要正常引用就要权限平移 ,b 也要被 const 修饰。
下图 c 自身可以修改,可读可写,但是 d 作为 c 的别名的时候权限缩小了,d 作为 c 的别名,只能读不能写,不是 c 的权限缩小了。
const int * p1 = &a;
int * p2 = p1;
const int * p2 = p1;
int * p3 = &e;
const int * p4 = p3;
如果不是改变形参影响实参的场景,引用传参在形参位置尽量加上 const,不这样写有很多限制,如图 y 可以传,但是 z 和常量就传不过去。
所以临时对象就是编译器需要一个空间暂存表达式的求职结果时临时创建的一个未命名的对象,C++ 中把这个未命名对象叫做临时对象。
不需要注意的是类似 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++ 中指针和引用就像是两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
语法概念上引用是 一个变量的取别名不开空间,指针式存储一个变量地址,要开空间
引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的
引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以再不断地改变指向对象
引用可以直接访问指向对象,指针需要解引用才是访问指向对象
sizeof 中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占 4 个字节,64 位下式 8 个字节)
指针很容易出现空指针和野指针的问题,引用很少出现(比如返回了一个局部对象的引用,类似野指针),引用使用起来相对更安全一些
inline int ret2 = ADD (1 , 2 ) * 3 ;
所以可以看出宏函数很容易出现问题,很复杂,还不能调试。
宏函数的优点就是可以提高效率,适用于高频调用的小函数,预处理阶段宏会替换,不建立栈帧,而且宏甚至可以传类型。
#define ADD(T, a, b) ((a) + (b))
int ret1 = ADD (int , 1 , 2 );
用 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。
这里传值返回是传到一个临时的寄存器当中,a mov 到 eax 之中,add 就是+=,把 b+=到 eax 中,相当把 a 和 b 加和的结果放到 eax 寄存器当中。
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)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
C++11 中引用 nullptr,nullptr 是一个特殊的关键字(就相当于语法层在编译的时候就特殊解决该问题了),nullptr 是一种特殊类型的字面量,它可以转换成任意其它类型的指针类型(值还为 0)。使用 nullptr 定义空指针可以避免类型转换的问题 ,因为 nullptr 只能被隐式转换 为指针类型,而不能被转换为整数类型。
C++ 中 NULL 可能被定义为字面常量 0,或者 C 中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,调用了 f(int x),因此与程序的初衷相悖。f((void*)NULL); 调用会报错。
在 C++ 之中,void* 给 int* 是需要强制类型转换的。
总结 以上就是 C++ 语法入门的全部内容了,C++ 知识点整理起来,比 C 多了很多。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online