C中宏使用小贴士与小技巧

译者 yaronli(http://www.yaronspace.cn/blog)

英文地址:http://www.mikeash.com/pyblog/friday-qa-2010-12-31-c-macro-tips-and-tricks.html

预处理 VS 编译

为了理解C中的宏,首先需要理解一个C程序是怎么编译的。特别的,你必须知道在预处理阶段和编译阶段发生的事情的不同之处。

就像名字所说的,预处理首先执行。它会做一些简单的文本操作,比如:

  • 去除注释
  • 处理#include 指令,用include文件内容对它们进行替换
  • 判断#if 和 #ifdef指令
  • 评估#define指令
  • 根据已经定义#define指令,扩展哪些在剩余代码中找到的宏

显然,最后两条与今天讨论的内容最相关。

可以看到,预处理器对它所处理的文本内容并不关心。对于这点也有例外,比如,它知道这是个字符串,并不会扩展里面的的宏:

#define SOMETHING hello
 
char *str = "SOMETHING, world!" // nope

同时它也可以计算括号的数目,所以它知道这个逗号不会传递给宏两个参数:

#define ONEARG(x) NSLog x
 
ONEARG((@"hello, %@", @"world"));

但是通常来说,预处理器对它所处理的内容并不了解。例如,你不能使用#if来判断一个类型是否已经定义:

// makes no sense
 
#ifndef MyInteger
 
typedef int MyInteger
 
#endif

即使MyInteger类型已经定义,#ifndef也会返回true,类型定义发现在编译阶段,此时还未发生。

同样的,对于#define定义的内容也不需要是语法正确的。下面的定义是完全合法的,尽管这是定义宏的很不好的方式:

#define STARTLOG NSLog(
 
#define ENDLOG , "testing");
 
STARTLOG "just %" ENDLOG

预处理只是盲目地用它们定义的内容来替换STARTLOG和ENDLOG。到时编译器会使此段代码有意义,而且它确实是有意义的,完全可以编译成为有效地代码。

警告

C语言中的宏利弊并存。它们的一些特点会使它变得很危险,所以需要小心对待。

C预处理器几乎是图灵完全的。用一个简单的驱动程序,可以使用使用预处理器来计算任何可计算的函数。然而,需要做到这一点是非常怪异的扭曲和困难,以至于使图灵完整的C++模板看起来比较简单的。(不太理解)

#define ADD(x, y) x+y
 
// produces 14, not 20
 
ADD(2, 3) * 4;
 
#define MULT(x, y) x*y
 
// produces 14, not 20
 
MULT(2 + 3, 4);

必须小心的对所有可能需要括号的地方加括号,认真对待可能传递给宏的参数和宏可能出现的场景。计算宏参数多次同样可能导致不可期望的结果:

#define MAX(x, y) ((x) > (y) ? (x) : (y))
 
int a = 0;
 
int b = 1;
 
int c = MAX(a++, b++);
 
// now a = 1, c = 1, and b = 3!
 
// (a++ > b++ ? a++ : b++)
 
// b++ gets evaluated twice

这种使用带参数的宏和调用函数的语法非常相似,但是不要太傻了。宏需要非常小心细节的。

宏的调试

宏像其它代码一样, 它们也会存在bug。宏的bug会在使用宏的地方以非常诡异的编译错误出现。这会极其让人困惑的。

为了降低迷惑,你就会想到看下预处理后的文件。这就意味着所有的宏都会展开,你会看到编译器所看到的原始的C代码,而不是仅仅扩展你的宏。由于扩展了所有的#include指令,结果文件会非常的大,但是你会在文件末尾找到你的代码。找到你宏使用的地方,弄明白宏是怎么错误的,并正确的修改。

多语句宏

写一个包含多条语句的宏是很普通的。例如:时间定义宏:

#define TIME(name, lastTimeVariable) NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime]; if(lastTimeVariable) NSLog(@"%s: %f seconds", name, now - lastTimeVariable); lastTimeVariable = now

你可以把它用在一些经常调用的函数中:

- (void)calledALot
 
{
 
// do some work
 
// time it
 
TIME("calledALot", _calledALotLastTimeIvar);
 
}

这个定义工作地很好,但是把所有的语句写到一行是非常丑陋的。我们把它分成多行。通常#define 是在行尾结束的,但是你可以在行尾放置\ ,这样就可以使预处理器知道下一行也是定义。

#define TIME(name, lastTimeVariable) \

NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime]; \
 
