如果在宏定义中声明一个变量会发生什么?

5
假设我定义了这样一个宏:
#define FOO(x,y) \
do {
  int a,b;
  a = f(x);
  b = g(x);
  y = a+b;
} while (0)
在展开宏时,GCC会保证a和b的任何形式的独特性吗?我的意思是,如果我像下面这样使用FOO:
int a = 1, b = 2;
FOO(a,b);
在预处理之后,代码将变成:
int a = 1, b = 2;
do {
  int a,b;
  a = f(a);
  b = g(b);
  b = a+b;
} while (0)
编译器能够区分do{}外部的a和do{}内部的a吗?除了让内部变量具有混乱的名称以使其他人不太可能使用相同的名称外,我可以使用什么技巧来保证任何形式的独特性?
(理想情况下,函数对于此更有用,但我的特定情况不允许使用函数)

1
http://en.wikipedia.org/wiki/Hygienic_macro - Josh Lee
5
不要将宏用于此类事情。我们有函数是有原因的。 - ThiefMaster
@jleedev:卫生宏听起来像是一个很好的想法...为什么gcc没有实现呢?看起来实现起来会很简单明了。 @ThiefMaster:同意函数对于这个很理想,但是它们不可能被考虑,因为我不能添加公共函数来达到这个目的。 - R.D.
1
@R.D. gcc是一种C编译器,实现了C标准规定的内容。它还实现了一些扩展功能,但这些扩展功能会鼓励人们编写不可移植的代码,应该避免使用。 - Jim Balter
您已经几乎回答了自己的问题。代码确实最终会像第二个代码块一样,不会看到外部的a和b,所以f(a)中的a和g(b)中的b都是未初始化的变量。为避免这种情况,请选择不符合您日常编码标准的变量名称。可以选择类似于MV_a MV_b这样的名称。 - CashCow
显示剩余3条评论
9个回答

7
如果考虑变量的作用域,do..while() 内部的 a,b 与外部定义的不同是有保证的。
对于您的情况,外部定义的 a,b 在 do..while() 中不存在。
使用宏时需要注意很多事项。

6

宏仅执行字符串替换。语义较低,编译器对预处理器的了解有限(基本上只有#pragma,实际上不是预处理器关键字,以及源行信息)。

在您的情况下,a和b是未初始化的本地值。行为是不可预测的。 您扩展的代码等同于以下代码。

int a = 1, b = 2;
do {
  int a___,b___;
  a___ = f(a___);
  b___ = g(b___);
  b___ = a___+b___;
} while (0)

为避免C++中出现这种情况,建议使用内联函数或模板。 如果您使用符合C语言1999年标准的编译器,则可以在C语言中使用inline。 http://en.wikipedia.org/wiki/Inline_function 在C语言中,您可以通过定义较长的变量并用()括起参数来创建更安全的宏:
#define FOO(x,y) \
do {
  int FOO__a,FOO__b;
  FOO__a = f(x);
  FOO__b = g(x);
  y = FOO__a+FOO__b + (y)*(y);
} while (0)

注意:我在您的示例中添加了(y)*(y)以说明情况。
只使用一次宏参数也是一个好习惯。这可以防止出现副作用,例如:
#define max(a,b) a>b?a:b
max(i++,--y)

Max不会返回您想要的内容。


2
在C语言中,创建一个(内联)函数。永远不要使用类似函数的宏。 - Lundin
但是当需要条件编译时,宏很有用。内联函数会被调用,并且其参数会被评估,这可能是无意义的或者会影响性能。如果你从未在不同环境之间进行过重要系统的实际移植,那么“永不”这个词对你来说可能是不存在的。 - Rob11311
@Rob11311,内联函数被调用并导致性能问题,这表明您没有使用正确的编译器标志。 - L. F.

4

变量a和b在本地作用域中被视为任何本地变量一样。C语言保证,如果这些变量恰好与外部作用域变量同名,则会更新本地变量。

以下是一个例子:

#include <stdio.h>

#define FOO(x) \
{              \
  int a;       \
  a = x;       \
  printf("%d\n", a); \
}


int main()
{
  int a = 1;

  {
    int a = 2;

    printf("%d\n", a); // 2

    FOO(3); // 3

    printf("%d\n", a); // 2
  }

  printf("%d\n", a); // 1

  getchar();
}

现在,当然,如果您的程序中每个变量都命名为"a"并不是一个好主意,因为C语言保证局部变量具有优先权。但从技术上讲,没有任何阻止您这样做的东西。
顺便提一下,MISRA-C禁止这种命名方式,要求每个变量无论作用域如何都必须具有唯一名称,以提高可读性和维护性。
(作为旁注,类似函数的宏是极其糟糕的编程风格,不应该使用。而应该使用真正的函数,并在性能关键时将它们内联。)

3

除了混淆之外,没有其他技巧。C和C++预处理器没有类似于lisp gensym或卫生宏的等效物。


1

不,没有唯一性的保证。

事实上,你的代码即将失败。

宏就像文本替换一样。

如果我在宏中,我通常会使用疯狂的变量名,就像这样:

#define FOO(x,y) \
do {
  int FOO_MACRO_a, FOO_MACRO_b;
  FOO_MACRO_a = f(x);
  FOO_MACRO_b = g(x);
  y = FOO_MACRO_a + FOO_MACRO_b;
} while (0)

你知道C语言中的命名空间规则吗?如果你声明了一个本地变量X,并且恰好在另一个作用域中也有一个名为X的变量,那么你的本地变量将是被更改的那个。完全没有必要混淆本地变量的命名。因此,代码不会失败,命名本地变量与外部作用域中的变量不同的唯一原因是可读性。 - Lundin
1
@Lundin:作用域和命名空间是两个不同的概念,请不要混淆术语。 - Christopher Creutzig
@Christopher 嗯,是的也不完全是。"Scope" 是标准文档中语言爱好者使用的术语,而 "namespace" 则是普通程序员在通用级别(与语言无关)上谈论编程时使用的术语。在 C 语言中,标准根本没有提到命名空间,因此对于 C 程序员来说,命名空间和作用域是相同的东西。在 C++ 中,它们是完全不同的东西,不应混淆。 - Lundin
@Lundin 当x恰好是外部作用域中的a时,将会失败的是a = f(x);。将其扩展为a = f(a);肯定会失败。 - Bo Persson

1
如果您的目标是gcc和/或g++,那么您可以使用它们的特殊宏块功能:
#define max(x, y) ({ typeof(x) a_ = (x); \
                     typeof(y) b_ = (y); \
                     (a_ > b_) ? a_ : b_ })

