C 预处理器

来源:互联网 发布:中控科门禁软件下载 编辑:程序博客网 时间:2024/06/07 04:52

gcc/cc xxx.c  可以编译链接C源程序生成一个可执行文件 a.out
整个过程中可以划分为以下的4步流程:
(1)预处理/预编译: 主要用于包含头文件的扩展,以及执行宏替换等 //加上 -E
(2)编译:主要用于将高级语言程序翻译成汇编语言,得到汇编语言    //加上 -S
(3)汇编:主要用于将汇编语言翻译成机器指令,得到目标文件           //加上 -c
(4)链接:主要用于将目标文件和标准库链接,得到可执行文件           //加上 -o

-E  实现预处理的执行,默认将处理结果输出到控制台,可以通过-o选项指定输出到xxx.i文件中,预处理文件中包含了很多头文件,类型的别名,以及各种函数的声明等等
-S  实现编译的处理,得到一个.s为后缀的汇编文件
-c  实现汇编的处理,得到一个.o为后缀的目标文件
gcc/cc xxx.o 实现链接的处理,默认生成可执行文件a.out,可以通过选项-o来指定输出文件名

参看:C语言再学习 -- GCC编译过程


根据上面的描述我们可以知道预处理的所在时期,编译程序之前,先由预处理器检查程序,根据程序中使用的预处理指令,预处理器用符号缩略语所代表的内容替换程序中的缩略语。下面详细介绍各个预处理指令:

一、#define的用法

#define 是一个预处理指令,这个预处理执行可以定义宏。与所有预处理指令一样,预处理指令#define用#符号作为行的开头。预处理指令从#开始,到其后第一个换行符为止。也就是说,指令的长度限于一行代码。如果想把指令扩展到几个物理行,可使用反斜线后紧跟换行符的方法实现,该出的换行符代表按下回车键在源代码文件中新起一行所产生的字符,而不是符号 \n 代表的字符。在预处理开始钱,系统会删除反斜线和换行符的组合,从而达到把指令扩展到几个物理行的效果。可以使用标准C注释方法在#define行中进行注释。

//使用反斜线+回车#define OW "hello\world!" /*注意第二行要左对齐*/

每一个#define行由三部分组成:

第一部分,指令#deine自身。

第二部分,所选择的缩略语,这些缩略语称为宏(分为对象宏和函数宏)。宏的名字中不允许有空格,而且必须遵循C变量命名规则:只能使用字母、数字和下划线(_),第一个字符不能为数字。习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。

