预编译和宏定义的相关知识

预编译和宏定义的相关知识

来自: http://www.neu.edu.cn/cxsj/online/c11/ch11_1.html
第十一章 预编译和宏定义

11.1 概述

软件工程中一个非常重要的问题是软件的可移植和可重用问题,例如在微机平台上开发的程序需要顺利的移植到大型计算机上去运行,同一套代码不加修改或经过少量的修改即可适应多种计算机系统。C语言作为软件工程中广泛使用的一门程序设计语言,需要很好的解决此类问题。为此ANSI C引入了预编译处理命令,主要规范和统一不同编译器的指令集合。通过这些指令,控制编译器对不同的代码段进行编译处理,从而生成针对不同条件的计算机程序。
ANSI C中主要定义如下三类预编译指令:
1) #define与#undef指令
2) #include 指令
3) #if #endif 和#if #else #endif指令

11.2 #define命令

#define命令主要可以实现两种功能:
1) 符号常量定义
2) 宏定义

11.2.1 符号常量定义

在介绍符号常量定义之前,首先分析如下的程序。
例11-1 输入全班20个同学的C语言成绩,并计算出最高分同学的姓名和成绩,并计算出全班C语言成绩的平均分。
#include "stdio.h"
void main()
{
float fAvg; /*平均成绩 */
float fMax; /*最高成绩 */
int iMax;
float fSum;
int i;
/*C语言成绩(20人) */
float fCScore[20]={78.5,65,89,65,45,62,89,99,85,85, 100,58,98,86,68,66,85.5,89.5,75,76};
/*全班同学的姓名(20人) */
char chNames[][20]= { "Maofen","LiuTao","Wangyu","Songbou", "Liyang", "Wangming","Renhon", "Wuyong","Luming","Sunce","Zhangjie","Yuanquan", "Yangjing","Yubing","XingWen","Huangxing","Dingwen","Quyang","Liufeng","Hanhan"
};
/*计算总分*/
fSum=0;
for(i=0;i<20;i++)
fSum=fSum+fCScore[i];
/*计算平均分*/
fAvg =fSum/20;
/*计算最高分*/
fMax = fCScore[0];
iMax=0;
for(i=0;i<20;i++)
{
if(fMax<fCScore[i])
{
iMax=i;
fMax= fCScore[i];
}
}
/*输出信息*/
printf("\nThe Score of C Programming Language\n");
printf("The first is %10s \n",chNames[iMax]);
printf("The Score is %10.2f \n",fCScore[iMax]);
printf("The Average is %10.2f \n",fAvg);
}
在上面的程序中,20是一整型常量,代表全班同学的总人数。根据求解问题的需要,20出现了多次。如果将此程序修改,以便处理一个具有25个同学的班级,则需要进行多处的改动。在实际的软件开发中,一个简单的常量可能在整个软件的不同部分出现上百次或上千次,并可能代表不同的信息。在移植过程中,采用手工修改,十分容易出现错误。因此C语言中引入了符号常量,形式如下:
#define 符号常量名称 替换文本
其中#define 为预编译命令,“替换文本”为“符号常量”所代表的字符序列集合,可以为任意字符的组合。在程序代码被编译之前,编译系统自动将所有符号常量出现的位置,被其代表的“替换文本”无条件的替换,然后再进行语法检查和编译。因此利用符号常量,例11-1可改写为
#include "stdio.h"
#define STUDENT_COUNT 20
void main()
{
……
/*C语言成绩(20人) */
float fCScore[STUDENT_COUNT ]={…};
char chNames[][STUDENT_COUNT ]={ …};
fSum=0;
for(i=0;i<STUDENT_COUNT;i++)
fSum=fSum+fCScore[i];
fAvg =fSum/STUDENT_COUNT ;

for(i=0;i<STUDENT_COUNT ;i++)
{
……
}
}
为了节省空间,上述程序仅仅显示了修改后的部分。结合例11-1,将所有20用STUDENT_COUNT替换即可。其中
#define STUDENT_COUNT 20
是定义符号常量STUDENT_COUNT,其替换文本为20,代表20这样一个字符序列集合,在程序编译时,所有STUDENT_COUNT出现的位置均被20 无条件的替换。
如果将上述程序修改为处理具有30位同学的班级,只需修改定义
#define STUDENT_COUNT 30
即可。从上面的分析可以看出,恰当的使用符号常量可以增强程序的可移植能力。
提示 符号常量出现的位置,最终将无条件替换为其代表的字符序列集合,并不做语法检查。在编译程序时再进行语法检查。另外,符号常量定义的末尾不能有分号,与普通的程序语句不同。
符号常量的所带代表字符序列集合,可以为空,形式如下:
#define 编译标志
此命令的主要用途是引入编译标志,根据不同的编译标志,编译系统可以完成对不同代码段的编译。例如下面的程序
例11-2 在Windows系统中int 为32位数,在Dos中long为32位数。为了程序在两个系统中移植,可以定义INT32,保证在两种系统中利用INT32声明的变量为32位整型数。
#include "stdio.h"
#ifdef MY_DOS
#define INT16 int
#define INT32 long
#endif
#ifdef MY_WIN32
#define INT16 short
#define INT32 int
#endif
void main()
{
INT32 a;
a=32;
printf("%ld",a);
}
其中MY_WIN32和MY_DOS为编译标志,本段程序的主要目的是保证INT32类型数总是32位的整型数,在DOS系统中编译的时候增加代码
#define MY_DOS
此时INT32代表long。
在Windows系统下编译的时候,修改为
#define MY_WIN32
此时INT32代表int。
提示 符号常量与变量常常混淆。符号常量是一个有名字,因此其与变量相似,但是其值在程序运行过程中不能更改,与常量类似。
一般符号常量的名称采用大写字母,用于区分变量。
将常用的常量定义为符号常量是一种良好的设计习惯。


11.2.2 #undef命令

