【问题标题】:When should you use macros instead of inline functions?什么时候应该使用宏而不是内联函数?
【发布时间】:2010-12-11 01:16:02
【问题描述】:

在之前的question 中,我认为好的答案被否决了,因为建议使用宏

#define radian2degree(a) (a * 57.295779513082)
#define degree2radian(a) (a * 0.017453292519)

而不是内联函数。请原谅新手的问题,但在这种情况下,宏有什么邪恶之处?

【问题讨论】:

  • 在这种情况下, degree2radian(45 + a / 2.0) 并没有按照你的想法做。总是把展开式中的每一个论点都括起来——嗯,几乎总是;主要的例外情况是当您进行标记粘贴或字符串化等操作时。
  • 乔纳森所说的 - 比看到一个由于所有该死的括号而阅读起来很痛苦的宏定义更糟糕的是,遇到一个没有所有这些该死的括号的宏定义.
  • 宏也有 ALLCAPS 约定,至少将它们放入自己的伪命名空间中。

标签: c macros inline


【解决方案1】:

大多数其他答案都讨论了为什么宏是邪恶的,包括您的示例如何具有常见的宏使用缺陷。这是 Stroustrup 的看法:http://www.research.att.com/~bs/bs_faq2.html#macro

但您的问题是,宏仍然适用于哪些方面。在某些事情上,宏比内联函数更好,这就是你正在做的事情是内联函数无法完成的,例如:

  • 令牌粘贴
  • 处理行号等(如在assert() 中创建错误消息)
  • 处理不是表达式的东西(例如有多少offsetof() 的实现使用类型名称来创建强制转换操作)
  • 获取数组元素计数的宏(不能用函数来完成,因为数组名称太容易衰减为指针)
  • 在模板不可用的 C 中创建类似于“类型多态”函数的东西

但是对于具有内联函数的语言,宏的更常见用途应该是不必要的。在处理不支持内联函数的 C 编译器时,我什至不愿意使用宏。如果可能的话,我尽量不使用它们来创建与类型无关的函数(创建几个函数,并将类型指示符作为名称的一部分)。

我也开始使用枚举来命名数字常量,而不是 #define

【讨论】:

  • 宏的另一个有用案例是初始化数据段中的对象:int global = radian2degree(2*PI);
  • 是的 - 我自己很少处理浮点数,所以我现在通常使用枚举而不是宏来命名数字常量。但显然,对于像 PI 这样的浮点数,这是无法做到的,所以它会是一个宏。
  • 在带有 gcc 扩展的编译器上,与内联函数不同,宏可以测试它们的参数是否可以作为常量计算。如果希望有一个像reverse8bits(x) 这样计算位反转字节值的“函数”,最好在x 是一个常量时使用(((x&128)>>7)|((x&64)>>5)...)(在这种情况下,看起来讨厌的表达式会产生一个常量) 但是当x 是一个变量时调用一个库例程。我希望C 允许根据参数是否为编译时常量来重载内联函数,因为这样会更简洁,而且...
  • ...在 C 中允许函数重载的命名相关问题(据我了解,这是人们尚未决定不允许此类重载的标准的原因)不适用于内联或静态函数(因为编译器总是可以随意命名这些函数,但他们认为合适,只要在不同模块中声明同名函数不会在链接时引起命名冲突)。
【解决方案2】:

关于宏有一些严格意义上的邪恶之处。

它们是文本处理,没有作用域。如果您#define foo 1,那么任何后续使用foo 作为标识符都将失败。这可能会导致奇怪的编译错误和难以发现的运行时错误。

他们不接受通常意义上的争论。您可以编写一个函数,该函数将采用两个int 值并返回最大值,因为参数将被评估一次,然后使用这些值。您不能编写宏来执行此操作,因为它会至少对一个参数求值两次,并且会以 max(x++, --y) 之类的内容失败。

还有一些常见的陷阱。很难在其中包含多个语句,并且它们需要很多可能是多​​余的括号。

在你的情况下,你需要括号:

#define radian2degree(a) (a * 57.295779513082)

需要

#define radian2degree(a) ((a) * 57.295779513082)

而且你仍然踩着任何在某个内部范围内编写函数 radian2degree 的人,确信该定义将在其自己的范围内工作。

【讨论】:

  • 我总是发现“功能‘x’是邪恶的,永远不要使用它”的说法似乎暗示程序员非常愚蠢。建议以在使用时将自身标识为宏的方式命名宏:RADIAN2DEGREEmac 或类似名称。
  • “功能 X 是邪恶的”并不意味着“不要使用功能 X”,我也没有说宏本身就是邪恶的。全大写约定很有价值,但并不能解决所有问题。与严格的括号和其他技巧相同。我建议尽可能避免使用类似函数的宏,因为通常有更好的方法来做你想做的任何事情。
  • @David 答案很好,但没有回答问题:“什么时候应该使用宏而不是内联函数?”似乎您的回答正好相反:为什么您不应该使用宏。但是什么时候使用宏而不是函数“更好”呢?
  • @styfle:正文中的问题是“在这种情况下,宏有什么邪恶之处?”,我回答了那个问题。诚然,我错过了问题标题中的那个。
  • @David:我对标题很感兴趣。虽然我找到了答案:某些事情你只能用宏来做,因为 C 没有像 C++ 这样的模板。
