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