改善C++ 程序的150个建议学习之建议25:尽量用const、enum、inline替换#define

建议25:尽量用const、enum、inline替换#define
在建议4中,我们已经详细说明了在使用宏时应注意的一些问题。“表面似和善、背后一长串”绝对是对宏的形象表述。宏的使用具有一些优点:能减少代码量(比如简单字符替换重复的代码),在某种程度上提供可阅读性(比如MFC的消息映射),提高运行效率(比如没有函数调用开销)。然而谈到宏,绝对绕不开预处理器。把C/C++源码从源文件的形式变成可执行的二进制文件通常需要三个主要步骤:预处理→编译→链接。在预处理阶段,预处理器会完成宏替换。因为此过程并不在编译过程中进行,所以难以发现潜在的错误及其他代码维护问题,这会使代码变得难以分析,繁于调试。所以,宏—这个C语言中的“大明星”在C++的世界里却变成了程序员深恶痛绝的东西。因为#define 的内容不属于语言自身的范畴,所以C++设计者为我们提供了替代宏的几大利器,建议我们尽量使用编译器管制下的const、enum、inline来实现#define的几大功能。如此看来,本建议的名称换做“尽量把工作交给编译器而非预处理器”或许更合适。接下来分析一下#define的弊端,请看下面的代码片段:
#define   PI   3.1415926
在预处理阶段,预处理器就完成了代码中符号PI的全部替换,因为这个过程发生在源代码编译以前,所以编译器根本接触不到PI这个符号名,这个符号名更不会被编译器列入到符号表中。如果因为在代码中使用了这个常量PI而引起问题,那这个错误将可能变得不易察觉,难以找到问题,出错信息只会涉及3.1415926,对PI则只字未提。如果PI是在某个大家并不熟悉的或出自别人之手的头文件中定义的,那么寻找数值3.1415926的出处就如同大海捞针,费时费力。不过这一切也并非是不可避免的,解决的办法很简单,就是“使用常量来代替宏定义”:
const double PI = 3.1415926; 
作为语言层面的常量,PI肯定会被编译器看到,并且会确保其进入符号表中,也就不会出现类似“3.1415926有错误”这样模糊不清的错误信息了。当出现问题时,我们也会有章可循,可以通过符号名顺藤摸瓜,消灭错误。另外,使用常量可以避免目标码的多份复制,也就是说生成的目标代码会更小。这是由于预处理器会对目标代码中出现的所有宏 PI复制出一份3.1415926,而使用常量时只会为其分配一块内存。在使用普通常量时,有一种特殊情形会让我们感觉棘手,那就是常量指针。用const去修饰指针的方式有多种,诸如:
const char* bookName = "150 C++ Tips";
char* const bookName = "150 C++ Tips";
const char* const bookName = "150 C++ Tips";

应该使用哪一种方式确实是一个需要明确的问题。const修饰指针的规则可以简单地描述为:如果const出现在*左边,表示所指数据为常量;如果出现在*右边,表示指针自身是常量。需要注意的是,在头文件中定义常量指针时,是将指针声明为const了,而不是指针指向的数据。所以,如果定义一个指向常量字符串的常量指针,我们选择的就是最后一种,需要用两个const进行修饰。然而在定义指向常量字符串的常量指针时,用两个const修饰并不是我们推荐的形式。我们推荐使用更加安全、更加高级的const string形式:

const string bookName("150 C++ Tips");
作为C++中最重要的概念,class与很多其他的关键字都产生了联系,const也肯定不会放过纠缠这个C++主角的机会,所以就有了常量数据成员。定义常量数据成员的主要目的就是为了将常量的作用域限制在一个特定的类里,为了让限制常量最多只有一份,还必须将该常量用static进行修饰,例如:
class CStudent

