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

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语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。

  宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。

  为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。

  文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。

  条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。

  使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。