- 由 zhongluqiang创建, 最后修改于3月 12, 2022
以"#"开头的命令是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};
条件编译
条件编译的功能使得我们可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于调试和移植程序是很有用的,条件编译有以下几种形式。
第1种形式
#ifdef 标识符 程序段 #endif
第2 种形式
#ifdef 标识符 程序段1 #else 程序段2 #endif
第3种形式
#ifdef 标识符 程序段1 #elif 标识符2 程序段2 #elif 标识符3 程序段3 ...... #endif
第4种形式
#ifndef 标识符 程序段1 #endif
第5种形式
#if 常量表达式 程序段1 #else 程序段2 #endif
第6种形式
#if defined(宏1) 程序段1 #elif defined(宏2) && defined(宏3) 程序段2 #elif !defined(宏4) && !defined(宏5) 程序段3 #endif
文件包含
文件包含是预处理的一个重要功能,它将多个源文件链接成一个源文件进行编译,结果将只生成一个目标文件。C语言提供#include命令来实现文件的包含,它有两种格式。
#include #include “filename”
其中,filename是要包含的文件名称,也称为头文件。用尖括号括起时,表示这个文件要到系统规定的路径中去获得这个文件(即C编译系统提供的存放头文件的路径,一般是/usr/include目录)。而用双引号括起的头文件,则表示这个头文件要到当前目录中去寻找,如果没找到,则到系统指定的目录去寻找。
头文件
在程序有多个源文件时,可以将函数或类型的声明写到头文件中,头文件的后缀一般是.h结尾。对于头文件的使用,最重要的是防止头文件重复包含。一般使用如下的格式避免这个问题。
#ifndef 头文件标识符 #define 头文件标识符 //头文件内容 #endif
注意一点,即使使用了防止重复包含语句,也不要在头文件中分配内存(定义变量或是malloc堆内存),因为使用了防止重复包含语句,也只是让一个文件中的多条#include语句防止重复包含,有多个源文件都包含同一个头文件时,定义的变量仍然会被定义很多次,产生重复定义的错误。所以,一般,全局变量都要放到源文件中去定义。
gcc编译流程
一般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
用宏可以实现的一些骚操作,比如:
#define HI_APPCOMM_LOG_AND_RETURN_IF_FAIL(ret, errcode, errstring) \ do { \ if ((ret) != HI_SUCCESS) { \ MLOGE("[%s] failed[0x%08X]\n", (errstring), (ret)); \ return (errcode); \ } \ } while (0)
上面这个宏可以处理常见的根据返回值退出的操作。
- 无标签