inline 与 nullptr:从宏的坑,到符号表的真相
我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!
C++ 入门的收尾两块:inline 和 nullptr。它们有个共同点------都是给 C 时代的老机制打补丁。inline 补的是宏函数,nullptr 补的是 NULL。而 inline 这块真正的看点,是把之前欠下的"符号表"账算清。
一、宏函数到底难写在哪
先看一个 ADD 宏的几种写法:
c
//#define ADD(int a, int b) return a + b; // 直接错,宏不是函数
//#define ADD(a, b) (a + b) // 少了外括号,有坑
#define ADD(a, b) ((a) + (b)) // 正确写法
宏不是函数,是预处理阶段的纯文本替换。想写对一个宏,括号一个都不能少:
- 外括号 :防运算符优先级。
ADD(1, 2) * 3若展开成1 + 2 * 3就全错了,所以整体必须套括号; - 内括号 :防参数里的表达式。
ADD(1 | 2, a & b)展开时+优先级高于|和&,参数不加括号照样错; - 甚至还有分号的坑:宏后面跟
;会展开成空语句。
宏的账面评价:优点是预处理阶段替换,不用建立栈帧,本质是一种提效;缺点是复杂、容易写错、没有类型安全检查、不能调试。为了一个加法要裹三层括号,这活没法干------C++ 的 inline 就是来接替它的。
二、inline:有条件的提效
inline 的思路跟宏一样:不建栈帧,直接在调用处展开。但它是有类型检查、能调试的真函数。
但不能无脑用。函数调用太多次、每次都原地展开,会有代价:
- 可执行程序变大------每处调用都复制一份代码;
- 展开太多反而效率降低。
所以 inline 的适用标准一句话:短小且频繁调用的函数,才用 inline。递归函数、长函数,编译器多半也不会真的展开它(inline 对编译器只是建议,不是命令)。
三、重头戏:inline 为什么不能声明与定义分离
这条规则是全篇最有味道的部分,它逼着你把之前欠的"符号表"账补上。
先回忆普通函数怎么跨文件工作的(预处理→编译→汇编→链接四步走):
F.h F.cpp Test.cpp
↓ 预处理
F.i Test.i // 头文件展开
↓ 编译(检查语法、生成汇编代码)
F.s Test.s
↓ 汇编(转二进制机器码)
F.o Test.o // 各自形成符号表
↓ 链接(合并符号表,找地址)
xxx.exe
普通函数的流程:汇编代码需要函数地址来 call。本文件里如果只有声明没有定义,编译器先留着(有声明才能去查表,没有声明它连表都不去查);等链接时所有 .o 的全局域合并成一张大符号表,去表里找到函数地址,call 过去。
inline 函数呢?它的特性是直接在调用处展开,压根不需要函数地址来 call------汇编指令里就没有 call 指令。所以:
inline 函数不会进入符号表。
于是声明定义分离这条路直接堵死:链接器拿着"找函数地址"这个任务去查表,inline 根本不在表里------找不到就是硬错误,连"未定义引用"的挣扎机会都没有。所以 inline 函数的定义必须跟声明绑在一起,通常直接写在头文件里。
顺带把 static 的账也算了:static 函数同样不进符号表(编译期就不让它进小表,链接时自然不进大表),所以其他文件查表也找不到它------这就是头文件里放 static 函数定义不会重定义的原因。三个角色一张表看清:
| 角色 | 需要函数地址 call 吗 | 进符号表吗 | 声明定义分离 |
|---|---|---|---|
| 普通函数 | 需要 | 进 | 可以 |
| inline 函数 | 不需要(编译期原地展开) | 不进 | 不行,找不到就是错 |
| static 函数 | 需要 | 不进 | 可以,但仅限本文件 |
四、nullptr:NULL 在 C++ 里水土不服
NULL 不是关键字,是个宏,stddef.h 里的真身:
c
#ifndef NULL
#ifdef __cplusplus
#define NULL 0 // C++ 里 NULL 就是 0
#else
#define NULL ((void *)0) // C 里 NULL 是空指针
#endif
#endif
为什么 C++ 不肯用 (void*)0?因为 C++ 的类型检查比 C 严:C 语言允许 void* 隐式转换成任意指针类型,C++ 不允许。那 NULL 在 C++ 里只能定义为 0------这就出事了:
cpp
void f(int x) { cout << "f(int)" << endl; }
void f(int* p) { cout << "f(int*)" << endl; }
f(NULL); // 调的是 f(int)!NULL 是 0,被当成整数
f(nullptr); // 调的是 f(int*)
NULL 本意是给空指针用的,结果在重载解析时被匹配成了 int------语义完全拧了。
C++11 引入 nullptr:它是一个特殊类型的字面量,只能被隐式转换为指针类型,不能转换为整数类型 。重载解析永远站在指针那边,f(nullptr) 稳稳落在 f(int*) 上。
一句话收尾:C++ 里给指针置空,写 nullptr,别再写 NULL------旧习惯该忘的就得忘。
inline 补宏的坑(类型检查 + 可调试),nullptr 补 NULL 的坑(类型精确)。两个补丁背后是同一件事:C++ 把类型系统收紧了。下一篇开始进真正的主场------类和对象,那是 C++ 语法层最大的范式转变。