if(lastTimeVariable) \
 
NSLog(@"%s: %f seconds", name, now - lastTimeVariable); \
 
lastTimeVariable = now

这样工作更简单了。但是,这个宏存在瑕疵,考虑下面的例子:

- (void)calledALot
 
{
 
if(...) // only time some calls
 
TIME("calledALot", _calledALotLastTimeIvar);
 
}

这个宏被展开为:

- (void)calledALot
 
{
 
if(...) // only time some calls
 
NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime];
 
if(_calledALotLastTimeIvar)
 
NSLog(@"%s: %f seconds", name, now - _calledALotLastTimeIvar);
 
_calledALotLastTimeIvar = now;
 
}

这个将不会编译。将NSTimeInterval声明在if语句中是不合法的。即使能够工作,只有第一条语句是在if段中的,接下来的语句无论如何都会执行,不是我们想要的。

可以在宏定义的两端加入大括号来解决这个问题:

#define TIME(name, lastTimeVariable) \

{ \
 
NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime]; \
 
if(_calledALotLastTimeIvar) \
 
NSLog(@"%s: %f seconds", name, now - _calledALotLastTimeIvar); \
 
_calledALotLastTimeIvar = now; \
 
}

现在它展开后的样子是:

- (void)calledALot
 
{
 
if(...) // only time some calls
 
{
 
NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime];
 
if(lastTimeVariable)
 
NSLog(@"%s: %f seconds", name, now - lastTimeVariable);
 
lastTimeVariable = now;
 
};
 
}

相当不错,除了在}后面的分号了,事实上这也是个问题,考虑如下情况:

- (void)calledALot
 
{
 
if(...) // only time some calls
 
TIME("calledALot", _calledALotLastTimeIvar);
 
else // otherwise do something else
 
// stuff
 
}

展开后的样子是:

- (void)calledALot
 
{
 
if(...) // only time some calls
 
{
 
NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime];
 
if(_calledALotLastTimeIvar)
 
NSLog(@"%s: %f seconds", name, now - _calledALotLastTimeIvar);
 
_calledALotLastTimeIvar = now;
 
};
 
else // otherwise do something else
 
// stuff
 
}

这个分号将导致语法错误。

现在你想可能想让用户使用该宏时,不要在后面加入分号。但是,这会很不自然而且对向代码自动对齐造成混乱的。

一个更好的解决方法是将代码块封装在do {…}while(0)中,这个结构需要在尾部加入一个分号,这正是我们所需要的。使用while(0)保证了这个循环永远不会执行循环,而且它的内容保证只执行一次。

#define TIME(name, lastTimeVariable) \

do { \
 
NSTimeInterval now = [[NSProcessInfo processInfo] systemUptime]; \
 
if(lastTimeVariable) \
 
NSLog(@"%s: %f seconds", name, now - lastTimeVariable); \
 
lastTimeVariable = now; \
 
} while(0)

这个宏在if语句和其他情况下工作的很好。多语句宏通常使用do{…}while(0)封装就是这个原因。

这个宏定义了变量now,对于宏变量来说,这是不好的名字,因为可能会与外部引起冲突。考虑下面的代码:

NSTimeInterval now; // ivar
 
TIME("whatever", now);

这个将不会工作,调试它也不是那么简单,因为这种错误太细微了。

很不幸,C语言并没有提供一个方法来生产唯一的变量名字,最好的方法就是加前缀,如下面代码:

#define TIME(name, lastTimeVariable) \

do { \
 
NSTimeInterval MA_now = [[NSProcessInfo processInfo] systemUptime]; \
 
if(lastTimeVariable) \
 
NSLog(@"%s: %f seconds", name, MA_now - lastTimeVariable); \
 
lastTimeVariable = MA_now; \
 
} while(0)

现在这个宏就非常安全了。

字符串连接

这个功能严格上来说并不是宏的一部分,但是它对创建宏很重要,所以值得在这里提一下。在C语言中,如果你在源代码中将两个字符串放到一起,它们将进行连接,这是一个少为人知的特点。

