前言

预处理是在进行编译的第一遍扫描(扫描语法和词法)之前所做的工作,是C语言的一个重要功能,它由预处理程序负责完成。

前面的代码已经多次使用#开头的预处理命令,例如:#include <stdio.h>等,在源程序中这些命令都放在函数之外,而且一般都放在源文件的前面。

C语言提供了多种预处理命令,如宏定义,文件包含,条件编译等。C语言的预处理命令均是以#开始,末尾不加分号。合理的使用预处理指令可以使得程序便于阅读,修改和调试。

宏定义

在C语言源程序中允许用一个标识符来表示一个字符串,称为宏。被定义为宏的标识符称为宏名。在编译预处理时,对程序中所有出现的宏名,都用宏定义的字符串去替换,这称为宏替换或者宏展开

宏定义是由源程序中的宏定义命令完成的,宏替换是由预处理程序自动完成的。

宏定义是C语言提供的三种常用预处理命令中的一种,使用宏定义可以防止出错,并且可以提高程序的可移植性和可读性。宏分为不带参数和带参数两种。

不带参数的宏定义

不带参数的宏定义语法格式如下:

1
2
3
4
//语法格式
#define 标识符 字符串
//代码示例
#define PI 3.14

其中#表示这是一条预处理命令。define为宏定义命令。标识符就是所谓的符号常量,也称为宏名。字符串可以是参数,表达式,格式串等。

预处理工作也称为宏展开,就是将宏名替换为字符串。掌握宏的关键就是“换”。例如:

1
#define PI 3.14

它的作用就是指定标识符PI来代替常量3.14。在编写程序的时候,所有的3.14都可以使用PI来表示,而对于源程序编译的时候,将由预处理程序进行宏替换,即将PI的部分再替换回3.14,然后再进行编译。

关于宏定义的说明:

  1. 宏名习惯上用大写字母表示,以便于与变量区别,不过你也可以使用小写

  2. 使用宏可以提高程序的拓展性和易读性。

  3. 预处理是在编译之前进行的处理,而编译的工作之一就是语法检查,也就是说,预处理不做语法检查

  4. 宏定义不是语句,在句末不必加分号,如果加上分号,则连分号一起替换

  5. 宏定义必须写在函数外,默认其作用域为:从宏定义命令开始到程序结束。

    如果要终止其作用域可以使用#undef命令,例如:

    1
    2
    3
    4
    5
    6
    #define PI 3.14
    int main(){
    //很多代码
    }
    #undef PI
    //又是很多代码

    表示该宏定义只在main函数中有效。

  6. 宏定义允许嵌套,在宏定义的字符串中可以使用已定义的宏名。在宏展开时由预处理程序层层替换。例如:

    1
    2
    3
    4
    5
    #define PI 3,14
    #define C 2*PI
    //以上宏定义的不限制顺序,例如下面的和上面的结果是一致的
    #define C 2*PI
    #define PI 3,14
  7. 宏定义不分配存储空间,变量定义才分配存储空间。

  8. 宏定义以回车符结束,如果宏定义超过一行,可以在行末加反斜杠\来续行。

  9. 宏定义中也可以没有替换的字符串,这种宏定义常作为条件编译检测的一个标志。例如:

    1
    #define FLAG

    【关于宏定义无字符串的意义】

    【说明】这部分解释需要学会下面的条件编译命令再看

    比如:#define USEHDMI表示定义USEHDMI这个宏,但内容是空的,这样的宏一般不会用于替换
    -用途:在程序中会这样用

    1
    2
    3
    4
    5
    \#ifdef USEHDMI
    ... //宏被定义时的处理程序
    \#else
    ... //宏未被定义时的程序
    \#endif

    这样假设我们在使用HDMI接口时会在头文件中写:#define USEHDMI,或写#define USEHDMI 1也是一样的,否则用默认模式可注释此句或写:#undef USEHDMI,即可实现程序增加处理HDMI接口的部分,或者去除。

    以上解释出自c++程序中宏定义只有宏名没有字符串是怎么一回事

  10. 字符,字符串和注释中永远不做宏处理,即如果在其中包含宏字符,不会进行宏替换处理。

    需要注意的是,变量和宏名不可以相同

    【实例】验证宏名和字符串

    【代码示例】

    1
    2
    3
    4
    5
    6
    #include <stdio.h>
    #define PI 3.14
    int main(){
    printf("PI %f\n", PI);
    return 0;
    }

    【输出】

    image-20220121162246646

带参数的宏定义

C语言允许宏带有参数。宏定义中的参数称为形参,宏调用中的参数称为实参。对于带参数的宏,在调用中,不仅要宏展开,还要用实参去代替形参。

带参数的宏定义一般形式为:

1
#define 宏名(参数列表) 字符串

在字符串中可以含有多个形参。

带参数宏调用的一般形式为:

1
宏名(实参列表)

例如:

1
2
#define S(a,b) a*b
area=S(3,2); //第一步替换为area=a*b,第二部替换为area=3

【实例】利用带参数的宏定义求三个数的最小值

【代码示例】

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#define Min(a,b) (a<b)?a:b
int main(){
int x, y, z, min; //声明三个数值和最小值
printf("请输入三个数值");
scanf("%d %d %d", &x, &y, &z);
min = Min(x, y);
min = Min(min, z);
printf("最小值为;%d", min);
return 0;
}

【输出结果】

image-20220121164749966

