一、什么是预处理命令
在编译程序之前对源文件进行简单的加工的过程就称为 预处理。预处理主要是处理以 #
开头的命令。预处理 是 C语言 的一个重大的功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分做处理,处理完毕自动进入对源程序的编译。
C语言 提供了多种预处理功能,如 宏定义、文件包含、条件编译 等,合理地使用预处理会使编写的程序便于阅读、修改、移植 和 调试,也有利于模块化程序设计。
二、宏定义
使用 #define
命令就是要定义一个可替换的宏。宏定义是预处理命令的一种,它提供了一种可以替换源代码中字符串的机制。根据宏定义中是否有参数,可以将宏定义分为不带参数的宏定义和带参数的宏定义两种。
2.1、不带参数的宏定义
宏定义指令 #define
用来定义一个标识符和一个字符串,以这个标识符来代表这个字符串,在程序中每次遇到标识符时就会用所定义的字符串替换它。宏定义的作用相当于给指定的字符串起一个别名。
宏定义不带参数的一般格式如下:
#define 宏名 字符串
#
:表示这个一条预处理命令;宏名
:是一个标识符,必须符合 C语言 标识符的规定;字符串
:字符串中国可以含任何字符,可以是常数、表达式、格式化字符串、函数等,预处理程序对它不做任何检查,如有错误,只能在编译已经被展开后的源程序时发现;
在预处理阶段,对程序中所有出现“宏名”的位置,预处理器都会用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。宏定义是由源程序中的宏定义命令 #define
完成的,宏替换是由预处理程序完成的。
#include <stdio.h>
#define PI 3.14
int main()
{
double radius = 0;
double perimeter = 0;
double area = 0;
printf("请输入圆的半径:");
scanf("%lf",&radius);
perimeter = 2*pi*radius;
printf("圆的周长为:%.2f\n",perimeter);
area = pi*radius*radius;
printf("圆的面积为:%.2f\n",area);
return 0;
}
宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一个简单的替换;
宏定义不是 C 语句,不需要在行末加分号,如果加上分号则连分号也一起替换;
宏名定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开的是由预处理程序层层嵌套;
如果在字符串中含有宏名,则不进行替换;
宏定义用于预处理命令,它不同于定义的变量,它只做字符替换,不分配内存空间;
2.2、带参数的宏定义
带参数的宏定义不只是简单的字符串替换,还要进行参数的替换,其一般形式如下:
#define 宏名(参数) 字符串
#include <stdio.h>
#define MAX(a,b) ((a>b)?a:b)
int main()
{
int num1 = 10,num2 = 20;
printf("num1: %d, num2 = %d\n",num1,num2);
printf("the max number is: %d\n",MAX(num1,num2));
return 0;
}
宏定义时,字符串内的形参要加括号,如果不加括号,其结果可能出错。
宏扩展时,必须使用括号来保护表达式中低优先级的操作符,以确保调用时能够达到想要的效果。
在对带参数的宏展开,只是将语句中的宏名后面内的实际参数字符串代替
#define
命令行中的形式参数。在宏定义时,形参之间可以出现空格,宏名与参数的括号之间不可以加空格,否则会将空格以后的字符都作为替代字符串的一部分。
在带参数的宏定义中,形式参数不会分配内存单元,因此不必指定数据类型,而在宏调用,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型;
带参宏定义和函数的区别:
宏展开仅仅是字符串的替换,不会带表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。用宏替换代替替代的函数的好处是宏替换增加了代码的速度,因为不存在函数调用,但是可以存在多次宏替换从而提高了代码的长度;
三、include指令
在一个源文件中使用 #include
指令可以将另一个源文件的全部内容包含进来,也就是将另外的文件包含到本文件中。#include
使编译程序将另一源文件嵌入带有 #include
的源文件中,被读入的源文件必须用 双引号 或者 尖括号 括起来。
#include <头文件名.h>
#include "头文件名.h"
使用 尖括号 时,系统带存放C库函数头文件所在的目录中寻找要包含的文件;使用 双引号 时,系统先在用户当前目录中寻找要包含的文件,若找不到,再到存放C库函数头文件所在的目录中寻找要包含的文件。
通常情况下,如果为调用库函数用 #include
命令来包含相关的头文件,则用尖括号可以节省查找的时间。如果要包含的时用户自己编写的文件,一般用双引号,用户自己编写的文件通常是在当前目录中,如果文件不在当前目录中,双引号中可以给出为文件路径。
经常用在文件头部的被包含的文件成为“标题文件”或“头部文件”,一般以 .h 为后缀。一般我们会把以下内容放在头文件中:
- 宏定义
- 结构体、联合体 和 枚举声明
- typeof 声明
- 外部函数声明
- 全局变量声明
一个 #include 命令只能指定一个被包含的文件;
文件包含是可以嵌套的,即在一个被包含文件中还可以包含另一个被包含的文件;
四、条件编译
预处理器提供了条件编译功能,一般情况下,源程序中所有的行都参与编译,但是有时候希望只对其中一部分内容在满足一定条件下时才进行编译,这时就需要使用到一些条件编译命令。使用条件编译可方便地处理程序的调试版本和正式版本,同时还会增强程序的可移植性。
4.1、#if、#elif、#else命令
#if
的基本含义是:如果 #if
命令后的参数表达式为真,则编译 #if
到 #endif
之间的程序段,否则跳过这段程序。#endif
命令用来表示 #if
段的结束。#if
命令的一般格式如下:
#if 常量表达式
语句段
#endif
#else
的作用是为 #if
为假时提供另有一种选择:
#if 常量表达式
语句段1
#else
语句段2
#endif
#elif
指用用来建立一种“如果...或者如果...”这样阶梯状多重编译操作选择:
#if 常量表达式1
语句段1
#elif 常量表达式2
语句段2
...
#elif 常量表达式n
语句段n
#endif
#include <stdio.h>
#define NUM 10
int main()
{
#if NUM < 10
printf("该数比10小\n");
#elif NUM > 10
printf("该数比10大\n");
#else
printf("该数等于10\n");
#endif
printf("该数为:%d\n",NUM);
return 0;
}
4.2、#ifdef、#ifndef命令
#ifdef
与 #ifndef
命令,分别表示“如果有定义”与“如果没有定义”。#ifdef
的一般形式如下:
#ifdef 宏替换名
语句段
#endif
其含义是:如果宏替换名已经定义过,则对“语句段”进行编译;如果为定义 #ifdef
后面的宏替换名,则不对语句进行编译;
#ifdef
可以与 #else
连用,构成的一般形式如下:
#ifdef 宏替换名
语句段1
#else
语句段2
#endif
其含义是:如果宏替换名已经被定义过,则对“语句段1”进行编译;如果未定义 #ifdef
后面的宏替换名,则对“语句段2”进行编译。
#include <stdio.h>
#define STR "Believe yourself\n"
int main()
{
#ifdef STR
printf(STR);
#else
printf("Don't give up\n");
#endif
return 0;
}
#ifndef
的一般格式如下:
#ifndef 宏替换名
语句段
#endif
其含义是:如果未定义 #ifndef
后面的宏替换名,则对“语句段”进行编译;如果定义 #ifndef
后面的宏替换名,则不执行语句段。
同样,#ifndef
也可以与 #else
连用,构成的一般形式如下:
#ifndef 宏替换名
语句段1
#else
语句段2
#endif
其含义是:如果未定义 #ifndef
后面的宏替换名,则对“语句段1”进行编译;如果定义 #ifndef
后面的宏替换名,则对“语句段2”进行编译。
#include <stdio.h>
int main()
{
#ifndef STR
printf("Don't give up\n");
#else
printf(STR);
#endif
return 0;
}
4.3、#undef命令
使用 #undef
命令可以删除事先定义的宏。#undef
命令的一般格式如下:
#undef 宏替换名
#include <stdio.h>
#define STR "Believe yourself\n"
int main()
{
#ifdef STR
printf(STR);
#else
printf("Don't give up\n");
#endif
#undef STR
#ifndef STR
printf("Don't give up\n");
#else
printf(STR);
#endif
return 0;
}
undef 的主要目的是将宏名局限在仅需要它们的代码段中;
4.4、#line命令
#line
命令可以改变 __LINE__
与 __FILE__
的内容,__LINE__
当前编译行的行号,__FILE__
存放当前编译的文件名。#line
命令的一般形式如下:
#line 行号["文件名"]
其中,行号为任一正整数,可选的文件名为任意有效文件标识符,行号为源程序中当前行号,文件名为源文件的名字。#line
命令主要用于调试及其它特殊应用。
#include <stdio.h>
#line 100 "test.c"
int main()
{
printf("当前编译的文件:%s\n",__FILE__);
printf("当前行号为:%d\n",__LINE__);
return 0;
}
ANSI 标准说明了以下 5 个预定义宏替换名:
__LINE__
:其含义是当前被编译代码的行号;__FILE__
:其含义是当前源程序的文件名称;__DATA__
:其含义是当前源程序的创建日期;__TIME__
:其含义是当前源程序的创建时间;__STDC__
:其含义是用来判断当前编译器是否为标准C,若其值为 1 则表示符合标准C,否则不是标准C;
4.5、#parama命令
#parama
命令的作用是设定编译器的状态,或者指示编译器完成一些特定的动作。#parama
命令的一般形式如下:
#param 参数
其中,参数可以分为以下几种:
- messaage 参数能够在编译信息输出窗口中输出相应的信息;
- cod_seg 参数设置程序中函数代码存放的代码段;
- once 参数保证头文件被编译一次