预处理详解

来源:互联网 发布:mac天翼飞young连不上 编辑:程序博客网 时间:2024/06/05 22:59

预处理最详解


预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。


C语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。


预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。  


部分预处理指令:

#空指令,无任何效果

#include包含一个源代码文件

#define定义宏

#undef取消已定义的宏

#if如果给定条件为真,则编译下面代码

#ifdef如果宏已经定义,则编译下面代码

#ifndef如果宏没有定义,则编译下面代码

#elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码

#endif结束一个#if……#else条件编译块

#error停止编译并显示错误信息


#运算符

出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符。例如:


#definePASTE(n)"adhfkj"#n


main()

{

printf("%s ",PASTE(15));

}

宏定义中的#运算符告诉预处理程序,把源代码中任何传递给该宏的参数转换成一个字符串。所以输出应该是adhfkj15


##运算符

##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。看下面的例子:


#defineNUM(a,b,c)a##b##c

#defineSTR(a,b,c)a##b##c


main()

{

printf("%d ",NUM(1,2,3));

printf("%s ",STR("aa","bb","cc"));

}


最后程序的输出为:

123

aabbcc


预处理分类:一、宏定义

                  二、文件包含

                  三、条件编译

……………………………………………………………………………………………


一、宏定义

1.宏定义分为两种:简单的宏、带参数的宏


2.简单的宏

简单的宏定义有如下格式:

#define  标识符 替换列表

替换列表:是一系列的C语言记号,包括标识符、关键字、数、字符常量、字符串字面量、运算符和标点符号。当预处理器遇到一个宏定义时,会做一个标识符代表替换列表的记录。在文件后面的内容中,不管标识符在任何位置出现,预处理器都会用替换列表代替它。


简单宏定义如: 

#define N  100

# define PI 3.1415926 


 使用宏的优点:

1>程序会更易读。一个认真选择的名字可以帮助读者理解常量的意义。否则,程序将包含大量的魔法数,使读者难以理解。如PI的使用就是如此。

2>程序会更易于修改。我们仅需要改变一个宏定义,就可以改变整个程序中出现的所有该常量的值。硬编码常量”会更难于修改,特别是程序中含有大量的硬编码常量时,或者是需要时常修改硬编码常量时。这是宏定义就体现出它的优势了,即只修要更改宏定义即可更改全部的硬编码。

3>可以帮助避免前后不一致或键盘输入错误。例如我们在程序中会输入PI(3.13159267)的时候,可能在一个地方输成3.14159367,在另一个地方输成3.14139267.这时宏定义可以很好的解决此问题。


使用宏时注意事项:

不要在宏定义中使用任何额外的符号,否则它们会被作为替换列表的一部分。


一种常见的错误是在宏定义中使用 =

#define N = 100

int a[N];

预处理后为: int a[= 100];


另一种常见的宏定义错误是使用分号结尾

#define N 100;

int a[N];

预处理后为:int a[100;];


3.带参数的宏

带参数的宏定义有如下格式:

#define 标识符(x1,x2,…, xn)替换列表

其中x1,x2,…, xn参数列表。这些参数可以在替换列表中根据需要出现任意次。

特别注意:在宏的名字和左括号之间必须没有空格。如果有空格,预处理器会认为是在定义一个简单的宏。例如:

假定我们定义了如下的宏:

#define MAX(x,y)     ((x)>(y) ? (x) :(y))

现在如果后面的程序中有如下语句:

i = MAX(j+k, m-n);

预处理器会将这些行替换为:

i = ((j+k)>(m-n)?(j+k):(m-n));


优点:

1>程序可能会稍微快些。一个函数调用在执行时通常会有些额外开销——存储上下文信息、复制参数的值等。而一个宏的调用则没有这些运行开销。

2>宏会更通用。与函数的参数不同,宏的参数没有类型。因此,只要预处理后的程序依然是合法的,宏可以接受任何类型的参数。例如,我们可以使用MAX宏从两个数中选出较大的一个,数的类型可以是intlong intfloatdouble等等。