关于带参数的宏定义需要注意以下几点:

  1. 宏名和参数的括号间不能有空格

  2. 宏替换只做替换,不做计算,不做表达式求解。

  3. 在宏定义中形参是标识符,而宏调用中的实参可以是表达式。

  4. 在带参数宏定义中,形参不分配存储空间,因此不必进行类型声明,而宏调用中的实参有具体的值,要用它们去替换形参,因此必须进行类型声明。

  5. 带参数的宏和带参数函数很相似,但有本质上的不同:

    函数调用在编译后程序运行时进行,占用运行时间(分配内存,保留现场,值传递,返回值);宏替换在编译前进行,不分配内存,不占用运行时间,只占用编译时间。

    在函数中,形参和实参是两个不同的量,各有自己的作用域,调用时要把实参传递给形参,进行“值传递”;而在带参数宏中,只是符号替换,不存在值传递。

    函数只有一个返回值,利用宏可以设法得到多个返回值;

    宏展开使源程序边长。

  6. 宏定义也可以用来定义多个语句,在宏调用时,把这些语句替换到源程序中。

撤销宏定义命令

宏定义命令#define应该写在函数外面,通常写在一个文件开头,这样宏定义的作用范围是整个文件。可以使用命令#undef撤销已定义的宏,终止该宏定义的作用域

【实例】撤销已定义的宏示例

【代码示例】

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#define PI 3.14 //宏定义PI=3.14
int main(){
printf("派值为;%f", PI);
Test();
return 0;
}
#undef PI //终止宏定义PI
void Test(){
printf("%f", PI);
}

【输出】

image-20220121170916060

文件包含命令

文件包含是指一个源文件可以将另一个源文件的全部内容包含进来,即将另一个文件包含到本文件中。文件包含命令是以#include开头的预处理命令,在前面的部分使用各种C语言自带的函数时已经使用了文件包含命令。在C语言中,这个命令可以使得程序分为多个模块,被不同的程序员编写。有些公用的符号常量或宏定义等可以单独组成一个文件,在其他文件的开头用文件包含命令包含该文件即可使用。这样可以避免在每个文件开头编写这些公用量,从而节省时间,减少出错。

文件包含命令语法格式如下:

1
2
3
4
//格式1
#include "文件名"
//格式2
#include <文件名>

格式1:系统先在本程序文件所在的磁盘和路径下寻找包含文件,若找不到,再按系统规定的路径搜索包含文件

格式2:系统仅按规定的路径搜索包含文件:在包含文件目录中查找(包含目录是由用户在设置环境是设置的),而不在源文件目录去查找。

需要注意的是:

  1. 一个#include命令只能包含一个文件,若有多个文件要包含,则需要用多个#include命令
  2. 为了避免寻找包含文件时出错,如果是包含系统头文件通常使用格式2,其他情况使用格式1
  3. 由于被包含文件的内容全部出现在源程序清单中,所以其内容必须是C语言的源程序清单,否则编译时会出错
  4. 文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。
  5. 文件包含命令还有一个很重要的功能:能将多个源程序清单合并成一个源程序进行编译。

【实例】包含多个文件合并编译

【代码示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//文件1
#include <stdio.h>
int Plus(a,b){
return a + b;
}

//文件2
#include <stdio.h>
void PutOut(){
printf("输出结果为:\n");
}

//文件3:主入口
#include <stdio.h>
#include "2.1.c" //载入文件1
#include "2.2.c" //载入文件2
int main(){
PutOut();
printf("%d",Plus(3,4));
return 0;
}

【输出】

image-20220121183257670

条件编译命令

一般情况下,源程序中所有的行都参加编译,但有时希望其中的部分内容只有在满足一定的条件下才进行编译,即对一部分内容指定编译条件,这就是条件编译。条件编译命令将决定哪些代码被编译,哪些不被编译。可将表达式的值或某个特定宏是否被定义作为编译条件。

条件编译有三种语法格式,代码如下:

  • 格式一:

    1
    2
    3
    4
    5
    6
    //格式1
    #ifdef 标识符
    程序段1
    #else
    程序段2
    #endif

    其功能是,如果标识符已使用#define命令定义则对程序段1进行编译,否则对程序段2进行编译。如果没有程序段2,则可以写为:

    1
    2
    3
    #ifdef 标识符
    程序段1
    #endif
  • 格式二:

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

    与格式一不同的是由#ifdef变为#ifndef,它的功能是,如果标识符未被#define命令定义,则对程序段1进行编译,否则对程序段2进行编译

    【实例】进行条件编译命令

    【代码示例】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>
    int main(){
    #ifdef PI
    printf("%f", PI);
    #else
    printf("PI未定义");
    #endif
    return 0;
    }

    【输出】

    image-20220121210007943
  • 格式三:

    1
    2
    3
    4
    5
    #if 常量表达式
    程序段1
    #else
    程序段2
    #endif

    其功能是,如果常量表达式的值为真(非0),则对程序段1进行编译,反之则对程序段2进行编译。因此可以使程序在不同条件下完成不同的功能。

    【实例】测试格式三

    【代码示例】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>
    #define c 1 //c的值非0时,执行加法,反之执行减法
    int main(){
    int a, b;
    scanf("%d %d", &a, &b);
    #if c
    printf("a加b的和为:%d", a + b);
    #else
    printf("a和b的差为:%d",a-b);
    #endif
    return 0;
    }

    【输出】

    image-20220121211515511

    对于预处理命令里的标识符或者变量,和代码中定义的不通用,且名称不可重复