译者 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程序是怎么编译的。特别的,你必须知道在预处理阶段和编译阶段发生的事情的不同之处。
就像名字所说的,预处理首先执行。它会做一些简单的文本操作,比如:
显然,最后两条与今天讨论的内容最相关。
可以看到,预处理器对它所处理的文本内容并不关心。对于这点也有例外,比如,它知道这是个字符串,并不会扩展里面的的宏:
#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提供一些内置的标识符,在构建宏是非常有用:
现在我们就可以这样定义日志宏:
#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宏复杂而强大。如果用它,一定要非常小心不要滥用。但是,在一些情况下,宏是非常有用的,利用这些小贴士和小技巧,使用宏可以使你的代码更容易些和阅读。