以"#"开头的命令是C语言的预处理指令,比如#include, #define等。所谓预处理是指在进行编译之前的第一遍扫描,由预处理器进行操作,主要包括宏定义,文件包含,条件编译等。合理使用预处理能使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序的设计。
在C语言中允许使用一个标识符来表示一个字符串,称为“宏”。被定义为宏的标识符称为“宏名”。在预处理时,会对程序中所有出现的宏名进行宏替换,用宏定义中的字符串去替换宏名,称为宏替换或是宏展开。定义宏包括两种,一种是无参数的宏,一种是有参数的宏。
无参宏经常使用的是数值宏常量和字符串宏常量,如下:
#define MAX 1000 #define PI 3.1415926 #define ERROR_INPUT -1 #define PATH "/myWork/day11" |
除此之外,还可以使用宏来定义表达式,比如:
#define SECOND_PER_HOUR 60*60 |
对于表达式的宏,我们建议一般加上括号,如下:
#define (60*60) #define SEC_PER_YEAR (60*60*24*365)UL |
宏定义一般写在函数之外,它的作用域为从本行开始到文件结束。对于定义过的宏,可以使用#undef来结束其作用域,比如:
#define MAX 1000 #undef MAX //此处及以下MAX宏的作用已终止 |
除此之外,对字符串中的宏,将不进行替换操作,如下:
#define OK 100 printf("OK"); //此处的OK不会进行宏替换 |
定义宏时也可以带上一定的参数,让宏在某种程序上具有“函数”的功能,比如:
#define SQR(x) x * x |
则SQR(6)
将会展开成6 * 6
。但是在在求取SQR(a + b)
时,这个宏会展开成a + b * a + b
。这显然与想要的结果不符合。为了解决这个问题,可以在宏里使用括号,如下:
#define SQR(x) ((x) * (x)) |
最外层的括号也别省略,看例子:
#define SUM(x) (x) + (x) |
如果x的值是个表达式,比如5+3
,而代码又写成这样:SUM(5+3) * SUM(5+3)
,则宏展开之后将变成:(5+3) + (5*3) * (5+3) + (5+3)
。这显然也是不对的,所以最外层的括号也别省略。以下是正确的写法:
#define SUM(x) ((x) + (x)) |
对于有参数的宏,最保险的做法就是,在所有参数两边都加上括号,然后在整个表达式加上括号。
另外,如果需要严谨一些的话,在定义有参数宏时,还需要自增自减运算符的影响。请对比以下两个MAX宏,分析自增自减运算符对运算结果的影响。
#define MAX1(a, b) ((a) > (b) ? (a) : (b)) #define MAX2(a, b) \ ({ \ typeof(a) _a = a; \ typeof(b) _b = b; \ _a > _b ? _a : _b; \ }) int main() { int a = 3; int b = 2; int c = MAX1(a++, b); //当使用MAX2宏时结果又如何? printf("c is %d\n", c); printf("a is %d\n", a); return 0; } |
GCC中预定义了几个可以直接使用的宏,如下:
宏名 | 描述 |
---|---|
__FILE__ | 表示正在编译的文件名字符串 |
__LINE__ | 表达正在编译行号 |
__func__或__FUNCTION__ | 表达正在编译的函数名字符串 |
__DATE__ | 表示编译时刻的日期字符串 |
__TIME__ | 表示正在编译时刻的时间字符串 |
可以用宏来定义语句,如下:
#define LOG(format, arg) printf(format, arg) |
则LOG("%d", 5)
会展开成printf("%d", 5)
。
宏定义只能在一行中完成,如果一行写不下,可以使用\
将跨行的语句连接成一行,如下:
#define SAFEFREE(p) \ do \ { \ if (p != NULL) \ { \ free(p); \ p = NULL; \ } \ } while (0) |
C99标准以后,GCC在预处理阶段,可以用可变参数宏__VA_ARGS__
来进行可变参数替换,如下:
#define LOG(format, ...) printf(format, ##__VA_ARGS__) |
其中的...
部分表示该宏可以接收可变参数,而__VA_ARGS__
则会替换所有的可变参数(“##”的作用用于处理可变参数为空的情况),以下是替换示例:
LOG("hello world\n"); ==> printf("hello world\n"); LOG("%d\n", 5); ==> printf("%d\n", 5); LOG("%d %d\n", 5, 6); ==> printf("%d %d\n", 5, 6); |
在宏定义中,可以使用#
将参数转化成字符串的形式,比如:
#define STRFY(x) #x |
则:
char *str = STRFY(123456); ==> char *str = "123456"; |
##
运算符可在宏定义中进行字符串的连接,比如:
#define VAR(n) a##n |
则:
int a1 = 1; int a2 = 2; int a3 = 3; int arr[] = {VAR(1), VAR(2), VAR(3)}; ==> int arr[] = {a1, a2, a3}; |
条件编译的功能使得我们可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于调试和移植程序是很有用的,条件编译有以下几种形式。
#ifdef 标识符 语句块 #endif |
这里只有标识符是被#define定义的才会执行语句块。
#ifdef 标识符 语句块1 #else 语句块2 #endif |
#ifdef 标识符1 语句块1 #elif 标识符2 语句块2 #elif 标识符3 语句块3 ... #endif |
#ifndef 标识符 语句块 #endif |
#if 常量表达式 语句块1 #else 语句块2 #endif |
这里对视常量表达式的值来判断执行哪个语句块,而之前的是判断标识符有没有被定义过。
#if defined(宏1) 语句块1 #elif defined(宏2) && defined(宏3) 语句块2 #elif !defined(宏4) && !defined(宏5) 语句块3 #endif |
文件包含是预处理的一个重要功能,它将多个源文件链接成一个源文件进行编译,结果将只生成一个目标文件。C语言提供#include
命令来实现文件的包含,它有两种格式。
#include <filename> #include "filename" |
其中,filename是要包含的文件名称,一般是后缀为.h的头文件。用尖括号括起时,表示这个文件要到系统规定的路径中去获得这个文件(一般是/usr/include目录),而用双引号括起的头文件,则表示这个头文件要到当前目录中去寻找,如果没找到,则到系统指定的目录去寻找。
在程序有多个源文件时,可以将函数或类型的声明写到头文件中,头文件的后缀一般是.h结尾。对于头文件的使用,最重要的是防止头文件重复包含。一般使用如下的格式避免这个问题。
#ifndef 头文件标识符 #define 头文件标识符 //头文件内容 #endif |
注意一点,即使使用了防止重复包含语句,也不要在头文件中分配内存(定义变量或是malloc堆内存),因为使用了防止重复包含语句,也只是让一个文件中的多条#include语句防止重复包含,有多个源文件都包含同一个头文件时,定义的变量仍然会被定义很多次,产生重复定义的错误。所以,一般,全局变量都要放到源文件中去定义。
一般C语言的编译流程分为四步,分别是:预处理,编译,汇编,链接。
预处理主要操作是的程序中以#开头的预处理指令,包括宏定义,条件编译,文件包含三项内容,预处理的选项是-E,生成的文件以.i作为后缀。
gcc -E hello.c -o hello.i |
预处理之后,编译器就可以开始把源文件翻译成机器码了。但是在编译之前,对于gcc编译器来说,还存在一个中间步骤,会把源文件先转化为汇编语言,然后再转化成机器码,转化为汇编语言这一步称为编译,使用-S选项。
gcc -S hello.i -o hello.s |
转化成汇编代码后,再把汇编代码转化成机器码,这一步称为汇编,gcc的汇编选项是-c,生成的文件以.o作为后缀,称为目标文件。
gcc -c hello.s -o hello.o |
最后一步,是将各个目标文件以及系统提供的库文件一起进行链接,生成一个最终的可执行文件。
gcc hello.o -o hello |