C语言宏定义详解

宏定义引入

源程序在编译之前,会先进行预处理。

预处理并不是C语言编译器的组成部分,不能直接对它们进行编译。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对预处理之后的源程序进行编译处理,再经过链接得到可供执行的目标代码。

C 语言提供的预处理功能有三种,分别为宏定义、文件包含和条件编译。

宏定义在 C语言源程序中允许用一个标识符来表示一个字符串,称为“宏/宏体” ,被定义为“宏”的标识符称为“宏名”。

在预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去完全替换,这称为“宏替换”或“宏展开”。

宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。

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

#define 标识符 字符串

#表示这是一条预处理命令(在C语言中凡是以#开头的均为预处理命令)

define:宏定义命令
标识符:所定义的宏名
字符串:可以是常数、表达式、格式串等

无参宏和有参宏示例:

// 不带参数的宏定义
#define MAX 10

/*带参宏定义*/
#define M(y) (((y)*(y))+(3*(y)))

/*宏调用*/
k=M(MAX);

注意宏不是语句,结尾不需要加“;”,否则会被替换进程序中,如:

#define N 10;               // 宏定义
int c[N];                   // 会被替换为: int c[10;]; 
//error:… main.c:133:11: Expected ']'

如果要写宏不止一行,则在结尾加反斜线符号使得多行能连接上,如:

#define HELLO "hello 
the world"

而且注意第二行要对齐,不要出现预期之外的空格,否则,如:

#define HELLO "hello the wo
  rld"
printf("HELLO is %sn", HELLO);
//输出结果为: HELLO is hello the wo  rld 

也就是行与行之间的空格也会被作为替换文本的一部分
而且由这个例子也可以看出:宏名如果出现在源程序中的“”内,则不会被当做宏来进行宏代换。

宏定义的优点:

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

提高程序的运行效率

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

宏定义的缺点:

1、由于是直接嵌入的,所以代码可能相对多一点;

2、嵌套定义过多可能会影响程序的可读性,而且很容易出错,不容易调试。

3、对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。

注意:如果宏体为空,就相当于用空字符来替换。但是,尽可能不要这么做,而且也没必要这么做。很多时候都会报错的。

什么时候用宏定义

宏函数,函数比较

从时间上来看

  • 宏只占编译时间,函数调用则占用运行时间(分配单元,保存现场,值传递,返回),每次执行都要载入,所以执行相对宏会较慢。
  • 使用宏次数多时,宏展开后源程序很长,因为每展开一次都使程序增长,但是执行起来比较快一点(这也不是绝对的,当有很多宏展开,目标文件很大,执行的时候运行时系统换页频繁,效率就会低下)。而函数调用不使源程序变长。

从安全上来看

  • 函数调用时,先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。
  • 函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
  • 对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
  • 宏的定义很容易产生二义性,如:定义#define S(a)  (a)*(a),代码S(a++),宏展开变成(a++)*(a++)这个大家都知道,在不同编译环境下会有不同结果。
  • 调用函数只可得到一个返回值,且有返回类型,而宏没有返回值和返回类型,但是用宏可以设法得到几个结果。
  • 函数体内有Bug,可以在函数体内打断点调试。如果宏体内有Bug,那么在执行的时候是不能对宏调试的,即不能深入到宏内部。
  • 附:C++中宏不能访问对象的私有成员,但是成员函数就可以。

宏函数的适用范围

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

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

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

所以几乎可以这样认为:内联函数就是带了参数静态类型检查的宏。

宏使用注意事项

1、宏可以嵌套,但不参与运算:

#define M 5                 // 宏定义
#define MM M * M            // 宏的嵌套
printf("MM = %dn", MM);    // MM 被替换为: MM = M * M, 然后又变成 MM = 5 * 5

宏代换的过程在上句已经结束,实际的 5 * 5 相乘过程则在编译阶段完成,而不是在预处理器工作阶段完成,所以宏不进行运算,它只是按照指令进行文字的替换操作。再强调下,宏进行简单的文本替换,无论替换文本中是常数、表达式或者字符串等,预处理程序都不做任何检查,如果出现错误,只能是被宏代换之后的程序在编译阶段发现。
 

