引言
C++的演进之路,是不断在性能与安全、灵活与严谨之间寻求平衡的艺术。
本文将深入剖析三大特性:'引用'、'内联函数'、'nullptr'。
理解它们,不仅是掌握语法,更是洞察C++设计哲学,书写更高效、更健壮代码的关键一步。
C++ 引用作为变量别名无需额外内存,常用于传参减少拷贝;内联函数由编译器在调用处展开以消除函数调用开销,优于宏定义;nullptr 关键字提供类型安全的空指针表示,解决 NULL 重载歧义问题。掌握这三项特性有助于编写高效且健壮的 C++ 代码。

C++的演进之路,是不断在性能与安全、灵活与严谨之间寻求平衡的艺术。
本文将深入剖析三大特性:'引用'、'内联函数'、'nullptr'。
理解它们,不仅是掌握语法,更是洞察C++设计哲学,书写更高效、更健壮代码的关键一步。
引用不是重新定义变量,而是给已经定义的变量起一个别名(编译器不会为引用变量开辟内存空间,共用同一块)。
形式:类型& 引用别名 = 引用对象;
C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如
<<,>>,引用也和取地址使用了同一个符号&,注意使用方法角度来区分。
#include <iostream>
using namespace std;
int main() {
int i = 10;
// 引用:j是i的别名
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;
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;
}
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() {
// swap函数交换指针
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();
// func() += 1; // 报错
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的值。
int main() {
int a[10];
// 越界读:没事
a[10];
return 0;
}
int main() {
int a[10];
// 越界写:有事
a[11] = 1;
a[15] = 1;
return 0;
}
const对象进行引用,但是必须在类型前加const。const引用也可以引用普通对象,因为对象的访问权限在引用过程中能够减小但是不能放大。int& rb = a*3; (表达式计算值会存到临时对象中)double d = 12.34; int& rd = d;(类型转换过程将中间值存在临时对象)C++规定这类对象具有常性(只读),也就是说需要常引用(避免权限放大)。#include <iostream>
using namespace std;
int main() {
// const对象
const int a = 10;
// 权限放大:不能
int& ra = a;
return 0;
// 权限缩小:可以
int b = 10;
const int& rb = b;
// 不属于权限放大
const int c = 1;
int rc = c;
// 权限放大缩小,对const 指针&引用
// 权限放大
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() {
// const int& a = 10;
int y = 0;
func(y);
const int z = 1;
func(z);
func(2);
return 0;
}
类型转换有:隐式类型转换、显式类型转换(强制类型转换)。
#include <iostream>
using namespace std;
int main() {
// C语言隐式转换
int i = 0;
double d = i;
// 强制转换:整型、指针
int p = (int)&i;
return 0;
}
**对于引用的类型转换:**必须用常引用!
#include <iostream>
using namespace std;
int main() {
int i = 1;
// double& ri = i; // 不行
const double rd = i; // 可以
// int& pi = (int)&i; // 不行
const int& rp = (int)&i; // 可以
return 0;
}
这就是因为上面说的:会产生临时对象(只读属性),需要对应常引用。
通常从语法层面来区分,底层层面只是在特定情况下辅助了解。
sizeof中,引用结果为引用类型的大小;指针始终是地址空间所占的字节数(32位--4字节,64位--8字节)。inline修饰的函数称为内联函数,编译时C++编译器会在调用函数的位置展开函数,这样就不需要建立栈帧,提高效率。debug版本默认不展开inline便于调试。若需要展开,请看下面如何设置。inline 对于编译器只是一个建议,编译器可以选择执行与否(不同编译器也不同)。inline适用于频繁调用的小函数,对于递归函数、代码多的函数,编译器会忽略inline。inline就为了替换宏函数。inline函数的声明、定义不能分到两个文件(放在同一头文件)。分离会导致内联无法展开,也找不到有效的函数定义,导致连接错误。找到解决方案资源管理器,鼠标右键你的项目:(红框框)
在弹出的小界面选择属性后:看图示
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++,选择展开:
10个a++就不展开了:看框框,call就代表调用函数,创建栈帧
划重点,划重点:面试问题:为什么只是'建议'??!
要完全将选择权交给程序员的话,那么就会发生代码指令恶行膨胀问题,导致可执行程序(安装包)过大!!
参照上后面的5个a++,指令变多了,要是调用的多,局都要展开,更多了。但是不展开,就是单次展开 + 次数 * 函数体,这就少多了。
为了避免问题,就将权力给了编译器。
(C++通过const,enum,inline替代宏。)
为什么C语言的宏函数很坑?
拿ADD宏函数为例:先记住宏就是替换
// ADD宏函数:第1种
#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))
第4种:考虑了参数是表达式的问题,最终正确。
从上面来看,用宏实现简单的加法函数就这么麻烦,要考虑很多,但是人有 **存在的意义:**将高频调用的小函数写成宏函数,可以提高效率,预处理阶段宏会替换,提高效率,不建立栈帧。
inline内联函数代替宏函数: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是一个宏,在传统的C头文件stddef.h中
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论如何定义,在使用空值的指针时,都会遇到麻烦,本想通过f(NULL)调用指针版本的f(int*)函数,但由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。f((void*)NULL);调用会报错。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(int x)
f(NULL); // 想调用第2个函数,但是NULL被定义为0/(void*)0,导致调用了第1个函数
f((int*)NULL); // 调用 f(int* ptr)
// f((void*)NULL); // void*不能隐式转换为int*,需要额外的类型转换
f(nullptr); // 调用 f(int* ptr)
return 0;
}
// 不要这样写
int* p1 = NULL;
int* p2 = 0;
// 应该这样写
int* p3 = nullptr;
f(nullptr); // 明确调用指针版本的重载函数
引用解决了指针传参的繁琐与风险,提供了更安全的别名机制;内联函数在编译时权衡空间与时间,取代了宏函数的不可预测性;
nullptr则以类型安全的方式终结了空指针的歧义。
它们共同展现了一个理念:在保持C语言效率同时,通过类型系统和语言机制提供更多安全保障。这正是C++能够在系统编程领域保持四十年前沿地位的重要原因。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online