预处理
预处理是进行源文件编译的第一个阶段(不是所有语言都有预处理过程)
预处理不对源文件进行分析,而是对源文件进行文本操作,如删除源文件中的注释,在源文件中插入包含文件的内容(#include),定义符号并替换源文件中的符号等(#define),通过这些处理,将会得到编译器实际进行分析的文本。
预处理器执行预处理的功能,而编译器往往将预处理器作为编译的第一个步骤,但是用户也可以单独调用预处理器,我们将C预处理器(C preprocessor)简写为 CPP 。
预处理指令
预处理器指令告知预处理器执行特定操作,使用预处理指令的优势常常体现在两个方面:
使程序易于修改;使源程序在不同的执行环境下能够进行恰当的编译。
预处理器语句使用的字符集与源文件语句相同,但是转义序列不受支持。
预处理指令可以识别下列指令:
指令 | 描述 |
---|---|
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译以下代码 |
#endif | 结束一个 #if … #else 条件编译块 |
#error | 当遇到标准错误时,输出错误信息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
#using | 将元数据导入程序编译 |
#line | 指令告诉预处理器将编译器内部存储的行号和文件名更改为给定行号和文件名 |
# | 空指令,无任何效果 |
预处理指令#include
-
包含文件:
我们一般将宏定义、结构声明、联合声明、枚举声明、typedef声明、外部函数声明、全局变量声明等放到一个文本文件中,该文件称为包含文件;
C语言包含文件拓展名一般采用 .h ,也可以采用其他拓展名;
文件的包含可以嵌套,即一个包含文件还可以包含其他包含文件
-
#include
指令功能:告知编译器将“包含文件”的内容包含在当前源文件,编译开始后预处理器将删除当前源文件中的
#include
指令,用“包含文件”的内容代替该指令;语法:
#include "path-spec"
或#include <path-spec>
-
path-spec
是一个现有文件的文件名,可以在其前放一个路径说明;
语法取决于运行编译程序的操作系统;
"path-spec"
方式表示在源文件当前目录查找包含文件,如果没有找到,则在“标准位置”查找;<path-spec>
方式表示编译器在“标准位置”查找包含文件,用户可以根据需要把其他目录添加到标准位置
-
预处理指令#define
宏定义
#define
创建一个宏,该宏是对标识符(或者是参数化标识符)与标记字符串的关联。在定义宏之后,编译器可用标记字符串替换源文件中标识符的每个匹配项
-
#define
替换规则在程序中扩展
#define
定义宏时,需要涉及几个步骤- 在调用宏时,首先对参数进行检查,看看是否包含任何由
#define
定义的符号。如果是,它们首先被替换 - 替换文本随后被插入到程序中原来文本的位置。对于带参数的宏,参数名被他们的值替换
- 最后,再次对文件进行扫描,看是否包含任何由
#define
定义的符号,如果是,就重复上述处理过程
- 在调用宏时,首先对参数进行检查,看看是否包含任何由
-
语法:
#define name stuff
(不带参数的宏定义);
#define name(parament-list) stuff
(带参数的宏定义);例:
-
不带参数的宏定义:
//头文件 global.h 内容如下: #define PI 3.14 #define WDITH 3.0 #define HEIGHT 2.0 #define STRHELLO "Hello World" //源文件 main.c 内容如下: #include <global.h> int main() { float areaC = 0.0, areaR = 0.0, r = 1.0; char exp[30] = STRHELLO; areaC = PI * r * r; areaR = WDITH * HEIGHT; return 0; } //预编译生成的 main.i 文件 int main() { float areaC = 0.0, areaR = 0.0, r = 1.0; char exp[30] = "Hello World"; areaC = 3.14 * r * r; areaR = 3.0 * 2.0; return 0; } //main.c中的PI、WDITH、HEIGHT、STRHELLO分别被3.14、3.0、2.0、"Hello World"代替
-
带参数的宏定义:
//头文件 global.h 内容如下: #define SWAP_1(x, y, t) do{\ t = x;\ x = y;\ y = t;\ }while(0) //值交换宏定义方法1 #define SWAP_2(x, y) do{\ x = x + y;\ y = x - y;\ x = x - y;\ }while(0) //值交换宏定义方法2 //源文件 main.c 内容如下: #include "global.h" int main() { int a = 90, b = 100, tem; SWAP_1(a, b, tem); SWAP_2(a, b); return 0; } //预编译生成的 main.i 文件 int main() { int a = 90, b = 100, tem; do { tem = a; a = b; b = tem; }while(0); do { a = a + b; b = a - b; a = a - b; }while(0); return 0; } // main.c 中的SWAP_1和SWAP_2分别被替换为上述两个 do-while 循环结构
-
带参数的宏定义使用的注意事项:
- 源文件中的宏被替换后将会与宏邻近的字符相互作用,在使用宏的时候,宏的实际参数避免采用表达式形式
- 在使用宏的时候,宏的参数如果可能会用到表达式的形式,那么定义宏的时候就应该对参数加上括号
- 为避免错误,一般来说要对参数加上括号
-
带表达式形式的宏常见错误
//头文件 global.h 内容如下: #define SQUARE(x) x*x //源文件 main.c 内容如下: #include "global.h" int main() { int x = 9, y; y = SQUARE(x); printf("%d\n", y); return 0; } //预编译生成的 main.i 文件 int main() { int x = 9, y; y = x * x; printf("%d\n", y); return 0; }//编译结果:81 运行结果正确,但是,如果我们把宏的参数修改为表达式 x+1,结果会怎么样 //修改之后的 main.c 内容如下: #include "global.h" int main() { int x = 9, y; y = SQUARE(x + 1); printf("%d\n", y); return 0; } //预编译生成的 main.i 文件 int main() { int x = 9, y; y = x + 1 * x + 1;//由于运算符的优先级问题,导致结果不正确 printf("%d\n", y); return 0; }//编译结果:19 与我们想要的结果不同
-
对上述错误的修改,将头文件 global.h
中对SQUARE
的定义修改为#define SQUARE(x) ((x) * (x))
-
注意:
- 在
define
定义标识符时,不要加上;
- 头文件中行末反斜线
\
的作用:当定义的宏不能用一行表达完整时,可以用\
表示下一行继续此宏的定义,但是最后一行不要 加续写符,反斜线后不能有空格。 - 带有参数的宏和
#define
定义中可以出现其他#define
定义的变量,但是对于宏,不能出现递归 - 当预处理器搜索
#define
定义的符号的时候,字符串常量的内容不被搜索
- 在
-
带副作用的宏参数
-
当宏参数在宏定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性失效。
-
例如:
x + 1;//不带副作用 x++;//带有副作用, x赋值给了某个变量之后,本身自己的值也会+1
-
MAX宏可以证明具有副作用的参数所引起的问题
#define MAX(a, b) ( (a)>(b) ? (a) : (b) ) ... x = 5; y = 8; z = MAX(x++,y++); printf("x=&d, y=%d, z=%d\n", x, y, z); //预处理器处理之后:z = ( (x++)>(y++) ? (x++) : (y++) ); //所以输出的结果:x=6, y=10, z=9
-
-
带参数的宏和函数
带参数的宏和函数之间的区别:
-
函数是在编译期间处理的,跳转执行函数后再返回,因此函数有比较大的调用开销;而宏定义是在预处理期间处理的,预处理程序将宏定义原地展开,因此没有调用开销
-
函数有明确的参数类型和返回类型;而宏定义不检查参数的类型。返回值也不会附带类型
特点 函数 带参数的宏 执行速度 慢 快 代码长度 短 长 对参数进行静态类型检查 编译器对参数进行静态类型检查 不进行类型检查 调试 逐语句调试 不方便调试 递归 可以递归 不能递归 -
一般来讲:当功能代码很短(尤其只有一句代码)时可以用宏定义来实现,一方面可以提高效率,同时代码长度的增加量少;利用宏可以方便的实现一些函数实现起来比较困难甚至无法实现的功能
-
例:定义分配空间的宏。数据类型无法作为参数传递给函数,但是可以把参数类型传递给带参的宏
#define MALLOC(n, type) ( (type * ) malloc ( (n) * sizeof(type) ) )
利用这个宏,可以为任何类型分配一块指定的空间大小,并返回指向这块空间的指针。
其确切的工作过程:
int* ptr; ptr = MALLOC(5, int);
宏展开以后的结果:
int* ptr; ptr = ( (int* ) malloc ( 5 * sizeof(int) ) );
-
预处理指令#undef
-
宏移除(取消定义)指令;
#undef
取消之前用#define
创建的宏 -
语法:
#undef identifier
;identifier
为使用#define
定义的宏; -
例:在一个程序块中使用完宏定义后,防止标识符冲突取消该定义
//头文件 global.h 内容: #define MAX 10 //源文件 main.c 内容: #include "global.h" int main() { int x; x = MAX; //使用宏定义标识符 //由于在后续代码中将使用标识符 MAX 作为变量,使用 undef 取消宏定义 MAX #undef MAX int MAX = 100; return 0; } //预编译生成的 main.i 文件: int main() { int x; x = 10; int MAX = 100; return 0; }
条件编译指令
条件编译
#if, #ifdef, #ifndef, #else, #elif, #endif
指令使用条件编译可以选择代码的一部分正常编译或者完全被忽略
-
第一种形式:
#ifdef 标识符 程序段1 #else 程序段2 #endif #ifdef 标识符 程序段1 #endif
它的功能是:如果标识符已被
define
定义过则对程序段1进行编译;否则对程序段2进行编译;若程序段2为空,则省略#else
-
第二种形式:
#ifndef 标识符 程序段1 #else 程序段2 #endif
它的功能是:如果标识符未被
define
定义过则对程序段1进行编译;否则对程序段2进行编译;(与第一种形式正好相反)若程序段2为空,则省略#else
-
第三种形式:
#if 常量表达式 程序段1 #else 程序段2 #endif
它的功能是:如果常量表达式的值为真(非0),则对程序段1进行编译,否则对程序段2进行编译。
预处理运算符
运算符 操作 字符串化运算符 # 导致对应的实参括在双引号内 标记粘贴运算符## 允许用作实参的标记串联以形成其他标记 预处理器运算符在宏指令中使用,主要包括两个预处理器特定运算符
#
和##
字符串化运算符 #
当#
出现在宏的 stuff
部分,其意义是将#
后的宏参数转换为字符数组(字符串)而不扩展参数定义;
常用于带参数的宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符#
例1:根据实际情况输出提示信息,如计算长方形面积和周长
//头文件 global.h 内容:
#define rect(x, y) printf(#x"%d\n", y)
//源文件 main.c 内容:
#include "global.h"
int main()
{
int x = 9, y = 3;
rect(Rect Area =, x * y);
rect(Rect Peri =, 2 * (x + y));
return 0;
}
//预编译生成的 main.i 文件:
#include "global.h"
int main()
{
int x = 9, y = 3;
printf("Rect Area=""%d\n", x + y);
printf("Rect Peri=""%d\n", 2 * (x + y));
return 0;
}//通俗点讲:#x 其实就是变成了 "x",变成了一个字符串常量
//而在这个宏定义中,就是 Rect Area= 变成了"Rect Area="
//编译结果:
//Rect Area=27
//Rect Peri=24
例2:通过宏输出变量的值,作为调试工具使用
//头文件 global.h 内容:
#define DEBUG_VALUE(V) printf(#v" is equal to %d.\n ", v)
//源文件 main.c 内容:
#include "global.h"
int main()
{
int x = 9;
DEBUG_VALUE(x);
return 0;
}
//预编译生成的 main.i 文件:
#include "global.h"
int main()
{
int x = 9;
printf("x"" is equal to %d.\n", x);
}//编译结果:x is equal to 9.
标识符粘贴运算符 ##
##
是一种分隔连接方式,它的作用是先分隔再进行强制连接,主要作用于把它两边的文本粘贴成一个标识符;
例:
//头文件 global.h 内容:
#define INITPORT(a, value) do\
{\
port##a = value;\
}while(0)
//源文件 main.c 内容:
#include "global.h"
int main()
{
int port1;
INITPORT(1, 1);
printf("%d\n", port1);
return 0;
}
//预编译生成的 main.i 文件
#include "global.h"
int main()
{
int port1;
do
{
port1 = 1;
}while(0)
printf("%d\n", port1);
return 0;
}
预定义宏
在定义时,宏是由预处理器在编译前扩展为指定的值。预定义的宏不采用任何参数,并且不能重新定义
__FILE__ //进行编译的源文件名称。源文件名称是字符串文字
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期。是一个固定长度的字符串 mmddyyyy,其中 mm 为一个缩写的月份名称
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
__FUNCTION__ //所在函数的函数名
#include <stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%s\n", __FUNCTION__);
return 0;
}
//编译结果:
//D:\code\Project\第一段程序.c
//101
//Jul 31 2022
//10:32:46
//main