取消符号常量定义命令,语法形式如下:
#undef符号常量名称(或编译标志)
其作用取消最近一次#define符号常量名称(或编译标志)命令,使定义的符号常量或编译标志失效。
例11-3 分析如下的代码
#include "stdio.h"
void Test();
int main(int argc, char* argv[])
{
#define CONST_NAME1 "CONST_NAME1"
printf("%s\n",CONST_NAME1);
#undef CONST_NAME1
printf("%s\n",CONST_NAME1); /*错误,CONST_NAME1的定义已经取消*/
{
#define CONST_NAME2 "CONST_NAME2"
printf("%s\n",CONST_NAME2);
}
printf("%s\n",CONST_NAME2);
return 0;
}
void Test()
{
printf("%s\n",CONST_NAME2);
}
在程序的编译的时候,系统提示如下信息
error C2065: 'CONST_NAME1' : undeclared identifier
出现上述编译错误的原因是,在第二次应用符号常量CONST_NAME1时,此符号常量已经被取消定义。
符号常量的有效范围是从第一次出现的位置开始,到#undef结束。如果没有对应的#undef指令,则到文件末尾结束。因此在本例中void Test函数可以直接使用CONST_NAME2,而无须定义。
提示 符号常量与变量的有效范围不同。变量根据其所在位置,例如某函数或文件决定其有效性,在函数内部定义的变量不能够为其他的函数所使用。但是符号常量仅仅与其出现先后位置,以及对应的#undef命令相关,与是否出现在具体函数无关。

11.2.3 宏定义

C语言还提供另外一种带参数的#define命令,其英文为macro,一般翻译为宏。其形式如下:
#define 宏(参数列表) 替换文本
其中一般称“替换文本”为宏定义。例如
#define Sum(a,b) a+b
中Sum为宏,a,b为参数列表,a+b为宏定义。在下面的代码中
a=Sum(2,3);
在编译阶段,被替换为
a=2+3;
然后,再编译成可执行代码。
例10-3 定义宏FAILED用于检测数据的正确性。
#define FAILED(Status) ((Status)<0)
#include "stdio.h"
void main()
{
int d;
printf ("Please input a integer number(n>0)\n");
do
{
scanf("%d" ,&d);
}while(FAILED(d));
}
其中while(FAILED(d))在编译之前被无条件替换为while(d<0)。
宏定义和调用在形式与函数比较相似,但是原理是不同。
例10-4 定义Add函数实现两个数的乘法。
double Add(int a,int b)
{
return a*b;
}
void main()
{
double a,b;
double e,f;
a=3;
b=2;
e=Add(a,2);
f=Add(b+1,2);
printf("e=%ff=%f",e,f);
}
程序的运行结果如下
e=6.000000f=6.000000
在下面代码与上面的代码类似,但是运行结果不同。
例10-5 定义Add宏实现两个数的乘法。
#define Add(a,b)? a*b
void main()
{
double a,b;
double e,f;
a=3;
b=2;
e=Add(a,2);
f=Add(b+1,2);
printf("e=%f,f=%f",e,f);
}
程序的运行结果如下
e=6.000000,f=4.000000
在编译之前,系统做如下的处理:
e=Add(a,2)替换为e=a*2;
f=Add(b+1,2)替换为f=b+1*2;

因此在使用宏的时候,一定要注意与函数区分。首先宏是一种预编译指令,函数为程序指令。其次宏的作用是定义一种带参数的代码替换方法,在编译之后不会形成独立的代码段;而函数在编译之后会形成相对独立的代码段。
提示 函数定义是将一块公共的代码封装成一个函数,系统编译之后,此部分的机器指令依然作为一个相对对立的代码段。宏的定义仅仅是定义一个带参数的文本替换编译指令,在编译的时候所有的宏将被其代表的信息替换,而在编译之后,对应的机器指令不独立存在。
宏的使用与函数的调用不同。宏的使用发生在程序代码编译成为机器指令之前,而函数的调用发生在程序运行的时候。函数调用会产生额外系统开销,而宏的使用会加大程序代码量,函数的使用会减少程序的代码量。使用宏可以获得更高的代码运行效率,因为其不存在函数调用。

11.3 #include命令

在实际的软件开发的时候,需要多个人构成的小组共同完成代码的编写与测试,因此需要借鉴和应用其他的结果,或者要借鉴前人的成果。例如在C语言中,经常使用的标准输入输出函数均为前人的工作成果。在使用时,只需将包含函数声明的头文件stdio.h,用#include指令包含当前的程序文件即可。
#include 命令的形式如下
#include <文件名>

#include “文件名”
其作用是在系统编译之前,将包含文件中的内容拷贝到当前文件的当前位置之后,再进行编译。
例11-6 文件file_1.c和file_2.c的程序分别如下:
/*file_1.c*/
double Add(int a,int b)
{
return a*b;
}
/*file_2.c*/
#include "file_1.c"
void main()
{
double a,b;
double e,f;
a=3;
b=2;
e=Add(a,2);
f=Add(b+1,2);
printf("e=%ff=%f",e,f);
}
在编译file_2.c的时候,系统根据#include "file_1.c"指令将file_1.c的内容拷贝当前文件的当前位置,所以在file_2.c可以直接使用double Add(int a,int b)函数。
在软件开发中一般将符号常量、全局变量、函数声明包含在头文件(.h文件)中,并将其定义放在.c文件中。然后在使用的时候,包含对应的头文件即可。下面是常用数学库函数头文件“math.h”中的部分代码,其中_Cdecl为Turbo C系统的中保留字。
/* math.h
Definitions for the math floating point package.
Copyright (c) Borland International 1987,1988
All Rights Reserved.
*/
int _Cdecl abs (int x);
double _Cdecl acos (double x);
double _Cdecl asin (double x);
double _Cdecl atan (double x);
double _Cdecl atan2 (double y, double x);
double _Cdecl atof (const char *s);
double _Cdecl ceil (double x);
double _Cdecl cos (double x);
如果在程序中需要使用数学库函数,在文件中加入如下的代码即可。
#include <math.h>

