预处理
C 程序编译流程:1. 预处理阶段 2. 编译阶段 3. 汇编阶段 4. 链接阶段 5. 运行可执行程序。
如何让编译停在预处理结束的时候呢?用参数 -E
,完整的示例为 : gcc -E -o src/example.i src/example.c
,即:编译 src 目录下的 example.c 文件,在预处理阶段结束后停止,将内容保存到 src 目录下的 example.i 文件中(你甚至可以不用 “.i” 结尾,但这已约定俗成)。
什么是预处理
在说什么是预处理之前,先讲讲预处理的好处吧。那就是:合理使用预处理编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
预处理阶段由预处理程序负责,这个程序会在进行编译的第一遍扫描(词法扫描和语法分析)之前工作。也就是说,预处理不会对语法什么的做分析,总之 “错就错了,我这里都让过”。
所以预处理要搞啥呢?预处理的任务大致有这 4 点:1. 删除注释;2. 插入被 #include
指令包含的文件内容; 3. 定义和替换由 #define
指令定义的符号(也叫宏替换或宏扩展);4. 条件编译。
预处理需要处理的是预处理部分,而预处理部分由预处理指令构成。C 语言对预处理指令的格式要求为:
- 以 # 开头;
- 占单独书写行(自然可以用连字符 “\” ,但你应该确保反斜线是换行符前的最后一个字符);
- 语句尾不加分号。
接下来就针对性的说说 宏定义 、条件编译 和 文件包含。
宏定义
C 语言中允许用一个标识符表示一个文本(文本可以是字符串,数字,代码语句),这个标识符称为宏。在预处理时,对程序中所有出现的宏,都用宏定义中的文本替换,这个过程称为宏替换或宏展开。如:
#define PI 3.1415926 // PI 称为宏 // 文本是 浮点数
#define MY_NAME "Jone" // 文本为 字符串
#define GREET printf("hello") // 文本为 代码语句
宏定义由源程序中的宏定义指令(#define
)完成;宏替换由预处理程序自动完成。C 语言中,宏定义分为:无参宏定义 和 带参宏定义。
无参宏定义
语法结构:
#define 标识符(宏名) [文本(宏体)]
功能:定义可在程序中使用的宏,宏的内容由文本(宏体)替代。像是下面这段代码:
// example.c
#define WORD "hello, world"
int main()
{
printf("%s", WORD);
}
预处理之后会变成这样:
// example.i
...
int main()
{
printf("%s", "hello, world");
}
可见,预处理部分不见了,WORD 也被宏体取而代之。同时,定义过的宏还可以被取消。取消宏有两种方式,一种是 “缺省宏体”;另一种是用预处理指令 #undef
。
两种方式会得到各自不同的结果:
// example.c
#define WORD "hello, world"
#define WORD // 缺省宏体
int main()
{
printf("%s", WORD);
}
/***************************/
// example.i
...
int main()
{
printf("%s", );
}
// example.c
#define WORD "hello, world"
#undef WORD // 预处理命令取消
int main()
{
printf("%s", WORD);
}
/***************************/
// example.i
...
int main()
{
printf("%s", WORD);
}
前面说过宏体也可以是代码语句,但需要注意的是,宏定义不是 C 语句,所以末尾不必加分号(尽管你可以这么做,但并不这么建议)。
// example.c
#define GREET printf("hello, world\n") // 没有分号
int main()
{
GREET; // 使用时需要加上分号
}
带参宏定义
带参宏也叫做类函数宏,语法结构:
#define 宏名(形参列表) 宏体
宏名(实参表); // 调用
对于带参数的宏,在调用中会进行宏展开,然后用实参替换形参。就像下面这个获取最大值的例子:
// example.c
#define MAX(a, b) ( (a)>(b)? (a):(b) )
int main()
{
int rlt = MAX(10, 1);
printf("%d", rlt);
}
/************************************/
// example.i
...
int main()
{
int rlt = ( (10)>(1)? (10):(1) );
printf("%d", rlt);
}
通过带参宏定义可以实现 C 语言的泛型编程。众所周知,C 语言中变量需要声明类型,但宏定义不需要声明类型。上述代码中的 MAX() 既可以传入整数类型的 10 和 1,也可以是浮点类型的 5.12 和 10.24 。另外,关于带参宏定义有以下规则值得遵守与注意:
- 宏名和形参列表之间不能有空格出现(如果出现空格,那么参数列表会被视作宏体);
- 形参不分配内存单元,因此不必做类型定义;
- 宏定义中的形参是标识符,宏调用中的实参可以是表达式;
宏体内的形参通常要用括号括起来以避免出错,如:
#define POWER(x) ((x) * (x)) // 此时 POWER(10+1) =》 ((10+1) * (10+1)) =》 结果:121 #define POWER(x) x * x // 此时 POWER(10+1) =》 10 + 1 * 10 + 1 =》 结果:20
- 为体现表达式的整体性,在表达式最外层加上括号也是值得的操作。
宏定义的注意事项
第一点:你无法通过宏展开的方式创建预处理命令。
// example.c
#define PRE_CMD #define PI 3.1415 // 替换文本是预处理命令
int main()
{
PRE_CMD
printf("%f", PI);
}
/********************************/
// example.i
int main()
{
#define PI 3.1415 // 替换后
printf("%f", PI);
}
可以看到,尽管 #define PI 3.1415
在形式上是正确的预处理命令,但预处理器不会执行它。
第二点:宏不可以递归地展开。此时分两种情况讨论:
当宏名为 WORD 时,宏体中也存在 WORD ,预处理器无法展开宏体中 WORD 。
//example.c #define WORD "hello" WORD // 替换文本是 "hello" WORD int main() { printf("%s", WORD); } /***************************/ // example.i ... int main() { printf("%s", "hello" WORD); // 不能对 WORD 做第二次展开 }
当有两个宏,WORD1 和 WORD2。如果 WORD1 的宏体中有 WORD2 ,同时 WORD2 的宏体有 WORD1 ,也不能展开。
// example.c #define WORD1 "hello" WORD2 #define WORD2 "world" WORD1 int main() { printf("%s", WORD1); } /**************************/ // example.i ... int main() { printf("%s", "hello" "world" WORD1); // 可见,对 WORD1 展开后,WORD2 也被展开了, // 但 WORD2 中的 WORD1 没被展开 // 即,宏定义时允许使用其他的宏,只是不允许自己使用自己 }
第三点:带参宏定义和带参函数很相似,但有本质上的不同。
~ | 带参宏定义 | 函数 |
---|---|---|
处理时间 | 编译时 | 程序运行时 |
参数类型 | 无参数类型 | 需要声明实参和形参类型 |
处理过程 | 不分配内存,只是简单的字符置换 | 分配内存,先求实参值,再传递给形参 |
程序长度 | 变长 | 不变 |
运行速度 | 不占运行时间 | 调用和返回占用时间 |
支持递归 | 不支持 | 支持 |
第四点:取消带参宏定义不需要写参数列表。
#define MAX(a, b) ( (a)>(b)? (a):(b) )
#undef MAX // 直接写宏名即可
条件编译
当我们希望对源代码中的一部分内容,在满足一定条件下才进行编译,就需要条件编译。也就是说,条件编译可以指定这段代码要不要被忽略。条件编译的好处是:有利于提升程序的可移植性,增强程序的灵活性。
条件编译的预处理命令有:#if
,#elif
,#else
,#endif
,#ifndef
,#ifdef
。
比如我们想灵活的控制调试信息,开发的时候需要日志调试,上线的时候取消调试信息。于是代码可以这样写:
#include <stdarg.h>
#define DEBUG 1 // DEBUG 状态
void debug(char* fmt, ...) // 行调试功能的函数
{
char msg[1024*4] = {};
va_list valist;
va_start(valist, fmt);
vsprintf(msg, fmt, valist);
va_end(valist);
printf("debug: ");
printf("%s\n", msg);
}
int main()
{
printf("running ...\n");
#if DEBUG // 如果是 DEBUG 模式
debug("%s", "this is a debug info");
#endif // 结束如果
printf("exit ...\n");
}
编译之后运行结果可得:
Guan@Orchard:~/CC++/19/src$ ./a.out
running ...
debug: this is a debug info
exit ...
当程序上线,只需要将 #define DEBUG 1
修改为 #define DEBUG 0
,再编译程序就不会有 debug 信息了。
条件编译与寻常的 if ... else ... 用法一致,只是末尾需要 #endif
作为结束标志。
#if 常量表达式
程序段1
#else
程序段2
#endif
/*******************/
#if 常量表达式1
程序段1
#elif 常量表达式2
程序段2
#else
程序段3
#endif
#ifdef
则是 #if defined
的缩写,表示后面接上的标识符是否被 #define
指令定义过。定义过返回真,否则返回假。相应的还有 #ifndef
,是 #if !defined
的缩写,表示如果没有定义,那么……
判断是否定义过某个标识符的用法极为常见,如防卫式声明。拿标准库 stdio.h 文件来说,其内容就如下:
#ifndef _TR1_STDIO_H // 如果没有定义过 _TR1_STDIO_H ,那就执行下面逻辑
#define _TR1_STDIO_H 1
#include <tr1/cstdio>
#endif
头文件应该总是有防卫式声明,避免重复导入。
文件包含
文件导入在实际使用中很平凡,比如 C 程序中一般都会有这样一句话:#include <stdio.h>
。这段代码翻译过来就是:“请把 stdio.h 中声明的代码给我用用”。
语法格式用两种:
#include <文件名>
// 或者
#include "文件名"
二者的不同在于搜索路径。用 #include <...>
时,预处理器会在特定的系统路径下搜索,在 Unix 中会是 /usr/local/include 和 /usr/include 。用 #incude "..."
时,预处理器通常首先在当前目录下寻找,如果没找到,再去系统的 include 路径中找。甚至允许添加导入文件的所在路径,如 #include "./demo/a.h"
(意为:去当前路径下的 demo 目录中,找 a.h 文件),尽管这种方式不推荐。最好是在编译时添加搜索路径,需要参数 I(大写 i),完整命令:gcc a.c -Idemo
。
#include
命令支持嵌套使用,举个例子:
// a.c 源码
#include <stdio.h>
#include "a.h"
int main()
{
printf("%f\n", PI);
}
// a.h 头文件
#include "b.h"
// b.h 头文件
#include "c.h"
// c.h 头文件
#define PI 3.14
此时编译 a.c 文件不会报错,运行程序也可以正常输出 PI 代表的值。这正是嵌套的意义(《C 语言核心技术》中说预处理器最多允许 15 层嵌套,我未作测试,不知现在还是不是)。
然而嵌套也会带来不必要的麻烦,也就是前面说的重复导入,换句话说,就是会引发重复定义。
现在我希望自定义一个 print_format ,让我的每次打印输出都会有特定的 “Guan say: ” 开头。因此我在 a.h 文件中定义 print_format 函数:
// a.h
#include <stdio.h>
#include <stdarg.h>
void print_format(char*, ...);
void print_format(char* fmt, ...)
{
char msg[1024*4] = {};
va_list valist;
va_start(valist, fmt);
vsprintf(msg, fmt, valist);
va_end(valist);
printf("Guan say: ");
printf("%s\n", msg);
}
然后需要在 b.h 中使用这个函数,b.h 里的内容如下:
// b.h
#include "a.h"
void say_a_word()
{
print_format("%s", "今天天气很好呢!");
}
最后源码 a.c 中既需要使用 a.h 中的 print_format(),也需要使用 b.h 中的 say_a_word()。a.c 内容如下:
#include "a.h"
#include "b.h"
int main()
{
print_format("%s", "开始说话。");
say_a_word();
print_format("%s", "说话结束。");
}
编译 a.c 出现错误:
Guan@Orchard:~/CC++/19/src$ gcc a.c
In file included from b.h:1:0,
from a.c:2:
a.h:9:6: error: redefinition of ‘print_format’
void print_format(char* fmt, ...)
^
In file included from a.c:1:0:
a.h:9:6: note: previous definition of ‘print_format’ was here
void print_format(char* fmt, ...)
^
会有这种问题是因为 #include
支持嵌套使用,导致重复导入了 a.h 。解决方案是在 a.h 中添加防卫式声明:
#ifndef __A_H__
#define __A_H__
...
#endif
最后编译正常,运行正常,输出如下:
Guan@Orchard:~/CC++/19/src$ ./a.out
Guan say: 开始说话。
Guan say: 今天天气很好呢!
Guan say: 说话结束。
(未完待续)
感谢
- 参考《C 语言核心技术(第 2 版)》
- 参考海同网校王正平老师的C 语言之预处理
还不快抢沙发