private: 
static const int NUM_LESSONS = 5; //声明常量
int scores[NUM_LESSONS]; //使用常量
}; 
注意,上述注释中说的是“声明常量”,而非“定义常量”,并且在声明的同时,完成了“特殊形式”的初始化。之所以谓之“特殊形式”,是因为我们熟悉的一般形式的初始化是不允许放在声明里的。这种“特殊形式”的初始化在C++中被称为“类内初始化”。还有一点需要明确的是,在不同的编译器中对类内初始化的支持情况也不尽相同。在VC++ 2010中,并不是所有的内置类型都可以实现类内初始化,它只对整数类型(比如int、char、bool)的静态成员常量才有效。如果静态成员变量是上述类型之外的其他类型,如double型,那么需要将该类的初始化放到其实现文件该变量的定义处,如下所示:
/* VC++ 2010 */
// CMathConstants声明文件(.h)
class CMathConstants

private: 
static const double PI; 
}; 
// CMathConstants实现文件(.cpp)
const double CMathConstants::PI = 3.1415926;
而在GCC编译器中,内置的float、double类型的静态成员常量都可以采用类内初始化,如下所示:
/* Gcc 4.3 */
// CMathConstants声明文件(.h)
class CMathConstants

private: 
static const double PI = 3.1415926; 
}; 

当然,如果不习惯类内初始化,讨厌其破坏了静态成员常量声明、定义的统一形式,可以选择将类内初始化全部搬到类实现文件中去,这也是我们比较推荐的形式。更何况早期的编译器可能不接受在声明一个静态的类成员时为其赋初值,那又何必去惹这些不必要的麻烦呢?另外,如果编译器不支持类内初始化,而此时类在编译期又恰恰需要定义的成员常量值,身处如此左右为难的境地,我们该采取怎样的措施?那就求助于enum!巧用enum来解决这一问题。这一技术利用了这一点:枚举类型可以冒充整数给程序使用。代码如下所示:

// CStudent声明文件(.h)
class CStudent

private: 
enum{ NUM_LESSONS = 5 }; 
int scores[NUM_LESSONS]; 

};
需要说明的一点是,类内部的静态常量是绝对不可以使用#define来创建的,#define的世界中没有域的概念。这不仅意味着#define不能用来定义类内部的常量,同时还说明它无法为我们带来任何封装效果。
#define的另一个普遍的用法是“函数宏”,即将宏定义得和函数一样,就像建议4中的:
#define ADD( a, b ) ((a)+(b))
#define MULTIPLE( a, b ) ((a)*(b))
这样的“函数宏”会起到“空间换时间”的效果,用代码的膨胀换取函数调用开销的减少。这样的宏会带来数不清的缺点,建议4中已经说得很清晰。如果使用宏,必须为此付出精力,而这是毫无意义的。幸运的是,C++中的内联函数给我们带来了福音:使用内联函数的模板,既可以得到宏的高效,又能保证类型安全,不必为一些鸡毛蒜皮的小问题耗费宝贵的精力。 
template<typename T> 
inline T Add(const T& a, const T& b) 

Return (a+b); 

template<typename T> 
inline T Multiple(const T& a, const T& b) 

Return (a*b); 

这一模板创建了一系列的函数,方便高效,而且没有宏所带来的那些无聊问题。与此同时,由于Add和Multiple都是真实函数,它也遵循作用域和访问权的相关规则。宏在这个方面上确实是望尘莫及。
虽然建议尽量把工作交给编译器而非预处理器,而且C++也为我们提供了足以完全替代#define的新武器,但是预处理器并未完全退出历史舞台,并没有完全被抛弃。因为#include在我们的C/C++程序中依旧扮演着重要角色,头文件卫士#ifdef/#ifndef还在控制编译过程中不遗余力地给予支持。但是如果将来这些问题有了更加优秀的解决方案,那时预处理器也许就真的该退休了。
请记住:对于简单的常量,应该尽量使用const对象或枚举类型数据,避免使用#define。对于形似函数的宏,尽量使用内联函数,避免使用#define。总之一句话,尽量将工作交给编译器,而不是预处理器。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值