IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    C中宏使用小贴士与小技巧[原创]

    admin发表于 2011-04-01 14:12:49
    love 0

    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 &gt; my_localy ? (my_localx) : (my_localy); \
     
    }())

    结论

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

    您可能对下面文章也感兴趣:

    • C++插件机制的一种实现方法(对象工厂)
    • scoped_ptr,shared_ptr和weak_ptr用法和实现方法
    • 模板成员函数为什么不能是虚函数
    • C++ Web编程
    • C++中extern “c”深层含义


沪ICP备19023445号-2号
友情链接