宏定义_c语言 宏定义 去除宏定义 - CSDN
宏定义 订阅
宏(Macro),是一种批量处理的称谓。计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。宏这一术语也常常被用于许多类似的环境中,它们是源自宏展开的概念,这包括键盘宏和宏语言。绝大多数情况下,“宏”这个词的使用暗示着将小命令或动作转化为一系列指令。 展开全文
宏(Macro),是一种批量处理的称谓。计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。宏这一术语也常常被用于许多类似的环境中,它们是源自宏展开的概念,这包括键盘宏和宏语言。绝大多数情况下,“宏”这个词的使用暗示着将小命令或动作转化为一系列指令。
信息
格    式
#define  标识符   字符串
归属门类
计算机C语言
别    名
宏代换
中文名
宏定义
功    能
预处理功能
外文名
macro definition
宏定义简介
宏(Macro),是一种批量处理的称谓。计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。宏这一术语也常常被用于许多类似的环境中,它们是源自宏展开的概念,这包括键盘宏和宏语言。绝大多数情况下,“宏”这个词的使用暗示着将小命令或动作转化为一系列指令。宏的用途在于自动化频繁使用的序列或者是获得一种更强大的抽象能力。计算机语言如C语言或汇编语言有简单的宏系统,由编译器或汇编器的预处理器实现。C语言的宏预处理器的工作只是简单的文本搜索和替换,使用附加的文本处理语言如M4,C程序员可以获得更精巧的宏。Lisp类语言如Common Lisp和Scheme有更精巧的宏系统:宏的行为如同是函数对自身程序文本的变形,并且可以应用全部语言来表达这种变形。一个C宏可以定义一段语法的替换,然而一个Lisp的宏却可以控制一节代码的计算。获得了控制代码的执行顺序(见惰性计算和非限制函数)的能力,使得新创建的语法结构与语言内建的语法结构不可区分。例如,一种Lisp方言有cond而没有if,就可以使用宏由前者定义后者。Lisp语法的去部主要扩展,比如面向对象的CLOS系统,可以由宏来定义。MacroML有型别语法宏,一种有效的理解方式是把这种语法宏看作是多阶段计算。 [1]  宏定义是指用一个宏名(名字)来代表一个字符串。宏定义的功能是在编译预处理时,对程序中所有出现的“宏名”都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。
收起全文
  • 宏定义

    2018-02-06 13:20:57
    定义一个带参的,使两个参数的值互换,并写出程序,输入两个数作为使用宏时的实参。输出已交换后的两个值。 输入 两个数,空格隔开 输出 交换后的两个数,空格隔开 样例输入 1 2 ...
    题目描述
    定义一个带参的宏,使两个参数的值互换,并写出程序,输入两个数作为使用宏时的实参。输出已交换后的两个值。
    输入
    两个数,空格隔开
    输出
    交换后的两个数,空格隔开
    样例输入
    1 2
    样例输出
    2 1
    #include<stdio.h>
    #define Y(a,b) t=a;a=b;b=t;//两数交换位置
    int main(){
        int a,b,t;
        scanf("%d%d",&a,&b);
        Y(a,b)
        printf("%d %d",a,b);
        return 0;
    }
    

     #define是C语言中提供的宏定义命令,其主要目的是为程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率,但在学习时往往不能理解该命令的本质,总是在此处产生一些困惑,在编程时误用该命令,使得程序的运行与预期的目的不一致,或者在读别人写的程序时,把运行结果理解错误,这对C语言的学习很不利。下面将分别对基本用法和特殊做详细介绍。

    一、#define的基本用法

    1 #define命令剖析

    1.1   #define的概念

    #define命令是C语言中的一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是简单的宏定义,另一种是带参数的宏定义。

    (1)   无参数宏定义:

    #define   <宏名>  <字符串>

    例:   #define PI 3.1415926

    注意几个错误的用法:

           #define NAME "zhangyuncong"

          #define 0x abcd

          #define NAME "zhang

         #define NAME "zhangyuncong"

    四个题答案都是否定的。 

    第一个,""内的东西不会被宏替换。这一点应该大都知道。 

    第二个,宏定义前面的那个必须是合法的用户标识符 

    第三个,宏定义也不是说后面东西随便写,不能把字符串的两个""拆开。 

    第四个:只替换标识符,不替换别的东西

    注意:

    1)不替换程序中字符串里的东西。 

    2)第一位置只能是合法的标识符(可以是关键字) 

    3)第二位置如果有字符串,必须把""配对。 

    4)只替换与第一位置完全相同的标识符

    也就是说,这种情况下记住:#define 第一位置 第二位置 

    (2) 带参数的宏定义

     #define   <宏名> (<参数表>)   <宏体>     例: #define   A(x) x

    则遇到MAX(1+2,value)则会把它替换成: 

    ((1+2)>(value)?(1+2):(value)) 

    注意事项和无参宏着不多。

    一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。

    还有就是老生常谈的话:记住这是简单的替换而已,不要在中间计算结果,一定要替换出表达式之后再算。

    2.1 简单宏定义使用中出现的问题

    在简单宏定义的使用中,当替换文本所表示的字符串为一个表达式时,容易引起误解和误用。如下例:

    例1   #define   N   2+2

    void main()

    {

    int   a=N*N;

     printf(“%d”,a);

    }

    (1) 出现问题:在此程序中存在着宏定义命令,宏N代表的字符串是2+2,在程序中有对宏N的使用,一般同学在读该程序时,容易产生的问题是先求解N为2+2=4,然什么结后在程序中计算a时使用乘法,即N*N=4*4=16,其实该题的结果为8,为果有这么大的偏差?

    (2) 问题解析:如1节所述,宏展开是在预处理阶段完成的,这个阶段把替换文本只是看作一个字符串,并不会有任何的计算发生,在展开时是在宏N出现的地方只是简单地使用串2+2来代替N,并不会增添任何的符号,所以对该程序展开后的结果是a=2+2*2+2,计算后=8,这就是宏替换的实质,如何写程序才能完成结果为16的运算呢?

    (3)解决办法:将宏定义写成如下形式

    #define   N   (2+2)

    这样就可替换成(2+2)*(2+2)=16

    2.2 带参数的宏定义出现的问题

    在带参数的宏定义的使用中,极易引起误解。例如我们需要做个宏替换能求任何数的平方,这就需要使用参数,以便在程序中用实际参数来替换宏定义中的参数。一般学生容易写成如下形式: #define   area(x)   x*x 这在使用中是很容易出现问题的,看如下的程序

    void main()

    {     int   y=area(2+2);     printf(“%d”,y);

    }

    按理说给的参数是2+2,所得的结果应该为4*4=16,但是错了,因为该程序的实际结果为8,仍然是没能遵循纯粹的简单替换的规则,又是先计算再替换了,在这道程序里,2+2即为area宏中的参数,应该由它来替换宏定义中的x,即替换成2+2*2+2=8了。那如果遵循(1)中的解决办法,把2+2 括起来,即把宏体中的x括起来,是否可以呢?  #define   area(x) (x)*(x),对于area(2+2),替换为(2+2)*(2+2)=16,可以解决;但是对于area(2+2)/area(2+2)又会怎么样呢?又错了,还是忘了遵循先替换再计算的规则了,这道题替换后会变为 (2+2)*(2+2)/(2+2)*(2+2)即4*4/4*4按照乘除运算规则,结果为16/4*4=4*4=16,那应该怎么呢?解决方法是在整个宏体上再加一个括号,即#define   area(x) ((x)*(x)),不要觉得这没必要,没有它,是不行的。      要想能够真正使用好宏定义,一定要记住先将程序中对宏的使用全部替换成它所代表的字符串,不要自作主张地添加任何其他符号,完全展开后再进行相应的计算,就不会写错运行结果。在编程使用宏替换时,当字符串中不只一个符号时,加上括号表现出优先级,如果是带参数的宏定义,则要给宏体中的每个参数加上括号,并在整个宏体上再加一个括号。

    展开全文
  • C语言宏定义用法大全

    2020-07-30 23:31:58
    关于C语言宏定义的用法,作了一个总结,除了常用宏定义外,还有类似函数的宏的用法
  • C语言中宏定义的使用

    2018-08-21 09:00:50
    1.1 宏定义的基本语法 1.2 宏定义的优点 1.3 宏定义的缺点 1.4 宏还是函数 2 使用宏时的注意点 2.1 算符优先级问题 2.2 分号吞噬问题 2.3 宏参数重复调用 2.4 对自身的递归引用 3. 宏函数的集中特定语法 3.1 ...

    1. 引言

    ==预处理==命令可以改变程序设计环境,提高编程效率,它们并不是 C 语言本身的组成部分,不能直接对 它们进行编译,必须在对程序进行编译之前,先对程序中这些特殊的命令进行“预处理” 。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对==预处理==之后的源程序进行==编译==处理,得到可供执行的目标代码。C 语言提供的预处理功能有三种,分别为==宏定义==、文件包含和条件编译。


    1.1 宏定义的基本语法

    宏定义在 C 语言源程序中允许用一个标识符来表示一个==字符串==,称为“==宏/宏体==” ,被定义为“宏”的==标识符==称为“==宏名==”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“==宏替换==”或“==宏展开==”。 宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。

    在 C 语言中,宏分为 有参数和无参数两种。无参宏的宏名后不带参数,其定义的一般形式为:

    #define 标识符 字符串

    #表示这是一条预处理命令(在C语言中凡是以#开头的均为预处理命令)
    ==define #117411==为宏定义命令
    ==标识符 #800023==为所定义的宏名,
    ==字符串 #800019==可以是常数、表达式、格式串等。符号常量

    // 不带参数的宏定义
    #define MAX 10
    
    /*带参宏定义*/
    #define M(y) y*y+3*y
    
    /*宏调用*/
    k=M(5);
    

    1.2 宏定义的优点

    方便程序的修改
    使用简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时, 我们可以用较短的有意义的标识符来写程序,这样更方便一些。
    相对于==全局变量==两者的区别如下:
    1. 宏定义在编译期间即会使用并替换,而全局变量要到运行时才可以。
    2. 宏定义的只是一段字符,在编译的时候被替换到引用的位置。在运行中是没有宏定义的概念的。而变量在运行时要为其分配内存。
    3. 宏定义不可以被赋值,即其值一旦定义不可修改,而变量在运行过程中可以被修改。
    4. 宏定义只有在定义所在文件,或引用所在文件的其它文件中使用。 而全局变量可以在工程所有文件中使用,只要再使用前加一个声明就可以了。换句话说,宏定义不需要extern。


    提高程序的运行效率

    使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生==函数调用 #800023==时,需要保留调用函数的现场,以便子 函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽 略,但如果子函数完成的功能比较少,甚至于只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问 题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。


    1.3 宏定义的缺点

    1. 由于是直接嵌入的,所以代码可能相对多一点;
    2. 嵌套定义过多可能会影响程序的可读性,而且很容易出错,不容易调试。
    3. 对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。

    1.4 宏还是函数

    宏函数,函数比较

    ==从时间上来看 #0c2ac0==

    1. 宏只占编译时间,函数调用则占用运行时间(分配单元,保存现场,值传递,返回),每次执行都要载入,所以执行相对宏会较慢。

    2. 使用宏次数多时,宏展开后源程序很长,因为每展开一次都使程序增长,但是执行起来比较快一点(这也不是绝对的,当有很多宏展开,目标文件很大,执行的时候运行时系统换页频繁,效率就会低下)。而函数调用不使源程序变长。

    ==从安全上来看 #0c2ac0==

    1. 函数调用时,先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。

    2. 函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。

    3. 对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。

    4. 宏的定义很容易产生二义性,如:定义==#define S(a) (a)*(a)==,代码==S(a++)==,宏展开变成==(a++)*(a++)==这个大家都知道,在不同编译环境下会有不同结果。

    5. 调用函数只可得到一个返回值,且有返回类型,而宏没有返回值和返回类型,但是用宏可以设法得到几个结果。

    6. 函数体内有Bug,可以在函数体内打断点调试。如果宏体内有Bug,那么在执行的时候是不能对宏调试的,即不能深入到宏内部。

    7. C++中宏不能访问对象的私有成员,但是成员函数就可以。


    内联函数
    在C99中引入了内联函数(==inline==),联函数和宏的区别在于,==宏是由预处理器对宏进行替代 #80001e==,而==内联函数是通过编译器控制来实现的 #80000f==。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的==展开==,所以取消了函数的参数压栈,减少了调用的开销。可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。

    内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。


    宏函数的适用范围

    1. 一般来说,用宏来代表简短的表达式比较合适。
    2. 在考虑效率的时候,可以考虑使用宏,或者内联函数。
    3. 还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。

    2 使用宏时的注意点

    2.1 算符优先级问题

    不仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:

    #define MULTIPLY(x, y) x * y

    ==MULTIPLY(1, 2) #80000a==没问题,会正常展开成==1 * 2 #80000a==。有问题的是这种表达式==MULTIPLY(1+2, 3) #800019==,展开后成了==1+2 * 3 #80000f==,显然优先级错了。
    对宏体和给引用的每个参数加括号,就能避免这问题。

    #define MULTIPLY(x, y) ((x) * (y))

    2.2 分号吞噬问题

    有如下宏定义

    #define foo(x) bar(x); baz(x)

    假设你这样调用:

    if (!feral)
        foo(wolf);

    这将被宏扩展为:

    if (!feral)
        bar(wolf);
    baz(wolf);

    ==baz(wolf);==,不在判断条件中,显而易见,这是错误。
    如果用大括号将其包起来依然会有问题,例如

    #define foo(x)  { bar(x); baz(x); }
    if (!feral)
        foo(wolf);
    else
        bin(wolf);

    判断语言被扩展成:

    if (!feral) {
        bar(wolf);
        baz(wolf);
    }>>++;++<< 
    else
        bin(wolf);

    ==else==将不会被执行

    通过==do{…}while(0) #80001e==能够解决上述问题

    #define foo(x)  do{ bar(x); baz(x); }while(0)
    if (!feral)
        foo(wolf);
    else
        bin(wolf);

    被扩展成:

    #define foo(x)  do{ bar(x); baz(x); }while(0)
    if (!feral)
        do{ bar(x); baz(x); }while(0);
    else
        bin(wolf);

    使用do{…}while(0)构造后的宏定义不会受到大括号、分号等的影响,总是会按你期望的方式调用运行。


    2.3 宏参数重复调用

    有如下宏定义:

    #define min(X, Y)  ((X) < (Y) ? (X) : (Y))

    当有如下调用时==next = min (x + y, foo (z));==,宏体被展开成==next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));==,可以看到,foo(z)有可能会被重复调用了两次,做了重复计算。更严重的是,如果foo是不可重入的(foo内修改了全局或静态变量),程序会产生逻辑错误。


    2.4 对自身的递归引用

    有如下宏定义:

    #define foo (4 + foo)

    按前面的理解,==(4 + foo)==会展开成==(4 + (4 + foo))==,然后一直展开下去,直至内存耗尽。但是,预处理器采取的策略是只展开一次。也就是说,foo只会展开成==4 + foo==,而展开之后foo的含义就要根据上下文来确定了。

    对于以下的交叉引用,宏体也只会展开一次。

    #define x (4 + y)
    #define y (2 * x)

    x展开成(4 + y) -> (4 + (2 * x)),y展开成(2 * x) -> (2 * (4 + y))

    注意,这是极不推荐的写法,程序可读性极差。


    3. 宏函数的集中特定语法

    3.1 利用宏参数创建字符串:”#运算符”

    在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:

    #include <stdio.h>
    
    #define PSQR(x) printf("the square of "#x" is %d.\n",((x)*(x)))
    #define PSQR2(x) printf("the square of %s is %d.\n",#x,((x)*(x)))
    
    int main() {
        int R=5;
    
        PSQR(R);  //the square of R is 25.
        PSQR2(R); // the square of R is 25.
    
        return 0;
    }

    这种用法可以用在一些出错处理中

    #include <stdio.h>
    
    #define WARN_IF(EXPR)\
    do {\
        if (EXPR)\
            fprintf(stderr, "Warning: EXPR \n");\
    } while(0)
    
    int main() {
        int R=5;
    
        WARN_IF(R>0);
    
        return 0;
    }

    3.2 预处理器的粘合剂:”##运算符”

    和#运算符一样,##运算符可以用于类函数宏的替换部分。另外,##还可以用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。例如

    #define XNAME(n) x ## n
    int x1=10;
    XNAME(1)+=1;  //x1 11
    

    这个地方还需要再添加一个常用的用法


    3.3 可变宏:… 和_VA_ARGS

    有些函数(如==prinft() #06906d==)可以接受可变数量的参数。

      int __cdecl printf(const char * __restrict__ _Format,...);

    实现思想就是在宏定义中参数列表的最后一个参数作为省略号(三个句号)。这样,预定义宏_VA_ARGS就可以被用在替换部分中,以表明省略号代表什么,

    例如

    输出

    #define PR(...) printf(__VA_ARGS_)
    PR("Howdy");
    PR("weight=%d,shipping=$%.2f.\n",wt,sp)

    参数初始化
    通过可以参数可以完成对多个参数的初始化,就像int数组的初始化那样
    例如动态数组的添加

    动态数组multi append

    darray(int)  arr=darray_new();
    int *i;
    darray_appends(arr, 0,1,2,3,4);
    darray_foreach(i, arr)
    {
        printf("%d ", *i);  
    }

    4 宏的常用用法

    4.1 通用数据结构封装

    宏是一种字符串替换==不做类型检查==,可以将==类型做为参数==传入宏函数,利用这种特性可以实现通用数据结构的封装,以动态数组darray,和循环链表list为例

    动态数组

    动态数组是把自己的结构体放在规定的结构体之内,还有一种实现方式,把规定的结构体放到自己的结构体之中,这种方式扩展性更好,这个时候需要根据成员指针得到结构体指针。通过==container_of==实现。

    ==#define container_of(member_ptr, containing_type, member)==

    循环链表list


    4.2 日志打印,出错处理

    合理的适用预定义宏如__FILE__,字符串化符号#可以封装很多打印功能,如打印日志,断言检查等功能。

    日志打印

    日志打印

    #define MacroLog(...)\
    {\
    FILE* file;\
    fopen_s(&file,"./a.txt","a");\
    if (file != nullptr)\
    {\
    fprintf(file, "%s: Line %d:\t", __FILE__, __LINE__);\
    fprintf(file, __VA_ARGS__);\
    fprintf(file, "\n");\
    }\
    fclose(file);\
    }
    
    void FuncLog(const char *filename, int line,const char * str)
    {
        FILE* file;
        fopen_s(&file, "./a.txt", "a");
        if (file != nullptr)
        {
            fprintf(file, "%s: Line %d:\t", filename, line);
            fprintf(file, str);
            fprintf(file, "\n");
        }
        fclose(file);
    }
    
    
    int main()
    {
        log("%s,%s", "hello","log");
        funclog(__FILE__, __LINE__,"hello");
    
        return 0;
    }

    断言

        _ACRTIMP void __cdecl _wassert(
            _In_z_ wchar_t const* _Message,
            _In_z_ wchar_t const* _File,
            _In_   unsigned       _Line
            );
    
        #define assert(expression) (void)(                                                       \
                (!!(expression)) ||                                                              \
                (_wassert(_CRT_WIDE(>>++#expression++<<), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \
            )
    int main()
    {
        int a = 10;
        assert(a == 1); 
        //Assertion failed: a == 1, file c:\users\10241258\source\repos\c_win64_test\c_win64_test\c_win64_test.cpp, line 38
    
        return 0;
    }

    常用预定义宏

    
        __FUNTION__  获取当前函数名 
        __LINE__ 获取当前代码行号 
        __FILE__ 获取当前文件名 
        __DATE__ 获取当前日期 
        __TIME__ 获取当前时间
        __STDC_VERSION__

    5 Reference

    [1] C Primer Plus

    [2] C语言宏定义的几个坑和特殊用法

    [3] CCAN

    展开全文
  • 宏定义详解

    2017-08-29 15:32:09
    宏定义的黑魔法 - 宏菜鸟起飞手册 宏定义在C系开发中可以说占有举足轻重的作用。底层框架自不必说,为了编译优化和方便,以及跨平台能力,宏被大量使用,可以说底层开发离开define将寸步难行。而在更高层级进行...

    宏定义的黑魔法 - 宏菜鸟起飞手册

    Happy define :)

    宏定义在C系开发中可以说占有举足轻重的作用。底层框架自不必说,为了编译优化和方便,以及跨平台能力,宏被大量使用,可以说底层开发离开define将寸步难行。而在更高层级进行开发时,我们会将更多的重心放在业务逻辑上,似乎对宏的使用和依赖并不多。但是使用宏定义的好处是不言自明的,在节省工作量的同时,代码可读性大大增加。如果想成为一个能写出漂亮优雅代码的开发者,宏定义绝对是必不可少的技能(虽然宏本身可能并不漂亮优雅XD)。但是因为宏定义对于很多人来说,并不像业务逻辑那样是每天会接触的东西。即使是能偶尔使用到一些宏,也更多的仅仅只停留在使用的层级,却并不会去探寻背后发生的事情。有一些开发者确实也有探寻的动力和意愿,但却在点开一个定义之后发现还有宏定义中还有其他无数定义,再加上满屏幕都是不同于平时的代码,既看不懂又不变色,于是乎心生烦恼,怒而回退。本文希望通过循序渐进的方式,通过几个例子来表述C系语言宏定义世界中的一些基本规则和技巧,从0开始,希望最后能让大家至少能看懂和还原一些相对复杂的宏。考虑到我自己现在objc使用的比较多,这个站点的读者应该也大多是使用objc的,所以有部分例子是选自objc,但是本文的大部分内容将是C系语言通用。

    入门

    如果您完全不知道宏是什么的话,可以先来热个身。很多人在介绍宏的时候会说,宏嘛很简单,就是简单的查找替换嘛。嗯,只说对了的一半。C中的宏分为两类,对象宏(object-like macro)和函数宏(function-like macro)。对于对象宏来说确实相对简单,但却也不是那么简单的查找替换。对象宏一般用来定义一些常数,举个例子:

    //This defines PI
    #define M_PI        3.14159265358979323846264338327950288
    

    #define关键字表明即将开始定义一个宏,紧接着的M_PI是宏的名字,空格之后的数字是内容。类似这样的#define X A的宏是比较简单的,在编译时编译器会在语义分析认定是宏后,将X替换为A,这个过程称为宏的展开。比如对于上面的M_PI

    #define M_PI        3.14159265358979323846264338327950288
    
    double r = 10.0;
    double circlePerimeter = 2 * M_PI * r;
    // => double circlePerimeter = 2 * 3.14159265358979323846264338327950288 * r;
    
    printf("Pi is %0.7f",M_PI);
    //Pi is 3.1415927
    

    那么让我们开始看看另一类宏吧。函数宏顾名思义,就是行为类似函数,可以接受参数的宏。具体来说,在定义的时候,如果我们在宏名字后面跟上一对括号的话,这个宏就变成了函数宏。从最简单的例子开始,比如下面这个函数宏

    //A simple function-like macro
    #define SELF(x)      x
    NSString *name = @"Macro Rookie";
    NSLog(@"Hello %@",SELF(name));
    // => NSLog(@"Hello %@",name);
    //   => Hello Macro Rookie
    

    这个宏做的事情是,在编译时如果遇到SELF,并且后面带括号,并且括号中的参数个数与定义的相符,那么就将括号中的参数换到定义的内容里去,然后替换掉原来的内容。 具体到这段代码中,SELF接受了一个name,然后将整个SELF(name)用name替换掉。嗯..似乎很简单很没用,身经百战阅码无数的你一定会认为这个宏是写出来卖萌的。那么接受多个参数的宏肯定也不在话下了,例如这样的:

    #define PLUS(x,y) x + y
    printf("%d",PLUS(3,2));
    // => printf("%d",3 + 2);
    //  => 5
    

    相比对象宏来说,函数宏要复杂一些,但是看起来也相当简单吧?嗯,那么现在热身结束,让我们正式开启宏的大门吧。

    宏的世界,小有乾坤

    因为宏展开其实是编辑器的预处理,因此它可以在更高层级上控制程序源码本身和编译流程。而正是这个特点,赋予了宏很强大的功能和灵活度。但是凡事都有两面性,在获取灵活的背后,是以需要大量时间投入以对各种边界情况进行考虑来作为代价的。可能这么说并不是很能让人理解,但是大部分宏(特别是函数宏)背后都有一些自己的故事,挖掘这些故事和设计的思想会是一件很有意思的事情。另外,我一直相信在实践中学习才是真正掌握知识的唯一途径,虽然可能正在看这篇博文的您可能最初并不是打算亲自动手写一些宏,但是这我们不妨开始动手从实际的书写和犯错中进行学习和挖掘,因为只有肌肉记忆和大脑记忆协同起来,才能说达到掌握的水准。可以说,写宏和用宏的过程,一定是在在犯错中学习和深入思考的过程,我们接下来要做的,就是重现这一系列过程从而提高进步。

    第一个题目是,让我们一起来实现一个MIN宏吧:实现一个函数宏,给定两个数字输入,将其替换为较小的那个数。比如MIN(1,2)出来的值是1。嗯哼,simple enough?定义宏,写好名字,两个输入,然后换成比较取值。比较取值嘛,任何一本入门级别的C程序设计上都会有讲啊,于是我们可以很快写出我们的第一个版本:

    //Version 1.0
    #define MIN(A,B) A < B ? A : B
    

    Try一下

    int a = MIN(1,2);
    // => int a = 1 < 2 ? 1 : 2;
    printf("%d",a);
    // => 1
    

    输出正确,打包发布!

    潇洒走一回

    但是在实际使用中,我们很快就遇到了这样的情况

    int a = 2 * MIN(3, 4);
    printf("%d",a);
    // => 4
    

    看起来似乎不可思议,但是我们将宏展开就知道发生什么了

    int a = 2 * MIN(3, 4);
    // => int a = 2 * 3 < 4 ? 3 : 4;
    // => int a = 6 < 4 ? 3 : 4;
    // => int a = 4;
    

    嘛,写程序这个东西,bug出来了,原因知道了,事后大家就都是诸葛亮了。因为小于和比较符号的优先级是较低的,所以乘法先被运算了,修正非常简单嘛,加括号就好了。

    //Version 2.0
    #define MIN(A,B) (A < B ? A : B)
    

    这次2 * MIN(3, 4)这样的式子就轻松愉快地拿下了。经过了这次修改,我们对自己的宏信心大增了…直到,某一天一个怒气冲冲的同事跑来摔键盘,然后给出了一个这样的例子:

    int a = MIN(3, 4 < 5 ? 4 : 5);
    printf("%d",a);
    // => 4
    

    简单的相比较三个数字并找到最小的一个而已,要怪就怪你没有提供三个数字比大小的宏,可怜的同事只好自己实现4和5的比较。在你开始着手解决这个问题的时候,你首先想到的也许是既然都是求最小值,那写成MIN(3, MIN(4, 5))是不是也可以。于是你就随手这样一改,发现结果变成了3,正是你想要的..接下来,开始怀疑之前自己是不是看错结果了,改回原样,一个4赫然出现在屏幕上。你终于意识到事情并不是你想像中那样简单,于是还是回到最原始直接的手段,展开宏。

    int a = MIN(3, 4 < 5 ? 4 : 5);
    // => int a = (3 < 4 < 5 ? 4 : 5 ? 3 : 4 < 5 ? 4 : 5);  //希望你还记得运算符优先级
    //  => int a = ((3 < (4 < 5 ? 4 : 5) ? 3 : 4) < 5 ? 4 : 5);  //为了您不太纠结,我给这个式子加上了括号
    //   => int a = ((3 < 4 ? 3 : 4) < 5 ? 4 : 5)
    //    => int a = (3 < 5 ? 4 : 5)
    //     => int a = 4
    

    找到问题所在了,由于展开时连接符号和被展开式子中的运算符号优先级相同,导致了计算顺序发生了变化,实质上和我们的1.0版遇到的问题是差不多的,还是考虑不周。那么就再严格一点吧,3.0版!

    //Version 3.0
    #define MIN(A,B) ((A) < (B) ? (A) : (B))
    

    至于为什么2.0版本中的MIN(3, MIN(4, 5))没有出问题,可以正确使用,这里作为练习,大家可以试着自己展开一下,来看看发生了什么。

    经过两次悲剧,你现在对这个简单的宏充满了疑惑。于是你跑了无数的测试用例而且它们都通过了,我们似乎彻底解决了括号问题,你也认为从此这个宏就妥妥儿的哦了。不过如果你真的这么想,那你就图样图森破了。生活总是残酷的,该来的bug也一定是会来的。不出意外地,在一个雾霾阴沉的下午,我们又收到了一个出问题的例子。

    float a = 1.0f;
    float b = MIN(a++, 1.5f);
    printf("a=%f, b=%f",a,b);
    // => a=3.000000, b=2.000000
    

    拿到这个出问题的例子你的第一反应可能和我一样,这TM的谁这么二货还在比较的时候搞++,这简直乱套了!但是这样的人就是会存在,这样的事就是会发生,你也不能说人家逻辑有错误。a是1,a++表示先使用a的值进行计算,然后再加1。那么其实这个式子想要计算的是取a和b的最小值,然后a等于a加1:所以正确的输出a为2,b为1才对!嘛,满眼都是泪,让我们这些久经摧残的程序员淡定地展开这个式子,来看看这次又发生了些什么吧:

    float a = 1.0f;
    float b = MIN(a++, 1.5f);
    // => float b = ((a++) < (1.5f) ? (a++) : (1.5f))
    

    其实只要展开一步就很明白了,在比较a++和1.5f的时候,先取1和1.5比较,然后a自增1。接下来条件比较得到真以后又触发了一次a++,此时a已经是2,于是b得到2,最后a再次自增后值为3。出错的根源就在于我们预想的是a++只执行一次,但是由于宏展开导致了a++被多执行了,改变了预想的逻辑。解决这个问题并不是一件很简单的事情,使用的方式也很巧妙。我们需要用到一个GNU C的赋值扩展,即使用({...})的形式。这种形式的语句可以类似很多脚本语言,在顺次执行之后,会将最后一次的表达式的赋值作为返回。举个简单的例子,下面的代码执行完毕后a的值为3,而且b和c只存在于大括号限定的代码域中

    int a = ({
        int b = 1;
        int c = 2;
        b + c;
    });
    // => a is 3
    

    有了这个扩展,我们就能做到之前很多做不到的事情了。比如彻底解决MIN宏定义的问题,而也正是GNU C中MIN的标准写法

    //GNUC MIN
    #define MIN(A,B)	({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })
    

    这里定义了三个语句,分别以输入的类型申明了__a__b,并使用输入为其赋值,接下来做一个简单的条件比较,得到__a__b中的较小值,并使用赋值扩展将结果作为返回。这样的实现保证了不改变原来的逻辑,先进行一次赋值,也避免了括号优先级的问题,可以说是一个比较好的解决方案了。如果编译环境支持GNU C的这个扩展,那么毫无疑问我们应该采用这种方式来书写我们的MIN宏,如果不支持这个环境扩展,那我们只有人为地规定参数不带运算或者函数调用,以避免出错。

    关于MIN我们讨论已经够多了,但是其实还存留一个悬疑的地方。如果在同一个scope内已经有__a或者__b的定义的话(虽然一般来说不会出现这种悲剧的命名,不过谁知道呢),这个宏可能出现问题。在申明后赋值将因为定义重复而无法被初始化,导致宏的行为不可预知。如果您有兴趣,不妨自己动手试试看结果会是什么。Apple在Clang中彻底解决了这个问题,我们把Xcode打开随便建一个新工程,在代码中输入MIN(1,1),然后Cmd+点击即可找到clang中 MIN的写法。为了方便说明,我直接把相关的部分抄录如下:

    //CLANG MIN
    #define __NSX_PASTE__(A,B) A##B
    
    #define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
    
    #define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); __typeof__(B) __NSX_PASTE__(__b,L) = (B); (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); })
    

    似乎有点长,看起来也很吃力。我们先美化一下这宏,首先是最后那个__NSMIN_IMPL__内容实在是太长了。我们知道代码的话是可以插入换行而不影响含义的,宏是否也可以呢?答案是肯定的,只不过我们不能使用一个单一的回车来完成,而必须在回车前加上一个反斜杠\。改写一下,为其加上换行好看些:

    #define __NSX_PASTE__(A,B) A##B
    
    #define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
    
    #define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); \
                                     __typeof__(B) __NSX_PASTE__(__b,L) = (B); \
                                     (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); \
                                  })
    

    但可以看出MIN一共由三个宏定义组合而成。第一个__NSX_PASTE__里出现的两个连着的井号##在宏中是一个特殊符号,它表示将两个参数连接起来这种运算。注意函数宏必须是有意义的运算,因此你不能直接写AB来连接两个参数,而需要写成例子中的A##B。宏中还有一切其他的自成一脉的运算符号,我们稍后还会介绍几个。接下来是我们调用的两个参数的MIN,它做的事是调用了另一个三个参数的宏__NSMIN_IMPL__,其中前两个参数就是我们的输入,而第三个__COUNTER__我们似乎不认识,也不知道其从何而来。其实__COUNTER__是一个预定义的宏,这个值在编译过程中将从0开始计数,每次被调用时加1。因为唯一性,所以很多时候被用来构造独立的变量名称。有了上面的基础,再来看最后的实现宏就很简单了。整体思路和前面的实现和之前的GNUC MIN是一样的,区别在于为变量名__a__b添加了一个计数后缀,这样大大避免了变量名相同而导致问题的可能性(当然如果你执拗地把变量叫做__a9527并且出问题了的话,就只能说不作死就不会死了)。

    花了好多功夫,我们终于把一个简单的MIN宏彻底搞清楚了。宏就是这样一类东西,简单的表面之下隐藏了很多玄机,可谓小有乾坤。作为练习大家可以自己尝试一下实现一个SQUARE(A),给一个数字输入,输出它的平方的宏。虽然一般这个计算现在都是用inline来做了,但是通过和MIN类似的思路我们是可以很好地实现它的,动手试一试吧 :)

    Log,永恒的主题

    Log人人爱,它为我们指明前进方向,它为我们抓虫提供帮助。在objc中,我们最多使用的log方法就是NSLog输出信息到控制台了,但是NSLog的标准输出可谓残废,有用信息完全不够,比如下面这段代码:

    NSArray *array = @[@"Hello", @"My", @"Macro"];
    NSLog (@"The array is %@", array);
    

    打印到控制台里的结果是类似这样的

    2014-01-20 11:22:11.835 TestProject[23061:70b] The array is (
        Hello,
        My,
        Macro
    )
    

    我们在输出的时候关心什么?除了结果以外,很多情况下我们会对这行log的所在的文件位置方法什么的会比较关心。在每次NSLog里都手动加上方法名字和位置信息什么的无疑是个笨办法,而如果一个工程里已经有很多NSLog的调用了,一个一个手动去改的话无疑也是噩梦。我们通过宏,可以很简单地完成对NSLog原生行为的改进,优雅,高效。只需要在预编译的pch文件中加上

    //A better version of NSLog
    #define NSLog(format, ...) do {                                                                          \
                                 fprintf(stderr, "<%s : %d> %s\n",                                           \
                                 [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \
                                 __LINE__, __func__);                                                        \
                                 (NSLog)((format), ##__VA_ARGS__);                                           \
                                 fprintf(stderr, "-------\n");                                               \
                               } while (0)
    

    嘛,这是我们到现在为止见到的最长的一个宏了吧…没关系,一点一点来分析就好。首先是定义部分,第2行的NSLog(format, ...)。我们看到的是一个函数宏,但是它的参数比较奇怪,第二个参数是...,在宏定义(其实也包括函数定义)的时候,写为...的参数被叫做可变参数(variadic)。可变参数的个数不做限定。在这个宏定义中,除了第一个参数format将被单独处理外,接下来输入的参数将作为整体一并看待。回想一下NSLog的用法,我们在使用NSLog时,往往是先给一个format字符串作为第一个参数,然后根据定义的格式在后面的参数里跟上写要输出的变量之类的。这里第一个格式化字符串即对应宏里的format,后面的变量全部映射为...作为整体处理。

    接下来宏的内容部分。上来就是一个下马威,我们遇到了一个do while语句…想想看你上次使用do while是什么时候吧?也许是C程序设计课的大作业?或者是某次早已被遗忘的算法面试上?总之虽然大家都是明白这个语句的,但是实际中可能用到它的机会少之又少。乍一看似乎这个do while什么都没做,因为while是0,所以do肯定只会被执行一次。那么它存在的意义是什么呢,我们是不是可以直接简化一下这个宏,把它给去掉,变成这个样子呢?

    //A wrong version of NSLog
    #define NSLog(format, ...)   fprintf(stderr, "<%s : %d> %s\n",                                           \
                                 [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \
                                 __LINE__, __func__);                                                        \
                                 (NSLog)((format), ##__VA_ARGS__);                                           \
                                 fprintf(stderr, "-------\n");                                               
    

    答案当然是否定的,也许简单的测试里你没有遇到问题,但是在生产环境中这个宏显然悲剧了。考虑下面的常见情况

    if (errorHappend)
        NSLog(@"Oops, error happened");
    

    展开以后将会变成

    if (errorHappend)
        fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
    (NSLog)((format), ##__VA_ARGS__); //I will expand this later
    fprintf(stderr, "-------\n");
    

    注意..C系语言可不是靠缩进来控制代码块和逻辑关系的。所以说如果使用这个宏的人没有在条件判断后加大括号的话,你的宏就会一直调用真正的NSLog输出东西,这显然不是我们想要的逻辑。当然在这里还是需要重新批评一下认为if后的单条执行语句不加大括号也没问题的同学,这是陋习,无需理由,请改正。不论是不是一条语句,也不论是if后还是else后,都加上大括号,是对别人和自己的一种尊重。

    好了知道我们的宏是如何失效的,也就知道了修改的方法。作为宏的开发者,应该力求使用者在最大限度的情况下也不会出错,于是我们想到直接用一对大括号把宏内容括起来,大概就万事大吉了?像这样:

    //Another wrong version of NSLog
    #define NSLog(format, ...)   {
                                   fprintf(stderr, "<%s : %d> %s\n",                                           \
                                   [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \
                                   __LINE__, __func__);                                                        \
                                   (NSLog)((format), ##__VA_ARGS__);                                           \
                                   fprintf(stderr, "-------\n");                                               \
                                 }
    

    展开刚才的那个式子,结果是

    //I am sorry if you don't like { in the same like. But I am a fan of this style :P
    if (errorHappend) {
        fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
        (NSLog)((format), ##__VA_ARGS__);
        fprintf(stderr, "-------\n");
    };
    

    编译,执行,正确!因为用大括号标识代码块是不会嫌多的,所以这样一来的话我们的宏在不论if后面有没有大括号的情况下都能工作了!这么看来,前面例子中的do while果然是多余的?于是我们又可以愉快地发布了?如果你够细心的话,可能已经发现问题了,那就是上面最后的一个分号。虽然编译运行测试没什么问题,但是始终稍微有些刺眼有木有?没错,因为我们在写NSLog本身的时候,是将其当作一条语句来处理的,后面跟了一个分号,在宏展开后,这个分号就如同噩梦一般的多出来了。什么,你还没看出哪儿有问题?试试看展开这个例子吧:

    if (errorHappend)
        NSLog(@"Oops, error happened");
    else
    	//Yep, no error, I am happy~ :)
    

    No! I am not haapy at all! 因为编译错误了!实际上这个宏展开以后变成了这个样子:

    if (errorHappend) {
        fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
        (NSLog)((format), ##__VA_ARGS__);
        fprintf(stderr, "-------\n");
    }; else {
        //Yep, no error, I am happy~ :)
    }
    

    因为else前面多了一个分号,导致了编译错误,很恼火..要是写代码的人乖乖写大括号不就啥事儿没有了么?但是我们还是有巧妙的解决方法的,那就是上面的do while。把宏的代码块添加到do中,然后之后while(0),在行为上没有任何改变,但是可以巧妙地吃掉那个悲剧的分号,使用do while的版本展开以后是这个样子的

    if (errorHappend) 
    	do {
            fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
            (NSLog)((format), ##__VA_ARGS__);
            fprintf(stderr, "-------\n");
        } while (0);
    else {
        //Yep, no error, I am really happy~ :)
    }
    

    这个吃掉分号的方法被大量运用在代码块宏中,几乎已经成为了标准写法。而且while(0)的好处在于,在编译的时候,编译器基本都会为你做好优化,把这部分内容去掉,最终编译的结果不会因为这个do while而导致运行效率上的差异。在终于弄明白了这个奇怪的do while之后,我们终于可以继续深入到这个宏里面了。宏本体内容的第一行没有什么值得多说的fprintf(stderr, "<%s : %d> %s\n",,简单的格式化输出而已。注意我们使用了\将这个宏分成了好几行来写,实际在最后展开时会被合并到同一行内,我们在刚才MIN最后也用到了反斜杠,希望你还能记得。接下来一行我们填写这个格式输出中的三个token,

    [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
    

    这里用到了三个预定义宏,和刚才的__COUNTER__类似,预定义宏的行为是由编译器指定的。__FILE__返回当前文件的绝对路径,__LINE__返回展开该宏时在文件中的行数,__func__是改宏所在scope的函数名称。我们在做Log输出时如果带上这这三个参数,便可以加快解读Log,迅速定位。关于编译器预定义的Log以及它们的一些实现机制,感兴趣的同学可以移步到gcc文档的PreDefine页面和clang的Builtin Macro进行查看。在这里我们将格式化输出的三个参数分别设定为文件名的最后一个部分(因为绝对路径太长很难看),行数,以及方法名称。

    接下来是还原原始的NSLog,(NSLog)((format), ##__VA_ARGS__);中出现了另一个预定义的宏__VA_ARGS__(我们似乎已经找出规律了,前后双下杠的一般都是预定义)。__VA_ARGS__表示的是宏定义中的...中的所有剩余参数。我们之前说过可变参数将被统一处理,在这里展开的时候编译器会将__VA_ARGS__直接替换为输入中从第二个参数开始的剩余参数。另外一个悬疑点是在它前面出现了两个井号##。还记得我们上面在MIN中的两个井号么,在那里两个井号的意思是将前后两项合并,在这里做的事情比较类似,将前面的格式化字符串和后面的参数列表合并,这样我们就得到了一个完整的NSLog方法了。之后的几行相信大家自己看懂也没有问题了,最后输出一下试试看,大概看起来会是这样的。

    -------
    <AppDelegate.m : 46> -[AppDelegate application:didFinishLaunchingWithOptions:]
    2014-01-20 16:44:25.480 TestProject[30466:70b] The array is (
        Hello,
        My,
        Macro
    )
    -------
    

    带有文件,行号和方法的输出,并且用横杠隔开了(请原谅我没有质感的设计,也许我应该画一只牛,比如这样?),debug的时候也许会轻松一些吧 :)

    hello cowsay

    这个Log有三个悬念点,首先是为什么我们要把format单独写出来,然后吧其他参数作为可变参数传递呢?如果我们不要那个format,而直接写成NSLog(...)会不会有问题?对于我们这里这个例子来说的话是没有变化的,但是我们需要记住的是...是可变参数列表,它可以代表一个、两个,或者是很多个参数,但同时它也能代表零个参数。如果我们在申明这个宏的时候没有指定format参数,而直接使用参数列表,那么在使用中不写参数的NSLog()也将被匹配到这个宏中,导致编译无法通过。如果你手边有Xcode,也可以看看Cocoa中真正的NSLog方法的实现,可以看到它也是接收一个格式参数和一个参数列表的形式,我们在宏里这么定义,正是为了其传入正确合适的参数,从而保证使用者可以按照原来的方式正确使用这个宏。

    第二点是既然我们的可变参数可以接受任意个输入,那么在只有一个format输入,而可变参数个数为零的时候会发生什么呢?不妨展开看一看,记住##的作用是拼接前后,而现在##之后的可变参数是空:

    NSLog(@"Hello");
    => do {
           fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
           (NSLog)((@"Hello"), );
           fprintf(stderr, "-------\n");
       } while (0);
    
    

    中间的一行(NSLog)(@"Hello", );似乎是存在问题的,你一定会有疑惑,这种方式怎么可能编译通过呢?!原来大神们其实早已想到这个问题,并且进行了一点特殊的处理。这里有个特殊的规则,在逗号__VA_ARGS__之间的双井号,除了拼接前后文本之外,还有一个功能,那就是如果后方文本为空,那么它会将前面一个逗号吃掉。这个特性当且仅当上面说的条件成立时才会生效,因此可以说是特例。加上这条规则后,我们就可以将刚才的式子展开为正确的(NSLog)((@"Hello"));了。

    最后一个值得讨论的地方是(NSLog)((format), ##__VA_ARGS__);的括号使用。把看起来能去掉的括号去掉,写成NSLog(format, ##__VA_ARGS__);是否可以呢?在这里的话应该是没有什么大问题的,首先format不会被调用多次也不太存在误用的可能性(因为最后编译器会检查NSLog的输入是否正确)。另外你也不用担心展开以后式子里的NSLog会再次被自己展开,虽然展开式中NSLog也满足了我们的宏定义,但是宏的展开非常聪明,展开后会自身无限循环的情况,就不会再次被展开了。

    作为一个您读到了这里的小奖励,附送三个debug输出rect,size和point的宏,希望您能用上(嗯..想想曾经有多少次你需要打印这些结构体的某个数字而被折磨致死,让它们玩儿蛋去吧!当然请先加油看懂它们吧)

    #define NSLogRect(rect) NSLog(@"%s x:%.4f, y:%.4f, w:%.4f, h:%.4f", #rect, rect.origin.x, rect.origin.y, rect.size.width, rect.size.height)
    #define NSLogSize(size) NSLog(@"%s w:%.4f, h:%.4f", #size, size.width, size.height)
    #define NSLogPoint(point) NSLog(@"%s x:%.4f, y:%.4f", #point, point.x, point.y)
    

    两个实际应用的例子

    当然不是说上面介绍的宏实际中不能用。它们相对简单,但是里面坑不少,所以显得很有特点,非常适合作为入门用。而实际上在日常中很多我们常用的宏并没有那么多奇怪的问题,很多时候我们按照想法去实现,再稍微注意一下上述介绍的可能存在的共通问题,一个高质量的宏就可以诞生。如果能写出一些有意义价值的宏,小了从对你的代码的使用者来说,大了从整个社区整个世界和减少碳排放来说,你都做出了相当的贡献。我们通过几个实际的例子来看看,宏是如何改变我们的生活,和写代码的习惯的吧。

    先来看看这两个宏

    #define XCTAssertTrue(expression, format...) \
        _XCTPrimitiveAssertTrue(expression, ## format)
    
    #define _XCTPrimitiveAssertTrue(expression, format...) \
    ({ \
        @try { \
            BOOL _evaluatedExpression = !!(expression); \
            if (!_evaluatedExpression) { \
                _XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 0, @#expression),format); \
            } \
        } \
        @catch (id exception) { \
            _XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 1, @#expression, [exception reason]),format); \
        }\
    })
    

    如果您常年做苹果开发,却没有见过或者完全不知道XCTAssertTrue是什么的话,强烈建议补习一下测试驱动开发的相关知识,我想应该会对您之后的道路很有帮助。如果你已经很熟悉这个命令了,那我们一起开始来看看幕后发生了什么。

    有了上面的基础,相信您大体上应该可以自行解读这个宏了。({...})的语法和##都很熟悉了,这里有三个值得注意的地方,在这个宏的一开始,我们后面的的参数是format...,这其实也是可变参数的一种写法,和...__VA_ARGS__配对类似,{NAME}...将于{NAME}配对使用。也就是说,在这里宏内容的format指代的其实就是定义的先对expression取了两次反?我不是科班出身,但是我还能依稀记得这在大学程序课上讲过,两次取反的操作可以确保结果是BOOL值,这在objc中还是比较重要的(关于objc中BOOL的讨论已经有很多,如果您还没能分清BOOL, bool和Boolean,可以参看NSHisper的这篇文章)。然后就是@#expression这个式子。我们接触过双井号##,而这里我们看到的操作符是单井号#,注意井号前面的@是objc的编译符号,不属于宏操作的对象。单个井号的作用是字符串化,简单来说就是将替换后在两头加上”“,转为一个C字符串。这里使用@然后紧跟#expression,出来后就是一个内容是expression的内容的NSString。然后这个NSString再作为参数传递给_XCTRegisterFailure_XCTFailureDescription等,继续进行展开,这些是后话。简单一瞥,我们大概就可以想象宏帮助我们省了多少事儿了,如果各位看官要是写个断言还要来个十多行的话,想象都会疯掉的吧。

    另外一个例子,找了人民群众喜闻乐见的ReactiveCocoa(RAC)中的一个宏定义。对于RAC不熟悉或者没听过的朋友,可以简单地看看Limboy的一系列相关博文(搜索ReactiveCocoa),介绍的很棒。如果觉得“哇哦这个好酷我很想学”的话,不妨可以跟随raywenderlich上这个系列的教程做一些实践,里面简单地用到了RAC,但是都已经包含了RAC的基本用法了。RAC中有几个很重要的宏,它们是保证RAC简洁好用的基本,可以说要是没有这几个宏的话,是不会有人喜欢RAC的。其中RACObserve就是其中一个,它通过KVC来为对象的某个属性创建一个信号返回(如果你看不懂这句话,不要担心,这对你理解这个宏的写法和展开没有任何影响)。对于这个宏,我决定不再像上面那样展开和讲解,我会在最后把相关的宏都贴出来,大家不妨拿它练练手,看看能不能将其展开到代码的状态,并且明白其中都发生了些什么。如果你遇到什么问题或者在展开过程中有所心得,欢迎在评论里留言分享和交流 :)

    好了,这篇文章已经够长了。希望在看过以后您在看到宏的时候不再发怵,而是可以很开心地说这个我会这个我会这个我也会。最终目标当然是写出漂亮高效简洁的宏,这不论对于提高生产力还是~震慑你的同事~提升自己实力都会很有帮助。

    另外,在这里一定要宣传一下关注了很久的@hangcom 吴航前辈的新书《iOS应用逆向工程》。很荣幸能够在发布之前得到前辈的允许拜读了整本书,可以说看的畅快淋漓。我之前并没有越狱开发的任何基础,也对相关领域知之甚少,在这样的前提下跟随书中的教程和例子进行探索的过程可以说是十分有趣。我也得以能够用不同的眼光和高度来审视这几年所从事的iOS开发行业,获益良多。可以说《iOS应用逆向工程》是我近期所愉快阅读到的很cool的一本好书。现在这本书还在预售中,但是距离1月28日的正式发售已经很近,有兴趣的同学可以前往亚马逊或者ChinaPub的相关页面预定,相信这本书将会是iOS技术人员非常棒的春节读物。

    最后是我们说好的留给大家玩的练习,我加了一点注释帮助大家稍微理解每个宏是做什么的,在文章后面留了一块试验田,大家可以随便填写玩弄。总之,加油!

    //调用 RACSignal是类的名字
    RACSignal *signal = RACObserve(self, currentLocation);
    
    //以下开始是宏定义
    //rac_valuesForKeyPath:observer:是方法名
    #define RACObserve(TARGET, KEYPATH) \
        [(id)(TARGET) rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]
        
    #define keypath(...) \
        metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
    
    //这个宏在取得keypath的同时在编译期间判断keypath是否存在,避免误写
    //您可以先不用介意这里面的巫术..
    #define keypath1(PATH) \
        (((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
    
    #define keypath2(OBJ, PATH) \
        (((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
    
    //A和B是否相等,若相等则展开为后面的第一项,否则展开为后面的第二项
    //eg. metamacro_if_eq(0, 0)(true)(false) => true
    //    metamacro_if_eq(0, 1)(true)(false) => false
    #define metamacro_if_eq(A, B) \
            metamacro_concat(metamacro_if_eq, A)(B)
    
    #define metamacro_if_eq1(VALUE) metamacro_if_eq0(metamacro_dec(VALUE))
    
    #define metamacro_if_eq0(VALUE) \
        metamacro_concat(metamacro_if_eq0_, VALUE)
    
    #define metamacro_if_eq0_1(...) metamacro_expand_
    
    #define metamacro_expand_(...) __VA_ARGS__
    
    #define metamacro_argcount(...) \
            metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
    
    #define metamacro_at(N, ...) \
            metamacro_concat(metamacro_at, N)(__VA_ARGS__)
            
    #define metamacro_concat(A, B) \
            metamacro_concat_(A, B)
    
    #define metamacro_concat_(A, B) A ## B
    
    #define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__)
    
    #define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)
    
    #define metamacro_head(...) \
            metamacro_head_(__VA_ARGS__, 0)
    
    #define metamacro_head_(FIRST, ...) FIRST
    
    #define metamacro_dec(VAL) \
            metamacro_at(VAL, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
    
    //调用 RACSignal是类的名字RACSignal *signal = RACObserve(self, currentLocation);
    展开全文
  • 宏定义#define

    2017-04-30 21:55:54
    #define N (10) // 宏定义 #define M (10 + N) #define PI (3.14) #define S(r) (PI * (r) * (r)) // 计算园面积的宏 #define L(r) (2 * PI * (r)) // 计算圆的周长的宏 // 题目,使用宏定义定义一个宏,...
    #define N (10)			// 宏定义
    
    #define M (10 + N)
    
    #define PI	(3.14)
    
    #define S(r) (PI * (r) * (r))		// 计算园面积的宏
    #define L(r) (2 * PI * (r))			// 计算圆的周长的宏
    
    // 题目,使用宏定义定义一个宏,表示一年中的秒数
    #define SEC_PER_YEAR (365 * 24 * 60 * 60)UL //UL表示强制把这个类型转换为unsigned long
    
    
    
    
    
    展开全文
  • 常用宏定义#define

    2020-07-30 23:32:14
    常用宏定义#define
  • #define是C语言中提供的宏定义命令,其主要目的是为程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率,但大家在学习时往往不能理解该命令的本质,总是在此处产生一些困惑,在编程时误用该命令,...
  • #define宏定义详解

    2017-10-10 10:05:55
    #define宏定义的详细用法
  • 宏定义#define整理

    2014-04-01 13:02:28
    一、宏定义#define    优点:一方面可以节省程序的空间上的篇幅,另外,恰当地使用宏定义可提高程序的时间效率。代码可以写的通俗易懂。可以提高程序的清晰性、可读性,使于修改移植等。   缺点:宏定义的...
  • C语言中用到宏定义的地方很多,如在头文件中为了防止头文件被重复包含,则用到: #ifndef cTest_Header_h #define cTest_Header_h //头文件内容 #endif 在我们常用的 stdio.h 头文件中也可以见到很多...
  • 详解宏定义(#define)

    2016-02-15 20:23:21
    C语言中用到宏定义的地方很多,如在头文件中为了防止头文件被重复包含,则用到:#ifndef cTest_Header_h #define cTest_Header_h //头文件内容 #endif在我们常用的 stdio.h 头文件中也可以见到很多宏定义,如:#...
  • 宏定义在C/C++中使用的非常多,一方面定义一些常量,另一方面定义一些通用函数,但是有些宏定义实现较为复杂,尤其是很多带#或##的宏定义,令很多人读起来很不解,下面就简单介绍一下宏定义中的#和##。 宏定义里面...
  • 宏定义有无参数宏定义和带参数宏定义两种。  无参数的宏定义的一般形式为  # define 标识符 字符序列 其中# define之后的标识符称为宏定义名(简称宏名),要求宏名与字符序列之间用空格符分隔。这种宏定义要求...
  • C语言常用宏定义常用宏定义数值相关的宏定义字符相关的宏定义byte相关的宏定义bit相关的宏定义数组与结构体相关的宏定义对齐的宏定义 常用宏定义 数值相关的宏定义 闰年的判断 ,年份可以整除4并且不能整除...
  • 宏定义是比较常用的预处理指令,即使用“标识符”来表示“替换列表”中的内容。标识符称为宏名,在预处理过程中,预处理器会把源程序中所有宏名,替换成宏定义中替换列表中的内容。 常见的宏定义有两种,不带参数的...
  • 要写好C语言,漂亮的宏定义是非常重要的。宏定义可以帮助我们防止出错,提高代码的可移植性和可读性等。  在软件开发过程中,经常有一些常用或者通用的功能或者代码段,这些功能既可以写成函数,也可以封装成为...
  • 使用#define指令进行宏定义 宏定义的功能是用一个标识符来表示字符串,标识符称为宏名。在预编译处理时,对程序中出现的宏名,在程序中出现的宏名,都用宏定义中出现的字符串去代替。  #define 指令的形式是: #...
  • LaTex简单宏定义

    2018-10-27 10:45:56
    Latex简单宏定义   众所周知,使用宏定义可以优化代码长度,那么latex是怎么使用宏定义的呢?本文为一个简单的宏定义例子。 定义格式 \def\NEWCOM#1\{\COMMAND{PARA}{#1}} 应用格式 \NEWCOM{PARA}。 例子 S1:...
  • 一般来说,宏定义(# define)是相当于字符替换,在代码预编译时就进行替换,编译时候所有宏定义的代码改变为被替换的代码,不限制任何代码,任何字符都可以进行宏定义宏定义可以写在程序中的任何位置,它的作用...
  • C语言基础之宏定义 宏定义:是C语言提供的三种预处理功能的其中一种,这三种预处理包括:宏定义、文件包含、条件编译。宏定义和操作符的区别是:宏定义是替换,不做计算,也不做表达式求解。#define预处理指令可以...
1 2 3 4 5 ... 20
收藏数 367,044
精华内容 146,817
关键字:

宏定义