一、预处理简介
1.1 预处理简介
在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)。
预处理主要是处理程序代码中以 #
开头的预处理语句指令(语句),例如 #include <stdio.h>
等。预处理指令要放在所有函数之外,而且一般都放在源文件的开头部分。
预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,编译系统将自动调用预处理程序对源程序中的预处理指令部分作处理,处理完毕自动进入对源程序的编译。
编译器会将预处理的结果保存到和源文件同名的 .i 文件中,例如 main.c 的预处理结果存放在 main.i 文件中。和 .c 一样,.i 也是文本文件,可以用编辑器打开直接查看内容。
C语言提供了多种预处理功能,如 宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
二、C语言预处理指令
2.1 预处理指令
预处理指令 是以 #
号开头的代码行,#
号必须是该行除了任何空白字符外的第一个字符。#
后面是指令关键字,在关键字和 #
号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换(预处理)。
2.2 C语言预处理指令及功能
2.2.1 C语言预处理指令列表
指令 | 说明 |
---|---|
# | 空指令,无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#else | 如果 #if 条件为假,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else条件编译块 |
#pragma | 设置编译器的状态或者指定编译器完成一些特定的动作 |
#error | 当预处理器预处理到#error命令时将停止编译并输出用户自定义的错误消息 |
预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。 使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。
2.2.2 C语言 #include
文件包含命令
#include
是C语言预处理命令的一种,叫做文件包含命令,用来引入对应的头文件(.h文件)。文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
#include
的用法有两种,如下所示:
|
|
使用尖括号 < >
和双引号 " "
的区别在于引入头文件的搜索路径不同:
- 使用尖括号
< >
,编译器会到系统路径下查找头文件;- 而使用双引号
" "
,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找;
也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。 stdio.h 是标准头文件,它们存放于系统路径下,所以使用尖括号和双引号都能够成功引入;而我们自己编写的头文件,一般存放于当前项目的路径下,通常使用双引号引入。
当然,可以把当前项目所在的目录添加到系统路径,这样就可以使用尖括号了。
一般的习惯是使用尖括号 < >
来引入标准头文件,使用双引号 " "
来引入自定义头文件(自己编写的头文件),这样一眼就能看出头文件的区别。
关于 #include
用法的注意事项:
- 一个
#include
命令只能包含一个头文件,多个头文件需要多个#include
命令。 - 同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。
- 文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
头文件中只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。
2.2.3 C语言 #define
宏定义命令
#define
叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,在预处理阶段,对程序中所有出现的“宏”标识符 ,预处理器都会用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。
宏定义是由源程序中的宏定义命令#define完成的,宏替换是由预处理程序完成的。 宏定义的一般形式为:
|
|
- # 表示这是一条预处理命令,所有的预处理命令都以 # 开头。
- 宏名 是标识符的一种,命名规则和变量相同。
- 字符串 可以是数字、表达式、if 语句、函数等。
程序中反复使用的表达式就可以使用宏定义,例如:
|
|
它的作用是指定标识符M来表示 (n*n+3*n
) 这个表达式。在编写代码时,所有出现 (n*n+3*n
) 的地方都可以用 M 来表示,而对源程序编译时,将先由预处理程序进行宏代替,即用 (n*n+3*n
) 去替换所有的宏名 M,然后再进行编译。
需要注意的是,在宏定义中表达式 (
n*n+3*n
) 两边的括号不能少,否则在宏展开以后可能会产生歧义。
- 对
#define
用法的几点说明
- 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以包含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
- 宏定义不是程序语句,在行末不必加分号,如加上分号则连分号也一起替换。
- 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用
#undef
命令。 - 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如:
|
|
- 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。例如:
|
|
- 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
- 可用宏定义表示数据类型,使书写方便。例如:
|
|
应注意用宏定义表示数据类型和用 typedef 定义数据说明符的区别:
- 宏定义只是简单的字符串替换,由预处理器来处理;
typedef
是在编译阶段由编译器处理的,它并不是简单的字符串替换,而是给原有的数据类型起一个新的名字,将它作为一种新的数据类型。 例子:
|
|
由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。在使用时要格外小心,以避出错。
- 带参数的宏定义
C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。 对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
带参宏定义的一般形式为:
|
|
在字符串中可以含有各个形参。
带参宏调用的一般形式为:
宏名(实参列表);
例如:
|
|
- 对带参宏定义的说明
- 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。例如:
|
|
- 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。 这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。 示例
|
|
这与函数的调用是不同的,函数调用时要把实参表达式的值求出来再传递给形参,而宏展开中对实参表达式不作计算,直接按照原样替换。
- 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
例如上面的宏定义中
(y)*(y)
表达式的 y 都用括号括起来,因此结果是正确的。 如果去掉括号,把程序改为y*y
, 由于宏展开只是简单的符号替换的过程,没有任何其它的处理。 宏替换后将得到语句:sq=a+1*a+1;
这显然与题意相违,因此参数两边的括号是不能少的。即使在参数两边加括号还是不够的,请看下面程序:
|
|
由此可见,对于带参宏定义不仅要在参数两侧加括号,还应该在整个字符串外加括号。 如下所示:
|
|
#
和 ##
分别字符串化和将两个符号连接成一个符号
- # 字符串化
|
|
- ## 将两个符号连接成一个符号
## 可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
|
|
与宏相关的作用符号
- 换行符 ‘' 在每行末尾(除了最后一行)加上"",代表换行的意思。这个目的是为了不让代码冗余,如果代码都挤在一段,代码就不美观,可读性不好。
- 取消宏定义#undef 这条指令用于移除一个宏定义。
|
|
在#undef之后SORT就相当于没有定义,失效了。再使用则会报错!
C语言带参宏定义和函数的区别 带参数的宏和函数很相似,但有本质上的区别:
- 宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。
- 函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。
示例:
|
|
带参数的宏也可以用来定义多个语句,在宏调用时,把这些语句又替换到源程序中,请看下面的例子:
|
|
C语言预定义宏
__STDC__ 是预定义宏。当它被定义后,编译器将按照ansic标准来编译你的c程序。
ANSIC标准定义了以下6种可供C语言使用的预定义宏:
__LINE__ 在源代码中插入当前源代码行号
__FILE__ 在源代码中插入当前源代码文件名
__DATE__ 在源代码中插入当前编译日期〔注意和当前系统日期区别开来〕
__TIME__ 在源代码中插入当前编译时间〔注意和当前系统时间区别开来〕
__STDC__ 当要求程序严格遵循ANSIC标准时该标识符被赋值为1。
__cplusplus 当用C++编译程序编译时,标识符__cplusplus就会被定义。
标识符__LINE__和__FILE__通常用来调试程序;
标识符__DATE__和__TIME__通常用来在编译后的程序中加入一个时间标志,以区分程序的不同版本;
当要求程序严格遵循ANSIC标准时,标识符__STDC__就会被赋值为1;
函数名标识:通过__func__宏,您可以在函数中输出当前函数的名字,有助于代码审查和日志记录。
2.3 C语言条件编译详解
条件编译 允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。 示例: #ifdef WIN32 … #endif
2.4 #pragma预处理指令
#pragma 是一种预处理指令,它的作用是设置编译器的状态或者指定编译器完成一些特定的动作。其格式一般为:
|
|
parameter 示例及功能介绍:
- #pragma message:message参数 用于在编译信息输出窗口输出相应的信息,用于源代码的信息控制。其格式为#pragma message(“SDK 1.01”)。
- #pragma once:一般用于头文件中,用来保证头文件只被编译一次。
- #pragma code_seg:用来设置程序中函数代码存放的代码段。
- #pragma pack:用来控制内存对齐的方式。#pragma pack(n)编译器将按照n个字节进行对齐。#pragma pack()编译器将取消自定义字节对齐方式。
2.5 #error命令
#error 命令是C/C++语言的预处理命令之一,当预处理器预处理到#error命令时将停止编译并输出用户自定义的错误消息。 格式为:
|
|
如程序针对Linux编写,不保证兼容Windows,那么可以这样做:
|
|
WIN32 是Windows下的预定义宏。当用户在Windows下编译该程序时,由于定义了WIN32这个宏,预处理时会执行 #error 命令,提示用户发生了编译错误,错误信息是:
This programme cannot compile at Windows Platform
当希望以C++的方式来编译程序时,可以这样做:
|
|
注意:#error命令 的报错信息不需要加引号" “,如果加上,引号会被一起输出。