二、文件包含

1.文件包含定义:文件包含处理是指在一个源文件中,通过文件包含命令将另一个源文件的内容全部包含在此文件中。在源文件编译时,连同被包含进来的文件一同编译,生成目标目标文件。


2.文件包含的处理方法:

1>处理时间:文件包含也是以"#"开头来写的(#include ),那么它就是写给预处理器来看了, 也就是说文件包含是会在编译预处理阶段进行处理的。

2>处理方法:在预处理阶段,系统自动对#include命令进行处理,具体做法是:降包含文件的内容复制到包含语句(#include )处,得到新的文件,然后再对这个新的文件进行编译。

 

3.文件包含分类:包含.h文件、包含.c文件

1>两情况都是按照上面说的方法来处理的。


2>包含.c文件和编译多文件程序 是不同的。

   多文件程序:是在源文件编译时把多个文件进行编译、连接在一起生成一个可执行文件。

   包含.c文件:按照我们上边的说法则是把多个文件合并为一个文件进行编译。


4.例子

1>包含.c文件:

   1: //file1:  main.c 

   2: #include 

   3: #include "fun.c"

   4: int main()

   5: {

   6:     int a=5,b=19;

   7:     c = a;    

   8:     sun(a,b);

   9:     printf("c=%d/n",c);

  10:     return 0;

  11: }

  12: //end of file1

   1: //file2: fun.c

   2: int c=0;

   3: void sun(int a, int b)

   4: {

   5:     printf("a+b=%d/n",a+b);

   6:     c=0;

   7:     printf("c=%d/n",c);

   8: }

   9: //end of file2   

  10:   

在编译时,直接去编译main.c文件,预处理器会先把fun.c文件中的内容复制到main.c中来,然后再对新的main.c进行编译。

编译命令:

    gcc main.c -o main

可以看到,这里并没有对fun.c进行编译,但还是生成了最终的main可执行程序。

也可以通过命令来观察一下预处理的结果:

编译命令:

   gcc -E main.c -o main.cpp

main.cpp文件末尾可以看来下面一段代码:

   1: //main.cpp文件中

   2: 931 # 2 "main.c" 2

   3: 932 # 1 "fun.c" 1

   4: 933 //注意这里是fun.c里边的内容

   5: 934 int c=0;

   6: 935 void sun(int a, int b)

   7: 936 {

   8: 937  printf("a+b=%d/n",a+b);

   9: 938  c=0;

  10: 939  printf("c=%d/n",c);

  11: 940 } 

  12:     //这里是main函数

  13: 941 # 3 "main.c" 2

  14: 942 int main()

  15: 943 { 

  16: 944  int a=5,b=19;

  17: 945  c = a;

  18: 946  printf("c=%d/n",c);

  19: 947  sun(a,b);

  20: 948  printf("c=%d/n",c);

  21: 949  return 0;

  22: 950 }

可见,其实就是将fun.c文件中的内容添加到了main函数之前,然后对新的文件进行编译,生成最终的可执行程序。

 

(2)编译多文件程序:

同样是上边的例子,把main.c“ #include "fun.c" ”注释掉,加上一句:“extern int c;”因为 c 变量在另外一个文件(fun.c)中定义。

   1: //file1:  main.c 

   2: #include 

   3: //#include "fun.c"  //注释掉

   4: extern int c;        //添加这一句

   5: int main()

   6: {

   7:     int a=5,b=19;

   8:     c = a;    

   9:     sun(a,b);

  10:     printf("c=%d/n",c);

  11:     return 0;

  12: }

  13: //end of file1

  14:  

  15:  

  16: //file2: fun.c

  17: int c=0;

  18: void sun(int a, int b)

  19: {

  20:     printf("a+b=%d/n",a+b);

  21:     c=0;

  22:     printf("c=%d/n",c);

  23: }

  24: //end of file2  

 

这次如果还是按照上面的方法只编译main.c的话就会出错,因为变量c和函数sun并没有在main.c中定义,所以编译时需要将fun.c一起编译:

编译命令:   

    gcc -c main.c -o main.o                 #编译main.c

    gcc -c fun.c -o fun.o                       #编译fun.c

    gcc main.o fun.o -o main              #main.o fun.o生成main


5.两种方法的优缺点

1>包含.c文件的方法:容易产生"重定义",大家想想如果一个工程中有多个文件都同时包含了某一个件,那么这个被包含文件的内容就会被复制到多个文件中去,也就相当于每个包含该文件的文件中都定义被包含文件中的变量和函数,这样在链接时就会产生"重定义"错误。

2>多文件分开编译的方法:这个比较好,不容易出现"重定义"之类的问题,这也是我们最常用的一种方法,但是并不是像上面这个例子中这样直接去用,而是使用"头文件"将各个.c文件联系起来。


6.说明

1>包含命令中的文件名可以用双引号括起来,也可以用尖括号括起来。例如以下写法都是允许的:

    #include"stdio.h"

    #include<math.h>

    但是这两种形式是有区别的:使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时设置的),而不在源文件目录去查找;

    使用双引号则表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。用户编程时可根据自己文件所在的目录来选择某一种命令形式。