第三部分,(#define行的其余部分)称为替换列表或主体。

注意,结尾没有分号

下面来看一个例子:

#include <stdio.h>#define OW 2 * 2#define OW 2  *  2//#undef OW  需要先取消宏定义#define OW 2*2int main (void){printf ("%d\n", OW);return 0;}输出结果:define.c:5:0: 警告: “OW”重定义 [默认启用]define.c:4:0: 附注: 这是先前定义的位置


相同定义意味着主体具有相同顺序的语言符号。因此,下面两个定义相同:

#define OW 2 * 2
#define OW 2  *  2

两者都有三个相同的语言符号,而且额外的空格不是主体的一部分。下面的定义则被认为是不同的:

#define OW 2*2

上式只有一个(而非三个)语言符号,因此与前面两个定义不同。可以使用#undef指令重新定义宏。


宏所代表的数字可以在编译命令中指定(使用-D选项)

/* 宏演示 */#include <stdio.h>int main(){int num=0;int arr[SIZE]={};   //使用gcc -D可以宏定义这个数字for(num = 0;num <= SIZE - 1;num++){arr[num]=num;printf("%d ",arr[num]);}printf("\n");return 0;}gcc -DSIZE=4 define.c输出结果:0 1 2 3

函数宏:

通过使用参数,可以创建外形和作用都与函数相似的类函数宏。宏的参数也用圆括号括起来。类函数宏的定义中,用圆括号括起来一个或多个参数,随后这些参数出现在替换部分。

#include <stdio.h>#define SQUARE(X) X*X#define PR(X) printf ("The result is %d\n", X)int main (void){int x = 4;int z;printf ("x = %d\n", x);z = SQUARE(x);printf ("Evaluating SQUARE(x): ");PR(z);z = SQUARE(2);printf ("Evaluating SQUARE(2): ");PR(z);printf ("Evaluating 100/SQUARE(2): ");PR(100/SQUARE(2));z = SQUARE(x+2);printf ("Evaluating SQUARE(x+2): ");PR(z);printf ("x is %d\n", x);z = SQUARE(++x);printf ("Eavluating SQUARE(++x): ");PR(SQUARE (++x));printf ("After incrementing, x is %x\n", x);return 0;}输出结果:x = 4Evaluating SQUARE(x): The result is 16Evaluating SQUARE(2): The result is 4Evaluating 100/SQUARE(2): The result is 100Evaluating SQUARE(x+2): The result is 14x is 4Eavluating SQUARE(++x): The result is 36After incrementing, x is 6

SQUARE(x+2) 输出结果是14,而不是想要的6*6 = 36。这是因为预处理器不进行计算,而只进行字符串替换。在出现x的地方,预处理都用字符串 x+2进行替换。x*x 变为 x+2*x+2 根据运算符优先级,则结果为 14


100/SQUARE(2)输出结果是 100,而不是想要的 25。因为,根据优先级规则,表达式是从左到右求值的。

100/2*2 = 100

要处理前面两个示例中的情况,需要如下定义:
#define SQUARE(x) ((x) * (x))
从中得到的经验是使用必须的足够多的圆括号来保证以正确的顺序进行运行和结合。


SQUARE(++x) 根据编译器的不同会出现两种不同的结果。解决这个问题的最简单的方法是避免在宏的参数中使用++x。一般来说,在宏中不要使用增量或减量运算符。

参看:C 语言再学习 -- 运算符与表达式


利用宏参数创建字符串:#运算符
在类函数宏的替换部分中,#符号用作一个预处理运算符,它可以把语言符号转化为字符串

例如:如果x是一个宏参量,那么#x可以把参数名转化为相应的字符串。该过程称为字符串化

#include <stdio.h>#define PSQR(x) printf ("The square of " #x" is %d\n", ((x)*(x)))int main (void){int y = 2;PSQR (y);PSQR (2 + 4);return 0;}输出结果:The square of y is 4The square of 2 + 4 is 36
#include <stdio.h>#include <string.h>#define VEG(n) #nint main(){char str[20];strcpy(str,VEG(num));//num printf("%s\n",str);//拷贝return 0;}输出结果:num

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

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

#include <stdio.h>#define XNAME(n) x##n#define PRINT_XN(n) printf ("x"#n" = %d\n", x##n)int main (void){int XNAME (1) = 14; //变为 int x1 = 14;int XNAME (2) = 20; //变为 int x2 = 20;PRINT_XN (1);       //变为 printf ("x1 = %d\n", x1);PRINT_XN (2);       //变为 printf ("x2 = %d\n", x2);return 0;}输出结果:x1 = 14x2 = 20

宏用于简单函数:

#include <stdio.h>#define MAX(x,y) ((x)>(y) ? (x) : (y))  /*比较大小*/#define ABS(x) ((x) < 0 ? -(x) : (x))   /*绝对值*/#define ISSIGN(x) ((x) == '+' || (x) == '-' ? 1 : 0)  /*正负号*/ int main(){printf ("较大的为: %d\n", MAX(5,3));printf ("绝对值为: %d\n", ABS (-2));printf ("正负号为: %d\n", ISSIGN ('+'));return 0;}输出结果:较大的为: 5绝对值为: 2正负号为: 1

下面是需要注意的几点:

1、宏的名字中不能有空格,但是在替代字符串中可以使用空格。ANSI C 允许在参数列表中使用空格。

2、用圆括号括住每个参数,并括住宏的整体定义

3、用大写字母表示宏函数名,便于与变量区分。

4、有些编译器限制宏只能定义一行。即使你的编译器没有这个限制,也应遵守这个限制。

5、宏的一个优点是它不检查其中的变量类型,这是因为宏处理字符型字符串,而不是实际值。


面试:用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题) 

#define SEC (60*60*24*365)UL

考察内容:

1、懂得预处理器将为你计算常量表达式的值,因此,可直接写出你是如何计算一年中有多少秒而不是计算出实际的值,这样更清晰而没有代价。

2、意识到这个表达式将使一个16 位机的整形数溢出,因此要用到长整形符号 L ,告诉编译器这个常数是长整形数。

3、如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。


面试:写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个

#define MIN(A,B) ((A) <= (B) ? (A) : (B))

考察内容:

1、三目表达式的使用

2、使用必须的足够多的圆括号来保证以正确的顺序进行运行和结合

