C语言中宏定义和函数的区别

125

我经常看到一些情况下,使用宏比使用函数更好。

有人能举个例子说明宏相比函数的劣势吗?


25
把问题反过来。在什么情况下宏更好?除非你能证明宏更好,否则请使用真实函数。 - David Heffernan
11个回答

135

宏由于依赖文本替换而不执行类型检查,所以容易出错。例如,这个宏:

#define square(a) a * a

当使用整数时,它可以正常工作:

square(5) --> 5 * 5 --> 25

但是在使用表达式时,它会做出非常奇怪的事情:

square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice

在参数周围加上括号可以帮助解决这些问题,但并不能完全消除它们。

当宏包含多个语句时,你可能会在控制流结构中遇到麻烦:

#define swap(x, y) t = x; x = y; y = t;

if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;

通常修复这个问题的策略是将语句放在一个“do {...} while (0)”循环中。

如果您有两个结构体,它们恰好包含具有相同名称但不同语义的字段,则相同的宏可能会对两者都起作用,结果很奇怪:

struct shirt 
{
    int numButtons;
};

struct webpage 
{
    int numButtons;
};

#define num_button_holes(shirt)  ((shirt).numButtons * 4)

struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8

最后,宏可以很难调试,产生奇怪的语法错误或运行时错误,你需要扩展来理解(例如使用gcc-E),因为调试器无法步进宏,如下面的例子:

#define print(x, y)  printf(x y)  /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */

内联函数和常量有助于避免宏带来的许多问题,但并不总是适用。当宏被有意使用以指定多态行为时,无意的多态可能难以避免。C++具有许多功能,如模板,可以帮助以类型安全的方式创建复杂的多态结构,而无需使用宏;有关详细信息,请参见Stroustrup的《C++程序设计语言》。


54
为什么要做C++广告? - Pacerier
4
同意,这是一个 C 语言问题,不需要添加偏见。 - ideasman42
30
C++是C语言的扩展,增加了许多功能,其中之一是旨在解决C语言的这个特定限制。虽然我不是C++的粉丝,但我认为它在这里是相关的。 - Chiara Coetzee
1
宏、内联函数和模板通常被用来提高性能。然而,它们经常被滥用,往往会因为代码膨胀而损害性能,从而降低 CPU 指令缓存的有效性。我们可以在 C 中制作快速的通用数据结构而不使用这些技术。 - Sam Watkins
1
根据ISO/IEC 9899:1999 §6.5.1规定:“在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的求值被修改一次。”(类似的措辞存在于先前和随后的C标准中。)因此,表达式x++*x++不能说会使x增加两次;它实际上会引发_未定义行为_,这意味着编译器可以自由地做任何它想做的事情——它可以将x增加两次、一次或不增加;它可以用错误中止甚至让恶魔从你的鼻子里飞出来 - Psychonaut
显示剩余6条评论

51

宏的特点:

  • 宏是预处理
  • 没有类型检查
  • 代码长度增加
  • 使用宏可能会导致副作用
  • 执行速度更快
  • 编译之前,宏名称被宏值替换
  • 在小代码多次出现时很有用
  • 不会检查编译错误

函数的特点:

  • 函数是已编译
  • 进行了类型检查
  • 代码长度保持相同
  • 没有副作用
  • 执行速度较慢
  • 在函数调用期间,控制的传输发生
  • 在大量代码多次出现时很有用
  • 函数检查编译错误

12
执行速度更快。需要参考资料。如果最近十年的任何一个编译器认为内联函数会提供性能优势,那么它都可以很好地进行内联处理。 - Voo
2
在低级MCU(例如ATMega32)计算的上下文中,宏是更好的选择,因为它们不会像函数调用一样增加调用堆栈,不是吗? - hardyVeles
3
@hardyVeles 不是这样的。即使是针对AVR的编译器也可以非常智能地内联代码。这里有一个例子:https://godbolt.org/z/Ic21iM - Edward

38

副作用是一个很大的问题。 这里有一个典型的案例:

#define min(a, b) (a < b ? a : b)

min(x++, y)

得到扩展为:

(x++ < y ? x++ : y)

x在同一语句中被增加了两次。(并且会导致未定义的行为)


编写多行宏也很麻烦:

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;
        

每行结尾都需要一个反斜杠\


除非将宏定义为单个表达式,否则无法“返回”任何内容:

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

除非使用GCC的语句表达式,否则无法在宏中执行此操作。 (编辑:您可以使用逗号运算符... 看漏了... 但可能仍然不太易读。)


运算顺序:(由@ouah提供)

#define min(a,b) (a < b ? a : b)

min(x & 0xFF, 42)

被扩展为:

(x & 0xFF < 42 ? x & 0xFF : 42)

但是&的优先级低于<。所以0xFF < 42会首先得到评估。


6
不在宏定义中使用括号将宏参数括起来可能会导致优先级问题,例如:min(a&0xFF,42) - ouah
啊,是的。在我更新帖子时没有看到你的评论。我想我也会提一下。 - Mysticial

16

有疑问时,请使用函数(或内联函数)。

然而,这里的大多数答案都解释了宏的问题,而不是从一个简单的角度来看待宏是邪恶的,因为可能会发生一些愚蠢的事故。
您可以意识到这些陷阱并学会避免它们。然后只有在有充分理由时才使用宏。