char *helloworld = "hello, " "world!";
 
// equivalent to "hello, world!"

你可以利用这点使用宏来讲宏参数与字符串常量进行连接:

#define COM_URL(domain) [NSURL URLWithString: "http://www." domain ".com"];
 
COM_URL("google"); // gives http://www.google.com
 
COM_URL("apple"); // gives http://www.apple.com

字符化

通过在参数名字前加入#,预处理器会将这个参数的内容转化为C字符串,例如:

#define TEST(condition) \

do { \
 
if(!(condition)) \
 
NSLog("Failed test: %s", #condition); \

} while(0)
 
TEST(1 == 2);
 
// logs: Failed test: 1 == 2

但是,你需要非常小心地使用。如果参数中包括了宏,它将不会被扩展。例如:

#define WITHIN(x, y, delta) (fabs((x) - (y)) < delta)
 
TEST(WITHIN(1.1, 1.2, 0.05));
 
// logs: Failed test: WITHIN(1.1, 1.2, 0.05)

Token Pasting(不知怎么翻译)

预处理器提供了##操作符来连接token。这就允许你在宏中build多个相关的item以降低冗余。a##b将生产新的token ab,如果a 或者b是宏参数,它们的内容将会被使用。一个没有意义的例子:

#define NSify(x) NS ## x
 
NSify(String) *s; // gives NSString

关于使用,请参考我的另一篇文章:http://www.yaronspace.cn/blog/index.php/archives/1216

变量参数列表

想像你想要写一个写日志的宏,如果全局变量被设置则记录日志:

#define LOG(string) \

do { \
 
if(gLoggingEnabled) \
 
NSLog("Conditional log: %s", string); \
 
} while(0)

这么调用:

LOG("hello");
//输出:
Conditional log: hello

这个非常方便,但是太简单了。NSLog接收一个字符串format和变量列表,如果LOG能够这么工作,宏才会非常有用:

LOG("count: %d  name: %s", count, name);

如果使用原来的定义,会产生错误,宏只接收一个参数,而你确提供了三个参数。

将…置于宏参数列表的末尾,这个宏就会接受可变的参数。然后你就可以使用__VA_ARGS__标识符在宏体中使用,它会被可变参数,逗号所替代。代码如下:

#define LOG(...) \

do { \
 
if(gLoggingEnabled) \
 
NSLog("Conditional log: " __VA_ARGS__); \
 
} while(0)

宏工作正常,但是在可变参数部分前面加一个固定的参数将会是非常有用的。例如,

#define LOG(fmt, ...) \

do { \
 
if(gLoggingEnabled) \
 
NSLog("Conditional log: --- " fmt " ---", __VA_ARGS__); \
 
} while(0)

这样会有一个问题,你不能仅仅提供一个参数,像LOG(“hello”),展开后为:

NSLog(@"Conditional log: --- " "hello" " ---", );

最后一个逗号会产生语法错误。

正确的方法是使用##,如果可变参数为空,预处理会删除最后的逗号。

#define LOG(fmt, ...) \

do { \
 
if(gLoggingEnabled) \
 
NSLog(@"Conditional log: --- " fmt " ---", ## __VA_ARGS__); \

} while(0)

特殊的标识符

C提供一些内置的标识符,在构建宏是非常有用:

  • __LINE__:扩展为当前行号
  • __FILE__:扩展为当前的源码文件
  • __func__:扩展为当前执行的函数名称

现在我们就可以这样定义日志宏:

#define LOG(fmt, ...) NSLog("%s:%d (%s): " fmt, __FILE__, __LINE__, __func__, ## __VA_ARGS__)

Typeof

这是gcc一个扩展,不是标准C的,它提供表达式的类型。

使用 __typeof__, 这个宏可以这么定义:

#define MAX(x, y) (^{ \

__typeof__(x) my_localx = (x); \
 
__typeof__(y) my_localy = (y); \
 
return my_localx > my_localy ? (my_localx) : (my_localy); \
 
}())

结论

C宏复杂而强大。如果用它,一定要非常小心不要滥用。但是,在一些情况下,宏是非常有用的,利用这些小贴士和小技巧,使用宏可以使你的代码更容易些和阅读。

来自yaronspace.cn  本文链接:http://yaronspace.cn/blog/archives/1248