【解决方案3】:

对于这个特定的宏,如果我使用如下:

int x=1;
x = radian2degree(x);
float y=1;
y = radian2degree(y);

不会进行类型检查,x,y 将包含不同的值。

还有如下代码

float x=1, y=2;
float z = radian2degree(x+y);

不会按照你的想法去做,因为它会转化为

float z = x+y*0.017453292519;

而不是

float z = (x+y)+0.017453292519;

这是预期的结果。

这些只是错误行为和滥用宏可能具有的几个示例。

编辑

你可以看到关于这个here的更多讨论

【讨论】:

  • 好答案。还值得一提的是命名空间污染(例如,我不能再声明名为 radian2degree 的局部变量或结构字段)。
  • 我不明白您的第一次投诉。 x 不会通过宏转换为浮点数,而是通过乘法。
  • 我的观点是这两种用法都是有效的并且可以编译,但你不会看到任何迹象。你的宏应该只接受浮点数,但它会接受任何东西。
  • @Jonathan,很好的接触。不仅编译不出来,还很难看出原因。
  • 强制接口是你必须使用内联函数的原因(即使宏通常是邪恶的......但有时它很好)
【解决方案4】:

如果可能,请始终使用内联函数。这些是类型安全的,不能轻易重新定义。

defines 可以重新定义 undefined,并且没有类型检查。

【讨论】:

  • C(与 C++ 相对)的缺点是并非所有编译器都必须支持它们 - 或者默认为支持它们的模式。它们是在 C99 中添加的,但某些编译器(例如 MSVC)不符合 C99。我不知道 MSVC 是否支持这一标准——这可能是因为它还潜伏着 C++ 支持。但是,它没有其他 C99 特性非常好。
  • 智能编译器将忽略 inline 指令,并根据个别调用指示是否内联。
  • @David +1,inline 是纯粹的推荐。
  • MSCV 使用 __inline 扩展支持 C 模式下的内联函数。
【解决方案5】:

宏相对经常被滥用,使用它们很容易出错,如您的示例所示。取表达式radian2degree(1 + 1):

  • 使用宏将扩展为 1 + 1 * 57.29... = 58.29...
  • 有了一个函数,它就是你想要的,即 (1 + 1) * 57.29... = ...

更一般地说,宏是邪恶的,因为它们看起来像函数,所以它们会诱使您像函数一样使用它们但它们有自己的微妙规则。在这种情况下,正确的写法是(注意 a 周围的括号):

#define radian2degree(a) ((a) * 57.295779513082)

但是你应该坚持使用内联函数。有关邪恶宏及其微妙之处的更多示例,请参阅 C++ FAQ Lite 中的这些链接:

【讨论】:

  • +1 但是 a) 宏并不是“一般的邪恶”。它们可以被滥用,但它们不是万恶之源,就像gotos 一样。 b) 为什么要链接到 C++ 常见问题解答 C 问题?问题中没有 C++ 标记,也没有 C++ 特定信息。
  • 你说的“一般的邪恶”是对的,它太苛刻和笼统了。我编辑了它。与 b) 相关,据我所见,我提供的 C++ FAQ 链接中的所有信息也适用于 C,并且我认为宏的问题/微妙之处已在此处清楚地解释,因此我将它们视为此问题的有价值链接。那么为什么不链接到 C++ 常见问题解答?
【解决方案6】:

编译器的预处理器是一个很麻烦的东西,因此是聪明的技巧的可怕候选者。正如其他人所指出的,编译器很容易误解你对宏的意图,你也很容易误解宏实际上会做什么,但最重要的是,你不能在调试器中单步执行宏!

【讨论】:

    【解决方案7】:

    宏是邪恶的,因为您最终可能传递的不仅仅是一个变量或一个标量,这可能会导致不需要的行为(定义一个 max 宏来确定 a 和 b 之间的最大值,但将 a++ 和 b++ 传递给宏,然后看看会发生什么)。

    【讨论】:

      【解决方案8】:

      如果您的函数无论如何都要内联,那么函数和宏之间没有性能差异。但是,函数和宏在可用性方面存在一些差异,所有这些差异都支持使用函数。

      如果您正确构建宏,则没有问题。但是如果你使用一个函数,编译器每次都会为你正确地执行它。所以使用函数会更难写出糟糕的代码。

      【讨论】:

        猜你喜欢
        • 2010-12-28
        • 1970-01-01
        • 2021-12-06
        • 1970-01-01
        • 2019-06-15
        • 2012-06-07
        • 1970-01-01
        • 2011-06-22
        相关资源
        最近更新 更多