有某些特殊情况下使用宏会有优势,包括:

  • 通用函数,如下所述,您可以拥有可用于不同类型输入参数的宏。
  • 可变数量的参数可以映射到不同的函数,而不是使用C的va_args
    例如:https://stackoverflow.com/a/24837037/432509
  • 它们可以可选地包含本地信息,例如调试字符串:
    __FILE____LINE____func__)。检查前/后条件,在失败时进行assert,甚至进行静态断言,以便代码在不当使用时无法编译(对于调试构建非常有用)。
  • 检查输入参数,您可以对输入参数进行测试,例如检查其类型、大小,检查struct成员是否存在于转换之前
    (对于多态类型可能很有用)。或检查数组是否满足某些长度条件。
    参见:https://dev59.com/CF0a5IYBdhLWcg3wzbfV#29926435
  • 虽然已经指出函数会进行类型检查,但C也会强制转换值(例如整数/浮点数)。在极少数情况下,这可能会有问题。可以编写比函数更严格的宏来检查其输入参数。请参见:https://stackoverflow.com/a/25988779/432509
  • 它们作为函数的包装器使用,在某些情况下,您可能希望避免重复自己,例如... func(FOO, "FOO");,您可以定义一个宏来为您扩展字符串func_wrapper(FOO);
  • 当您想要在调用者的本地范围内操作变量时,指针传递指针通常很好,但在某些情况下,仍然使用宏会更少麻烦。
    (对于每个像素操作的分配多个变量,是您可能更喜欢宏而不是函数的示例...尽管这仍然取决于上下文,因为inline函数可能是一种选择)

诚然,其中一些依赖于不是标准C的编译器扩展。这意味着您可能会得到更少可移植的代码,或者必须将它们放入ifdef中,以便只有在编译器支持时才能利用它们。


避免多参数实例化

这是宏中最常见的错误之一,需要注意(例如,传递 x++ 时,可能会导致宏多次增加)。

可以编写避免多个参数实例化的宏,从而避免副作用。

C11通用

如果您想要一个适用于各种类型并支持C11的square宏,则可以这样做...

inline float           _square_fl(float a) { return a * a; }
inline double          _square_dbl(float a) { return a * a; }
inline int             _square_i(int a) { return a * a; }
inline unsigned int    _square_ui(unsigned int a) { return a * a; }
inline short           _square_s(short a) { return a * a; }
inline unsigned short  _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */

#define square(a)                        \
    _Generic((a),                        \
        float:          _square_fl(a),   \
        double:         _square_dbl(a),  \
        int:            _square_i(a),    \
        unsigned int:   _square_ui(a),   \
        short:          _square_s(a),    \
        unsigned short: _square_us(a))

语句表达式

这是由GCC、Clang、EKOPath和Intel C++编译器支持的扩展功能(但不包括MSVC)

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })

使用宏的不利之处在于您需要知道如何使用它们,并且它们的支持范围不如其他功能广泛。
其中一个好处是,在这种情况下,您可以为许多不同类型使用相同的“square”函数。

1
“…被广泛支持…” 我敢打赌你提到的语句表达式在cl.exe(微软的编译器)中并没有得到支持? - gideon
1
@gideon,回答得很好,虽然针对每个功能提到的内容,不确定是否需要有一些编译器功能支持矩阵。 - ideasman42

15

Example 1:

#define SQUARE(x) ((x)*(x))

int main() {
  int x = 2;
  int y = SQUARE(x++); // Undefined behavior even though it doesn't look 
                       // like it here
  return 0;
}

鉴于:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}

例子2:

struct foo {
  int bar;
};

#define GET_BAR(f) ((f)->bar)

int main() {
  struct foo f;
  int a = GET_BAR(&f); // fine
  int b = GET_BAR(&a); // error, but the message won't make much sense unless you
                       // know what the macro does
  return 0;
}

相比之下:

struct foo {
  int bar;
};

int get_bar(struct foo *f) {
  return f->bar;
}

int main() {
  struct foo f;
  int a = get_bar(&f); // fine
  int b = get_bar(&a); // error, but compiler complains about passing int* where 
                       // struct foo* should be given
  return 0;
}

12

没有参数类型检查和代码重复,这可能导致代码膨胀。宏语法也可能会导致许多奇怪的边角案例,其中分号或优先顺序可能会妨碍代码。这里有一个链接展示了一些宏 有害


6
添加到这个答案中...
宏是由预处理器直接替换到程序中的(因为它们基本上是预处理指令)。因此,它们不可避免地使用比相应函数更多的内存空间。另一方面,函数需要更多的时间来调用和返回结果,这种开销可以通过使用宏来避免。
此外,宏有一些特殊工具,可以帮助在不同平台上实现程序的可移植性。
与函数不同,宏不需要为其参数分配数据类型。
总体而言,它们是编程中一个有用的工具。根据情况,可以使用宏指令和函数。

6

宏的一个缺点是调试器读取源代码,而源代码并没有展开的宏,所以在宏中运行调试器不一定有用。无需解释,您不能像函数一样在宏内设置断点。


断点在这里非常重要,感谢您指出。 - Hans

6

函数进行类型检查。这为您提供了额外的安全层。


4

在上面的答案中,我没有注意到一个我认为非常重要的函数优于宏的优点:

函数可以作为参数传递,而宏则不行。

具体例子:你想编写一个替代标准“strpbrk”函数的版本,该版本将接受一个(指向)函数作为参数,而不是在另一个字符串中搜索一组显式字符。该函数将返回0,直到找到通过某些测试(用户定义)的字符。你可能希望这样做的原因之一是,这样你就可以利用其他标准库函数:而不是提供一个充满标点符号的显式字符串,你可以传递ctype.h的'ispunct'等等。如果'ispunct'仅作为宏实现,那么这将无法实现。

还有许多其他例子。例如,如果您的比较是通过宏而不是函数完成的,则无法将其传递给stdlib.h的'qsort'。

Python中类似的情况是版本2和版本3中的“print”(不可传递语句与可传递函数)。


1
感谢您的回答。 - Kyrol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接