我总是看到使用宏比使用函数更好的例子和情况。
有人可以用一个例子向我解释宏与功能相比的缺点吗?
答案 0 :(得分:101)
宏容易出错,因为它们依赖于文本替换而不执行类型检查。例如,这个宏:
#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 ++编程语言。
答案 1 :(得分:30)
副作用很大。以下是典型案例:
#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
。
答案 2 :(得分:28)
宏功能:
功能特征:
答案 3 :(得分:13)
#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;
}
答案 4 :(得分:11)
不重复参数和代码的类型检查,这可能导致代码膨胀。宏语法也可能导致任何数量的奇怪边缘情况,其中分号或优先顺序可能会妨碍。这是一个演示宏evil
的链接答案 5 :(得分:11)
如有疑问,请使用函数(或内联函数)。
然而,这里的答案主要解释宏的问题,而不是简单地认为宏是邪恶的,因为愚蠢的事故是可能的。
你可以意识到陷阱并学会避免它们。然后仅在有充分理由的情况下使用宏。
某些例外案例中使用宏有优势,其中包括:
va_args
。例如:https://stackoverflow.com/a/24837037/432509。__FILE__
,__LINE__
,__func__
)。检查前/后条件,失败时assert
,甚至是静态断言,因此代码不会在不正确的使用时编译(主要用于调试版本)。struct
成员在铸造之前是否存在(对多态类型有用)。func(FOO, "FOO");
,你可以定义一个为你扩展字符串的宏func_wrapper(FOO);
inline
函数可能是一个选项)。< / LI>
不可否认,其中一些依赖于非标准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&amp;英特尔C ++ (但不是MSVC);
#define square(a_) __extension__ ({ \
typeof(a_) a = (a_); \
(a * a); })
因此,宏的缺点是您需要知道使用它们,并且它们不受广泛支持。
在这种情况下,一个好处是,您可以对许多不同类型使用相同的square
函数。
答案 6 :(得分:6)
宏的一个缺点是调试器读取的源代码没有扩展的宏,因此在宏中运行调试器并不一定有用。不用说,你不能像使用函数一样在宏内部设置断点。
答案 7 :(得分:6)
添加到这个答案..
预处理器将宏直接替换为程序(因为它们基本上是预处理程序指令)。因此,它们不可避免地使用比相应功能更多的存储空间。另一方面,函数需要更多的时间来调用并返回结果,并且可以通过使用宏来避免这种开销。
此外,宏还有一些特殊工具,可以帮助在不同平台上实现程序可移植性。
与功能相比,不需要为其参数分配数据类型。
总的来说,它们是编程中的有用工具。并且可以根据具体情况使用宏指令和函数。
答案 8 :(得分:5)
函数进行类型检查。这为您提供了额外的安全保障。
答案 9 :(得分:2)
我没有注意到,在上面的答案中,我认为函数优于宏的一个优点是非常重要的:
函数可以作为参数传递,宏不能传递。
具体示例:您想要编写标准&#39; strpbrk&#39;的替代版本。将接受的函数,而不是在另一个字符串中搜索的显式字符列表,一个(指向一个)函数的函数,该函数将返回0,直到找到通过某个测试的字符(用户定义的)。你可能想要这样做的一个原因是你可以利用其他标准的库函数:你可以通过ctype.h&#39; ispunct&#39;而不是提供一个充满标点符号的显式字符串。相反,如果&#39; ispunct&#39;仅作为宏实现,这不会起作用。
还有很多其他的例子。例如,如果您的比较是通过宏而不是功能完成的,则无法将其传递给stdlib.h&#39; qsort&#39;。
Python中类似的情况是&#39; print&#39;在版本2与版本3(不可通过的语句与可通过的函数)。
答案 10 :(得分:0)
如果将函数作为参数传递给宏,则每次都会对其进行评估。 例如,如果您调用最流行的宏之一:
#define MIN(a,b) ((a)<(b) ? (a) : (b))
喜欢
int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));
functionThatTakeLongTime函数将被评估5次,这可能会大大降低性能