这个答案分为4个部分:
- 块宏的建议解决方案。
- 该解决方案的简要总结。
- 讨论了宏原型语法。
- 为类似函数的宏提出的解决方案。
- (重要更新:)破坏我的代码。
(1.) 第一种情况。块宏(或非返回值宏)
让我们首先考虑简单的例子。假设我们需要一个打印整数平方的“命令” ,后跟“\n”。我们决定用宏来实现它。但是我们希望编译器将参数验证为int
. 我们写:
#define PRINTINT_SQUARE(X) { \
int x = (X); \
printf("%d\n", x*x); \
}
- 周围的括号
(X)
避免了几乎所有的陷阱。
- 此外,括号有助于编译器正确诊断语法错误。
- 宏参数
X
只在宏内部调用一次。这避免了问题示例 3 的陷阱。
- 的值
X
立即保存在变量中x
。
- 在宏的其余部分,我们使用变量
x
来代替X
。
- [重要更新:](此代码可能被破坏:参见第5节)。
如果我们把这门学科系统化,就可以避免宏的典型问题。
现在,像这样正确打印 9:
int i = 3;
PRINTINT_SQUARE(i++);
显然,这种方法可能有一个弱点:x
宏内部定义的变量可能与程序中的其他变量发生冲突,也称为x
. 这是一个范围问题。但是,这不是问题,因为宏体已被编写为由 . 包围的块{ }
。这足以处理每个范围问题,并且解决了“内部”变量的每个潜在问题x
。
可以说变量x
是一个额外的对象,可能不是我们想要的。但是x
(仅)具有临时持续时间:它在宏开始时创建,带有开头{
,并在宏结尾处被销毁,带有关闭}
。这样,x
它作为函数参数工作:创建一个时间变量来保存参数的值,并在宏“返回”时最终被丢弃。我们没有犯任何功能尚未完成的罪!
更重要的是:当程序员试图用错误的参数“调用”宏时,编译器会给出与函数在相同情况下 给出的相同的诊断。
因此,似乎每个宏陷阱都已解决!
但是,我们有一点语法问题,如您在此处看到的:
因此,必须(我说)do {} while(0)
在块状宏定义中添加一个构造:
#define PRINTINT_SQUARE(X) do { \
int x = (X); \
printf("%d\n", x*x); \
} while(0)
现在,这些do { } while(0)
东西工作得很好,但它是反美学的。问题是它对程序员没有直观的意义。我建议使用有意义的方法,如下所示:
#define xxbeg_macroblock do {
#define xxend_macroblock } while(0)
#define PRINTINT_SQUARE(X) \
xxbeg_macroblock \
int x = (X); \
printf("%d\n", x*x); \
xxend_macroblock
(包含}
inxxend_macroblock
避免了与 的一些歧义while(0)
)。当然,这种语法不再安全了。必须仔细记录以避免误用。考虑以下丑陋的例子:
{ xxend_macroblock printf("Hello");
(2.) 总结
如果我们按照规范的风格编写不返回值的块定义的宏,它们的行为就可以像函数一样:
#define xxbeg_macroblock do {
#define xxend_macroblock } while(0)
#define MY_BLOCK_MACRO(Par1, Par2, ..., ParN) \
xxbeg_macroblock \
desired_type1 temp_var1 = (Par1); \
desired_type2 temp_var2 = (Par2); \
/* ... ... ... */ \
desired_typeN temp_varN = (ParN); \
/* (do stuff with objects temp_var1, ..., temp_varN); */ \
xxend_macroblock
- 对宏的调用
MY_BLOCK_MACRO()
是一个语句,而不是一个表达式:没有任何类型的“返回”值,甚至没有void
。
- 宏参数必须在宏开始时只使用一次,并将它们的值传递给具有块作用域的实际临时变量。在宏的其余部分中,只能使用这些变量。
(3.) 能否为宏的参数提供接口?
虽然我们解决了参数类型检查的问题,但程序员无法弄清楚参数“具有”什么类型。有必要提供某种宏原型!这是可能的,而且非常安全,但我们也必须容忍一些棘手的语法和一些限制。
你能弄清楚以下几行的作用吗?
xxMacroPrototype(PrintData, int x; float y; char *z; int n; );
#define PrintData(X, Y, Z, N) { \
PrintData data = { .x = (X), .y = (Y), .z = (Z), .n = (N) }; \
printf("%d %g %s %d\n", data.x, data.y, data.z, data.n); \
}
PrintData(1, 3.14, "Hello", 4);
- 第一行“定义”了宏的原型
PrintData
。
- 下面,声明了类似函数的宏 PrintData。
- 第三行声明了一个临时变量
data
,它一次收集宏的所有参数。
- 这一步需要程序员手动编写……但它是一种简单的语法,编译器会拒绝(至少)分配给类型错误的临时变量的参数。
- (但是,编译器将对“反向”赋值保持沉默
.x = (N), .n = (X)
)。
为了声明一个原型,我们xxMacroPrototype
使用 2 个参数编写:
- 宏的名称。
将在宏内部使用的“局部”变量的类型和名称列表。我们将调用这个项目:宏的 伪参数。
伪参数列表必须写成类型变量对的列表,用分号 (;) 分隔(并结束)。
在宏的主体中,第一条语句将是这种形式的声明:
MacroName foo = { .pseudoparam1 = (MacroPar1), .pseudoparam2 = (MacroPar2), ..., .pseudoparamN = (MacroParN) }
- 在宏内部,伪参数以
foo.pesudoparam1
、foo.pseudoparam2
等形式调用。
xxMacroPrototype() 的定义如下:
#define xxMacroPrototype(NAME, ARGS) typedef struct { ARGS } NAME
很简单,不是吗?
- 伪参数实现为
typedef struct
.
- 保证 ARGS 是构造良好的类型标识符对列表。
- 保证编译器将给出可理解的诊断。
- 伪参数列表与
struct
声明具有相同的限制。(例如,可变大小的数组只能位于列表的末尾)。(特别是,建议使用指针而不是可变大小的数组声明符作为伪参数。)
- 不能保证 NAME 是一个真正的宏名(但这个事实不太相关)。
重要的是我们知道“那里”定义了一些结构类型,与宏的参数列表相关联。
- 不能保证 ARGS 提供的伪参数列表实际上在某种程度上与真实宏的参数列表一致。
- 不能保证程序员会在宏中正确使用它。
- struct-type 声明的范围
xxMacroPrototype
与完成调用 的点相同。
- 推荐的做法是将宏原型放在一起,紧接着是相应的宏定义。
但是,使用这种声明很容易受到约束,程序员也很容易遵守规则。
块宏可以“返回”一个值吗?
是的。实际上,它可以通过引用传递参数来检索任意数量的值,就像scanf()
这样做一样。
但是您可能正在考虑其他事情:
(4.) 第二种情况。类函数宏
对于他们来说,我们需要一些不同的方法来声明宏原型,其中包含返回值的类型。此外,我们必须学习一种(不难)技术,让我们保持块宏的安全性,返回值具有我们想要的类型。
可以实现参数的类型检查,如下所示:
在块宏中,我们可以NAME
在宏本身内部声明结构变量,
从而使其对程序的其余部分隐藏。对于类似函数的宏,这是无法做到的(在标准 C99 中)。我们必须NAME
在调用宏之前定义一个类型的变量。如果我们准备好为此付出代价,那么我们就可以获得所需的“类似安全函数的宏”,并返回特定类型的值。
我们通过示例显示代码,然后对其进行注释:
#define xxFuncMacroPrototype(RETTYPE, MACRODATA, ARGS) typedef struct { RETTYPE xxmacro__ret__; ARGS } MACRODATA
xxFuncMacroPrototype(float, xxSUM_data, int x; float y; );
xxSUM_data xxsum;
#define SUM(X, Y) ( xxsum = (xxSUM_data){ .x = (X), .y = (Y) }, \
xxsum.xxmacro__ret__ = xxsum.x + xxsum.y, \
xxsum.xxmacro__ret__)
printf("%g\n", SUM(1, 2.2));
第一行定义了函数宏原型的“语法”。
这样的原型有 3 个参数:
- “返回”值的类型。
- 用于保存伪参数的“typedef 结构”的名称。
- 伪参数列表,以分号 (;) 分隔(并结束)。
“返回”值是结构中的一个附加字段,具有固定名称:xxmacro__ret__
.
为了安全起见,这被声明为结构中的第一个元素。然后伪参数列表被“粘贴”。
当我们使用这个接口时(如果你让我这样称呼它),我们必须遵循一系列规则,依次为:
- 编写一个原型声明,为 xxFuncMacroPrototype() 提供 3 个参数(示例的第二行)。
- 第二个参数是
typedef struct
宏本身构建的 a 的名称,所以你不用担心,直接使用它(在示例中这个类型是xxSUM_data
)。
- 定义一个变量,其类型只是该结构类型(在示例中:)
xxSUM_data xxsum;
。
- 使用适当数量的参数定义所需的宏:
#define SUM(X, Y)
.
- 宏的主体必须用括号括
( )
起来,以便获得一个 EXPRESSION (因此,一个“返回”值)。
- 在这个括号内,我们可以使用逗号运算符 (,) 分隔一长串操作和函数调用。
- 我们需要的第一个操作是将宏 SUM(X,Y) 的参数 X、Y“传递”给全局变量
xxsum
。这是通过以下方式完成的:
xxsum = (xxSUM_data){ .x = (X), .y = (Y) },
观察到一个类型的对象是在C99 语法提供的复合文字的帮助下在空中xxSUM_data
创建的。为安全起见,该对象的字段通过读取宏的参数 X、Y 填充一次,并用括号括起来。
然后我们评估一个表达式和函数列表,它们都用逗号运算符 (,) 分隔。
最后,在最后一个逗号之后,我们只写,它被认为是逗号表达式中的最后一项,因此是宏的“返回”值。
xxsum.xxmacro__ret__
为什么所有这些东西?为什么一个typedef struct
?使用结构体比使用单个变量要好,因为信息全部打包在一个对象中,并且数据对程序的其余部分保持隐藏。我们不想定义“很多变量”来保存程序中每个宏的参数。相反,通过系统地定义typedef struct
关联到一个宏,我们有一个更容易处理这样的宏。
我们可以避免上面的“外部变量” xxsum 吗?由于复合文字是左值,因此可以相信这是可能的。
其实我们可以定义这种宏,如下图:
但在实践中,我找不到以安全方式实施它的方法。
例如,上面的宏 SUM(X,Y) 不能只用这个方法来实现。
(我试图用指向结构的指针+复合文字来做一些技巧,但这似乎是不可能的)。
更新:
(5.) 破坏我的代码。
第 1 节中给出的示例可以这样分解(正如 Chris Dodd 在下面的评论中向我展示的那样):
int x = 5; /* x defined outside the macro */
PRINTINT_SQUARE(x);
由于在宏内部还有另一个名为 x 的对象(this: int x = (X);
,X
宏的形参在哪里PRINTINT_SQUARE(X)
),实际上作为参数“传递”的不是宏外部定义的“值” 5,而是另一个:垃圾价值。
为了理解它,让我们在宏展开之后展开上面的两行:
int x = 5;
{ int x = (x); printf("%d", x*x); }
块内的变量x
被初始化......到它自己的未确定值!
一般来说,在第 1 到第 3 节中为块宏开发的技术可以以类似的方式破坏,而我们用来保存参数的 struct 对象是在块内声明的。
这说明这种代码是可以破解的,所以是不安全的:
不要试图在宏“内部”声明“局部”变量来保存参数。
- 有“解决方案”吗?我回答“是”:我认为,为了避免在块宏的情况下出现这个问题(如第 1 到第 3 节所述),我们必须重复我们对类函数宏所做的事情,即:声明宏外部的保持参数结构,就在该
xxMacroPrototype()
行之后。
这不那么雄心勃勃,但无论如何它回答了这个问题:“有多少可能......?”。另一方面,现在我们对两种情况采用相同的方法:块宏和类函数宏。