C语言中的宏与函数


101

我经常看到使用宏胜于使用函数的示例和案例。

有人可以举例说明宏与函数相比的缺点吗?


21
反问。在什么情况下宏更好?除非可以证明宏更好,否则请使用实函数。
David Heffernan

Answers:


113

宏易于出错,因为宏依赖于文本替换并且不执行类型检查。例如,此宏:

#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 ++编程语言


43
C ++广告有什么用?
Pacerier

4
同意,这是一个C问题,无需添加偏见。
ideaman42

16
C ++是C的扩展,它添加了(除其他功能之外)旨在解决C特定限制的功能。我不喜欢C ++,但是我认为它在这里很热门。
D Coetzee

1
宏,内联函数和模板经常用于提高性能。它们被过度使用,并且由于代码膨胀而易于损害性能,从而降低了CPU指令高速缓存的效率。我们可以在不使用这些技术的情况下使用C建立快速的通用数据结构。
山姆·沃特金斯2015年

1
根据ISO / IEC 9899:1999§6.5.1,“在上一个序列点与下一个序列点之间,对象的存储值最多只能通过对表达式的求值修改一次”。(在以前的C标准和后续的C标准中都存在类似的措词。)因此,x++*x++不能说该表达式增加x两次。它实际上会调用未定义的行为,这意味着编译器可以自由地执行其所需的任何操作,它可以递增x两次,一次或根本不递增;它可能因错误而中止,甚至使恶魔从你的鼻子里飞出来
Psychonaut

39

宏功能

  • 宏已预处理
  • 无类型检查
  • 代码长度增加
  • 使用宏可能会导致副作用
  • 执行速度更快
  • 在将编译宏名称替换为宏值之前
  • 在小代码多次出现的地方很有用
  • 宏也没有检查编译错误

功能特点

  • 功能已编译
  • 类型检查已完成
  • 代码长度保持不变
  • 副作用
  • 执行速度较慢
  • 在函数调用期间,发生控制转移
  • 在大代码多次出现的地方很有用
  • 功能检查编译错误

2
需要“执行速度更快”参考。过去十年中,即使有能力的编译器都认为内联函数会带来性能上的好处,但它会内联。
Voo

1
难道不是,在低级MCU(AVR,即ATMega32)计算的上下文中,宏是更好的选择,因为它们不像函数调用那样增加调用堆栈?
hardyVeles

1
@hardyVeles不是。即使对于AVR,编译器也可以非常智能地内联代码。这是一个示例:godbolt.org/z/Ic21iM
爱德华

33

副作用很大。这是一个典型的情况:

#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的expression语句,否则无法在宏中执行此操作。(编辑:尽管您可以使用逗号运算符...忽略了它,但是它仍然不太可读。)


操作顺序:(由@ouah提供)

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

min(x & 0xFF, 42)

扩展到:

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

&优先级低于<。因此0xFF < 42首先被评估。


5
而不在宏定义中加上带有宏参数的括号会导致优先级问题:例如,min(a & 0xFF, 42)
ouah 2012年

是的。我在更新帖子时没有看到您的评论。我想我也会提到。
Mysticial

14

范例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;
}

13

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

但是,这里的答案大多解释了宏的问题,而不是简单地认为宏是邪恶的,因为可能发生愚蠢的事故。
您可以意识到陷阱并学会避免陷阱。然后仅在有充分理由时才使用宏。

在某些特殊情况下,使用宏具有优势,其中包括:

  • 如下所述,泛型函数可以具有可用于不同类型的输入参数的宏。
  • 可变的参数个数可以映射到不同的功能,而不是用C的va_args
    例如:https : //stackoverflow.com/a/24837037/432509
  • 它们可以任选包括本地信息,如调试字符串:
    __FILE____LINE____func__)。检查前/后条件,assert失败情况,甚至检查静态声明,以便代码不会在使用不当时编译(这对调试版本最有用)。
  • 检查输入args,可以对输入args进行测试,例如检查其类型,sizeof,struct在转换之前检查成员是否存在
    (对于多态类型有用)
    或检查数组是否满足某些长度条件。
    参见:https : //stackoverflow.com/a/29926435/432509
  • 尽管它注意到函数会进行类型检查,但C也会强制转换值(例如ins / floats)。在极少数情况下,这可能会带来问题。可以编写比其输入参数更严格的宏。参见:https : //stackoverflow.com/a/25988779/432509
  • 它们用作函数的包装器,在某些情况下,您可能希望避免重复自己,例如... func(FOO, "FOO");,您可以定义一个宏来为您扩展字符串func_wrapper(FOO);
  • 当您想在调用者本地范围内操作变量时,将指针传递给指针通常可以正常工作,但在某些情况下,仍然可以使用宏的麻烦较小。
    (对于每个像素的操作,分配多个变量是一个示例,您可能更喜欢宏而不是函数……尽管它仍然在很大程度上取决于上下文,因为inline函数可能是一个选项)

诚然,其中一些依赖于不是标准C的编译器扩展。这意味着您最终可能会获得较少的可移植代码,或者不得不将ifdef它们引入其中,因此只有在编译器支持时才能利用它们。


避免多参数实例化

注意这一点,因为它是宏错误的最常见原因之一x++例如,传入的宏可能会多次递增)

可以编写避免参数多次实例化的副作用的宏。

C11通用

如果您希望square宏可以与各种类型一起使用并具有C11支持,则可以执行此操作...

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不支持您提到的语句表达式?(MS的编译器)
gideon

1
@gideon,正确编辑的答案,尽管提到了每个功能,但不确定是否有必要具有一些编译器功能支持矩阵。
ideaman42

12

不会重复参数和代码的类型检查,这可能导致代码膨胀。宏语法还会导致出现很多奇怪的情况,在这些情况下,分号或优先级顺序可能会受到影响。这是一个演示一些宏观邪恶的链接


6

宏的一个缺点是调试器读取了没有扩展宏的源代码,因此在宏中运行调试器不一定有用。不用说,您不能像使用函数那样在宏内设置断点。


断点在这里非常重要,感谢您指出。
汉斯(Hans)

6

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


6

添加到此答案..

宏被预处理器直接替换到程序中(因为它们基本上是预处理器指令)。因此,它们不可避免地会比各自的功能使用更多的存储空间。另一方面,一个函数需要更多的时间来调用并返回结果,而使用宏可以避免这种开销。

宏还具有一些特殊工具,这些工具无法帮助程序在不同平台上移植。

与函数相比,不需要为宏的参数分配数据类型。

总体而言,它们是编程中的有用工具。并且根据情况可以使用宏指令和功能。


3

在上面的答案中,我没有注意到函数相对于宏的优势,我认为这是非常重要的:

函数可以作为参数传递,宏不能。

具体示例:您想编写一个标准版本的“ strpbrk”函数的替代版本,该函数将返回0直到某个字符被接受,而不是在另一个字符串中搜索的明确的字符列表。发现通过了一些测试(用户定义)。您可能要执行此操作的一个原因是,您可以利用其他标准库函数:可以提供ctype.h的'ispunct'等,而不是提供充满标点的显式字符串,等等。如果'ispunct'仅实现为宏,这是行不通的。

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

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


1
感谢您的回答
Kyrol '18

1

如果将函数作为参数传递给宏,则每次都会对其进行评估。例如,如果调用最流行的宏之一:

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

像那样

int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));

functionThatTakeLongTime将被评估5次,这可能会大大降低性能

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.