2、宏定义必须写在函数之外,其作用域是 #define 开始,到源程序结束。如果要提前结束它的作用域则用 #undef命令,如:

#define M 5                 // 宏定义
printf("M = %dn", M);      // 输出结果为: M = 5
#undef M              // 取消宏定义
printf("M = %dn", M);      // error:… main.c:138:24: Use of undeclared identifier 'M'

一般情况下宏定义命令#define出现在函数的外部,宏名的有效范围是:从定义命令之后,到本文件结束。通常,宏定义命令放在文件开头处。但是是可以放在程序的函数内部的,只不过此时就违反了宏定义的初衷,而且只能在该函数内部使用。反正,就当做不能用在函数里就行,有的地方用也没错。

3、可以用宏定义表示数据类型,可以使代码简便(但不推荐):

#define STU struct Student      // 宏定义STU
struct Student{                 // 定义结构体Student
    char *name;
    int sNo;
};
STU stu = {"Jack", 20};         // 被替换为:struct Student stu = {"Jack", 20};
printf("name: %s, sNo: %dn", stu.name, stu.sNo);

4、如果重复定义宏(应当避免),则不同的编译器采用不同的重定义策略。有的编译器认为这是错误的,有的则只是提示警告。Xcode中采用第二种方式。如:

#define M 5                 //宏定义
#define M 100               //重定义,warning:… main.c:26:9: 'M' macro redefined

5、建议给宏体和引用的每个参数加括号

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

#define MULTIPLY(x, y) x * y

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

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

6、注意分号吞噬问题

有如下宏定义:

#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将不会被执行,因为if语句后有个分号;表示判断语句就结束了,后面的else是无效代码。

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

#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)构造后的宏定义不会受到大括号、分号等的影响,总是会按你期望的方式调用运行。这是一种形式上的解决方案,代码其实也只会执行一次,只是巧妙借用do…while的特性来将其包装成一整条语句。

7、对自身的递归引用

有如下宏定义:

#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))。

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

条件编译

假如现在要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?

这个程序的难点在于,不同平台下控制文字颜色的代码不一样,我们必须要能够识别出不同的平台

Windows 有专有的宏_WIN32,Linux 有专有的宏__linux__,以现有的知识,我们很容易就想到了 if else,请看下面的代码:

1

2

3

4

5

6

7

8

9

10

11

12

#include <stdio.h>

int main(){

    if(_WIN32){

        system("color 0c");

        printf("http://www.baidu.comn");

    }else if(__linux__){

        printf("33[22;31mhttp://www.baidu.comn33[22;30m");

    }else{

        printf("http://www.baidu.comn");

    }

    return 0;

}

但这段代码是错误的,在 Windows 下提示 __linux__ 是未定义的标识符,在 Linux 下提示 _Win32 是未定义的标识符。对上面的代码进行改进:

1

2

3

4

5

6

7

8

9

10

11

12

#include <stdio.h>

int main(){

    #if _WIN32

        system("color 0c");

        printf("http://www.baidu.comn");

    #elif __linux__

        printf("33[22;31mhttp://www.baidu.comn33[22;30m");

    #else

        printf("http://www.baidu.comn");

    #endif

    return 0;

}

#if、#elif、#else 和 #endif 都是预处理命令,整段代码的意思是:如果宏 _WIN32 的值为真,就保留第 4、5 行代码,删除第 7、9 行代码;如果宏 __linux__ 的值为真,就保留第 7 行代码;如果所有的宏都为假,就保留第 9 行代码。

这些操作都是在预处理阶段完成的,多余的代码以及所有的宏都不会参与编译,不仅保证了代码的正确性,还减小了编译后文件的体积

这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能

条件编译有多种形式,下面一一讲解。

#if 命令

#if 命令的完整格式为:

它的意思是:如常量“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else。这一点和 if else 非常类似。

需要注意的是,#if 命令要求判断条件为“整型常量表达式”也就是说,表达式中不能包含变量,而且结果必须是整数;而 if 后面的表达式没有限制,只要符合语法就行。这是 #if 和 if 的一个重要区别。

