避免使用宏

本文探讨了C++中宏的问题及替代方法,如const、enum、inline、template等,并给出了宏使用不当的示例,同时指出了宏在特定场景下的必要性。

概述:

        宏是C和C++语言的抽象设施中最生硬的工具,它是披着函数外衣的饥饿的狼,很难驯服,它会我行我素地游走于各处。要避免使用宏。

讨论:

        在C++中,几乎从不需要使用宏。

        可以用const或者enum定义易于理解的常量,用inline避免函数调用的开销,用template指定函数系列和类型系列,用namespace避免名称冲突。

    C++的宏的主要问题在于,它们表面上看起来很好,而实际上做的却是另一回事。宏会忽略作用域,忽略类型系统,忽略所有其他的语言特性和规则,而且会劫持它为文件其余部分所定义(#define)的符号。宏调用看上去很像符号或者函数调用,但实际上并非如此。宏不太“卫生”,也就是说,它会根据自己被使用时所处的环境引人注目而且令人惊奇地展开为各种东西。宏需要进行文本替换,因此编写远距离也正确的宏接近于一种魔法,而精通这种魔法既无意义又无趣味。

    不少人认为与模板相关的错误都是最难以解读的,他们可能还没有看到误写和误用的宏所引起的那些错误。模板是C++类型系统的一部分,因此编译器可以更好地对它们进行处理,而宏天生是与语言本身割裂开来的,因此很难处理。更糟的是,与模板不同,宏可能展开为在偶然情况下能够编译的“传输线噪音”。最后,宏中的错误可能只有在宏展开之后才能被报告出来,而不是在定义时。

     即使在极少的情况下,有正当理由编写宏,也决不要考虑编写一个以常见词或者缩略语为名字的宏。尽可能快的取消宏的定义(#undef)。

示例:

    1、定义一个宏#define  min(n, m)    ((n) < (m) ? (n) : (m)) 

            定义两个变量a和b,min(++a, b) 传入之后是这样 ((++a) < (b) ? (++a) : (b))   如果++a小于b的话,a就自加了两次,很明显不符合宏使用的初衷。

    2、将模板实例化转给宏,宏仅能理解C语言的小括号和方括号,并将其进行匹配。然而,C++又定义了一个新的括号结构,即模板中使用的尖括号<和>。宏无法正确的匹配它们,这意味着在下面的宏调用中:

       MACRO(Foo<int,double>)

       宏会认为传给自己的是两个参数,即Foo<int和double>,而事实上该结构是一个C++实体。

例外情况:

    宏仍然是几个重要任务的唯一解决方案,比如#include保护符,条件编译中的#ifdef和#ifndef,以及assert的实现。

    在条件编译中,要避免在代码中到处杂乱地插入#ifdef。相反,应该对代码进行组织,利用宏在驱动一个公共接口的多个实现,然后始终使用该接口。

    如果不想到处复制粘贴代码段,那么可以使用宏,但要非常小心。


### 3.1 定义的编程规范与使用注意事项 定义在C/C++编程中是一种预处理机制,它在编译前进行文本替换,不涉及类型检查和语义分析。因此,使用定义时需要特别注意其副作用和替换规则,以避免逻辑错误和难以调试的问题。 定义表达式在使用时应始终用括号包裹整个表达式,特别是在涉及运算符优先级的情况下。例如,在定义一个用于计算两个数的平方时,如果未正确使用括号,可能会导致计算顺序错误: ```cpp #define pow2(x) x*x ``` 在调用 `pow2(3+1)` 时,实际替换为 `3+1*3+1`,这将导致错误的结果。正确的写法是将整个表达式用括号包围,以确保替换后的表达式保持正确的运算顺序: ```cpp #define pow3(x) ((x)*(x)) ``` 这种做法可以有效防止由于替换过程中未考虑运算符优先级而导致的计算错误 [^2]。 ### 3.2 定义表达式的括号使用原则 定义中涉及多个操作数或表达式时,不仅应为整个表达式加括号,还应对每个参数加括号。例如,定义一个用于计算两个数的差值: ```cpp #define SUB(a) (a)-(a) ``` 在调用 `SUB(a+b)` 时,替换为 `(a+b)-(a+b)`,这在某些上下文中可能不是预期行为。如果 `a+b` 是一个更复杂的表达式,如函数调用或带副作用的表达式,可能导致不可预测的结果。因此,应确保定义中的每个参数都用括号保护,以防止展开时被错误地拆分 [^4]。 ### 3.3 定义与表达式优先级问题 定义的另一个常见问题是替换后表达式的优先级问题。例如,定义一个用于表示位掩码: ```cpp #define VRRP_DEBUG_ALL 0x1|0x2|0x4|0x8 ``` 在使用 `num & VRRP_DEBUG_ALL` 时,替换为 `num & 0x1|0x2|0x4|0x8`,由于 `&` 的优先级低于 `|`,该表达式等价于 `(num & 0x1) | 0x2 | 0x4 | 0x8`,而不是预期的 `num & (0x1 | 0x2 | 0x3 | 0x8)`。因此,正确的做法是将整个定义用括号包围,以确保其在表达式中具有正确的优先级: ```cpp #define VRRP_DEBUG_ALL (0x1|0x2|0x4|0x8) ``` 这种写法可以避免由于替换后表达式优先级错误而导致的逻辑错误 [^1]。 ### 3.4 定义与函数调用的对比 虽然定义可以实现类似函数调用的功能,但它不具备函数调用的安全性。定义不进行类型检查,也不对参数进行求值,而是直接进行文本替换。例如,在定义一个用于计算矩形周长时: ```cpp #define PERIMTER(X,Y) (2*X+2*Y) ``` 在调用 `PERIMTER(length,width)*high` 时,替换为 `(2*length+2*width)*high`,这在大多数情况下是安全的。但如果 `X` 或 `Y` 是一个具有副作用的表达式(如自增操作),则可能导致多次求值,从而改变程序行为。因此,从可维护性和安全性角度考虑,应优先使用函数而非定义 [^5]。 ### 3.5 定义的替代方案 为了避免定义带来的潜在问题,现代C++推荐使用 `const` 常量、`inline` 函数和模板元编程等机制来替代定义。例如,使用 `const` 常量替代位掩码: ```cpp const int VRRP_DEBUG_ALL = 0x1 | 0x2 | 0x4 | 0x8; ``` 这种方式不仅具有类型安全性,还能在调试器中被识别和查看。对于需要重复计算的表达式,应使用 `inline` 函数: ```cpp inline int square(int x) { return x * x; } ``` `inline` 函数在编译阶段被展开,同时保留了函数调用的语义和类型检查机制,从而避免定义的潜在问题 。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值