#include “math.h”
#include <math.h>与#include “math.h”的区别在于遇到#include <math.h>命令时系统从缺省的头文件目录中查找文件math.h文件;遇到#include “math.h” 时系统首先从当前的目录中搜索,如果没有找到再在缺省的头文件目录中查找文件math.h文件。因此包含系统提供的库函数使用#include <math.h>方式搜索速度比较快;如果包含用户自定义的.h文件使用#include “math.h”方式,,搜索速度比较快。
提示 在使用#include指令的时候,对系统文件,使用#include <math.h>形式;对用户自定义文件,则使用#include “math.h”形式。

11.4 条件编译

为了实现在编译程序的时候,控制哪些代码参与编译,哪些代码不参与编译,C语言中引入了条件编译指令。条件编译的引入,可以将针对于不同硬件平台或软件平台的代码,编写在于同一程序文件中,从而方便程序的维护和移植。在进行软件移植的时候,可以针对不同的情况,控制不同的代码段被编译。
      #ifdef … #else …#endif
      #if defined… #else …#endif
      #ifndef … #else …#endif
      #if !defined … #else …#endif
      #ifdef …#elif … #elif …#else … #endif

11.4.1 #ifdef … #else …#endif

此编译指令类似于C语言中的if else 语句,是一种典型的条件编译指令。其语法形式如下:
#ifdef 常量表达式
代码段1
#else
代码段2
#endif
其中常量表达式可以仅仅为一个编译标志。
如果常量表达式的值为真(非零值),编译代码段1部分的代码,否则编译代码段2部分的代码。
当常量表达式为简单的编译标志时,如果此编译标志在前面的代码中,已经使用#define指令定义过,且在当前的代码段中有效,则编译代码段1部分的代码,否则编译代码段2部分的代码。
例11-7 定义符号常量CONST_TRUE 和TAG_TRUE,根据不同的条件进行不同操作。
#define CONST_TRUE 1
#define TAG_TRUE
void main()
{
#ifdef CONST_TRUE
printf("The CONST_TRUE is true\n");
#else
printf("The CONST_TRUE is false\n");
#endif
#ifdef TAG_TRUE
printf("The TAG_TRUE is defined\n");
#else
printf("The TAG_TRUE is not defined\n");
#endif
}
程序的运行结果如下
The CONST_TRUE is true
The TAG_TRUE is defined
因为CONST_TRUE代表1,所以系统编译printf("The CONST_TRUE is true\n")部分代码,因此程序运行输出“The CONST_TRUE is true”的结果。如果CONST_TRUE代表0,则系统编译printf("The CONST_TRUE is false\n")部分代码。由于TAG_TRUE已经定义,所系统编译printf("The TAG_TRUE is defined\n")部分代码。
此编译指令的简单形式为单分支的条件编译指令,其语法格式如下:
#ifdef常量表达式
代码段
#endif
如果常量表达式的值为真,则编译“代码段”部分的代码,否则跳过此部分的代码。当常量表达式为编译标志时,如果此编译标志有效,则编译“代码段”部分的代码,否则跳过此部分的代码。
下面程序中,只有定义了TAG_TRUE 之后,才可以编译printf("The TAG_TRUE is defined\n")语句。
#ifdef TAG_TRUE
printf("The TAG_TRUE is defined\n");
#endif

11.4.2 #if defined… #else …#endif

此编译指令为#ifdef … #else …#endif的等价编译指令。其语法格式如下:
#if defined 常量表达式
代码段1
#else
代码段2
#endif

#if defined (常量表达式)
代码段1
#else
代码段2
#endif
此编译指令的简单形式为单分支的条件编译指令,其语法形式如下:
#if defined常量表达式
代码段1
#endif

11.4.3 #ifndef … #else …#endif

语法形式如下:
#ifndef 常量表达式
代码段1
#else
代码段2
#endif
如果常量表达式的值为假(零值)时,编译代码段部分的代码,否则编译代码段2部分的代码。当常量表达式为编译标志时,如果此编译标志无效则编译代码段1部分的代码,否则编译代码段2部分的代码。
此编译指令的简单形式为单分支的选择编译指令,其语法格式如下:
#ifndef常量表达式
代码段1

#endif

11.4.4 #if !defined … #else …#endif

此编译指令与#ifndef … #else …#endif等价,其语法形式如下:
# if !defined 常量表达式
代码段1
#else
代码段2
#endif

# if !defined(常量表达式)
代码段1
#else
代码段2
#endif
提示 头文件的重复包含,造成变量重复定义或函数的重复声明,此错误是初学者经常遇到的一个问题。主要的原因是在编辑自己的头文件的时候,并没有加入有效的预防措施。解决此类问题,在文件开始与结尾加入类似代码
#if !defined(MY__INCLUDED_) /*此头文件对应的编译标志*/
#define MY__INCLUDED /*此头文件没有被包含,所以编译此部分的代码*/
……
#endif // !defined(MY__INCLUDED)

11.4.5 #ifdef …#elif … #elif …#else … #endif

此条件编译指令为多分支的条件编译指令,类似于多分支的选择结构if …else if .. else ,其语法形式为
# ifdef 常量表达式1
代码段1
#elif 常量表达式2
代码段2
#elif 常量表达式3
代码段3
#endif
其中elif为else if的意思。常量表达式1、常量表达式2、常量表达式3、……为一个常量表达式或编译标志。当常量表达式1为常量表达式时,如果其值为真(非零值)则编译代码段1部分的代码,否则如果常量表达式2值为真(非零值)则编译代码段2部分的代码,依次类推。
其他等价的多分支条件编译指令如下:
形式1
# ifndef 常量表达式1
代码段1
#elif 常量表达式2
代码段2
#elif 常量表达式3
代码段3
#endif
形式2
# if defined 常量表达式1
代码段1
#elif 常量表达式2
代码段2
#elif 常量表达式3
代码段3
#endif
形式3
# if !defined ( 常量表达式1)
代码段1
#elif 常量表达式2
代码段2
#elif 常量表达式3
代码段3
#endif
提示 条件编译指令的作用是控制不同的代码被编译形成机器指令;而条件语句是控制哪些机器指令可以执行。条件编译中不同分支的代码不会同时编译并存在同一可执行文件中;而条件语句所有分支中的语句均会编译机器指令,并存放在同一可执行文件中。