#elif 和 #else 也可以省略,如下所示:

1

2

3

4

5

6

7

8

9

10

11

12

13

#include <stdio.h>

int main(){

    #if _WIN32

        printf("This is Windows!n");

    #else

        printf("Unknown platform!n");

    #endif

    

    #if __linux__

        printf("This is Linux!n");

    #endif

    return 0;

}

#ifdef 命令

这个命令通常是结合宏定义来使用。

#ifdef 命令的格式为:

它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译

也可以省略 #else

注意,因为一个宏要么定义了要么没定义,只有两种状态,所以最多只有一个分支。

举例:

VS/VC 有两种编译模式,Debug 和 Release。

  • 在学习过程中,我们通常使用 Debug 模式,这样便于程序的调试;
  • 而最终发布的程序,要使用 Release 模式,这样编译器会进行很多优化,提高程序运行效率,删除冗余信息

为了能够清楚地看到当前程序的编译模式,我们不妨在程序中增加提示,请看下面的代码:

1

2

3

4

5

6

7

8

9

10

11

#include <stdio.h>

#include <stdlib.h>

int main(){

    #ifdef _DEBUG

        printf("正在使用 Debug 模式编译程序...n");

    #else

        printf("正在使用 Release 模式编译程序...n");

    #endif

    system("pause");

    return 0;

}

当以 Debug 模式编译程序时,宏 _DEBUG 会被定义,预处器会保留第 5 行代码,删除第 7 行代码。反之会删除第 5 行,保留第 7 行。  

#ifndef 命令

#ifndef 命令的格式为:

与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反

区别

最后需要注意的是,

  • #if 后面跟的是“整型常量表达式”,
  • 而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的

例如,下面的形式只能用于 #if:

1

2

3

4

5

6

7

8

9

10

#include <stdio.h>

#define NUM 10

int main(){

    #if NUM == 10 || NUM == 20

        printf("NUM: %dn", NUM);

    #else

        printf("NUM Errorn");

    #endif

    return 0;

}

运行结果:NUM: 10

再如,两个宏都存在时编译代码A,否则编译代码B:

1

2

3

4

5

6

7

8

9

10

11

12

13

#include <stdio.h>

#define NUM1 10

#define NUM2 20

int main(){

    #if (defined NUM1 && defined NUM2)

        //代码A

        printf("NUM1: %d, NUM2: %dn", NUM1, NUM2);

    #else

        //代码B

        printf("Errorn");

    #endif

    return 0;

}

运行结果:NUM1: 10, NUM2: 20

#ifdef 可以认为是 #if defined 的缩写。 


#ifndef和#if !defined的区别

//---------------------------------------------------------------------------------------------------------------------
//第一种方式:使用ifndef
#ifndef __HEADFILE_H__
#define __HEADFILE_H__

……头文件内容

#endif
//---------------------------------------------------------------------------------------------------------------------
//第二种方式:使用if !defined
#if !defined(__HEADFILE_H__)
#define __HEADFILE_H__

……头文件内容

#endif
//---------------------------------------------------------------------------------------------------------------------

两种方式的效果是一样的,以前常用的是第一种方式,但是在使用类似如下逻辑表达的时候,使用第二种方式将更加方便。

#if !defined(HEADFILE1) && !defined(HEADFILE2)

#ifdef或者#ifndef都只能后接一个宏定义,如果有多个宏定义的逻辑表达式,就可以使用#if加上defined或者!defined的形式。

补充

1、预定义宏:


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

2、宏参数创建字符串:”#运算符”

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

#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;
}

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

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

4、可变宏:… 和_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)

宏定义中的空格问题

宏定义分为三个部分,宏定义指令,宏名,宏体

这三个部分是通过空格来识别以分隔的,三者之间至少需要1个空格才能被区分为三个部分,第二个部分,即宏名部分的有效部分是从第一个非空格字符开始,到遇到第一个空格字符结束;同理,宏体部分也是如此,以第一个非空格字符开始,直到遇到第一个空格字符作为结束。

也就是说,不管三者之间隔了多少个空格,都会被忽略,只认第一个非空格字符作为起始。

