C语言程序在被编译成可执行文件时,也许你使用的IDE只需点击一下编译按钮,或者使用gcc编译器的,也是一条命令就完成了(当然使用命令时,会有些必须的参数)。但是真正实际情况,这个过程需要经历:预处理——》编译——》链接——》装载。当然我们的编译器已经高度发达,将这些内容全部实现了。
而预处理最长被我们使用一条指令是#include。我们用它来加载头文件。同时还会有些使用预处理指令的C宏或是编译条件等。
一:首先先将C语言所有预处理指令简单介绍一下:
1.#define指令:
该条指令最常被用来定义符号常量或明显的常量,当然定一个“类函数”宏也是需要使用它的。
每个#define行由三部分组成,第一部分 是指令自身#define,第二部分是所选择的缩略语,这些缩略语称为宏(macro)。第三部分为宏替换的主体(或列表)。举例如下:
#define PX printf("x is %d/n",x)
#define |
PX |
printf("x is %d/n",x) |
预处理指令 |
宏 |
主体(替换列表) |
其中对于第二部分(宏)有如下要求:宏的名字中不能有空格,而且必须遵循C变量命名规则:只能使用字母、数字和下划线(_)而且第一个字符不能为数字。同时要注意,对于所有预处理指令都是从#开始到第一个换行符结束。#define指令的作用范围是从出现位置开始到文件结束。
预处理器在处理时,将源文件中出现 的"宏"用其"主体"做替换。而且这种替换是可嵌套的。这种嵌套替换举例如下:
#define TWO 2
#define FOUR TWO*TWO
则FOUR将被做如下处理:第一次替换后变为:TWO*TWO,再次替换后变为2*2。
在const关键字得到C的支持后,又提供了一种创建常量的灵活方法。使用const 可以创建全局常量和局部常量、数字常量、数组常量和结构常量。对使用#define定义的符号常量可以被用来指定标准数组的大小,与const的简单举例类比。
#define LIMIT 20
const int LIM = 50;
static int data1[LIMIT]; //合法
static int data2[LIM]; //无效
const int LIM2 = 2*LIMIT; //合法
const int LIM3 = 2 *LIM; //无效
在 #define中使用参数。通过使用参数可以创建外形和作用都与函数相似的类函数宏。宏的参数也用圆括号括起来。举例如下:
#define |
MEAN(X,Y) |
(((X)+(Y))/2) |
预处理指令 |
宏(X和Y为宏的参数) |
替换主体 |
带参数的宏外形与函数非常相似,但是在使用时与真正的函数调用不完全相同。如果不能理解替换这种预处理形式很有可能出现意料之外的错误。见如下例子程序:
/* mac_arg.c -- macros with arguments */
#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 SQUARE(x+2): ");
PR(SQUARE(x+2));
printf("Evaluating 100/SQUARE(2): ");
PR(100/SQUARE(2));
printf("x is %d./n", x);
printf("Evaluating SQUARE(++x): ");
PR(SQUARE(++x));
printf("After incrementing, x is %x./n", x);
return 0;
}
使用gcc4.3.2编译后输出的结果如下:(在不同的编译器出现的结果可能不同)
x = 4
Evaluating SQUARE(x): The result is 16.
Evaluating SQUARE(2): The result is 4.
Evaluating SQUARE(x+2): The result is 14.
Evaluating 100/SQUARE(2): The result is 100.
x is 4.
Evaluating SQUARE(++x): The result is 36.
After incrementing, x is 6.
前两项的结果正确的但是接下来的则有些出乎意外。PR(SQUARE(x+2));时x值为4,那么x+2的SQUARE(x+2)时应该是6*6结果应该为36。而程序运行的结果则是14.如果你使用替换原则将SQUARE(x+2)替换则变为:
x+2*x+2
这时x=4则计算结果正是14.要处理这种情况(优先级问题),可以采用对替换主题加圆括号保证优先级。最好参数本身也使用括号,保证优先级正确性。修改如下:
#define SQUARE(X) ((X)*(X))
之后的情况类似。特别指出:
Evaluating SQUARE(++x): The result is 36.
将被替换成:++x*++x。这种运算顺序C语言没有给出规定,而这时对不同的编译器可能出现不同的结果,gcc4.3.2是在乘法运算前进行了x自加操作。而编译器也可以产生5*6的情况。不一而别。这里就需要注意了,在宏中一般不要使用自增或自减运算符。
2.#运算符:在宏中利用宏参数创建字符串。
如果定义如下一个宏:
#define PSQR(X) printf(“The square of X is %d/n”,((X)*(X)))
这样使用宏:
PSQR(8);
则输出为:
The square of X is 64。
该处双引号中的X并没有被替换,这是预处理的替换规则决定的。但是如果你希望在字符串中(双引号括起来的C语言认为是字符串)包含宏的参数。那么C语言提供了#运算符来实现。例子程序如下:
/* subst.c -- substitute in string */
#include <stdio.h>
#define PSQR(x) printf("The square of " #x " is %d./n",((x)*(x)))
int main(void)
{
int y = 5;
PSQR(y);
PSQR(2 + 4);
return 0;
}
输出如下:
The square of y is 25.
The square of 2 + 4 is 36.
可以看出第一次,用“y”代替了X。第二次用“2+4”代替了X。
3.##运算符:预处理器的粘合剂:
和#运算符一样,##运算符可以用于宏的替换部分。另外##还可以用于类对象宏的的替换部分。这个运算符吧两个语言符号组合成单个语言符号。例子程序如下:
// glue.c -- use the ## operator
#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 = 14
x2 = 20
可以看到使用##运算符后将x与n合并成一个字符串。
可变参数宏:...和_ _VA_ARGS_ _
具体应用举例如下:
#define PR(...) printf(_ _VA_ARGS_ _)
假设之后用下面的方式调用该宏
PR("Howdy");
PR("wetigt = %d,shipping =¥%f/n",wt,sp);
则第一次调用中_ _VA_ARGS_ _展开为一个参数。
"Howdy"
第二次调用中_ _VA_ARGS_ _展开为3个参数。
"wetigt = %d,shipping =¥%f/n",wt,sp
展开后代码为:
printf("Howdy");
printf("wetigt = %d,shipping =¥%f/n",wt,sp);
需要注意一点,省略号只能代替最后的宏参数。如下定义是错误的。
#define WRONG (X, ... , Y) #X#_ _VA_ARGS_ _#Y
4.#include:文件包含。
预处理器发现该指令后,就会寻找后跟的文件名并把该文件的内容复制包含到当前文件中。
详细内容可参见《C语言的变量作用域及头文件》中关于头文件部分介绍。
5.#undef 取消定义:
#undef指令取消已定义的一个给定#define。举例如下:
例如;
#define LIMIT 400
....
#undef LIMIT //此处之后LIMIT是没有定义的。
但是注意,C语言提供的几个预定义宏如: _ _DATE_ _和_ _FILE_ _等不可被取消。
6.:#ifdef 、#else和#endif 及#ifndef:条件编译指令组合
#ifdef 、#else和#endif 及#ifndef通过判断之后跟随的变量是否被#define定义而形成编译的条件。#ifdef判读后面的标识符是否为定义,而#ifndef则相反,判断是否未定义。#else则与两者中其一组合使用类似,if。。。else形式。#endif用于指示结束位置。
7.#if与#elif:条件编译指令组合。
#if指令更像C语言中if;#if后跟常量整数表达式,如果表达式为非零值,则表达式为真。#elif则可以与#if组合形成 if 。。。else if。。。else if 的条件组合序列。(早期预处理器可能不支持#elif)。举例如下:
#if SYS= =1
#include "test1.h"
#elif SYS = = 2
#include "test2.h"
#elif SYS = = 3
#include "test3.h"
#else
#include "test.h"
#endif
8.#line和#error:
#line指令用于重置_ _LINE_ _和_ _FILE_ _宏报告的行号和文件名。使用举例:
#line 1000 //把当前行重置为1000
#line 10 "cool.c"//把行号重置为10,文件名重置为cool.c
#error指令是预处理器发出一条错误消息,该消息饱和值了中的文本信息。可能的话编译过程应该中断。使用举例:
#if _ _STDC_VERSION_ _ != 199901L
#error Not C99
#endif
9.#pragma :编译器参数设定。
这个指令在现代编译器中被扩展的比较强大,同时通过必要的指令集来完成编译指示。不做详细介绍。
掌握以上预处理指令,和宏的实现。通过一些宏可以在编写程序时达到意想不到结果。
二:宏的使用技巧举例:
例一、用C宏,书写代码更简洁这段代码写网络程序的朋友都很眼熟,是Net/3中mbuf的实现。
struct mbuf
{
struct m_hdr mhdr;
union {
struct
{
struct pkthdr MH_pkthdr; /* M_PKTHDR set */
union
{
struct m_ext MH_ext; /* M_EXT set */
char MH_databuf[MHLEN];
} MH_dat;
} MH;
char M_databuf[MLEN]; /* !M_PKTHER, !M_EXT*/
} M_dat;
};
上面的代码,假如我想访问最里层的MH_databuf,那么我必须写M_dat.MH.MH_dat.MH_databuf; 这是不是很长,很难写呀?这样的代码阅读起来也不明了。其实,对于MH_pkthdr、MH_ext、MH_databuf来说,虽然不是在一个结构层次上,但是如果我们站在mbuf之外来看,它们都是mbuf的属性,完全可以压扁到一个平面上去看。所以,源码中有这么一组宏:
#define m_next m_hdr.mh_next
#define m_len m_hdr.mh_len
#define m_data m_hdr.mh_data
... ...
#define m_pkthdr M_dat.MH.MH_pkthdr
#define m_pktdat M_dat.MH.MH_dat.MH_databuf
... ...
这样写起代码来,是不是很精练呢!
例二、用C宏,实现跨平台和编译器的需要这方面的例子太好举了,一举一大摞,就从VC的库源码中随意copy一段出来吧。
#ifndef _CRTAPI1
#if _MSC_VER >= 800 && _M_IX86 >= 300
#define _CRTAPI1 __cdecl
#else /* _MSC_VER >= 800 && _M_IX86 >= 300 */
#define _CRTAPI1
#endif /* _MSC_VER >= 800 && _M_IX86 >= 300 */
#endif /* _CRTAPI1 */
#ifndef _SIZE_T_DEFINED
typedef unsigned int size_t;
#define _SIZE_T_DEFINED
#endif /* _SIZE_T_DEFINED */
#ifndef _MAC
#ifndef _WCHAR_T_DEFINED
typedef unsigned short wchar_t;
#define _WCHAR_T_DEFINED
#endif /* _WCHAR_T_DEFINED */
#endif /* _MAC */
#ifndef _NLSCMP_DEFINED
#define _NLSCMPERROR 2147483647 /* currently == INT_MAX */
#define _NLSCMP_DEFINED
#endif /* _NLSCMP_DEFINED */
当然想非常熟练而且正确的使用C语言的宏是需要时间和经验的。
:这四个步骤是在从源文件到完整可运行程序的过程。如果,你需要编译成可加载库,或者其他特殊使用,则有可能不同。