3、进一步讨论,在宏中不要使用增量或减量运算符


参看:宏名必须用大写字母吗?

研究:C语言中用宏定义(define)表示数据类型和用typedef定义数据类型有什么区别?

宏定义只是简单的字符串代换,是在预处理完成的,而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能。

请看下面的例子:

#define P1 int *

typedef (int *) P2

从形式上看这两者相似,但在实际使用中却不相同。

下面用P1、P2说明变量时就可以看出它们的区别:

P1 a, b;  在宏代换后变成: int *a, b;  表示 a 是指向整型的指针变量,而 b 是整型变量。

P2 a, b;  表示a,b都是指向整型的指针变量。因为PIN2是一个类型说明符。

由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟是作字符代换。在使用时要分外小心,以避出错。


总结,typedef和#define的不同之处:

1、与#define不同,typedef 给出的符号名称仅限于对类型,而不是对值。

2、typedef 的解释由编译器,而不是是处理器执行。

3、虽然它的范围有限,但在其受限范围内,typedef 比 #define 更灵活。


二、#undef 指令

取消定义一个给定的 #define。

例如有如下宏定义:

#define LIMIT 40

则指令

#undef LIMIT

会取消该定义。

现在就可以重新定义LIMIT,以使它有一个新的值。即使开始没有定义LIMIT,取消LIMIT的定义也是合法的。如果想使用一个特定名字,但又不能确定前面是否已经使用了该名字,为安全起见,就可以取消该名字的定义。


注意:#define 宏的作用域从文件中的定义点开始,直到用 #undef 指令取消宏为止,或直到文件尾为止(由二者中最先满足的那个结束宏的作用域)。还应注意,如果用头文件引入宏,那么,#define 在文件中的位置依赖 #define 指令的位置。


三、文件包含:#include

预处理器发现#include指令后,就会寻找后跟的文件名并把这个文件的内容包含但当前文件中。被包含文件中的文件将替换源代码文件中的#include指令,就像你把被包含文件中的全部内容键入到源文件中的这个特定位置一样。

#include 指令有两种使用形式:

1) #include <filename.h>    文件名放在尖括号中

在UNIX系统中,尖括号告诉预处理器在一个或多个标准系统目录中寻找文件。 

如: #include <stdio.h>

查看:
ls /usr/include
ls kernel/include 


2) #include "filename.h"    文件名放在双引号中

在UNIX系统中,双引号告诉预处理器现在当前目录(或文件名中指定的其他目录)中寻找文件,然后在标准位置寻找文件。

如: #include "hot.h"     #include "/usr/buffer/p.h"


习惯上使用后缀 .h 表示头文件,这类文件包含置于程序头部的信息。头文件经常包含预处理语句。有些头文件由系统提供。但也可以自由创建自己的头文件。

扩展:C语言再学习 -- 常用头文件和函数(转)


四、条件编译

参看:条件编译#ifdef的妙用详解_透彻

#if:表示如果...
#ifdef: 表示如果定义...
#ifndef:表示如果没有定义...
#else: 表示否则...与#ifdef/#ifndef搭配使用  //笔试题  注意,没有#elseif
#elif: 表示否则如果...与#if/#ifdef/#ifndef搭配使用
#endif: 表示结束判断,与#if/#ifdef/#ifndef搭配使用

注意:#if 和 if 区别

#if=>主要用于编译期间的检查和判断
if   =>主要用于程序运行期间的检查和判断Z

最常见的形式:

#ifdef 标识符 程序段1 #else 程序段2 #endif 