这允许您创建唯一的本地变量,非常类似于编写函数。

当然,为了可移植性,不建议使用。另一方面,如果您只计划在提供gcc / g ++的系统上工作,它将在所有这些系统上运行。

来源:http://gcc.gnu.org/onlinedocs/gcc-3.0.1/cpp_3.html#SEC30

此外,使用gcc / g ++,您可以使用-Wshadow命令行选项。如果您无意中重用具有相同名称的局部变量,则会发出警告。您还可以使用-Werror将这些警告转换为错误。现在,如果存在混淆变量的可能性,您无法编译。但是,您需要确保使用一个块。像其他人呈现的do / while()就可以完成任务。

int a;

// code from macro;
do { int a = 5; ... } while(false);

使用我刚刚描述的组合(-Wshadow+-Werror),当您执行int a = 5时会出现错误。

不需要使用do while,你可以直接使用some code {some code in block} some more code来执行代码。 - Tomer Wolberg
@TomerWolberg 请在此处查看所选答案 https://dev59.com/YHNA5IYBdhLWcg3wKai3 ——它并不总是必需的。 - Alexis Wilke

1

您可以通过以下方法确保无论x和y是什么,都不会出现此问题:

#define FOO(x,y) \
do\
{\
  int x##y##a,x##y##b;\
  x##y##a = f(x);\
  x##y##b = g(x);\
  y = x##y##a + x##y##b;\
} while (0)

通过确保a和b的名称包含x和y的名称,您就知道它们是不同的。但这是一种非常糟糕的代码,您可能不应该编写这样的代码。

1
有趣的建议!但仍存在两个潜在问题:与原始宏不同,这个新宏不能用作第一个参数的表达式,而且您应该删除宏末尾的“;”。 - chqrlie
@chqrlie 你说得对,我已经删掉了 ;。但是我很抱歉第一个问题无法解决。 - Tomer Wolberg
我猜可以使用一个单变量,即由2个int组成的数组,但我没有看到一个不会与x冲突的保证命名方式。给它命名为y##y使它与y不同,但是x可以是任何名称和/或表达式,因此可以包括任何标识符。我同意您的结论:可能不应该像这样编写代码。 - chqrlie

0
在你的宏中,确实存在危险,而且可能会重复使用未保证为左值的x,从而导致其值在宏中被使用两次后发生改变。
即使你确实需要一个宏,它仍然可以是一个轻量级的内联函数包装器,这样的函数将接受x并同时返回f(x)g(x),而无需重新评估x,这样做肯定是安全的。
在你的情况下,可以考虑以下代码:
template< typename T >
struct Foo
{
   T& x;

   explicit Foo(T&x_) : x(x_)
   {
   }   

   int f();
   int g();
};

template<typename T>
Foo<T> makeFoo(T& x)
{
     return Foo<T>(x);
}

#define FOO(x,y)
{
   Foo FOO_VAR(x);
   y = FOO_VAR.f() + FOO_VAR.g();
}

这样做事情会更安全。当然,如果你根本不需要宏,就把它删掉。


0

宏在文本上被展开,除了参数替换外,编译器无法提供您所要求的保证 - 正如您在展开中看到的那样,a参数将引用内部的a而不是外部的a。解决方案确实是使用“混淆”的名称,例如int FOO_a,FOO_b;


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接