Answers:
宏易于出错,因为宏依赖于文本替换并且不执行类型检查。例如,此宏:
#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
两次,一次或根本不递增;它可能因错误而中止,甚至使恶魔从你的鼻子里飞出来。
宏功能:
功能特点:
副作用很大。这是一个典型的情况:
#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
首先被评估。
min(a & 0xFF, 42)
#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;
}
如有疑问,请使用函数(或内联函数)。
但是,这里的答案大多解释了宏的问题,而不是简单地认为宏是邪恶的,因为可能发生愚蠢的事故。
您可以意识到陷阱并学会避免陷阱。然后仅在有充分理由时才使用宏。
在某些特殊情况下,使用宏具有优势,其中包括:
va_args
。__FILE__
,__LINE__
,__func__
)。检查前/后条件,assert
失败情况,甚至检查静态声明,以便代码不会在使用不当时编译(这对调试版本最有用)。struct
在转换之前检查成员是否存在func(FOO, "FOO");
,您可以定义一个宏来为您扩展字符串func_wrapper(FOO);
inline
函数可能是一个选项)。诚然,其中一些依赖于不是标准C的编译器扩展。这意味着您最终可能会获得较少的可移植代码,或者不得不将ifdef
它们引入其中,因此只有在编译器支持时才能利用它们。
注意这一点,因为它是宏错误的最常见原因之一(x++
例如,传入的宏可能会多次递增)。
可以编写避免参数多次实例化的副作用的宏。
如果您希望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
功能用于许多不同的类型。
不会重复参数和代码的类型检查,这可能导致代码膨胀。宏语法还会导致出现很多奇怪的情况,在这些情况下,分号或优先级顺序可能会受到影响。这是一个演示一些宏观邪恶的链接
在上面的答案中,我没有注意到函数相对于宏的优势,我认为这是非常重要的:
函数可以作为参数传递,宏不能。
具体示例:您想编写一个标准版本的“ strpbrk”函数的替代版本,该函数将返回0直到某个字符被接受,而不是在另一个字符串中搜索的明确的字符列表。发现通过了一些测试(用户定义)。您可能要执行此操作的一个原因是,您可以利用其他标准库函数:可以提供ctype.h的'ispunct'等,而不是提供充满标点的显式字符串,等等。如果'ispunct'仅实现为宏,这是行不通的。
还有很多其他示例。例如,如果比较是通过宏而不是函数完成的,则不能将其传递给stdlib.h的'qsort'。
在Python中类似的情况是版本2与版本3中的“打印”(不可传递的语句与可传递的函数)。