作用:当标识符已经被定义过(一般用#define命令定义),则对程序段1进行编译,否则编译程序段2。其中#else部分也可以没有,即:

#ifdef 标识符程序段1#endif
这里的“程序段”可以是语句组,也可以是命令行。这种条件编译可以提高C源程序的通用性。如果一个C源程序在不同计算机系统上运行,而不同的计算机又有一定的差异。例如,我们有一个数据类型,在Windows平台中,应该使用long类型表示,而在其他平台应该使用float表示,这样往往需要对源程序做必要的修改,这就降低了程序的通用性。可以用以下的条件编译:

#ifdef WINDOWS #define MYTYPE long #else #define MYTYPE float #endif 
如果在Windows上编译程序,则可以在程序的开始加上
#define WINDOWS
这样则编译下面的命令行:
#define MYTYPE long
如果在这组条件编译命令之前曾出现以下命令行:
#define WINDOWS 0
则预编译后程序中的MYTYPE都用float代替。这样,源程序可以不必任何修改就可以用于不同类型的计算机系统。当然以上介绍的只是一种简单的情况,可以根据此思路设计出其他的条件编译。

例如,在调试程序时,常常希望输出一些所需的信息,而在调试完成后不再输出这些信息。可以在源程序中插入以下的条件编译段:

#ifdef DEBUG print ("device_open(%p)\n", file); #endif      
如果在它的前面有以下命令行:
#define DEBUG
则在程序运行时输出file指针的值,以便调试分析。调试完成后只需将这个define命令行删除即可。有人可能觉得不用条件编译也可以达到此目的,即在调试时加一批printf语句,调试后一一将prntf语句删除。的确,这是可以的。但是,当调试时加的printf语句比较多时,修改的工作量是很大的。用条件编译,则不必一一删除printf语句。只需删除前面的一条#define DEBUG 命令即可,这时所有的用DEBUG 作标识符的条件编译段都使其中的printf语句不起作用,起到统一控制的作用,如同一个“开关”一样。

有时也采用下面的形式:

#ifndef 标识符 程序段1 #else 程序段2 #endif 

只是第一行与第一种形式不同:将“#ifdef”改为“#ifndef”。它的作用是,若标识符未被定义则编译程序段1,否则编译程序段2。这种形式与第一种形式的作用相反。

一般地,当某文件包含几个头文件,而且每个头文件都可能定义了相同的宏,使用#ifndef可以防止该宏重复定义

/*test.h*/#ifndef SIZE#define SIZE 100#endif

#ifndef 指令通常用于防止多次包含同一文件,也就是说,头文件可采用类似下面几行的设置:

//头文件卫士#ifndef THINGS_H_#define THINGS_H_#endif

还有一种形式,就是#if 后面跟一个表达式,而不是一个简单的标识符:

#if 表达式 程序段1 #else 程序段2 #endif 
它的作用是:当指定的表达式为真(非零)时就编译程序段1,否则编译程序段2.可以事先给定一定条件,使程序在不同的条件下执行不同的功能。例如:

#include <stdio.h>#define LETTER 1int main (void){#if LETTERprintf ("111\n");#elseprintf ("222\n");#endifreturn 0;}输出结果:111
这种形式也可以用作注释用:#if 1  和 #if 0

#include <stdio.h>int main (void){#if 0printf ("111\n");#endifprintf ("222\n");return 0;}输出结果:222

最后一种形式

#if 标识符#elif程序段1#elfi程序段2。。。#else程序段n#endif
#if...#elif(任意多次)...#else...#endif,以上结构可以从任意逻辑表达式选择一组编译,这种结构可以根据任意逻辑表达式进行选择。

/* 条件编译演示 */#include <stdio.h>#define SANint main(){#ifdefined(YI)  //布尔值printf("1\n");#elifdefined(ER)//布尔值printf("2\n");#elifdefined(SAN)printf("3\n");#elseprintf("4\n");#endifreturn 0;}输出结果:3
这里,define是一个预处理运算符。如果 define 的参数已用#define定义过,那么define返回1,否则返回 0 。这种方法的优点在于它可以和#elif一起使用。


应用示例:
我们主要使用以下几种方法,假设我们已在程序首部定义:

#define DEBUG

#define TEST

1、利用#ifdef / #endif 将程序功能模块包括进去,以向某用户提供该功能.

在程序首部定义#define HNLD:#ifdef HNLDinclude"n166_hn.c"#endif
如果不许向别的用户提供该功能,则在编译之前将首部的HNLD加下划线即可。


2、在每一个子程序前加上标记,以便追踪程序的运行。

#ifdef DEBUGprintf(" Now is in hunan !");#endif


3、避开硬件的限制。有时一些具体应用环境的硬件不一样,但限于条件,本地缺乏这种设备,于是绕过硬件,直接写出预期结果。具体做法是:

#ifndef TESTi=dial();//程序调试运行时绕过此语句#elsei=0;#endif调试通过后,再屏蔽TEST的定义并重新编译,即可发给用户使用了。

有一个问题,如何确保使用的标识符在其他任何地方都没有定义过?

通常编译器提供商采用下述方法解决这个问题:用文件名做标识符,并在文件名中使用大写字母、用下划线代替文件名中的句点字符、用下划线(可能使用两条下划线)做前缀和后缀。例如,检查头文件read.h,可以发现许多类似的语句:

#ifndef  __READ_H__    //作为开头的预处理指令则当它后面的宏名称被定义过则编译后一组否则编译前一组#define __READ_H__     //防止被重复定义extern int num=0;#endif  __READ_H__

参看:C语言再学习 -- 标识符


扩展:extern "C"

通过 extern "C" 可以要求 C++ 编译器按照 C方式处理函数接口,即不做换名,当然也就无法重载。

1) C 调 C++,在 C++ 的头文件如下设置:

