
◆ 博主名称: 晓此方-CSDN博客
大家好,欢迎来到晓此方的博客。
⭐️个人专栏:
◆数据结构系列
◆C语言系列
◆C++系列
⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰
目录
0.1概述&前言
C语言的宏函数和NULL空指针存在许多缺陷,在C++中,引入了inline函数和nullptr来代替他们,解决了定义不严谨等众多问题。同时也提升了编译语言的编写体验。本篇此方将从底层出发,为大家带来内联函数和nullptr的详细内容。讲解深入骨髓,细节无微不至。以真诚换真心,倾尽全力做到最好。现在,让我们开始吧。
一,内联函数
1.1内联函数的定义
用inline修饰 的函数叫做内联函数 ,编译时C++编译器会在调用的地方展开内联函数 ,这样调用内联函数就不需要建立栈帧 了,就可以提高效率。
内联函数的出现,直接取代了C语言中的宏函数。
1.2宏函数的问题所在
1.2.1直接文本替换
宏函数编译采用直接文本替换 ,**缺乏类型检查,**存在潜在的安全问题
cpp
// 实现一个ADD宏函数的常见问题
//#define ADD(int a, int b) return a + b;
//#define ADD(a, b) a + b;
//#define ADD(a, b) (a + b)
// 正确的宏实现
#define ADD(a, b) ((a) + (b))
- 为什么宏函数不能加分号?
- 为什么要有外括号?
- 为什么要有内括号?
cpp
printf("%d",Add(1,2));
1.2.1.1情况一
宏函数在使用时会进行宏替换 ,将宏名 部分全部替换为宏体,如果宏体有分号,会被一块儿替换进去,产生编译错误。
cpp
cout <<Add(1,2)*5<<endl;
1.2.1.2情况二
优先级错误一:此时宏替换,如果没有外扩号,就会变成(a)+(b)*5,先计算5*b,再计算加减。导致结果错误。
cpp
Add(x&y,x|y);
1.2.1.3情况三
优先级错误二:此时宏替换,如果没有内括号,就会变成x&(y+x)|y,先加减,再与或,导致计算结果错误。
1.2.2无法调试
宏函数不是一个真正意义上的函数,在预处理阶段 就会直接进行文本替换,而调试器看到的是预处理后的代码 。此外,即使在宏函数所在行设置断点也无意义,调试器无法在断点处找到一个有效的函数
1.3如何进行内联展开
1.3.1内联函数的使用方式
直接在一个函数前面加上inline关键字即可。
cpp
inline Add(int x,int y)
{
int z = x + y ;
return z ;
}
1.3.2准备工作
vs编译器debug版本 下面默认是不展开 inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方。
在项目 处右击进入项目属性:

这样,你的编译器就可以将内联函数展开以提升运行效率了。
1.4内联展开的底层剖析
inline对于编译器而言只是一个建议 ,也就是说,你加了inline编译器也可以选择 在调用的地方不展开 ,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定 这个。inline适用于频繁调用的短小函数 ,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。
关键点: inline是对编译器的"建议 ",不是强制命令!
1.4.1编译器是否展开内联函数
1.4.1.1编译器没有展开内联函数时:
cpp
inline int Add(int a, int b)
{
int ret = a + b;
ret++;
ret++;
ret++;
ret++;
ret++;
ret++;
ret++;
ret++;
ret++;
return ret;
}
int main()
{
cout << Add(4, 9)<<endl;
return 0;
}
- 使用了
call指令 → 表示跳转到函数地址执行。 - 执行完后返回。
- 这说明:虽然写了
inline,但编译器没展开,而是当作普通函数调用。