11.5 其他指令

在ANSI C中除了定义上述常用的预编译指令之外,还定义了其他一些预编译指令。
     1. #error
      2. # pragma
      3. #line
      4. 运算符#和##

11.5.1 #error

语法格式如下:
#error token-sequence
其主要的作用是在编译的时候输出编译错误信息token-sequence,从方便程序员检查程序中出现的错误。例如下面的程序
#include "stdio.h"
int main(int argc, char* argv[])
{
#define CONST_NAME1 "CONST_NAME1"
printf("%s\n",CONST_NAME1);
#undef CONST_NAME1
#ifndef CONST_NAME1
#error No defined Constant Symbol CONST_NAME1
#endif
{
#define CONST_NAME2 "CONST_NAME2"
printf("%s\n",CONST_NAME2);
}
printf("%s\n",CONST_NAME2);
return 0;
}
在编译的时候输出如编译信息
fatal error C1189: #error : No defined Constant Symbol CONST_NAME1

11.5.2 # pragma

其语法格式如下:
# pragma token-sequence
此指令的作用是触发所定义的动作。如果token-sequence存在,则触发相应的动作,否则忽略。此指令一般为编译系统所使用。例如在Visual C++.Net 中利用# pragma once 防止同一代码被包含多次。

11.5.3 #line

此命令主要是为强制编译器按指定的行号,开始对源程序的代码重新编号,在调试的时候,可以按此规定输出错误代码的准确位置。
形式1
语法格式如下:
# line constant “filename”
其作用是使得其后的源代码从指定的行号constant重新开始编号,并将当前文件的名命名为filename。例如下面的程序如下:
#include "stdio.h"
void Test();
#line 10 "Hello.c"
int main(int argc, char* argv[])
{
#define CONST_NAME1 "CONST_NAME1"
printf("%s\n",CONST_NAME1);
#undef CONST_NAME1
printf("%s\n",CONST_NAME1);
{
#define CONST_NAME2 "CONST_NAME2"
printf("%s\n",CONST_NAME2);
}
printf("%s\n",CONST_NAME2);
return 0;
}
void Test()
{
printf("%s\n",CONST_NAME2);
}
提示如下的编译信息:
Hello.c(15) : error C2065: 'CONST_NAME1' : undeclared identifier
表示当前文件的名称被认为是Hello.c, #line 10 "Hello.c"所在的行被认为是第10行,因此提示第15行出错。
形式2
语法格式如下:
# line constant
其作用在于编译的时候,准确输出出错代码所在的位置(行号),而在源程序中并不出现行号,从而方便程序员准确定位。

11.5.4 运算符#和##

在ANSI C中为预编译指令定义了两个运算符——#和##。
# 的作用是实现文本替换,例如
#define HI(x) printf("Hi,"#x"\n");
void main()
{
HI(John);
}
程序的运行结果
Hi,John
在预编译处理的时候, "#x"的作用是将x替换为所代表的字符序列。在本程序中x为John,所以构建新串“Hi,John”。
##的作用是串连接。
例如
#define CONNECT(x,y) x##y
void main()
{
int a1,a2,a3;
CONNECT(a,1)=0;
CONNECT(a,2)=12;
a3=4;
printf("a1=%d\ta2=%d\ta3=%d",a1,a2,a3);
}
程序的运行结果为
a1=0 a2=12 a3=4
在编译之前, CONNECT(a,1)被翻译为a1, CONNECT(a,2)被翻译为a2。

11.6 预定义常量

在ANSI C中预先定义了如下几个符号常量(表11-1),在定义符号常量的时候,不能定义与预定义符号常量同名的常量。
md
表11-1 预定义符号常量

11.7 VC++中的预编译指令

      1. 预编译头文件说明
      2. VC的预编译功能

11.7.1 预编译头文件说明

所谓头文件预编译,就是把一个工程(Project)中使用的一些MFC标准头文件(如Windows.H、 Afxwin.H)预先编译,以后该工程编译时,不再编译这部分头文件,仅仅使用预编译的结果。这样可以加快编译速度,节省时间。
预编译头文件通过编译stdafx.cpp生成,以工程名命名,由于预编译的头文件的后缀是“pch”,所以编译结果文件是projectname.pch。
编译器通过一个头文件stdafx.h来使用预编译头文件。stdafx.h这个头文件名是可以在project的编译设置里指定的。编译器认为,所有在指令#include "stdafx.h"前的代码都是预编译的,它跳过#include "stdafx. h"指令,使用projectname.pch编译这条指令之后的所有代码。
因此,所有的CPP实现文件第一条语句都是:#include "stdafx.h"。
另外,每一个实现文件CPP都包含了如下语句:
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
这是表示,如果生成调试版本,要指示当前文件的名称。__FILE__是一个宏,在编译器编译过程中给它赋值为当前正在编译的文件名称。
VC默认情况下使用预编译头(/Yu),不明白的在加入新.h文件后编译时总出现fatal errorC1010: 在查找预编译头指令时遇到意外的文件结尾的错误。解决方法是在include头文件的地方加上#include"stdafx.h",或者打项目属性,找到“C/C++”文件夹,单击“预编译头”属性页。修改“创建/使用预编译头”属性为“不使用预编译头”。

11.7.2 VC的预编译功能

