C语言小结--预处理器

1 什么是预处理

编译一个C程序涉及很多步骤,其中第一个步骤称为预处理(preprocessing)阶段。C预处理器在源代码编译之前对其进行一些文本性质的操作。它的主要任务包括删除注释、插入被#include指令包含的文件的内容、定义和替换#define指令定义的符号以及确认代码部分内容是否应该根据一些条件编译指令进行编译。

1.2 预定义符号

预定义符号是由预处理器定义的符号,他们的值或者是字符串常量、或者是十进制数字常量。__FILE____LINE__在确认调试输出来源方面很有用处。__DATA____TIME常常用来在编译程序中加入版本信息。__STDC__用于那些在ANSI环境都必须进行编译程序中结合条件编译。

下面我们介绍一下,如何善用__FILE____LINE__来调试程序。

#include 
#define __DEBUG

#ifdef __DEBUG
#define DEBUG(format,...) printf("[File: "__FILE__", Line: %05d]    "format"\n", __LINE__, ##__VA_ARGS__)
#else
#define DEBUG(fmt, ...)
#endif
int main()
{
        DEBUG("%s, %d ", "hello world!", 100);
        return 0;
}

运行结果如下:

root@ubuntu:/mymnt# gcc test2.c
root@ubuntu:/mymnt# ./a.out 
[File: test2.c, Line: 00013]    hello world!, 100 

我们简单的分析一下这句:#define DEBUG(format,...) printf("[File: "__FILE__", Line: %05d] "format"\n", __LINE__, ##__VA_ARGS__) 这句定义是用后面的printf来替换DEBUG,首先"[File: "会当做一个字符串输出__FILE__在预处理器中被替换为当前的文件信息,也是一个字符串,", Line: %05d] "当做一个字符串输出,format替换DEBUG中的format,"\n"做为一个字符串输出。到此相当于printf中前半部分完成了。__LINE__替换前面的格式化参数%d,##__VA_ARGS__替换...

1.3 预处理指令

在C中,#define指令把一个符号名与一个任意的字符序列联系在一起。例如,这些字符可能是一个字符值常量、表达式或者程序语句。这些序列到该行的末尾结束。如果序列过长,可以把他们分开数行,但是需要在除最后一行之外的其他行末尾加一个反斜杠,表示连接。宏就是一个被定义的序列,他的参数值将被替换。当一个宏被调用时,他的每一个参数值都被具体的值替换,为了防止可能出现于表达式中与宏有关的错误,在宏完整定义的两边应该加上括号。如:#define NUM (300)。同样,在宏定义的每个参数两边也要加上括号。如:#define SQUARE(n) ((n)*(n))#define指令可以__重写__C语言,使其看上起更像是其他语言,但是不建议读者进行这种操作。

#argument结构由预处理器转换为字符串常量“argument” 、 ##操作符用于把两边的文本粘贴成同一个标识符。

有些任务使用宏也可以实现使用函数也同样可以实现。但是,宏与类型无关,这是一个优点。宏的执行速度快于函数,因为他不存在函数调用/返回的开销。但是,使用宏通常会增加程序的长度,而函数却不会。同样,具有副作用的参数可以能在宏的使用过程中产生不可预知的结果,而函数的错误更容易预测。由于这些区别,使用一种命名约定,让程序员更容易判断一个标识符是函数还是宏是非常重要的。所以一般情况下,宏定义都使用大写字符表示。

在许多编译器中,符号可以从命令行定义。所以在命令行中输入不同的符号表示不同的编译过程。#undef指令将导致一个名字的原来定义被忽略。

使用条件编译,你可以从一组单一的源文件创建程序的不同版本,#if指令根据编译时的测试结果,包含或者忽略一个序列的代码。当同时使用#elif 和 #else指令时,你可以从几个序列的代码中选择其中之一进行编译。除了测试常量表达式之外,这些指令还可以测试某几个符号是否已经被定义。#ifdef 和 #ifndef也可以执行这个任务。

#include指令用于实现文件包含。它具有两种形式。如果文件名位于一对见括号中,如:#include,编译器将在编译器定义的标准位置查找这个文件。这种形式通常用于包含函数库的头文件时。另一种方式,文件名出现在一对双引号中。如#include "touch.h",不同的编译器可以使用不同的方式处理这种形式。但是,如果用于处理本地头文件的任务特殊处理方法无法找到这个头文件时,编译器会使用标准查找过程来寻找他。这种形式通常用于包含你自己编写的头文件。文件包含可以嵌套,但是很少需要进行超过一层或者两层的文件包含嵌套。嵌套的包含文件将会增加多次包含同一文件的危险,而且我们更难以确定某个特定的源文件依赖的究竟是哪个头文件。

#error指令在编译时产生一条错误信息,信息中包含的是你选择的文本。#line指令允许你告诉编译器下一行输入的行号,如果他加上了可选内容,它将告诉编译器输入源文件的名字。因编译器而异的#progma指令允许编译器提供不标准的处理过程。比如向函数插入内联的汇编代码(AVR的中断处理函数前就使用的#progma指令)。

1.4 总结

1 不要在一个宏定义的末尾加上分号,使其成为一条完整的语句。
2 在宏定义中使用参数,不要忘了在他们周围加上括号,防止歧义。
3 整个宏定义的两边不要忘了加上括号
4 避免使用#define指令定义可以使用函数实现的很长序列的代码
5 #define宏定义的字符全部都用大写字符来实现,防止很函数名发生歧义
6 头文件只应该包含一组函数和数据的声明
7 不同集合的声明分离到不同的头文件中可以改善信息隐藏
8 避免使用嵌套的#include文件,否则我们很难判断源文件之间的依赖关系

你可能感兴趣的:(c)