目录
1. 预处理器的工作原理
2. 预处理指令
3. 宏定义
3.1 简单的宏(对象式宏)
3.2 带参数的宏(函数式宏)
3.3 #define替换规则
3.4 #和##
3.5 带副作用的宏参数
3.6 宏和函数对比
3.7 命名约定
3.8 #undef
3.9 预定义宏
4. 文件包含
5. 条件编译
5.1 简单的条件编译
5.2 多个分支的条件编译
5.3 判断是否被定义
5.4 嵌套指令
6. 其他预处理指令
6.1 #error指令
6.2 #line指令
6.3 #pragma指令
1. 预处理器的工作原理
预处理器的行为是由预处理指令(由#字符开头的一些命令)控制的。
预处理器的输入是一个C语言程序,程序可能包含指令。预处理器会执行这些指令,并在处理过程中删除这些指令。预处理器的输出是另一个C程序:原程序编辑后的版本,不再包含指令。预处理器的输出被直接交给编译器,编译器检查程序是否有错误,并将程序翻译为目标代码(机器指令)。
2. 预处理指令
- 宏定义:#define指令定义一个宏,#undef指令删除一个宏定义
- 文件包含:#include指令导致一个指定文件的内容被包含到程序中
- 条件编译:#if、#ifdef、#ifndef、#elif、#else和#endif指令可以根据预处理器可以测试的条件来确定是将一段文本块包含到程序中还是将其排除在程序之外
- 其他预处理指令:#error、#line、#pragma
适用于所有指令的规则:
- 指令都以#开始:#符号不需要在一行的行首,只要它之前只有空白字符就行。在#后是指令名,接着是指令所需要的其他信息。
- 在指令的符号之间可以插入任意数量的空格或水平制表符。
- 指令总是在第一个换行符处结束,除非明确地指明要延续。如果想在下一行延续指令,我们必须在当前行的末尾使用'\'字符。
- 指令可以出现在程序中的任何地方。但我们通常将#define和#include指令放在文件的开始,其他指令则放在后面,甚至可以放在函数定义的中间。
- 注释可以与指令放在同一行。实际上,在宏定义的后面加一个注释来解释宏的含义是一种比较好的习惯。
3. 宏定义
3.1 简单的宏(对象式宏)
#define 标识符 替换列表
宏的替换列表可以包括标识符、关键字、数值常量、字符常量、字符串字面量、操作符和排列。当预处理器遇到一个宏定义时,会做一个“标识符”代表“替换列表”的记录。在文件后面的内容中,不管标识符在哪里出现,预处理器都会用替换列表代替它。
#include <stdio.h>
#define MAX 100
#define STR "abcdef"
int main()
{
printf("%d\n", MAX);//100
int a = MAX;
printf("%d\n", a);//100
printf("%s\n", STR);//abcdef
return 0;
}
#define INT_PTR int*
INT_PTR a, b;//a是int*类型,b是int类型
#define是宏定义,仅仅是直接替换。INT_PTR a, b; 进行宏替换后代码为:
int* a, b;
不是a和b都是int*类型的意思,而是a是int*类型,b是int类型。可以看成是如下代码:
int *a, b;
3.2 带参数的宏(函数式宏)
#define 标识符(x1,x2,…,xn) 替换列表
其中x1,x2,…,xn是宏的参数,参数列表可以为空。宏名和左括号之间不能有空格,如果有空格,预处理器会认为是在定义一个简单的宏,其中(x1,x2,…,xn)是替换列表的一部分。
带参数的宏经常用来作为简单的函数使用。
#define ADD(x,y) ((x)+(y))
//ADD:宏名
//x y:宏的参数,参数是无类型
//((x)+(y)):宏体
#define ADD2(x,y) (x)+(y)//err
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("%d\n", ADD(a, b));//((a)+(b))=10+20=30
printf("%d\n", 10 * ADD2(a, b));//10*(a)+(b)=10*10+20=120
return 0;
}
如#define ADD(x,y) ((x)+(y)),用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
带参数的宏不仅适用于模拟函数调用,还经常用作需要重复书写的代码段模式。
如果不想写printf("%d\n", i);可以定义宏。
#define PRINT_INT(n) printf("%d\n", n)
PRINT_INT(i / j);
进行宏替换后代码为:
printf("%d\n", i / j);
3.3 #define替换规则
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
3.4 #和##
#:把一个宏参数变成对应的字符串。
例如,#val会被与处理器处理为:"val"
#define PRINT(val, format) printf("the value of "#val" is "format"\n", val)
//"the value of " #val " is " format "\n"
//"the value of " "val" " is " format "\n"
#include <stdio.h>
int main()
{
int a = 10;
PRINT(a, "%d");
//printf("the value of a is %d\n", a);
int b = 20;
PRINT(b, "%d");
//printf("the value of b is %d\n", b);
float f = 3.5f;
PRINT(f, "%f");
//printf("the value of f is %f\n", f);
return 0;
}
##:把位于它两边的符号合成一个符号。
#define RESULT(A,B) A##B
#include <stdio.h>
int main()
{
int value_a = 10;
printf("%d\n", RESULT(value, _a));//10
return 0;
}
3.5 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
int x = 1;
int y = x+1;//y=2 x=1 不带副作用
int y = ++x;//y=2 x=2 带有副作用(x自身也改变了)
#define MAX(x,y) ((x)>(y)?(x):(y))
#include <stdio.h>
int main()
{
int a = 3;
int b = 4;
int m = MAX(++a, ++b);
printf("m=%d a=%d b=%d\n", m, a, b);//m=6 a=4 b=6
return 0;
}
如何计算((++a)>(++b)?(++a):(++b))?
- ++a:a先自增,a=4,然后++a本身=4
- ++b:b先自增,b=5,然后++b本身=5
- 4>5为假,不执行++a,直接执行++b
- ++b:b先自增,b=6,然后++b本身=6
- ((++a)>(++b)?(++a):(++b))的结果为6
3.6 宏和函数对比
宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。
#define MAX(x,y) ((x)>(y)?(x):(y))
为什么不用函数来完成这个任务?
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
宏的缺点:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏不能调试。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int*)malloc(10 * sizeof(int));
宏和函数的一个对比:
属性 | 宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符 优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
3.7 命名约定
一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。习惯是把宏名全部大写,函数名不要全部大写。
3.8 #undef
这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
3.9 预定义宏
C语言有一些预定义宏,每个宏表示一个整数常量或字符串字面量,这些宏提供了当前编译或编译器本身的信息。
名字 | 描述 |
---|---|
__LINE__ | 被编译的文件中的行号 |
__FILE__ | 被编译的文件名 |
__DATE__ | 编译的日期(格式"mm dd yyyy") |
__TIME__ | 编译的时间(格式"hh:mm:ss") |
__STDC__ | 如果编译器符合C标准(C89或C99),那么值为1 |
#include <stdio.h>
int main()
{
printf("编译的日期:%s,时间:%s\n", __DATE__, __TIME__);
return 0;
}
4. 文件包含
#include指令告诉预处理器打开指定的文件,并且把此文件的内容插入到当前文件中。因此,如果想让几个源文件可以访问相同的信息,可以把此信息放入一个文件中,然后利用#include指令把该文件的内容带进每个源文件中。把按照此种方式包含的文件称为头文件(有时称为包含文件),头文件的扩展名为.h。
#include指令主要有两种书写格式。
第一种格式用于属于C语言自身库的头文件:
#include <文件名>
第二种格式用于所有其他头文件,也包含任何自己编写的文件:
#include "文件名"
这两种格式间的细微差异在于编译器定位头文件的方式。大多数编译器遵循的规则:
- #include <文件名>:搜寻系统头文件所在的目录(或多个目录)
- #include "文件名":先搜寻当前目录,然后搜寻系统头文件所在的目录(或多个目录)
5. 条件编译
C语言的预处理器可以识别大量用于支持条件编译的指令。条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片断。
5.1 简单的条件编译
#if 常量表达式
//语句
#endif
如果常量表达式为真,执行语句;如果常量表达式为假,不执行语句。
5.2 多个分支的条件编译
#if 常量表达式1
//语句1
#elif 常量表达式2
//语句2
#else
//语句3
#endif
如果表达式1为真,不管表达式2的真假,执行语句1;
如果表达式1为假,且表达式2为真,执行语句2;
如果表达式1和2都为假,执行语句3。
5.3 判断是否被定义
#if defined(symbol)//等价于#ifdef symbol
//语句1
#endif
#if !defined(symbol)//等价于#ifndef symbol
//语句2
#endif
如果定义了symbol,执行语句1;如果没定义symbol,执行语句2。
5.4 嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
6. 其他预处理指令
6.1 #error指令
#error 消息
其中,消息是任意的记号序列。如果预处理器遇到#error指令,它会显示一条包含消息的出错消息。对于不同的编译器,出错消息的具体形式也可能会不一样。格式可能类似:
Error directive: 消息
或者
#error 消息
遇到#error指令预示着程序中出现了严重的错误,有些编译器会立即终止编译而不再检查其他错误。
#error指令通常与条件编译指令一起用于检测正常编译过程中不应出现的情况。例如,假定我们需要确保一个程序无法在一台int类型不能存储100000的机器上编译。最大允许的int值用INT_MAX宏表示,所以我们需要做的就是当INT_MAX宏小于100000时调用#error指令:
#if INT_MAX < 100000
#error int type is too small
#endif
如果试图在一台以16位存储整数的机器上编译这个程序,将产生一条出错消息:
Error directive: int type is too small
#error指令通常会出现在#if—#elif—#else序列中的#else部分:
#if defined(WIN32)
...
#elif defined(MAC__OS)
...
#elif defined(LINUX)
...
#else
#error No operating system specified
#endif
6.2 #line指令
#line指令是用来改变程序行编号方式的(程序行通常是按1,2,3,…来编号的)。我们也可以使用这条指令使编译器认为它正在从一个有不同名字的文件中读取程序。
#line指令有两种形式。一种形式只指定行号:
#line n
n必须是介于1和32767(C99中是2147483647)之间的整数。这条指令导致程序中后续的行被编号为n、n+1、n+2等。
#line指令的第二种形式同时指定行号和文件名:
#line n "文件"
指令后面的行会被认为来自文件,行号由n开始。n和文件字符串的值可以用宏指定。
#line指令的一种作用是改变__LINE__宏(可能还有__FILE__)的值。更重要的是,大多数编译器会使用来自#line指令的信息生成出错消息。例如,假设下列指令出现在文件foo.c的开头:
#line 10 "bar.c"
现在,假设编译器在foo.c的第5行发现一个错误。出错消息会指向bar.c的第13行,而不是foo.c的第5行。为什么是第13行呢?因为指令占据了foo.c的第1行,因此对foo.c的重新编号从第2行开始,并将这一行作为bar.c的第10行。
乍一看,#line指令使人迷惑。为什么要使出错消息指向另一行,甚至是另一个文件呢?这样不是会使程序变得难以调试吗?
实际上,程序员并不经常使用#line指令。它主要用于那些产生C代码作为输出的程序。最著名的程序之一是yacc(Yet Another Compiler-Compiler),它是一个用于自动生成编译器的一部分的UNIX工具(yacc的GNU版本称为bison)。在使用yacc之前,程序员需要准备一个包含yacc所需要的信息以及C代码段的文件。通过这个文件,yacc生成一个C程序y.tab.c,并合并程序员提供的代码。程序员接着按照正常方法编译y.tab.c。通过在y.tab.c中插入#line指令, yacc会使编译器认为代码来自原始文件——也就是程序员写的那个文件。于是,任何编译y.tab.c时产生的出错消息会指向原始文件中的行,而不是y.tab.c中的行。其最终结果是:调试变得更容易,因为出错消息都指向程序员编写的文件,而不是由yacc生成的(那个更复杂的)文件。
6.3 #pragma指令
#pragma指令为要求编译器执行某些特殊操作提供了一种方法。这条指令对非常大的程序或需要使用特定编译器的特殊功能的程序非常有用。
#pragma 记号
其中,记号是任意记号。#pragma指令可以很简单(只跟着一个记号),也可以很复杂:
#pragma data(heap_size => 1000, stack_size => 2000)
#pragma指令中出现的命令集在不同的编译器上是不一样的。你必须通过查阅你所使用的编译器的文档来了解可以使用哪些命令,以及这些命令的功能。顺便提一下,如果#pragma指令包含了无法识别的命令,预处理器必须忽略这些#pragma指令,不允许给出出错消息。
C89中没有标准的编译提示(pragma),它们都是在实现中定义的。C99有3个标准的编译提示,都使用STDC作为#pragma之后的第一个记号。这些编译提示是FP_CONTRACT 、CX_LIMITED_RANGE 和FENV_ACCESS。