预处理器

C预处理器不是编译器的组成部分,它是编译过程中一个单独的步骤,本质上是一个文本替换工具,其在源代码编译之前对其进行一些文本性质的操作,它主要任务包括删除注释、头文件展开、定义和替换宏定义的符号以及条件编译的展开

所有预处理器命令都是#开头,预处理指令不是语句,所以它们不会以分号结尾
预处理:选项gcc -E test.c -o test.i预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中
常见指令:#define,#include,#undef,#ifdef,#ifndef,#if,#else,#elif,#endif,#error,#pragma

预定义宏

描述
__DATE__文件被编译日期,一个以 “MMM DD YYYY” 格式表示的字符常量
__TIME__文件被编译时间,一个以 “HH:MM:SS” 格式表示的字符常量
__FILE__当前文件名,一个字符串常量
__LINE__当前行号,一个十进制常量
__STDC__当编译器以 ANSI 标准编译时,则定义为 1

#define

1
2
3
4
5
6
7
#define MAX 100 // 将所有MAX替换为100,增强可读性
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把break写上
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n", \
__FILE__,__LINE__, \
__DATE__,__TIME__) // 测试预定义宏

参数化的宏

格式:
#define name(parament-list) stuff其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
注意:参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

替换过程:

  1. 对参数进行检查,是否包含任何由#define定义的符号。如果是,它们首先被替换
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程

注意事项:
由于宏替换的本质是文本替换所以在定义的时候需要注意符号优先级

1
2
#define SQUARE(x) x * x
#define SQUARE(x) ((x) * (x)) // 这样写会避免因符号优先级带来的错误

#与##

把一个宏的参数转换为字符串常量时,使用字符串常量化运算符#

1
2
3
4
5
#define PRINT(FORMAT, VALUE) \
printf("the value of " #VALUE " is "FORMAT "\n", VALUE);
...
int i = 0;
PRINT("%d", i+3); // the value of i+3 is 3

宏定义内的标记粘贴运算符##会合并两个参数

1
2
3
4
5
6
7
8
9
10
11
12
#define ADD_TO_SUM(num, value) \
sum##num += value;
...
int sum1 = 0;
int sum2 = 0;
ADD_TO_SUM(1, 3); // 给sum1变量增加3
ADD_TO_SUM(2, 4); // 给sum2变量增加4

// 由于宏定义本质是文本替换,所以以下操作是错误的
for (int i = 1; i < 3; ++i) {
ADD_TO_SUM(i,1); // 给sumi变量增加1,sumi未定义,所以会报错
}

宏和函数

  1. 宏的执行速度快,没有函数栈帧开销,但如果定义比较长的宏,代码长度会增加
  2. 宏的参数与类型无关,所以不存在类型检查
  3. 宏的书写比较复杂,需要考虑操作符优先级问题和副作用的参数
  4. 不方便调试,不可递归

命令行定义

1
2
3
4
5
// test.c
#include <stdio.h>
int main() {
printf("%d\n",MAX); // 直接编译会显示MAX未定义
}

可以这样编译gcc -DMAX=7 test.c同一份代码,可以根据不同选项在不同环境下执行,同样-Uname将导致程序中符号name的初始定义被忽略

文件包含

函数库文件包含
#include <filename>编译器通过观察由编译器定义的一系列标准位置查找函数库头文件,如unix系统上的C编译器在/user/include目录查找函数库头文件
本地文件包含
#include "filename"标准允许编译器自行决定是否把本地形式的#include和函数库形式的#include区别对待,常见的策略就是在源文件所在的当前目录进行查找,如果未找到,编译器就像查找函数库头文件一样在标准位置查找本地文件