我经常看到一些情况下,使用宏比使用函数更好。
有人能举个例子说明宏相比函数的劣势吗?
我经常看到一些情况下,使用宏比使用函数更好。
有人能举个例子说明宏相比函数的劣势吗?
宏由于依赖文本替换而不执行类型检查,所以容易出错。例如,这个宏:
#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++程序设计语言》。
x++*x++
不能说会使x
增加两次;它实际上会引发_未定义行为_,这意味着编译器可以自由地做任何它想做的事情——它可以将x
增加两次、一次或不增加;它可以用错误中止甚至让恶魔从你的鼻子里飞出来。 - Psychonaut宏的特点:
函数的特点:
副作用是一个很大的问题。 这里有一个典型的案例:
#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
会首先得到评估。
min(a&0xFF,42)
。 - ouah有疑问时,请使用函数(或内联函数)。
然而,这里的大多数答案都解释了宏的问题,而不是从一个简单的角度来看待宏是邪恶的,因为可能会发生一些愚蠢的事故。
您可以意识到这些陷阱并学会避免它们。然后只有在有充分理由时才使用宏。
有某些特殊情况下使用宏会有优势,包括:
va_args
。__FILE__
,__LINE__
,__func__
)。检查前/后条件,在失败时进行assert
,甚至进行静态断言,以便代码在不当使用时无法编译(对于调试构建非常有用)。struct
成员是否存在于转换之前func(FOO, "FOO");
,您可以定义一个宏来为您扩展字符串func_wrapper(FOO);
inline
函数可能是一种选择)。诚然,其中一些依赖于不是标准C的编译器扩展。这意味着您可能会得到更少可移植的代码,或者必须将它们放入ifdef
中,以便只有在编译器支持时才能利用它们。
这是宏中最常见的错误之一,需要注意(例如,传递 x++
时,可能会导致宏多次增加)。
可以编写避免多个参数实例化的宏,从而避免副作用。
如果您想要一个适用于各种类型并支持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); })
#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;
}
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;
}
宏的一个缺点是调试器读取源代码,而源代码并没有展开的宏,所以在宏中运行调试器不一定有用。无需解释,您不能像函数一样在宏内设置断点。
函数进行类型检查。这为您提供了额外的安全层。
在上面的答案中,我没有注意到一个我认为非常重要的函数优于宏的优点:
函数可以作为参数传递,而宏则不行。
具体例子:你想编写一个替代标准“strpbrk”函数的版本,该版本将接受一个(指向)函数作为参数,而不是在另一个字符串中搜索一组显式字符。该函数将返回0,直到找到通过某些测试(用户定义)的字符。你可能希望这样做的原因之一是,这样你就可以利用其他标准库函数:而不是提供一个充满标点符号的显式字符串,你可以传递ctype.h的'ispunct'等等。如果'ispunct'仅作为宏实现,那么这将无法实现。
还有许多其他例子。例如,如果您的比较是通过宏而不是函数完成的,则无法将其传递给stdlib.h的'qsort'。
Python中类似的情况是版本2和版本3中的“print”(不可传递语句与可传递函数)。