C中的宏与函数

时间:2012-02-01 22:50:24

标签: c function c-preprocessor

我总是看到使用宏比使用函数更好的例子和情况。

有人可以用一个例子向我解释宏与功能相比的缺点吗?

11 个答案:

答案 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)

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

答案 4 :(得分:11)

不重复参数和代码的类型检查,这可能导致代码膨胀。宏语法也可能导致任何数量的奇怪边缘情况,其中分号或优先顺序可能会妨碍。这是一个演示宏evil

的链接

答案 5 :(得分:11)

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

然而,这里的答案主要解释宏的问题,而不是简单地认为宏是邪恶的,因为愚蠢的事故是可能的。
你可以意识到陷阱并学会避免它们。然后仅在有充分理由的情况下使用宏。

某些例外案例中使用宏有优势,其中包括:

  • 通用函数,如下所述,您可以拥有一个可用于不同类型输入参数的宏。
  • 可变数量的参数可以映射到不同的函数,而不是使用C va_args。例如:https://stackoverflow.com/a/24837037/432509
  • 他们可以可选包含本地信息,例如调试字符串:
    __FILE____LINE____func__)。检查前/后条件,失败时assert,甚至是静态断言,因此代码不会在不正确的使用时编译(主要用于调试版本)。
  • 检查输入参数,你可以对输入参数进行测试,例如检查它们的类型,sizeof,检查struct成员在铸造之前是否存在(对多态类型有用)
    或检查阵列是否符合某种长度条件。
    见:https://stackoverflow.com/a/29926435/432509
  • 虽然它注意到函数进行类型检查,但C也会强制转换值(例如ints / floats)。在极少数情况下,这可能会有问题。它可以编写宏,这些宏比关于输入args的函数更严格。见:https://stackoverflow.com/a/25988779/432509
  • 它们用作函数的包装器,在某些情况下你可能想避免重复自己,例如... func(FOO, "FOO");,你可以定义一个为你扩展字符串的宏func_wrapper(FOO);
  • 当您想要在调用者本地范围内操作变量时,将指针传递给指针的工作正常,但在某些情况下,使用宏的麻烦仍然较少。
    (赋值给多个变量) ,对于每像素操作,是一个例子,你可能更喜欢宏而不是函数......虽然它仍然在很大程度上取决于上下文,因为inline函数可能是一个选项)。< / LI>

不可否认,其中一些依赖于非标准C的编译器扩展。这意味着您最终可能会使用较少的可移植代码,或者必须使用ifdef,因此它们只会在编译器支持。


避免多个参数实例化

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

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

C11 Generic

如果您希望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))

Statement expressions

这是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次,这可能会大大降低性能