C语言中的宏及与函数的比较

本文介绍C语言中的宏定义,并比较宏与函数的异同。

编译一个C程序的第一个步骤称为预处理(preprocessing)阶段。在此阶段下,由#define指令定义的符号将被替换。

简单的宏

简单的宏(对象式宏)的定义如下:

1
#define name stuff

使用该指令,可以将任何文本替换到程序中,预处理器遇到一个宏定义时,会将namestuff进行替换。stuff可以包括标识符、关键字、数值常量、字符常量、字符串字面量、操作符和排列。

通常,我们会在以下两种情况下用宏来为常量命名,替代字符或字符串字面量:

  • 常量被不止一次地使用
  • 常量日后可能需要修改

可以用#undef name指令移除一个宏定义。

使用#define为常量命名的优点

  • 程序更易读

    帮助读者理解常量的意义,免于受大量「魔法数」的困惑。

  • 程序更易修改

    仅需要改变一个宏定义,就可以改变程序中出现的所有常量值。对比「硬编码」常量,若在大型程序中多处出现,修改时很可能漏掉某处。

  • 避免前后不一致或键盘输入错误

    例如数值常量\(\pi\)的值。

  • 可以对C语法进行修改

    对于习惯使用其他编程语言的程序员可以使用宏定义来修改语法。但,修改语法并不是个好主意,会使程序难被其他程序员理解,并且可能造成混淆,使可读性下降。避免用#define创建新语言。

  • 对类型重命名

  • 控制条件编译

指令的规则

以下规则也适用于其他预处理指令。

  • 指令以#开始

    #符号不需要出现在一行的行首,只要之前有空白字符就行。在#后是指令名,接着是指令所需要的其他信息。

  • 指令总是在第一个换行符处结束,除非明确指明要延续

    如果想要在下一行延续指令,必须在当前行末尾使用\字符。

    利用相邻字符串常量被自动链接为一个字符串的特性,可以使用如下声明来调试一个存在许多涉及一组变量的不同计算过程的程序:

    1
    2
    3
    4
    #define DEBUG_PRINT printf("File %s line %d:" \ 
    "x = %d, y = %d, z = %d", \
    __FILE__, __LINE__, \
    x, y, z)

    注意:为避免出错,不要在末尾放置分号。

    使用此调试语句示例:

    1
    2
    3
    4
    x *= 2;
    y += x;
    z = x * y;
    DEBUG_PRINT;
    • 指令可以出现在程序中的任何地方

      我们常将#define指令放在文件的开始。

    • 注释可以与指令放在同一行

      在宏定义后面加一个注释来解释宏的含义是一种好习惯。

    带参数的宏

    #define机制允许将参数替换到文本中,称为带参数的宏/函数式宏/定义宏:

    1
    #define name(paramter-list) stuff

    为了区分带参数宏和函数,通常使用大写来命名宏。

    带参数宏常用于执行简单的函数,如:

    1. 求两数最大值

      1
      #define MAX(x, y) ((x)>(y)?(x):(y)
    2. 判断奇偶性

      1
      #define IS_EVEN(n) ((n) % 2 == 0)
    3. 交换两个数

      1
      #define SWAP(x, y, t) ((t) = (x), (x) = (y), (y) = (t)

    带参数的宏不仅适用于模拟函数调用,也经常用作需要重复书写的代码段,如:

    1
    2
    #define MALLOC(n, type)\
    ((type *)malloc((n) * sizeof(type)))

    带参数宏与函数的比较

    执行速度

    程序执行时调用/返回函数通常会有额外开销,包括存储上下文信息、复制参数值等。

    调用宏则没有这些运行开销。

    代码长度

    每一处宏调用都会导致插入宏的替换列表,由此导致程序源代码增加,编译后的代码也变大。

    函数调用的函数代码仅出现于一个地方:每次使用函数,调用同一份代码。

    如果相同的代码需要出现在程序的几个地方,更好的方法是实现为一个函数。避免用#define指令定义可以用函数实现的长序列代码。

    参数求值

    参数每次用于宏定义时,都将重新求值。因此宏可能会不止一次地计算它的参数,由于多次求值,具有副作用的参数可能会产生不可预料的结果。

    参数在函数被调用前只求值一次,在函数中多次使用参数不会导致多种求值过程。

    考虑上面的求两数最值的一种使用情况:

    1
    2
    3
    x = 5;
    y = 8;
    z = MAX(x++, y++);

    宏替换后产生以下代码:

    1
    z = ((x++) > (y++) ? (x++) : (y++));

    较小的值增加了一次,而较大的值却增加了两次,第一次在比较时,第二次在执行后面的表达式时,因此此时得到的结果是x = 6, y = 10, z = 9

    操作符优先级

    宏参数的求值是在所有周围表达式的上下文环境里,邻近操作符的优先级可能会产生不可预料的结果

    函数只在函数调用时求值一次,结果值传递给函数,表达式的求值结果更容易预测。

    考虑以下代码:

    1
    2
    #define SQUARE(x)
    printf("%d\n", SQUARE(a + 1));

    宏定义后产生的代码为:

    1
    printf("%d\n", a + 1 * a + 1);

    表达式没有按照预想的次序求值。

    注意所有用于对数值表达式进行的宏定义都应该在宏定义每个参数周围加上括号,且在整个宏定义的两边也加上括号。

    参数类型

    宏与类型无关,只要对参数的操作何发,可以用于任何类型参数。

    函数与类型有关,函数类型不同,需要使用不同函数,即使执行任务相同。

    例如上面举例的MAX宏,可以接受多种类型数:int, long, float, double等。

    例如上面举例的MALLOC宏,type参数是一种类型,无法作为函数参数进行传递。

    参考资料

    • 《C语言程序设计现代方法(第2版)》
    • 《C和指针》