这里介绍VC6的预编译功能的使用,由于预编译详细使用比较的复杂,这里只介绍几个最重要的预编译指令: /Yu, /Yc,/Yx,/Fp。其它的详细资料可以参考: MSDN -> Visual Studio 6.0 Document -> Visual C++ 6.0 Document -> VC++ Programmer Guider ->Compiler and Linker -> Details -> Creating Precompiled Header files
预编译头的概念:
所谓的预编译头就是把一个工程中的那一部分代码,预先编译好放在一个文件里(通常是以.pch为扩展名的),这个文件就称为预编译头文件这些预先编译好的代码可以是任何的C/C++代码,甚至是inline的函数,但是必须是稳定的,在工程开发的过程中不会被经常改变。如果这些代码被修改,则需要重新编译生成预编译头文件。注意生成预编译头文件是很耗时间的。同时你得注意预编译头文件通常很大,通常有6-7M大。注意及时清理那些没有用的预编译头文件。
也许你会问:现在的编译器都有Time stamp的功能,编译器在编译整个工程的时候,它只会编译那些经过修改的文件,而不会去编译那些从上次编译过,到现在没有被修改过的文件。那么为什么还要预编译头文件呢?答案在这里,我们知道编译器是以文件为单位编译的,一个文件经过修改后,会重新编译整个文件,当然在这个文件里包含的所有头文件中的东西(.eg Macro, Preprocessor )都要重新处理一遍。 VC的预编译头文件保存的正是这部分信息。以避免每次都要重新处理这些头文件。
根据上文介绍,预编译头文件的作用当然就是提高便宜速度了,有了它你没有必要每次都编译那些不需要经常改变的代码。编译性能当然就提高了。
要使用预编译头,我们必须指定一个头文件,这个头文件包含我们不会经常改变的代码和其他的头文件,然后我们用这个头文件来生成一个预编译头文件(.pch文件)想必大家都知道 StdAfx.h这个文件。很多人都认为这是VC提供的一个“系统级别”的,编译器带的一个头文件。其实不是的,这个文件可以是任何名字的。我们来考察一个典型的由AppWizard生成的MFC Dialog Based 程序的预编译头文件。(因为AppWizard会为我们指定好如何使用预编译头文件,默认的是StdAfx.h,这是VC起的名字)。我们会发现这个头文件里包含了以下的头文件:
#include <afxwin.h> // MFC core and standard components
#include <afxext.h> // MFC extensions
#include <afxdisp.h> // MFC Automation classes
#include <afxdtctl.h> // MFC support for Internet Explorer 4 Common Controls
#include <afxcmn.h>
这些正是使用MFC的必须包含的头文件,当然我们不太可能在我们的工程中修改这些头文件的,所以说他们是稳定的。
那么我们如何指定它来生成预编译头文件。我们知道一个头文件是不能编译的。所以我们还需要一个cpp文件来生成.pch 文件。这个文件默认的就是StdAfx.cpp。在这个文件里只有一句代码就是:#include“Stdafx.h”。原因是理所当然的,我们仅仅是要它能够编译而已―――也就是说,要的只是它的.cpp的扩展名。我们可以用/Yc编译开关来指定StdAfx.cpp来生成一个.pch文件,通过/Fp编译开关来指定生成的pch文件的名字。打开project ->Setting->C/C++ 对话框。把Category指向Precompiled Header。在左边的树形视图里选择整个工程,Project Options(右下角的那个白的地方)可以看到 /Fp “debug/PCH.pch”,这就是指定生成的.pch文件的名字,默认的通常是 <工程名>.pch。然后,在左边的树形视图里选择StdAfx.cpp,这时原来的Project Option变成了 Source File Option(原来是工程,现在是一个文件,当然变了)。在这里我们可以看到 /Yc开关,/Yc的作用就是指定这个文件来创建一个Pch文件。/Yc后面的文件名是那个包含了稳定代码的头文件,一个工程里只能有一个文件的可以有YC开关。VC就根据这个选项把 StdAfx.cpp编译成一个Obj文件和一个PCH文件。
这样,我们就设置好了预编译头文件。也就是说,我们可以使用预编译头功能了。以下是注意事项:
1)如果使用了/Yu,就是说使用了预编译,我们在每个.cpp文件的最开头,包含你指定产生pch文件的.h文件(默认是stdafx.h)不然就会有问题。如果你没有包含这个文件,就告诉你Unexpected file end.
2)如果你把pch文件不小心丢了,根据以上分析,你只要让编译器生成一个pch文件就可以了。也就是说把 stdafx.cpp(即指定/Yc的那个cpp文件)重新编译一遍就可以了。

【结束】

几个预编译指令的用法

来自:http://blog.csdn.net/codewarrior/article/details/760690
分类: C/C++
2006-05-29 14:30 8133人阅读 评论(0) 收藏 举报
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。
在C语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。
预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。下面是部分预处理指令:

        指令             用途
         #           空指令,无任何效果
         #include    包含一个源代码文件
         #define     定义宏
         #undef      取消已定义的宏
         #if         如果给定条件为真,则编译下面代码
         #ifdef      如果宏已经定义,则编译下面代码
         #ifndef     如果宏没有定义,则编译下面代码
         #elif       如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
         #endif      结束一个#if……#else条件编译块
         #error      停止编译并显示错误信息

一、文件包含
    #include预处理指令的作用是在指令处展开被包含的文件。包含可以是多重的,也就是说一个被包含的文件中还可以包含其他文件。标准C编译器至少支持八重嵌套包含。
    预处理过程不检查在转换单元中是否已经包含了某个文件并阻止对它的多次包含。这样就可以在多次包含同一个头文件时,通过给定编译时的条件来达到不同的效果。例如:

        #define AAA
        #include "t.c"
        #undef AAA
        #include "t.c"

    为了避免那些只能包含一次的头文件被多次包含,可以在头文件中用编译时条件来进行控制。例如:
        /*my.h*/
        #ifndef MY_H
        #define MY_H
          ……
        #endif

    在程序中包含头文件有两种格式:
        #include <my.h>
        #include "my.h"
    第一种方法是用尖括号把头文件括起来。这种格式告诉预处理程序在编译器自带的或外部库的头文件中搜索被包含的头文件。第二种方法是用双引号把头文件括起来。这种格式告诉预处理程序在当前被编译的应用程序的源代码文件中搜索被包含的头文件,如果找不到,再搜索编译器自带的头文件。
    采用两种不同包含格式的理由在于,编译器是安装在公共子目录下的,而被编译的应用程序是在它们自己的私有子目录下的。一个应用程序既包含编译器提供的公共头文件,也包含自定义的私有头文件。采用两种不同的包含格式使得编译器能够在很多头文件中区别出一组公共的头文件。