#define                 A                             123

比如以上宏定义,宏名就是A,宏体就是123,里面没有任何空格。

正因为如此,所以任意部分之间都不能存在空格,一旦存在空格,就等于不止三个部分了,此时,宏定义就会报错,比如:

#define                 A                             12       3  

这是就会报错了。所以,三部分都是一个不能以空格作为分隔的整体。

对比下typedef中的空格问题:

typedef在定义形式上和宏定义类似,所以,想看看这里的空格是什么样的情况。

首先能确定的是,正常情况下,typedef的三部分和宏定义是一样的:

typedef                      int                    int32;

这三部分不以空格作为分隔的整体,并且,三部分之间至少要有一个空格,多的空格被忽略。

但是有时候,typedef不止三个部分,比如:

这种情况仍然可行,看语句中的颜色,可以猜想到,对于类型来说,是作为一个整体来看待的,不管中间有多少空格,都会给缩到一个空格,按照unsigned short int类型来作为一个整体。

debug宏

可以用宏定义来实现条件编译
程序有DEBUG版本和RELEASE版本,区别就是编译时有无定义DEBUG宏。

程序调试的debug宏
程序调试的常见方案:单步调试、裸机LED调试、打印信息、log文件
1、利用调试器进行单步调试(譬如IDE中,Jlink)适用于新手,最大的好处就是直观,能够帮助找到问题。缺点是限制性大、速度慢。

2、裸机使用LED、蜂鸣器等硬件调试,适合单片机裸机程序。
3、printf函数打印调试,比较常用,好处是具有普遍性,几乎在所有的情况下都能用。
4、log文件(日志文件)是系统运行过程中在特定时候会打印一些调试信息,日志文件记录下来这些调试信息以供后续查找追查问题。适合于系统级或者大型程序的调试。

打印信息不能太多也不能太少,调试信息太少会不够信息找到问题所在。调试信息太多会有大量的无用的信息淹没有用信息,导致有用信息无法看见,等于没有。

调试(DEBUG)版本和发行(RELEASE)版本的区别
DEBUG版本就是包含了调试信息输出的版本,在程序测试过程中会发布debug版本,这种版本的程序运行时会打印出来调试信息/log文件,这些信息可以辅助测试人员判断程序的问题所在。DEBUG版本的坏处是输出调试信息占用了系统资源,拖慢了系统运行速度。因此DEBUG版本的性能低于RELEASE版本。


RELEASE版本就是最终的发布版本,相较于DEBUG版本的功能代码是一样的,但是去掉了所有的调试信息。适合最终测试通过要发布的程序,因为去掉了调试信息所以程序的运行效率要更高。


DEBUG和RELASE版本其实是一套源代码。源代码中是有很多的打印调试信息的语句的,如何来控制生成DEBUG和RELEEASE版本?靠条件编译,靠一个宏。

DEBUG宏大概的原理是:

#ifdef DEBUG
#define dbg() printf()
#else
#define dbg()    
#endif

工作方式是:如果我们要输出DEBUG版本则在条件编译语句前加上#define DEBUG即可,这样程序中的调试语句dbg()就会被替换成printf从而输出;如果我们要输出RELEASE版本则去掉#define DEBUG,则dbg()就会被替换成空,则程序中所有的dbg()语句直接蒸发了,这样的程序编译时就会生成没有任何调试信息的代码。

分析几个DEBUG宏
1、应用程序中DEBUG宏

#ifdef DEBUG
#define DBG(...) fprintf(stderr, " DBG(%s, %s(), %d): ", __FILE__, __FUNCTION__, __LINE__); fprintf(stderr, __VA_ARGS__)
#else
#define DBG(...)
#endif

注:__FILE__等是C语言中的预定义宏,就是说这个东西是个宏定义,但是是C语言自己定义的。这些宏具有特殊的含义,譬如__FILE__表示当前正在编译的c文件的文件名。

2、内核中的DEBUG宏

#ifdef DEBUG_S3C_MEM
#define DEBUG(fmt, args...)     printk(fmt, ##args)
#else
#define DEBUG(fmt, args...)     do {} while (0)
#endif