1、预处理命令的定义
使用库函数之前,应该用#include引入对应的头文件。这种以#号开头的命令称为预处理命令。
所谓预处理是指在进行编译时的第一遍扫描(词法扫描和语法分析)之前所做的工作。预处理是C语言的一个重要功能,它由于处理程序负责完成。当编译一个程序时,系统将自动调用预处理程序对程序中“#”开头的预处理部分进行处理,处理完毕之后可以进入源程序的编译阶段。
C语言源文件要经过编译、链接才能生成可执行程序:
(1)编译(Compile)会将源文件(.c
文件)转换为目标文件。对于 VC/VS,目标文件后缀为.obj
;对于GCC,目标文件后缀为.o
。编译是针对单个源文件的,一次编译操作只能编译一个源文件,如果程序中有多个源文件,就需要多次编译操作。
(2)链接(Link)是针对多个文件的,它会将编译生成的多个目标文件以及系统中的库、组件等合并成一个可执行程序。
在实际开发中,有时候在编译之前还需要对源文件进行简单的处理。例如,我们希望自己的程序在 Windows 和 Linux 下都能够运行,那么就要在 Windows 下使用 VS 编译一遍,然后在 Linux 下使用 GCC 编译一遍。但是现在有个问题,程序中要实现的某个功能在 VS 和 GCC 下使用的函数不同(假设 VS 下使用 a(),GCC 下使用 b()),VS 下的函数在 GCC 下不能编译通过,GCC 下的函数在 VS 下也不能编译通过,怎么办呢?
这就需要在编译之前先对源文件进行处理:如果检测到是 VS,就保留 a() 删除 b();如果检测到是 GCC,就保留 b() 删除 a()。
这些在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)。
预处理主要是处理以#
开头的命令,例如#include <stdio.h>
等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。
预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
编译器会将预处理的结果保存到和源文件同名的.i
文件中,例如 main.c 的预处理结果在 main.i 中。和.c
一样,.i
也是文本文件,可以用编辑器打开直接查看内容。
C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
C语言中提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理的使用预处理便于阅读、修改、移植和调试,也有利于模块化程序设计。
2、include的用法
#include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种。
#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。
#include 的用法有两种,如下所示:
#include <stdio.h> //使用尖括号表示在系统头文件目录中去找 #include “math.h” //使用双引号表示首先在当前的源文件目录中去查找,若未找到再到系统头文件目录中去找
使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:
使用尖括号< >,编译器会到系统路径下查找头文件;
而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。
stdio.h 和 stdlib.h 都是标准头文件,它们存放于系统路径下,所以使用尖括号和双引号都能够成功引入;而我们自己编写的头文件,一般存放于当前项目的路径下,所以不能使用尖括号,只能使用双引号。
当然,你也可以把当前项目所在的目录添加到系统路径,这样就可以使用尖括号了,但是一般没人这么做,纯粹多此一举,费力不讨好。
在以后的编程中,大家既可以使用尖括号来引入标准头文件,也可以使用双引号来引入标准头文件;不过,我个人的习惯是使用尖括号来引入标准头文件,使用双引号来引入自定义头文件(自己编写的头文件),这样一眼就能看出头文件的区别。
关于 #include 用法的注意事项:
- 一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。
- 同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。
- 文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
3、宏定义define
#define 叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。
(1)、无参宏定义
格式:#define 宏名 字符串
#
表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名
是标识符的一种,命名规则和变量相同。字符串
可以是数字、表达式、if 语句、函数等。
字符串可以是常量、表达式、格式串等
注意:字符串为表达式时该加括号时记得加括号,如果字符串后面有分号会连分号一同替换。
例如:
#define N 100
N
为宏名,100
是宏的内容(宏所表示的字符串)。在预处理阶段,对程序中所有出现的“宏名”,预处理器都会用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。
宏定义是由源程序中的宏定义命令#define
完成的,宏替换是由预处理程序完成的。
宏定义只是简单的字符串代换,是在预处理完成的,而typedef是在编译时处理的,它不是简单地代换,而是对类型说明符重新命名。
宏定义的作用域包括从宏定义命名起到源程序结束,如果要终止其作用域可以使用#undef命令来取消宏定义:
格式:#undef 标识符
(2)、带参宏定义
格式:#define 宏名(形参表) 字符串
注意:宏名域形参表之间不能有空格出现,否则会把形参当做字符串处理
C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
带参宏定义和函数的区别:
带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。
(3)C语言宏参数的字符串化和宏参数的连接
在宏定义中,有时还会用到#
和##
两个符号,它们能够对宏参数进行操作:
#
用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。例如有如下宏定义:
#define STR(s) #s 那么: printf("%s", STR(c.biancheng.net)); printf("%s", STR("c.biancheng.net")); 分别被展开为: printf("%s", "c.biancheng.net"); printf("%s", "\"c.biancheng.net\"");
可以发现,即使给宏参数“传递”的数据中包含引号,使用#
仍然会在两头添加新的引号,而原来的引号会被转义。
##
称为连接符,用来将宏参数或其他的串连接起来。例如有如下的宏定义:
#define CON1(a, b) a##e##b #define CON2(a, b) a##b##00 那么: printf("%f\n", CON1(8.5, 2)); printf("%d\n", CON2(12, 34)); 将被展开为: printf("%f\n", 8.5e2); printf("%d\n", 123400);
对 #define 用法的几点说明
(1)宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
(2)宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
(3)宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef
命令。
(4)代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替。
(5)宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。
(6)习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
(7)可用宏定义表示数据类型,使书写方便。
应注意用宏定义表示数据类型和用 typedef 定义数据说明符的区别。宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。
4、预定义宏
在C语言中,有一些预处理定义的符号串,他们的值是字符串常量,或者是十进制数字常量,通常在调试程序时用于输出源程序的各项信息。
符号 |
|
含义 |
_FILE_ |
字符串常量 |
正在预编译的源文件名 |
_FUNCTION_ |
当前所在的函数名 |
|
_DATE_ |
预编译文件的日期 |
|
_TIME_ |
预编译文件的时间 |
|
_LINE_ |
整数常量 |
文件当前行的行号 |
_STDC_ |
如果编译器遵循ANSI C,则值为1
|
5、条件编译
预处理程序提供了条件编译的功能,可以按不同的条件去编译不同的程序代码,从而产生不同的目标代码文件,这对于程序的移植和调试是很有用的。避免了重复引用相同的头文件。
(1)#if的一般格式
#if 标识符 程序段1 #else 程序段2 #endif #if 标识符 程序段1 #elif 标识符 程序段2 #endif #if 标识符 程序段1 #elif 标识符 程序段2 #elif 标识符 程序段3 #else 程序段4 #endif 标识符值为真,就执行程序段1,否则执行程序段2.
(2)#ifdef的一般格式
#ifdef 标识符 程序段 #endif #ifdef 标识符 程序段1 #else 程序段2 #endif 若标识符已经被define宏定义过,就执行程序段1,否则执行1程序段2。
(3)#ifndef的一般格式
#ifndef 标识符 程序段 #endif #ifndef 标识符 程序段1 #else 程序段2 #endif 若标识符没有被define宏定义过,就执行程序段1,否则执行1程序段2。
三者之间的区别
最后需要注意的是,#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。
6、#error命令
#error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下:
#error error_message
例如,我们的程序针对 Linux 编写,不保证兼容 Windows,那么可以这样做:
#ifdef WIN32 #error This programme cannot compile at Windows Platform #endif
WIN32 是 Windows 下的预定义宏。当用户在 Windows 下编译该程序时,由于定义了 WIN32 这个宏,所以会执行 #error 命令,提示用户发生了编译错误,错误信息是:
This programme cannot compile at Windows Platform
7、预处理总结
预处理指令是以#
号开头的代码行,# 号必须是该行除了任何空白字符外的第一个字符。# 后是指令关键字,在关键字和 # 号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。
下面是本章涉及到的部分预处理指令:
指令 | 说明 |
---|---|
# | 空指令,无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else条件编译块 |
预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。
宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。
为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。