跳到主要内容 C++ 引用、内联函数与 nullptr 详解 | 极客日志
C++ 算法
C++ 引用、内联函数与 nullptr 详解 C++ 引用作为变量别名无需额外内存,常用于传参减少拷贝;内联函数在编译期展开调用以优化性能但受编译器限制;nullptr 关键字提供类型安全的空指针表示,避免 NULL 宏带来的重载歧义。三者共同提升代码效率与安全性。
DevStack 发布于 2026/2/6 更新于 2026/4/18 1 浏览引言
C++ 的演进之路,是不断在性能与安全、灵活与严谨之间寻求平衡的艺术。本文将深入剖析三大特性:引用、内联函数、nullptr。理解它们,不仅是掌握语法,更是洞察 C++ 设计哲学,书写更高效、更健壮代码的关键一步。
一、引用:C++前期重难点
1.1 一览:引用的方方面面
引用不是重新定义变量 ,而是给已经定义的变量起一个别名 (编译器不会为引用变量开辟内存空间,共用同一块)。
形式:类型& 引用别名 = 引用对象;
C++中为了避免引入太多的运算符,会复用 C 语言的一些符号,比如<<,>>,引用也和取地址使用了同一个符号&,注意使用方法角度来区分。
引用在定义时必须初始化。
一个变量可以有多个引用。
引用一旦引用一个实体,再不能引用其他实体。
#include <iostream>
using namespace std;
int main ()
{
int i = 10 ;
int & j = i;
int & k = i;
int & a = j;
cout << &i << '\n' ;
cout << &j << '\n' ;
cout << &k << '\n' ;
cout << &a << endl;
return 0 ;
}
(观察地址都是一个变量!)
#include <iostream>
using namespace std;
int main ()
{
int & j;
cout << j << endl;
;
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
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
return
0
#include <iostream>
using namespace std;
int main ()
{
int a = 10 ;
int & b = a;
int c = 20 ;
b = c;
cout << "b=" << b << endl;
return 0 ;
}
1.2 划重点:引用的正确使用
引用的主要实践用途是通过引用传参和引用返回来减少数据拷贝提高效率,以及在修改引用对象时同步改变被引用的原对象。 (引用返回值相对复杂,先简单了解。)
引用传参跟指针传参功能是类似的,引用传参相对更方便一些。
引用和指针相辅相成,并不能完全替代。
引用传参——>代替指针(大部分情况引用可以代替指针)
void Swap (int * a, int * b)
{
if (*a > *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
}
void Swap (int & rx, int & ry)
{
if (rx > ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
}
int main ()
{
int x = 2 ;
int y = 1 ;
Swap (&x, &y);
cout << x << ' ' << y << '\n' ;
Swap (x, y);
cout << x << ' ' << y << '\n' ;
return 0 ;
}
在逻辑上,rx,ry是x,y的别名,本质上就是x,y,所以交换rx,ry就是交换x,y。
对于函数传参时,引用的初始化:因为只有调用函数才会定义引用,传参就相当于赋值 int& rx = x, int& ry = y。
#include <iostream>
using namespace std;
void swap (int ** x, int ** y)
{
int * tmp = *x;
*x = *y;
*y = tmp;
}
void swap (int *& x, int *& y)
{
int * tmp = x;
x = y;
y = tmp;
}
int main ()
{
int a = 10 ;
int b = 17 ;
int * pa = &a;
int * pb = &b;
swap (&pa, &pb);
cout << *pa << " " << *pb << '\n' ;
swap (pa, pb);
cout << *pa << " " << *pb << '\n' ;
return 0 ;
}
对于链表、树等,节点定义位置,只能使用指针。因为 C++ 的引用无法改变指针指向,但是节点一定存在改变指向的情况。
#include <iostream>
using namespace std;
int func ()
{
int ret = 0 ;
return ret;
}
int main ()
{
int x = func ();
return 0 ;
}
看传值返回,func 函数返回的其实 ret 的一个拷贝(相当于临时变量),调用结束,函数销毁,看下面的 func() += 1; 就会报错。
#include <iostream>
using namespace std;
int & func ()
{
int ret = 0 ;
return ret;
}
int main ()
{
int x = func ();
cout << x << endl;
return 0 ;
}
看传引用返回,实际上函数返回的是 ret 的别名(比如 tmp)。与传值返回不同的是,函数销毁后,将空间返回操作系统(但仍然指向这块空间),如果别人对空间进行操作,就会改变,这相当于野指针的访问!很危险!!
#include <iostream>
using namespace std;
int & func1 ()
{
int ret = 0 ;
return ret;
}
int & func2 ()
{
int y = 123 ;
return y;
}
int main ()
{
int & x = func1 ();
cout << x << endl;
func2 ();
cout << x << endl;
return 0 ;
}
可以看到,我们并没有修改 x 的值,为什么再次输出 x 确是 y 的数值?
已经知道,函数销毁后,将空间返回给操作系统(但是别名 x 仍然指向这块空间),意味着这块空间可以分配给其他操作 。那么新创建的函数就会在这块空间上,又因为故意的将两个函数结构写的类似,代表二者的栈帧一样大 。
既然栈帧一样大,x 就会接收 func2 返回的别名,也就是 y 的值。
1.3 存疑的地方
传引用返回相当于野指针访问,为什么不报错?
要知道,越界不一定报错!数组的越界存在边界检查。边界检查:在临近数组结束的位置进行检查。
int main ()
{
int a[10 ];
a[10 ];
return 0 ;
}
int main ()
{
int a[10 ];
a[11 ] = 1 ;
a[15 ] = 1 ;
return 0 ;
}
1.4 const 引用
可以对 const 对象进行引用,但是必须在类型前加 const。const 引用也可以引用普通对象,因为对象的访问权限在引用过程中能够减小但是不能放大。
类似 int& rb = a*3; (表达式计算值会存到临时对象中)double d = 12.34; int& rd = d;(类型转换过程将中间值存在临时对象)C++ 规定这类对象具有常性(只读) ,也就是说需要常引用(避免权限放大) 。
临时对象:编译器需要一个空间暂存表达式的计算结果时创建的一个未命名的对象,C++ 规定为临时对象。
#include <iostream>
using namespace std;
int main ()
{
const int a = 10 ;
int b = 10 ;
const int & rb = b;
const int c = 1 ;
int rc = c;
const int * p1 = &a;
int * p2 = p1;
int e = 1 ;
int * p3 = &e;
const int * p4 = p3;
}
不能权限放大: 变量 a 被 const 修饰,代表只能读不能写,但是后面的引用仿佛在说'可以通过别名对变量进行读写'。但是本体都只能读,一个别名带还想翻天?!,这肯定是错的!(指针同理。但不是别名)
#include <iostream>
using namespace std;
int main ()
{
const int c = 1 ;
int rc = c;
return 0 ;
}
可以权限缩小: 本体 b 可以读写,对于别名 rb 只进行读是允许的。
函数传参使用 const:
以后函数传参都会使用引用,一方面是减少拷贝提高效率,另一方面则是形参会影响实参(少部分)。既然如此,传参以后建议 const 修饰,这样就可以传普通对象、const 对象、常量。
void func (const int & x)
{}
int main ()
{
int y = 0 ;
func (y);
const int z = 1 ;
func (z);
func (2 );
return 0 ;
}
解释第 2 条:
类型转换有:隐式类型转换、显式类型转换(强制类型转换)。
#include <iostream>
using namespace std;
int main ()
{
int i = 0 ;
double d = i;
int p = (int )&i;
return 0 ;
}
#include <iostream>
using namespace std;
int main ()
{
int i = 1 ;
const double rd = i;
const int & rp = (int )&i;
return 0 ;
}
这就是因为上面说的:会产生临时对象(只读属性),需要对应常引用。
1.5 引用和指针的关系(面试必看) 通常从语法层面来区分,底层层面 只是在特定情况下辅助了解 。
语法概念上,引用是变量的别名,不需要另开空间;指针存储一个变量的地址,需要额外空间。
引用在定义时必须初始化;指针建议初始化,语法上不必须。
引用只能引用一个实体,不能改变;指针可以不断改变指向对象。
指针容易出现空指针、野指针的情况;引用很少。
引用可以直接访问引用对象;指针需要解引用。
在 sizeof 中,引用结果为引用类型的大小;指针始终是地址空间所占的字节数(32 位 -4 字节,64 位 -8 字节)。
二、inline 内联函数
用 inline 修饰的函数称为内联函数,编译时 C++ 编译器会在调用函数的位置展开函数 ,这样就不需要建立栈帧,提高效率 。
我使用的是 VS 编译器,debug 版本默认不展开 inline 便于调试。若需要展开,请看下面如何设置。
inline 对于编译器只是一个建议,编译器可以选择执行与否(不同编译器也不同)。inline适用于频繁调用的小函数,对于递归函数、代码多的函数,编译器会忽略 inline 。
C 语言实现的宏函数会在预处理时展开,但实现复杂,易出错,不方便调试。C++ 设计 inline 就为了替换宏函数。
inline 函数的声明、定义不能分到两个文件 (放在同一头文件)。分离会导致内联无法展开,也找不到有效的函数定义,导致连接错误。
2.1 对要点的详细解释
解释第 2 条: 如何设置?
找到解决方案资源管理器,鼠标右键你的项目:在弹出的小界面选择属性后查看配置。
解释第 3 条: inline 只是建议
从汇编辅助理解:以简单的函数为例,得出编译器的选择应该有临界值,超过就不展开。
inline int Add (int a, int b)
{
a++;
a++;
a++;
a++;
a++;
return a + b;
}
int main ()
{
int ret = Add (1 , 2 ) * 3 ;
cout << ret << endl;
return 0 ;
}
要完全将选择权交给程序员的话,那么就会发生代码指令恶性膨胀问题,导致可执行程序(安装包)过大!!
参照上面的 5 个 a++,指令变多了,要是调用的多,都要展开,更多了。但是不展开,就是单次展开 + 次数 * 函数体,这就少多了。
为了避免问题,就将权力给了编译器。
解释第 4 条: 替代宏函数
(C++ 通过 const,enum,inline 替代宏。)
为什么 C 语言的宏函数很坑?
拿 ADD 宏函数为例:先记住宏就是替换
#include <iostream>
#define ADD(int a,int b)return a+b;
第 1 种错误:根据宏的形式,后面多 ';',在后面替换时会导致有两个分号。
#include <iostream>
using namespace std;
#define ADD(a,b) a + b
int main ()
{
int ret = ADD (1 ,2 )*3 ;
cout << ret << endl;
return 0 ;
}
第 2 种错误:替换后看似正确,期望输出 9,但是由于优先级输出 7。
#include <iostream>
#define ADD(a, b)(a + b)
int main ()
第 3 种错误(最接近正确):改进了第 2 种,确保输出 9。但在 a,b 都是表达式时,就又会发生优先级的问题。
#include <iostream>
#define ADD(a, b)((a)+(b))
从上面来看,用宏实现简单的加法函数就这么麻烦,要考虑很多,但是人有 **存在的意义:**将高频调用的小函数写成宏函数,可以提高效率,预处理阶段宏会替换,提高效率,不建立栈帧。
inline int Add (int a, int b)
{
return a + b;
}
int main ()
{
int ret = Add (1 , 2 ) * 3 ;
cout << ret << endl;
return 0 ;
}
正常定义函数,只需要前面加上关键字 inline。使得函数象宏函数一样,不会再创建栈帧。
三、宏:nullptr nullptr是一个宏,在传统的 C 头文件 stddef.h 中
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
C++ 中 NULL 可能被定义为字面常量 0 ,或者 C 中被定义为无类型指针 (void*) 的常量 。不论如何定义,在使用空值的指针时,都会遇到麻烦,本想通过 f(NULL) 调用指针版本的 f(int*) 函数,但由于 NULL 被定义成 0,调用了 f(int x),因此与程序的初衷相悖。f((void*)NULL); 调用会报错。
C++11 中引入 nullptr,nullptr 是⼀个特殊的关键字,是⼀种特殊类型的字面量,它可以转换成任意其他类型的指针类型 。使用 nullptr 定义空指针可以避免类型转换的问题,因为 nullptr只能被隐式地转换为指针类型 ,而不能被转换为整数类型。
#include <iostream>
using namespace std;
void f (int x)
{
cout << "f(int x)" << endl;
}
void f (int * ptr)
{
cout << "f(int* ptr)" << endl;
}
int main ()
{
f (0 );
f (NULL );
f ((int *)NULL );
f (nullptr );
return 0 ;
}
int * p1 = NULL ;
int * p2 = 0 ;
int * p3 = nullptr ;
f (nullptr );
总结 引用解决了指针传参的繁琐与风险,提供了更安全的别名机制;内联函数在编译时权衡空间与时间,取代了宏函数的不可预测性;nullptr 则以类型安全的方式终结了空指针的歧义。
它们共同展现了一个理念:在保持 C 语言效率同时,通过类型系统和语言机制提供更多安全保障。这正是 C++ 能够在系统编程领域保持四十年前沿地位的重要原因。