extern "C" int add (int x, int y);extern "C" {int add (int x, int y);int sub (int x, int y);}
//示例 add.h#ifndef _ADD_H#define _ADD_H#ifdef __cplusplusextern "C" {#endifint add (int ,int );#ifdef __cplusplus}#endif#endif

2)C++ 调 C,在C++ 的主函数如下设置:

extern "C" {#include "chead.h"}
//示例 main.cpp#include <iostream>using namespace std;extern "C" {#include "05sub.h"}int main (void) {int x=456,y=123;cout << x << "+" << y << "="<< sub(x, y) << endl;return 0;}


五、预定义宏

__DATE__进行预处理的日期(“Mmm dd yyyy”形式的字符串文字)

__FILE__代表当前源代码文件名的字符串文字

__BASE_FILE__获取正在编译的源文件名

__LINE__代表当前源代码文件中的行号的整数常量

__TIME__源文件编译时间,格式为“hh: mm: ss”

__STDC__设置为 1时,表示该实现遵循 C标准

__STDC_HOSTED__为本机环境设置为 1,否则设为 0

__STDC_VERSION__为C99时设置为199901L

__FUNCTION__或者 __func__  获取所在的函数名(预定义标识符,而非预定义宏)

#include <stdio.h>int main (void){printf ("The file is %s\n", __FILE__);printf ("The base_file is %s\n", __BASE_FILE__);printf ("The line is %d\n", __LINE__);printf ("The function is %s\n", __FUNCTION__);printf ("The func is %s\n", __func__);printf ("The date is %s\n", __DATE__);printf ("The time is %s\n", __TIME__);return 0;}输出结果:The file is part.cThe base_file is part.cThe line is 6The function is mainThe func is mainThe date is Nov 22 2016The time is 15:46:30

六、常用的新指令

#line 整数n =>表示修改代码的行数/指定行号   插入到程序中表示从行号n开始执行,修改下一行的行号为n
#error  字符串 => 表示产生一个错误信息
#warning 字符串 => 表示产生一个警告信息

//#line 预处理指令的使用#include <stdio.h>#line 200 int main(void){printf("The line is %d\n",__LINE__);return 0;}输出结果:The line is 202

//#error和#warning的使用#include <stdio.h>#define VERSION 4#define VERSION 2#define VERSION 3#if(VERSION < 3)#error "版本过低"#elif(VERSION > 3)#warning "版本过高"#endifint main(void){printf("程序正常运行\n");return 0;}输出结果:警告: #warning "版本过高"//错误: #error "版本过低"//程序正常运行


七、#pragma

#pragma GCC dependency 文件名 
表示当前文件依赖于指定的文件,如果当前文件的最后一次,修改的时间早于依赖的文件,则产生警告信息

#include <stdio.h>//当前程序依赖于01print.c文件#pragma GCC dependency "01print.c"int main(void){printf("Good Good Study,Day Day Up!\n");return 0;}输出结果:致命错误: 01print.c:没有那个文件或目录编译中断。

#pragma GCC poison 标示符
表示将后面的标示符设置成毒药,一旦使用标示符,则产生错误或警告信息

//毒药的设置#include <stdio.h>//#define GOTO goto//将goto设置为毒药#pragma GCC poison gotoint main(void){//GOTO ok;goto ok;printf("main函数开始\n");ok:printf("main函数结束\n");return 0;}输出结果:错误: 试图使用有毒的“goto”

#pragma pack (整数n)
表示按照整数n倍进行补齐和对齐

//设置结构体的对齐和补齐方式#include <stdio.h>//设置结构体按照2的整数倍进行对齐补齐#pragma pack(2) //8//#pragma pack(1) //6 //#pragma pack(3) //error //char short int long int  long longint main(void){struct S{char c1;int i;char c2;};printf("sizeof(struct S) = %d\n",sizeof(struct S));//12return 0;}输出结果:sizeof(struct S) = 8

0 0