二、宏
    宏定义了一个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。宏最常见的用法是定义代表某个值的全局符号。宏的第二种用法是定义带参数的宏,这样的宏可以象函数一样被调用,但它是在调用语句处展开宏,并用调用时的实际参数来代替定义中的形式参数。
    1.#define指令
        #define预处理指令是用来定义宏的。该指令最简单的格式是:首先神明一个标识符,然后给出这个标识符代表的代码。在后面的源代码中,就用这些代码来替代该标识符。这种宏把程序中要用到的一些全局值提取出来,赋给一些记忆标识符。
            #define MAX_NUM 10
            int array[MAX_NUM];
            for(i=0;i<MAX_NUM;i++)  /*……*/
       
        在这个例子中,对于阅读该程序的人来说,符号MAX_NUM就有特定的含义,它代表的值给出了数组所能容纳的最大元素数目。程序中可以多次使用这个值。作为一种约定,习惯上总是全部用大写字母来定义宏,这样易于把程序红的宏标识符和一般变量标识符区别开来。如果想要改变数组的大小,只需要更改宏定义并重新编译程序即可。
        宏表示的值可以是一个常量表达式,其中允许包括前面已经定义的宏标识符。例如:
            #define ONE 1
            #define TWO 2
            #define THREE (ONE+TWO)
        注意上面的宏定义使用了括号。尽管它们并不是必须的。但出于谨慎考虑,还是应该加上括号的。例如:
            six=THREE*TWO;
        预处理过程把上面的一行代码转换成:
            six=(ONE+TWO)*TWO;
        如果没有那个括号,就转换成six=ONE+TWO*TWO;了。
        宏还可以代表一个字符串常量,例如:
            #define VERSION "Version 1.0 Copyright(c) 2003"
    2.带参数的#define指令
        带参数的宏和函数调用看起来有些相似。看一个例子:
            #define Cube(x) (x)*(x)*(x)
        可以时任何数字表达式甚至函数调用来代替参数x。这里再次提醒大家注意括号的使用。宏展开后完全包含在一对括号中,而且参数也包含在括号中,这样就保证了宏和参数的完整性。看一个用法:
            int num=8+2;
            volume=Cube(num);
        展开后为(8+2)*(8+2)*(8+2);
        如果没有那些括号就变为8+2*8+2*8+2了。
        下面的用法是不安全的:
            volume=Cube(num++);
        如果Cube是一个函数,上面的写法是可以理解的。但是,因为Cube是一个宏,所以会产生副作用。这里的擦书不是简单的表达式,它们将产生意想不到的结果。它们展开后是这样的:
            volume=(num++)*(num++)*(num++);
        很显然,结果是10*11*12,而不是10*10*10;
        那么怎样安全的使用Cube宏呢?必须把可能产生副作用的操作移到宏调用的外面进行:
            int num=8+2;
            volume=Cube(num);
            num++;
    3.#运算符
        出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符。例如:

            #define PASTE(n) "adhfkj"#n

            main()
            {
               printf("%s/n",PASTE(15));
            }
        宏定义中的#运算符告诉预处理程序,把源代码中任何传递给该宏的参数转换成一个字符串。所以输出应该是adhfkj15。
    4.##运算符
        ##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。看下面的例子:

            #define NUM(a,b,c) a##b##c
            #define STR(a,b,c) a##b##c

            main()
            {
                printf("%d/n",NUM(1,2,3));
                printf("%s/n",STR("aa","bb","cc"));
            }

        最后程序的输出为:
                 123
                 aabbcc
        千万别担心,除非需要或者宏的用法恰好和手头的工作相关,否则很少有程序员会知道##运算符。绝大多数程序员从来没用过它。

三、条件编译指令
    条件编译指令将决定那些代码被编译,而哪些是不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。
    1.#if指令
        #if指令检测跟在制造另关键字后的常量表达式。如果表达式为真,则编译后面的代码,知道出现#else、#elif或#endif为止;否则就不编译。
    2.#endif指令
        #endif用于终止#if预处理指令。

            #define DEBUG 0
            main()
            {
                #if DEBUG
                    printf("Debugging/n");
                #endif
                    printf("Running/n");
            }

        由于程序定义DEBUG宏代表0,所以#if条件为假,不编译后面的代码直到#endif,所以程序直接输出Running。
        如果去掉#define语句,效果是一样的。
    3.#ifdef和#ifndef
        #define DEBUG

        main()
        {
            #ifdef DEBUG
                printf("yes/n");
            #endif
            #ifndef DEBUG
                printf("no/n");
            #endif
        }
        #if defined等价于#ifdef; #if !defined等价于#ifndef
    4.#else指令
        #else指令用于某个#if指令之后,当前面的#if指令的条件不为真时,就编译#else后面的代码。#endif指令将中指上面的条件块。

        #define DEBUG

        main()
        {
            #ifdef DEBUG
                printf("Debugging/n");
            #else
                printf("Not debugging/n");
            #endif
                printf("Running/n");
       }

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

        #define TWO

        main()
        {
            #ifdef ONE
                printf("1/n");
            #elif defined TWO
                printf("2/n");
            #else
                printf("3/n");
            #endif
        }
        程序很好理解,最后输出结果是2。

    6.其他一些标准指令
        #error指令将使编译器显示一条错误信息,然后停止编译。
        #line指令可以改变编译器用来指出警告和错误信息的文件号和行号。
        #pragma指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。


C宏定义的简单总结