2>一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。

3>文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。


三、条件编译指令


程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。条件编译指令将决定那些代码被编译,而哪些是不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。


1>#if/#endif/#else/#elif指令


    #if指令检测跟在制造另关键字后的常量表达式。如果表达式为真,则编译后面的代码,直到出现#else#elif#endif为止;否则就不编译。

    #endif用于终止#if预处理指令。

    #else指令用于某个#if指令之后,当前面的#if指令的条件不为真时,就编译#else后面的代码。


使用方法:(#if #elif #else #endif

 #if 条件 1
       代码段 1
    #elif 条件 2
      代码段 2
    ...
    #elif 条件 n
       代码段 n
    #else
       代码段 n+1
    #endif


例子:


#define DEBUG       //此时#ifdef DEBUG为真

//#define DEBUG 0  //此时为假

int main()

{

   #ifdef DEBUG

      printf("Debugging\n");

   #else

      printf("Not debugging\n");

   #endif

   printf("Running\n");

   return 0;

}


这样我们就可以实现debug功能,每次要输出调试信息前,只需要#ifdef DEBUG判断一次。不需要了就在文件开始定义#define DEBUG 0


#elif预处理指令综合了#else#if指令的作用。


例子:

#define TWO

int main()

{

   #ifdef ONE

          printf("1\n");

   #elif defined TWO

          printf("2\n");

   #else

          printf("3\n");

   #endif

}

//输出结果是2 


2> #ifdef#ifndef


条件编译的另一种方法是用#ifdef#ifndef命令,它们分别表示如果有定义如果无定义。有定义是指在编译此段代码时是否有某个宏通过 #define 指令定义的宏,#ifndef指令指找不到通过#define定义的某宏,该宏可以是在当前文件此条指令的关面定义的,也可以是在其它文件中,但在此指令之前包含到该文件中的。

#ifdef的一般形式是:

 #ifdef macro_name
    代码段 1
    #else
    代码段 2
    #endif


或者:


#ifdef的一般形式是:

    #ifndef macro_name
    代码段 2
    #else
    代码段 1
    #endif


这二者主要用于防止重复包含。我们一般在.h头文件前面加上这么一段:


//头文件防止重复包含

//funcA.h

#ifndef FUNCA_H

#define FUNCA_H

//头文件内容

#end if


这样,如果a.h包含了funcA.hb.h包含了a.hfuncA.h,重复包含,会出现一些type redefination之类的错误。


#if defined等价于#ifdef;       #if !defined等价于#ifndef

0 0
原创粉丝点击