C++避坑之#define常量和形似函数的宏

2023-05-17 21:57:42 浏览数 (1)

尽量避免#define定义常量

在C 中,定义常量应该尽量避免使用#define来定义一个常量,主要原因宏定义只做替换,不做类型检查和计算,不仅没有作用域限制,而且容易产生错误。例如:

代码语言:javascript复制
#include <iostream>
#include <string>
using namespace std;

#define A 10

void func1() {
    #define A 20
    cout << "func1 A = " << A << endl;
}

void func2() { cout << "func2 A = " << A << endl; }

int main() {
    cout << "main A = " << A << endl;
    func1();
    func2();

    return 0;
}

输出结果:

代码语言:javascript复制
main A = 20
func1 A = 20
func2 A = 20

从上一个例子我们可以看出,输出结果全为20。由于#define只做字面上的替换,且全局有效,因此不管定义在哪里都会在预处理的时候全部替换掉,因此带来的效果就是定义的变量貌似在全局均可访问。上例子中,在func1中重新定义了变量A,导致后面所有的A都变成了20。我们不妨将func1的实现放在main函数之后,看看有什么结果,如下例所示:

代码语言:javascript复制
#include <iostream>
#include <string>
using namespace std;

#define A 10

void func1();

void func2() { cout << "func2 A = " << A << endl; }

int main() {
  cout << "main A = " << A << endl;
  func1();
  func2();

  return 0;
}

void func1() {
#define A 20
  cout << "func1 A = " << A << endl;
}

输出结果:

代码语言:javascript复制
main A = 10
func1 A = 20
func2 A = 10

从这个例子我们可以看出,在编译器的预处理阶段,#define确实是按照顺序来全局进行替换,初始定义A的值为10,因此main函数中的Afunc2中的A均被替换为10,而最后在处理到func1的函数体的时候,A重新被定义为20,所以func1中的A被替换为20

由于宏定义只做替换,所以没有名称的概念,而且宏在编译器预处理的时候就被替换了,因此在代码调试过程中更不容易发现问题。例如上例中,在预编译阶段A全部被替换为数字1020,编译器在编译的时候根本就感知不到A的存在,假如代码确实在这个宏定义A的地方出现了问题,我们debug的时候,只能看到相应的数字1020,并不知道从哪里追踪它们的来源,增加我们定位问题的难度。

因此,在C 中我们尽量避免使用#define来定义一个常量,应使用constenum来定义常量。

尽量避免形似函数的宏

#define的另外一个需要注意的地方就是,尽量减少形似函数宏的使用。例如下面的例子:

代码语言:javascript复制
#include <iostream>
#include <string>
using namespace std;

#define T a   a
#define TT T - T
#define MAXF(a, b) func(a > b ? a : b)

void func(int m) { cout << "func1 = " << m << endl; }

int main() {
    int a = 1;
    cout << "a = " << a << endl;
    cout << "T = " << T << endl;
    cout << "TT = " << TT << endl;
    int b = 0;
    MAXF(  a, b);       // a被累加2次
    MAXF(a  , b);       // a被累加2次
    MAXF(  a, b   20);  // a被累加1次
    cout << "a = " << a << endl;
    return 0;
}

输出结果:

代码语言:javascript复制
a = 1
T = 2
TT = 2
func1 = 3
func1 = 4
func1 = 20
a = 6

输出结果可能与我们的预期存在出入,例如我们可能会认为TT的输出应该为0MAXF的输出可能与预期的不太一致。实际上,在上例中预编译阶段,把所有的宏替换为相应的表达式。其中:

  • 对于T替换为a aT的输出结果为2TT替换为a a-a aTT的输出结果也为2
  • 对于MAXF( a, b);,首先被替换为func( a > b ? a : b);,由于 a是先递增再比较,20大,因此func的参数应为 aa累加了两次,因此MAXF( a, b);输出的结果为3
  • 对于MAXF(a , b);,首先被替换为func(a > b ? a : b);,由于a 在这里是先比较再递增,30大,因此func的参数为a ,这时候a应先将值传递给func,然后再累加,因此func打出来的结果为4。实际上此时a的值已经变为5
  • 对于MAXF( a, b 20);a在比较大小的时候累加了一次,6没有20大,因此传入func的参数是20,因此打印输出结果为20
  • 最终a总共累加了5次,最终结果为6

使用形似函数的宏有时候的确会给我们带来方便,但有时候在直观上也会带来使用上的歧义,实际上也不是宏的错,大部分情况是我们把情况简单化、直观化了,实际上如果将其展开并替换后,我们也能及时发现问题,但问题是按照宏的逻辑再次展开分析,已经把我们的工作变得更复杂了,背离了当初我们简单化的初衷。那我们如何防止这些意外的发生呢?对于一些简单的宏表达式,我们可以通过添加括号等方法,强化我们的逻辑,避免不必要的歧义发生,对于形似函数的宏,尽量使用inline函数来替换上面的宏定义,具体的实现如下所示:

代码语言:javascript复制
#include <iostream>
#include <string>
using namespace std;

#define T a   a
#define TT (T) - (T)

template <typename F>
void func(F m) {
    cout << "func1 = " << m << endl;
}

template <typename F>
inline void MAXF(F a, F b) {
    func(a > b ? a : b);
}

int main() {
    int a = 1;
    cout << "a = " << a << endl;
    cout << "T = " << T << endl;
    cout << "TT = " << TT << endl;
    int b = 0;
    MAXF(  a, b);
    MAXF(a  , b);
    MAXF(  a, b   20);
    cout << "a = " << a << endl;
    return 0;
}

输出结果:

代码语言:javascript复制
a = 1
T = 2
TT = 0
func1 = 2
func1 = 2
func1 = 20
a = 4

使用inline函数替代形似函数的宏,使得代码更加易用,同时也实现了类似define的效果。同时,因为我们使用了函数,因此也遵守了作用域和访问的规则,使得我们的代码更具标准性和规则性。

总结

在C 中,尽量避免#define常量和形似函数宏的使用。对于一些简单的表达式的宏,要避免宏嵌套宏,尽量做到简单,对于嵌套宏要做好运算符优先级检查和每一层的嵌套隔离,避免歧义的产生。引用《Effective C 》中的话来做总结就是:

对于单纯常量,最好以const对象和enum替换#define。 对于形似函数的宏,最好改用inline函数替换#define

0 人点赞