来自:http://www.cppblog.com/BlueSky/archive/2007/11/20/37000.html
今天在网上突然发现了下面几个关于c代码中的宏定义的说明,回想下,好像在系统的代码中也见过这些零散的定义,但没有注意,看到别人总结了下,发现果然很有用,虽然不知有的道可用与否,但也不失为一种手段,所以就先把它摘抄下来,增加一点见识:

1,防止一个头文件被重复包含
#ifndef BODYDEF_H
#define BODYDEF_H
  //头文件内容
#endif
2,得到指定地址上的一个字节或字
#define  MEM_B( x )  ( *( (byte *) (x) ) )
#define  MEM_W( x )  ( *( (word *) (x) ) )
3,得到一个field在结构体(struct)中的偏移量
#define FPOS( type, field ) ( (dword) &(( type *) 0)-> field )
4,得到一个结构体中field所占用的字节数
#define FSIZ( type, field ) sizeof( ((type *) 0)->field )
5,得到一个变量的地址(word宽度)
#define  B_PTR( var )  ( (byte *) (void *) &(var) )
#define  W_PTR( var )  ( (word *) (void *) &(var) )
6,将一个字母转换为大写
#define  UPCASE( c ) ( ((c) >= ''a'' && (c) <= ''z'') ? ((c) - 0x20) : (c) )
7,判断字符是不是10进值的数字
#define  DECCHK( c ) ((c) >= ''0'' && (c) <= ''9'')
8,判断字符是不是16进值的数字
#define  HEXCHK( c ) ( ((c) >= ''0'' && (c) <= ''9'') ||((c) >= ''A'' && (c) <= ''F'') ||((c) >= ''a'' && (c) <= ''f'') )
9,防止溢出的一个方法
#define  INC_SAT( val )  (val = ((val)+1 > (val)) ? (val)+1 : (val))
10,返回数组元素的个数
#define  ARR_SIZE( a )  ( sizeof( (a) ) / sizeof( (a[0]) ) )
11,使用一些宏跟踪调试
ANSI标准说明了五个预定义的宏名。它们是:
_LINE_ (两个下划线),对应%d
_FILE_     对应%s
_DATE_   对应%s
_TIME_    对应%s
_STDC_

 
宏中"#"和"##"的用法
我们使用#把宏参数变为一个字符串,用##把两个宏参数贴合在一起.
#define STR(s)     #s
#define CONS(a,b)  int(a##e##b)
Printf(STR(vck));           // 输出字符串"vck"
printf("%d\n", CONS(2,3));  // 2e3 输出:2000

当宏参数是另一个宏的时候
需要注意的是凡宏定义里有用"#"或"##"的地方宏参数是不会再展开.
#define A          (2)
#define STR(s)     #s
#define CONS(a,b)  int(a##e##b)
printf("%s\n", CONS(A, A));               // compile error 
这一行则是:
printf("%s\n", int(AeA));
INT_MAX和A都不会再被展开, 然而解决这个问题的方法很简单. 加多一层中间转换宏.
加这层宏的用意是把所有宏的参数在这层里全部展开, 那么在转换宏里的那一个宏(_STR)就能得到正确的宏参数
#define STR(s)      _STR(s)          // 转换宏
#define CONS(a,b)   _CONS(a,b)       // 转换宏
printf("int max: %s\n", STR(INT_MAX));          // INT_MAX,int型的最大值,为一个变量 #include<climits>
输出为: int max: 0x7fffffff
STR(INT_MAX) -->  _STR(0x7fffffff) 然后再转换成字符串;

printf("%d\n", CONS(A, A));
输出为:200
CONS(A, A)  -->  _CONS((2), (2))  --> int((2)e(2))

"#"和"##"的一些应用特例
1、合并匿名变量名
#define  ___ANONYMOUS1(type, var, line)  type  var##line
#define  __ANONYMOUS0(type, line)  ___ANONYMOUS1(type, _anonymous, line)
#define  ANONYMOUS(type)  __ANONYMOUS0(type, __LINE__)
例:ANONYMOUS(static int);  即: static int _anonymous70;  70表示该行行号;
第一层:ANONYMOUS(static int);  -->  __ANONYMOUS0(static int, __LINE__);
第二层:                        -->  ___ANONYMOUS1(static int, _anonymous, 70);
第三层:                        -->  static int  _anonymous70;
即每次只能解开当前层的宏,所以__LINE__在第二层才能被解开;

2、填充结构
#define  FILL(a)   {a, #a}

enum IDD{OPEN, CLOSE};
typedef struct MSG{
  IDD id;
  const char * msg;
}MSG;

MSG _msg[] = {FILL(OPEN), FILL(CLOSE)};
相当于:
MSG _msg[] = {{OPEN, "OPEN"},
              {CLOSE, "CLOSE"}};

3、记录文件名
#define  _GET_FILE_NAME(f)   #f
#define  GET_FILE_NAME(f)    _GET_FILE_NAME(f)
static char  FILE_NAME[] = GET_FILE_NAME(__FILE__);

4、得到一个数值类型所对应的字符串缓冲大小
#define  _TYPE_BUF_SIZE(type)  sizeof #type
#define  TYPE_BUF_SIZE(type)   _TYPE_BUF_SIZE(type)
char  buf[TYPE_BUF_SIZE(INT_MAX)];
     -->  char  buf[_TYPE_BUF_SIZE(0x7fffffff)];
     -->  char  buf[sizeof "0x7fffffff"];
这里相当于:
char  buf[11]; 

 
来自  :http://blog.21ic.com/user1/8499/archives/2012/92721.html

C语言宏定义技巧 作者 仙帝将王 日期 2012-6-13 7:25:00
1,防止一个头文件被重复包含 
#ifndef COMDEF_H 
#define COMDEF_H 
  //头文件内容 
