条款02:尽量以 const、enum、inline 代替 #define
在定好基调之后的,第一个正文条目,就是对C语言的预编译器机制开刀,庖丁解牛,找到第一个坑点,即#define,对此要进行逐步理解:
a、了解 预编译机制;
b、了解 #define 和它的坑;
c、理解跳过坑的方式;
一、预编译机制
预处理,顾名思义,是发生在实际编译前的处理。是编译过程的预备阶段,在这一阶段,预处理器(Preprocessor)对源代码文件进行初步的文本处理。其目标就是整理源代码到最适合编译器处理的状态。
预处理指令通常以#开头,例如#include、#define、#if等。
其工作流程通常分为几个主要步骤:宏定义替换、文件包含处理、条件编译、移除注释、添加行号和文件名信息 和 生成预处理后的源代码 等。
#define 就是宏定义替换的一部分。
二、#define和它的坑
#define是C语言中就提供了的宏定义命令,目的是:
为了程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率
但其 复杂易错 和 不易维护 的恶名,也是c++开发者们众所周知的。
1、坑一:常量
举个例子,我们涉及算圆相关的算法经常会与圆周率(π)打交道,这个数值就很有意思,无限不循环的圆周率,我们不可能无底线的用,一般会把其与 3.1415 画等号。
#define PI 3.1415
因此,针对这个圆周率,会有两个要求:
a、会有多个地方要涉及这个 3.1415 值,如果当 3.1415 不满足精度的时候,例如3.1415 变成 3.1415926时,就需要一次改动,全部都变;
b、圆周率这样的值,算是一种不变的标准,一般是不希望在程序运行的时候发生改变的,程序运行到算法A部分圆周率为3.1415,运行到算法b部分,圆周率变成3.14,就很容易出现错误;
因此,c语言提供了的宏定义(#define),满足了以上的需求。
至于#define的坑,原书中举的例子就已经明确了,简单来说,#define定义的常量,在代码还未送到编译器手中就已经在源代码中变成一个它指代的对象了,当出现错误的时候,编译器只会报指代的对象的错误,甚至不报具体错误,让回溯根源变得很困难无比。
PS:初读没有感受,直到多人合作的项目查错的时候,一个人一杯茶一个晚上,一段令人印象深刻的痛苦经历。
2、坑二:宏函数
宏函数,又名函数式宏,相比较普通函数会更加灵活,传入参数更 “ 宽容 ”,而且还能避免普通函数中间过程的开销;
通过例子来看一下:
这是普通函数,根据传入参数,要写多个,要明确传入参数。
int sqr_int(int x)
{
return x*x;
}
double sqr_double(double x)
{
return x*x;
}
这是宏函数,定义一个,传入的参数可以是多种类型,其本质是把你写 sqr(x) 的地方在预编译阶段替换为为 ((x)(x)) ,这时候并没有发生计算,编译阶段会按照 ((x)(x)) 计算,而这一过程就叫做宏展开。
#define sqr(x) ((x)*(x))
对比一下,看上去好简单,好方便,就像魔法,但问题来了,代价是什么呢?
int main() {
int n1 = 1;
int n2 = 1;
std::cout <<"1的平方(普通函数)是:" << sqr_int(n1++) << std::endl;
std::cout <<"1的平方(宏函数)是:" << sqr(n2++) << std::endl;
return 0;
}
输出结果如下:
1的平方(普通函数)是:1
1的平方(宏函数)是:2
出现这样的结果其原因是 sqr(n2++) 在预编译阶段被替换成了 ( n2++ )* ( n2++ ) ,编译的时候就变成就是(1 * 2),这样就与普通函数早就的效果不同了,这种生硬的替换(展开),在宏定义应用经验不丰富的人手里,简直就是噩梦。
3、跳过坑的方式
对于以上两种坑,原书可以总结为两个简单规则:
a、对于单纯常量,最好类外用 const变量,类内用 enums
b、 对于形似函数的宏,最好改用inline函数,也可用template
1、对于单纯常量,可以用 const变量 和 enums
const float PI = 3.1415;
const变量,就是把变量变得不可修改,而且尽量把这个定义放在实现文件内,例如 .cpp 文件。
enum { ArrayNum=5 };
int array[ArrayNum];
而用 enums 的原因是,有的旧编译器是不支持变量作为数组的长度使用的。
2、对于形似函数的宏,最好改用inline函数,也可用template
相较于常量,形似函数的宏的也是一样要分析情况进行选择,为了避免普通函数中间过程的开销(即大量反复被调用)就采用inline函数,为了传入参数更加灵活,就可以用template;
inline int funName (int Value1, int Value2, ...) { /** 函数体 **/ };
template <typename T>
void funName (const T & a, const T & b)
{
f(a>b?a:b);
}
甚至于,我还在网上发现了内联函数模板,虽然,我没用过,尴尬。
template <typename T>
inline void funName (const T & a, const T & b)
{
f(a>b?a:b);
}
总结:老手视情况,新手就尽量别用 #define。