人们总是说宏是不安全的,而且它们不是(直接)对其参数进行类型检查,等等.更糟糕的是:当发生错误时,编译器会给出内在和难以理解的诊断,因为宏只是一团糟.
是否有可能以与函数几乎相同的方式使用宏,通过安全的类型检查,避免典型的陷阱以及编译器提供正确诊断的方式.
让我们快速记住宏生成的一些典型陷阱.
例1
#define SQUARE(X) X*X
int i = SQUARE(1+5);
Run Code Online (Sandbox Code Playgroud)
预期值i:36.真值i:11(宏观扩展:) 1+5*1+5.陷阱!
(典型)解决方案(例2)
#define SQUARE(X) (X)*(X)
int i = (int) SQUARE(3.9);
Run Code Online (Sandbox Code Playgroud)
预期价值i:15.真实价值i:11(宏观扩张后:(int) (3.9)*(3.9)).陷阱!
(典型)解决方案(例3)
#define SQUARE(X) ((X)*(X))
Run Code Online (Sandbox Code Playgroud)
它适用于整数和浮点数,但它很容易被破坏:
int x = 2;
int i = SQUARE(++x);
Run Code Online (Sandbox Code Playgroud)
预期值i:9(因为(2+1)*(2+1)...).真值i:12(宏扩展:((++x)*(++x)),给出3*4).陷阱!
在这里可以找到一种很好的宏检查方法:
但是我想要更多:某种界面或"标准"语法,以及(少量)易于记忆的规则.意图是"能够使用(不实现)宏"与尽可能类似的功能.这意味着:写得好的假函数.
为什么在某种程度上有趣?
我认为这是在C中实现的一个有趣的挑战.
它有用吗?
编辑:在标准C中无法定义嵌套函数.但是,有时候,人们更愿意能够定义inline嵌套在其他函数中的short()函数.因此,类似功能的原型宏可能会被考虑在内.
这个答案分为4个部分:
(1.)第1个案例.块宏(或非返回值宏)
让我们先考虑简单的例子.假设我们需要一个"命令"来打印整数的平方,然后是'\n'.我们决定用宏来实现它.但是我们希望编译器将参数验证为int.我们写:
#define PRINTINT_SQUARE(X) { \
int x = (X); \
printf("%d\n", x*x); \
}
Run Code Online (Sandbox Code Playgroud)
(X)几乎避免了所有的陷阱.X仅在宏内部调用一次.这避免了问题的示例3的陷阱. X立即保存在变量中x. x来代替X.如果我们将这个学科系统化,将避免宏的典型问题.
现在,这样的东西正确打印9:
int i = 3;
PRINTINT_SQUARE(i++);
Run Code Online (Sandbox Code Playgroud)
显然,这种方法可能有一个弱点:x宏内部定义的变量可能与程序中的其他变量发生冲突x.这是一个范围问题.但是,这不是一个问题,因为宏体已被写成一个封闭的块{ }.这足以处理每个范围问题,并且解决了"内部"变量的每个潜在问题x.
可以认为变量x是一个额外的对象,可能并不需要.但是x(只有)临时持续时间:它是在宏的开头创建的,带有开头{,并且在宏的末尾被销毁,并且关闭}.以这种方式,x它作为一个函数参数工作:创建一个时间变量来保存参数的值,当宏"返回"时它最终被丢弃.我们没有犯任何功能还没有完成的罪!
更重要的是:当程序员试图用一个错误的参数"调用"宏时,编译器会给出一个函数在相同情况下给出的相同诊断.
所以,似乎每个宏观陷阱都已经解决了!
但是,我们有一个小的语法问题,你可以在这里看到:
因此,必须(我说)向do {} while(0)块状宏定义中添加一个构造:
#define PRINTINT_SQUARE(X) do { \
int x = (X); \
printf("%d\n", x*x); \
} while(0)
Run Code Online (Sandbox Code Playgroud)
现在,这个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
Run Code Online (Sandbox Code Playgroud)
(列入}在xxend_macroblock避免一些模糊带while(0)).当然,这种语法不再安全.必须仔细记录以避免误用.考虑以下丑陋的例子:
{ xxend_macroblock printf("Hello");
Run Code Online (Sandbox Code Playgroud)
(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
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
简单,不是吗?
typedef struct. struct声明具有相同的限制.(例如,可变大小的数组只能位于列表的末尾).(特别是,建议使用指针代替可变大小的数组声明符作为伪参数.) xxMacroPrototype调用完成的点相同. 但是,使用这种声明很容易受到纪律处分,程序员很容易遵守规则.
块宏可以"返回"一个值吗?
是.实际上,它可以通过简单地通过引用传递参数来检索任意数量的值,就像scanf()这样.
但你可能正在考虑其他事情:
(4.)第二种情况.类似函数的宏
对于他们来说,我们需要一个不同的方法来声明宏原型,其中包含返回值的类型.此外,我们必须学习一种(非硬)技术,让我们保持块宏的安全性,返回值具有我们想要的类型.
可以实现参数的类型检查,如下所示:
在块宏中,我们可以NAME在宏本身内部声明struct变量,
从而将其隐藏到程序的其余部分.对于类似函数的宏,这是不可能的(在标准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));
Run Code Online (Sandbox Code Playgroud)
第一行定义了函数宏原型的"语法".
这样的原型有3个参数:
"return"值是结构中的附加字段,具有固定名称:xxmacro__ret__.
为安全起见,这是结构中的第一个元素.然后"粘贴"伪参数列表.
当我们使用这个界面时(如果你让我这样称呼它),我们必须遵循一系列规则,按顺序:
typedef struct宏本身构建的名称,因此您不必担心,只需使用它(在此类型的示例中xxSUM_data). xxSUM_data xxsum;. #define SUM(X, Y).( ),以获得EXPRESSION(因此,"返回"值). 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);
Run Code Online (Sandbox Code Playgroud)
因为在宏内部还有另一个名为x的对象(this:,int x = (X);其中X是宏的形式参数PRINTINT_SQUARE(X)),实际上"传递"的是因为参数不是在宏外定义的"值"5,而是另一个:垃圾值.
要理解它,让我们在宏扩展后展开上面的两行:
int x = 5;
{ int x = (x); printf("%d", x*x); }
Run Code Online (Sandbox Code Playgroud)
x块内的变量被初始化...到它自己未确定的值!
通常,块宏的第1节到第3节中开发的技术可以以类似的方式打破,而我们用来保存参数的struct对象在块内声明.
这表明这种代码可以被破坏,所以它是不安全的:
不要试图在宏"内"声明"本地"变量来保存参数.
xxMacroPrototype()在行之后.这不那么雄心勃勃,但无论如何它回答了这个问题:"有多少可能......?".另一方面,现在我们对两种情况采用相同的方法:块和类函数宏.