一、引言
由于时间关系,上次我们只讨论了C++对C语言的部分改进。今天,让我们继续深入了解这位编程伙伴的其他特性。
二、inline
在学习inline之前,让我们先来回顾一下C语言里面的宏函数,因为这可是C++的祖师爷为了补宏函数的坑设计的。
下面我们来通过代码看看宏函数的坑和正确写法:
cpp
#include<iostream>
using namespace std;
// 实现⼀个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))
// 为什么不能加分号?
// 为什么要加外⾯的括号?
// 为什么要加⾥⾯的括号?
int main()
{
int ret = ADD(1, 2);
cout << ADD(1, 2) << endl;
cout << ADD(1, 2)*5 << endl;
int x = 1, y = 2;
ADD(x & y, x | y); // -> (x&y+x|y)
return 0;
}
思考一下我提的三个为什么,平常宏替换是将函数名替换成后面的内容,第一如果加了分号,那cout <<ADD(1,2)<<endl; 不就等于cout <<ADD(1,2);<<endl;了吗;第二个如果不加外面外面的括号,那么大家看看ADD(1,2)*5,宏替换后是否还满足我们所需要的结果;第三个我们不加里面的括号,ADD(x&y,x|y)是否就变成了x&(y+x)|y,因为加法的运算符等级更高。
让我们仔细思考这三个问题:
-
如果宏定义末尾加了分号,那么
cout << ADD(1,2) << endl;展开后就变成了cout << ADD(1,2); << endl;,这显然会导致语法错误。 -
如果不给宏定义加外层括号,例如
ADD(1,2)*5展开后就会变成1+2*5,由于运算符优先级问题,结果将不符合预期。 -
如果宏参数不加括号,比如
ADD(x&y,x|y)展开后会变成x&y + x|y,由于加法运算符优先级高于位运算,实际运算顺序会变成x&(y + x)|y,这将导致计算结果错误。
所以C++在设计的时候就发明了inline去避免宏函数造成的这么坑。
inline:
• 使用
inline修饰的函数称为内联函数。在编译时,C++编译器会将内联函数在调用处直接展开,从而避免函数调用时的栈帧开销,提升执行效率。•
inline关键字对编译器而言仅是建议性指令,编译器有权决定是否真正展开。不同编译器对内联展开的处理策略各异,因为C++标准未对此作强制规定。内联机制最适合短小且频繁调用的函数,而递归函数或较长的函数即使声明为内联也通常会被编译器忽略。• C语言通过宏函数实现类似功能(预处理阶段展开),但宏函数编写复杂、易出错且难以调试。C++引入内联函数正是为了取代C的宏函数方案。
• 内联函数不建议将声明与定义分离到不同文件,否则会导致链接错误。由于内联展开后函数地址消失,链接阶段将无法正确解析引用。
额外告诉大家一个看到内联函数是否展开的小妙招哦:
cpp
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
return ret;
}
int main()
{
// 可以通过汇编观察程序是否展开
// 有call Add语句就是没有展开,没有就是展开了
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
return 0;
}
三、nullptr
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL
#else
#define NULL
#endif
#endif
• 在C++中,NULL可能被定义为字面量0,或在C中被定义为无类型指针(void*)的常量。无论采用哪种定义,使用空指针时都可能出现问题。例如,调用f(NULL)本意是想调用指针版本的f(int*),但由于NULL被定义为0,实际调用了f(int),导致与预期不符。而f((void*)NULL)调用则会报错。
• C++11引入的nullptr是一个特殊关键字,属于特定类型的字面量。它可以隐式转换为任意指针类型,但不会转换为整数类型。使用nullptr定义空指针能有效避免类型转换问题,确保代码意图的准确表达
cpp
#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被定义成0,调⽤了f(int x),因此与程序的初衷相悖。
f(NULL); //结果为f(int x),与f(0)一样
//f((int*)NULL);
// 编译报错:error C2665: "f": 2 个重载中没有⼀个可以转换所有参数类型
// f((void*)NULL);
f(nullptr);
return 0;
}
四、总结
总的来说,C++ 这两个新特性,其实都是在给 C 语言的老问题 "打补丁"------ 既保留了 C 语言的高效,又把那些容易踩的坑给填上了。
先说说 inline 内联函数,它就是宏函数的 "升级版"。C 语言的宏函数看着好用,不用函数调用的开销,但写的时候得小心翼翼加各种括号,稍不注意就因为运算符优先级搞出 bug,还没法调试。而 inline 就省心多了,写起来和普通函数一样,编译器会帮我们在调用的地方直接展开,既快又安全,还能支持类型检查和重载。不过也别随便用,要是函数体太长或者有递归,编译器根本不会给你内联,反而白忙活一场。
再看 nullptr,它就是来解决 NULL 的尴尬的。C 语言里 NULL 本质上就是 0,在 C++ 里写函数重载的时候,想传个空指针调用指针版本的函数,结果 NULL 被当成整数,直接调用了 int 版本的,完全违背初衷。而 nullptr 就专门用来表示空指针,只能传给指针类型,再也不会搞混,代码意图也更清晰。现在写 C++ 代码,建议直接把 NULL 换成 nullptr,尤其是用重载和智能指针的时候,能少踩很多坑。
如果大家在 inline 或者 nullptr 上遇到具体问题,比如不知道怎么判断内联有没有生效,或者在老项目里替换 NULL 怕出问题,欢迎在评论区一起讨论~ 编程就是这样,把细节里的坑都踩明白,技术才能慢慢提升,咱们继续在 C++ 的世界里探索吧!其实从宏函数到 inline从 NULL 到 nullptr,能看出来 C++ 的设计思路 ------ 不是要把 C 语言推翻重来,而是在它的基础上,让我们写代码的时候更省心、更安全。