33. 用 const、enum、inline 代替 #define
文章目录
- 引言
- 一、用 `const` 代替 `#define` 定义常量
- 1.1 const 变量是"真正的变量"——有地址、有类型、能调试
- 1.2 类内常量——static const 成员
- 1.3 C++17 inline 变量——头文件中的常量定义
- 1.4 constexpr——编译期常量
- 二、用 `enum` 代替 `#define` 定义整型常量集合
- 2.1 枚举比宏更有语义
- 2.2 enum hack——类内需要编译期整数时的传统方案
- 三、用 `inline` 函数代替宏函数
- 3.1 宏函数的致命问题
- 3.2 为什么是 `inline`
- 3.3 用模板 inline 函数替代所有宏函数
- 四、宏在 C++ 中仍然合法的场景
- 4.1 包含头文件保护
- 4.2 条件编译
- 4.3 字符串化(#)和连接(##)
- 4.4 断言和源码位置
- 总结
本系列为《C++深度修炼:基础、STL源码与多线程实战》第33篇,对应 Effective C++ 条款1-2
前置条件:理解 C 语言的#define宏,了解 const(第8篇)和引用(第9篇)
引言
C 程序员迁移到 C++ 时,第一个"文化冲击"往往来自预处理器的角色变化。在 C 里,#define几乎是常数定义和宏函数的唯一选择。C++ 则提供了三种更好的替代品——const、enum和inline函数——它们不仅类型安全,而且能被调试器看到,还能遵守作用域规则。
// C 语言的日常——到处都是 #define#definePI3.14159#defineMAX(a,b)((a)>(b)?(a):(b))#defineBUFFER_SIZE1024问题是什么?#define在编译之前就被预处理器替换掉了——编译器看到的是字面值,调试器也看不到符号名。错误信息里出现的是3.14159而不是PI,1024而不是BUFFER_SIZE。当你在一个大型项目里看到编译器报错说1024越界,你怎么知道是哪个1024出了问题?
一、用const代替#define定义常量
1.1 const 变量是"真正的变量"——有地址、有类型、能调试
// C 方式——预处理器在编译前执行文本替换#definePI3.14159// C++ 方式——这是一个真正的常量,编译器认识它,调试器也认识它constdoublePi=3.14159;区别:
#define PI→ 预处理器在编译器看到代码之前就把PI换成了3.14159。调试器不知道PI的存在。const double Pi→ 编译器产生一个只读变量,有类型、有地址(如果你取地址)、能被调试器显示。
1.2 类内常量——static const 成员
// 头文件中:classCircle{staticconstdoublePi;// 声明doubleradius_;public:doublearea()const{returnPi*radius_*radius_;}};// .cpp 文件中:constdoubleCircle::Pi=3.14159;// 定义(C++17 起可以用 inline 变量放在头文件)对于整数类型的类内常量,可以在类内直接初始化:
classBuffer{staticconstintDefaultSize=1024;// 整数常量可以在类内初始化chardata_[DefaultSize];};1.3 C++17 inline 变量——头文件中的常量定义
// C++17 起——在头文件中用 inline 定义常量,不需要 .cpp// my_constants.h#pragmaonceinlineconstexprdoublePi=3.141592653589793;inlineconstexprintBufferSize=4096;inlineconstexprconstchar*AppName="MyApp";1.4 constexpr——编译期常量
// const:值在运行时不可变(但可能在运行时才确定)constintsize=get_size();// 运行时初始化// constexpr:值在编译期就确定——可以用在数组大小、模板参数等编译期上下文中constexprintMaxConnections=1000;intconnections[MaxConnections];// ✅ constexpr 可以当数组大小// constexpr 函数——编译期计算constexprdoublecircle_area(doubler){returnPi*r*r;}constexprdoublearea=circle_area(5.0);// 编译期计算,不产生运行时开销二、用enum代替#define定义整型常量集合
2.1 枚举比宏更有语义
// C 方式——散落的宏没有任何关联#defineCOLOR_RED0#defineCOLOR_GREEN1#defineCOLOR_BLUE2// C++ 方式——这些值属于同一个"颜色"类型enumclassColor{Red,Green,Blue};// C++11 scoped enum——不会污染命名空间enumDirection{North,East,South,West};// 传统 enumenum class比传统enum更好的原因:
- 不会隐式转换为
int(防止Color::Red + 5这种无意义的操作) - 枚举值在枚举名的作用域内——
Color::Red而不是Red(避免命名冲突)
2.2 enum hack——类内需要编译期整数时的传统方案
classPlayer{// 传统方式——class 内不能对非整数 static const 成员初始化// static const int MaxLevel = 100; // 这个其实可以,但有时候需要取地址时才定义// enum hack——保证值是编译期常量,不会占用对象内存enum{MaxLevel=100};intlevels_[MaxLevel];// 用 enum 值作为数组大小};C++11 起,static constexpr已经取代了 enum hack:
classPlayer{staticconstexprintMaxLevel=100;intlevels_[MaxLevel];};三、用inline函数代替宏函数
3.1 宏函数的致命问题
// C 方式——宏函数#defineMAX(a,b)((a)>(b)?(a):(b))intmain(){intx=5,y=10;intm1=MAX(++x,++y);// 展开后:((++x) > (++y) ? (++x) : (++y))// ++x → 6, ++y → 11, 6 > 11 为 false// 所以执行 : 后面的 ++y → 12// x 被递增了 1 次,y 被递增了 2 次——结果完全不可预测printf("m1=%d, x=%d, y=%d\n",m1,x,y);// m1=12, x=6, y=12——诡异}C++ 的解决方案——模板 inline 函数:
// C++ 方式——类型安全,参数只被求值一次template<typenameT>inlineTmax(constT&a,constT&b){returna>b?a:b;}intmain(){intx=5,y=10;intm=max(++x,++y);// ++x → 6, ++y → 11, 比较: 6 > 11? 返回 11// x=6, y=11, m=11——每个参数恰好求值一次,和预期一致}3.2 为什么是inline
inline告诉编译器"请在调用点展开这个函数(像宏一样)"。但和宏不同:
inline只是一个建议——编译器可以选择不展开inline函数有完整的函数语义——参数求值、类型检查、作用域规则都和普通函数一样inline函数定义通常放在头文件中——每个翻译单元都能看到定义
3.3 用模板 inline 函数替代所有宏函数
// 所有可以用宏函数实现的,都可以用模板 inline 函数实现——且更安全template<typenameT>inlineTmin(constT&a,constT&b){returna<b?a:b;}template<typenameT>inlineTclamp(constT&v,constT&lo,constT&hi){returnv<lo?lo:(hi<v?hi:v);}template<typenameT>inlinevoidswap(T&a,T&b){T t=a;a=b;b=t;}四、宏在 C++ 中仍然合法的场景
尽管宏大多可以被替代,但在某些场景下宏仍然是必要的:
4.1 包含头文件保护
// 现代方式——#pragma once(大多数编译器支持)#pragmaonce// 传统方式——#ifndef / #define / #endif(标准保证)#ifndefMY_HEADER_H#defineMY_HEADER_H// ...#endif4.2 条件编译
#ifdef_DEBUG#defineLOG(msg)std::cerr<<"[DEBUG] "<<msg<<'\n'#else#defineLOG(msg)// 发布版本——零开销#endif4.3 字符串化(#)和连接(##)
#defineTO_STRING(x)#x// 把参数变成字符串#defineCONCAT(a,b)a##b// 连接两个符号std::cout<<TO_STRING(hello)<<'\n';// "hello"intCONCAT(my,Var)=42;// int myVar = 42;4.4 断言和源码位置
// __FILE__ 和 __LINE__ 是宏——没有其他办法获取当前源码位置#defineASSERT(cond)\if(!(cond)){\std::cerr<<"Assertion failed: "#cond\<<" at "<<__FILE__<<":"<<__LINE__<<'\n';\std::abort();\}总结
Effective C++ 条款 1-2 的核心思想:尽量让编译器和链接器代替预处理器来工作——因为编译器能给你类型安全、作用域规则、调试信息和可预测的行为:
- 用
const(或constexpr)代替#define常量——常量的类型被编译器看到,调试器能显示符号名,而且有作用域 - 用
enum class代替#define枚举值——枚举值有类型,不会隐式转换,不会污染命名空间 - 用模板
inline函数代替宏函数——参数只求值一次,类型安全,遵守作用域规则,能被调试器进入 - 宏只在编译器无法替代的场景使用——条件编译、字符串化、
__FILE__/__LINE__、头文件保护 - C++17 的
inline constexpr变量让你在头文件中定义常量而不需要 .cpp 定义——彻底消灭了为常量写 .cpp 文件的麻烦
动手练习:
- 找一段你以前写的 C 代码,把里面的
#define常量和宏函数全部分别替换为constexpr变量和模板inline函数——编译对比结果是否正确,代码行数是否增加- 写一个
enum class来替代一组相关的#define常量——然后尝试将枚举值赋值给int(你会看到编译错误,这就是类型安全)- 用宏写一个
SQUARE(x)函数——然后故意传x++进去,观察副作用。对比有/没有括号的宏的展开差异- 写一个
constexpr函数计算斐波那契数列——验证在编译期调用和运行期调用得到相同结果- 用
#ifdef写一个跨平台的头文件——在 Windows 和 Linux 上选择不同的 API 函数