详细调用过程:
- RCX <- 4 (第一个参数),RCX<- 9 (第二个参数)
- 将call指令 下一条指令的地址(即main函数中call之后的下一行代码地址)压入当前线程的栈顶。
- 跳转到jmp指令 ,jmp指令 又跳转到函数的第一条指令,调用完成。
1.4.1.2编译器展开内联函数时:
如果编译器决定展开,则不会出现 call ,而是在调用位置直接插入函数体的代码。
cpp
inline int Add(int a, int b)
{
int ret = a + b;
ret++;
ret++;
ret++;
ret++;
return ret;
}
int main()
{
cout << Add(4, 9)<<endl;
return 0;
}

1.5内联函数的优劣
1.5.1展开的好处
根据上文从底层的剖析,我们知道,展开可以省去函数调用等一系列复杂操作 。从而提升函数的运行效率。虽然展开同时会影响编译速度,但是"对函数运行效率的提升"对比"对函数编译速度的下降"的影响始终要大。
1.5.2展开的代价
展开来提升效率固然不错。但是展开也不是绝对好。
编译器并不会 无条件展开,言听计从。
设想一个场景 : 如果一个内联函数在项目中存在10000个调用点 ,该内联函数本身有100条语句 ,每条语句的大小大概在2 到 10 字节(假设6字节) ,那么全部展开将增加100万条语句 ------也就是600万字节 ,相当于:**5.722 MB,**一个内联函数让一个安装包平白无故增加6MB的空间,显然是不值得的。
但是合理使用可以将内联函数的缺点缩小到极小(可忽略不计)。编译器会帮助你合理使用。
1.5.3声明与定义分离
内联函数不可以进行声明和定义分离。如果分离,内联函数的调用点将找不到内联函数本体。

原理:由于C++的一些规定,内联函数无法像普通函数那样被使用函数的声明发现。
或者将定义直接放在调用点所在文件,或者将内联函数定义连同声明一同放在头文件中。
二,nullptr(c++11新增内容)
传统C语言头文件stddef.h中,可以看到如下代码:
cpp
1 #ifndef NULL
2 #ifdef __cplusplus
3 #define NULL 0
4 #else
5 #define NULL ((void *)0)
6 #endif
7 #endif
解释:
- 在 C++ 中:NULL被定义为 0(整数常量)
- 在 C 中:NULL被定义为(void*)0,即一个指向空的void*指针.
但是这个设计不是很ok。c语言中还好,c++中有很大的坑 ,所以C++不使用NULL,取而代之的是nullptr ,意思是空指针 。
2.1NULL的坑
cpp
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
return 0;
}
猜猜看打印结果是什么?结果是。
cpp
f(int x)
f(int x)
由于以上的这种定义特性 ,C++中的NULL无法表示实际的空指针 。那么**强转成void***不就可以了?还是不行:

C++有比C语言更加严格的类型检查,俩个都不匹配,这个时候就要进行隐式类型转换。但是不知道转成什么。
那么C语言为什么可以这么定义?实际上C语言和c++的这种定义确实都是不好的。C语言的定义中,C语言自己这么定义问题不大。因为C语言定义允许void*类型的指针给任意类型的其他指针不需要强转 (这让C语言malloc之后完全可以不需要强转直接给,因为malloc结果是void*类型的指针),但是C++不允许 (C++兼容C语言的绝大多数语法,但是也会有像这样的小差异),如以下代码必须强制类型转换才能生效:
cpp
void* p1 = NULL;
int* p2 = p1;
C++11中引入nullptr,nullptr是一个特殊的关键字 ,nullptr是一种特殊类型 的字面量,它可以转换成任意其他类型的指针类型 。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能 被隐式地转换为指针类型 ,而不能被转换为整数类型。
nullptr也是0 ,但是对比NULL更好,解决了以上的特殊情况。**在C++以后都用nullptr!**C语言还是原来那套没问题。
以上就是本篇的全部内容了,C++的全部基础内容到此为止 。此刻,你我正式步入了C++的世界。如果本篇对你有帮助,不要忘了点赞+投币+收藏三联一波哦!我是此方,我们下期再见。