C中的宏与函数

Kyr*_*rol 93 c function c-preprocessor

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

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

D C*_*zee 107

宏很容易出错,因为它们依赖于文本替换而不执行类型检查.例如,这个宏:

#define square(a) a * a
Run Code Online (Sandbox Code Playgroud)

与整数一起使用时工作正常:

square(5) --> 5 * 5 --> 25
Run Code Online (Sandbox Code Playgroud)

但与表达式一起使用时会有很奇怪的事情:

square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice
Run Code Online (Sandbox Code Playgroud)

在括号周围加上括号有助于但不能完全消除这些问题.

当宏包含多个语句时,您可能会遇到控制流构造的问题:

#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;
Run Code Online (Sandbox Code Playgroud)

解决这个问题的通常策略是将语句放在"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
Run Code Online (Sandbox Code Playgroud)

最后,宏可能难以调试,产生奇怪的语法错误或运行时错误,您必须扩展才能理解(例如使用gcc -E),因为调试器无法单步执行宏,如下例所示:

#define print(x, y)  printf(x y)  /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */
Run Code Online (Sandbox Code Playgroud)

内联函数和常量有助于避免宏中的许多这些问题,但并不总是适用.在故意使用宏来指定多态行为的情况下,可能难以避免无意的多态性.C++具有许多功能,例如模板,可以在不使用宏的情况下以类型安全的方式创建复杂的多态结构; 有关详细信息,请参阅Stroustrup的C++编程语言.

  • C++广告有什么用? (40认同)
  • C++是C的扩展,它添加(以及其他)用于解决C的这个特定限制的功能.我不是C++的粉丝,但我认为这是主题. (14认同)
  • 同意,这是一个C问题,无需添加偏见. (4认同)

zan*_*ngw 35

宏功能:

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

功能特点:

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

  • 我喜欢简单明了的功能列表。+1 (2认同)
  • 需要“执行速度更快”参考。过去十年中,即使有能力的编译器都认为内联函数会带来性能上的好处,但它会内联。 (2认同)
  • 难道不是吗,在低级 MCU(AVR,即 ATMega32)计算的背景下,宏是更好的选择,因为它们不会像函数调用那样增加调用堆栈? (2认同)
  • @hardyVeles 并非如此。编译器,即使对于 AVR,也可以非常智能地内联代码。这是一个例子:https://godbolt.org/z/Ic21iM (2认同)

Mys*_*ial 30

副作用很大.这是一个典型案例:

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

min(x++, y)
Run Code Online (Sandbox Code Playgroud)

扩大到:

(x++ < y ? x++ : y)
Run Code Online (Sandbox Code Playgroud)

x在同一语句中增加两次.(和未定义的行为)


编写多行宏也很痛苦:

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;
Run Code Online (Sandbox Code Playgroud)

它们需要\在每一行的末尾.


除非你把它作为单个表达式,否则宏不能"返回"任何东西:

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}
Run Code Online (Sandbox Code Playgroud)

除非您使用GCC的表达式语句,否则无法在宏中执行此操作.(编辑:你可以使用逗号运算符......忽略了......但它可能仍然不太可读.)


运营顺序:(由@ouah提供)

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

min(x & 0xFF, 42)
Run Code Online (Sandbox Code Playgroud)

扩大到:

(x & 0xFF < 42 ? x & 0xFF : 42)
Run Code Online (Sandbox Code Playgroud)

&优先级低于<.所以先0xFF < 42得到评估.

  • 并且不在宏定义中添加带有宏参数的括号可能导致优先级问题:例如,`min(a&0xFF,42)` (5认同)

Fle*_*exo 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;
}
Run Code Online (Sandbox Code Playgroud)

然而:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

例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;
}
Run Code Online (Sandbox Code Playgroud)

相比:

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;
}
Run Code Online (Sandbox Code Playgroud)


ide*_*n42 12

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

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

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

  • 通用函数,如下所述,您可以拥有一个可用于不同类型的输入参数的宏.
  • 可变的参数个数可以映射到不同的功能,而不是用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函数可能是一个选项).

不可否认,其中一些依赖于非标准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))
Run Code Online (Sandbox Code Playgroud)

声明表达式

这是GCC,Clang,EKOPath和Intel C++支持的编译器扩展(但不支持MSVC) ;

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })
Run Code Online (Sandbox Code Playgroud)

因此,宏的缺点是你需要知道使用它们开始,并且它们不被广泛支持.

一个好处是,在这种情况下,您可以square为许多不同类型使用相同的功能.


Mic*_*gan 11

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


jim*_*ara 6

宏的一个缺点是调试器读取的源代码没有扩展的宏,因此在宏中运行调试器并不一定有用.毋庸置疑,您无法像使用函数一样在宏内部设置断点.


ncm*_*ist 6

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


Nik*_*kos 6

添加到这个答案..

宏由预处理器直接替换为程序(因为它们基本上是预处理器指令).因此,它们不可避免地使用比相应功能更多的存储空间.另一方面,函数需要更多时间来调用并返回结果,并且可以通过使用宏来避免这种开销.

此外,宏还有一些特殊的工具,可以帮助不同平台上的程序可移植性.

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

总的来说,它们是编程中的有用工具.并且可以根据情况使用宏指令和函数.