#endif 
2,重新定义一些类型,防止由于各种平台和编译器的不同,而产生的类型字节数差异,方便移植。 
typedef  unsigned char      boolean;     /* Boolean value type. */ 
typedef  unsigned long int  uint32;      /* Unsigned 32 bit value */ 
typedef  unsigned short     uint16;      /* Unsigned 16 bit value */ 
typedef  unsigned char      uint8;       /* Unsigned 8  bit value */ 
typedef  signed long int    int32;       /* Signed 32 bit value */ 
typedef  signed short       int16;       /* Signed 16 bit value */ 
typedef  signed char        int8;        /* Signed 8  bit value */ 
//下面的不建议使用 
typedef  unsigned char     byte;         /* Unsigned 8  bit value type. */ 
typedef  unsigned short    word;         /* Unsinged 16 bit value type. */ 
typedef  unsigned long     dword;        /* Unsigned 32 bit value type. */ 
typedef  unsigned char     uint1;        /* Unsigned 8  bit value type. */ 
typedef  unsigned short    uint2;        /* Unsigned 16 bit value type. */ 
typedef  unsigned long     uint4;        /* Unsigned 32 bit value type. */ 
typedef  signed char       int1;         /* Signed 8  bit value type. */ 
typedef  signed short      int2;         /* Signed 16 bit value type. */ 
typedef  long int          int4;         /* Signed 32 bit value type. */ 
typedef  signed long       sint31;       /* Signed 32 bit value */ 
typedef  signed short      sint15;       /* Signed 16 bit value */ 
typedef  signed char       sint7;        /* Signed 8  bit value */ 
3,得到指定地址上的一个字节或字 
#define  MEM_B( x )  ( *( (byte *) (x) ) ) 
#define  MEM_W( x )  ( *( (word *) (x) ) ) 
4,求最大值和最小值 
   #define  MAX( x, y ) ( ((x) > (y)) ? (x) : (y) ) 
   #define  MIN( x, y ) ( ((x) < (y)) ? (x) : (y) ) 
5,得到一个field在结构体(struct)中的偏移量 
#define FPOS( type, field ) \ 
/*lint -e545 */ ( (dword) &(( type *) 0)-> field ) /*lint +e545 */
6,得到一个结构体中field所占用的字节数 
#define FSIZ( type, field ) sizeof( ((type *) 0)->field ) 
7,按照LSB格式把两个字节转化为一个Word 
#define  FLIPW( ray ) ( (((word) (ray)[0]) * 256) + (ray)[1] ) 
8,按照LSB格式把一个Word转化为两个字节 
#define  FLOPW( ray, val ) \ 
  (ray)[0] = ((val) / 256); \ 
  (ray)[1] = ((val) & 0xFF) 
9,得到一个变量的地址(word宽度) 
#define  B_PTR( var )  ( (byte *) (void *) &(var) ) 
#define  W_PTR( var )  ( (word *) (void *) &(var) ) 
10,得到一个字的高位和低位字节 
#define  WORD_LO(xxx)  ((byte) ((word)(xxx) & 255)) 
#define  WORD_HI(xxx)  ((byte) ((word)(xxx) >> 8)) 
11,返回一个比X大的最接近的8的倍数 
#define RND8( x )       ((((x) + 7) / 8 ) * 8 ) 
12,将一个字母转换为大写 
#define  UPCASE( c ) ( ((c) >= ’a’ && (c) <= ’z’) ? ((c) - 0x20) : (c) ) 
13,判断字符是不是10进值的数字 

#define  DECCHK( c ) ((c) >= ’0’ && (c) <= ’9’) 


14,判断字符是不是16进值的数字 

#define  HEXCHK( c ) ( ((c) >= ’0’ && (c) <= ’9’) ||\ 

                       ((c) >= ’A’ && (c) <= ’F’) ||\ 

((c) >= ’a’ && (c) <= ’f’) ) 


15,防止溢出的一个方法 

#define  INC_SAT( val )  (val = ((val)+1 > (val)) ? (val)+1 : (val)) 


16,返回数组元素的个数 

#define  ARR_SIZE( a )  ( sizeof( (a) ) / sizeof( (a[0]) ) ) 


17,返回一个无符号数n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n) 

#define MOD_BY_POWER_OF_TWO( val, mod_by ) \ 

           ( (dword)(val) & (dword)((mod_by)-1) ) 


18,对于IO空间映射在存储空间的结构,输入输出处理 

  #define inp(port)         (*((volatile byte *) (port))) 

  #define inpw(port)        (*((volatile word *) (port))) 

  #define inpdw(port)       (*((volatile dword *)(port))) 

  

  #define outp(port, val)   (*((volatile byte *) (port)) = ((byte) (val))) 

  #define outpw(port, val)  (*((volatile word *) (port)) = ((word) (val))) 

  #define outpdw(port, val) (*((volatile dword *) (port)) = ((dword) (val))) 

[2005-9-9添加] 


19,使用一些宏跟踪调试 

A N S I标准说明了五个预定义的宏名。它们是: 

_ L I N E _ 

_ F I L E _ 

_ D A T E _ 

_ T I M E _ 

_ S T D C _ 

如果编译不是标准的,则可能仅支持以上宏名中的几个,或根本不支持。记住编译程序 

也许还提供其它预定义的宏名。 

_ L I N E _及_ F I L E _宏指令在有关# l i n e的部分中已讨论,这里讨论其余的宏名。 

_ D AT E _宏指令含有形式为月/日/年的串,表示源文件被翻译到代码时的日期。 

源代码翻译到目标代码的时间作为串包含在_ T I M E _中。串形式为时:分:秒。 

如果实现是标准的,则宏_ S T D C _含有十进制常量1。如果它含有任何其它数,则实现是 

非标准的。 

可以定义宏,例如: 

当定义了_DEBUG,输出数据信息和所在文件所在行 

#ifdef _DEBUG 

#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_) 

#else 

      #define DEBUGMSG(msg,date) 

#endif 
20,宏定义防止使用是错误 

用小括号包含。 

例如:#define ADD(a,b) (a+b) 
用do{}while(0)语句包含多语句防止错误 
例如:#difne DO(a,b) a+b;\ 

                   a++; 

你可能感兴趣的:(预编译和宏定义的相关知识)