预处理
在C语言程序源码中,凡是以井号(#)开头的语句被称为预处理语句,这些语句严格意义上并不属于C语言语法的范畴,它们在编译的阶段统一由所谓预处理器(cc1)来处理。所谓预处理,顾名思义,指的是真正的C程序编译之前预先进行的一些处理步骤,这些预处理指令包括:
- 头文件:#include
- 定义宏:#define
- 取消宏:#undef
- 条件编译:#if、#ifdef、#ifndef、#else、#elif、#endif
- 显示错误:#error
- 修改当前文件名和行号:#line
- 向编译器传送特定指令:#progma
- 基本语法
- 一个逻辑行只能出现一条预处理指令,多个物理行需要用反斜杠连接成一个逻辑行
- 预处理是整个编译全过程的第一步:预处理 - 编译 - 汇编 - 链接
- 可以通过如下编译选项来指定来限定编译器只进行预处理操作:
gcc example.c -o example.i -E
编译过程
编译过程分为以下四个阶段:
1)预处理:处理预处理语句(#开头的语句),删除注释、头文件展开、宏替换...
gcc hello.c -o hello.i -E
2)编译:将C语言程序转化为汇编语言
gcc hello.c -o hello.s -S
3)汇编:将程序代码转化为二进制代码
gcc hello.c -o hello.o -c
4)链接:将所有二进制代码合并起来,根据应用规则生成一个专门针对某个平台执行的应用程序镜像
gcc hello.c -o hello
宏的概念
宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。例如:
#define PI 3.14
此处,PI 就是宏(宏一般习惯用大写字母表达,以区分于变量和函数,但这并不是语法规定,只是一种习惯),是一段特定的字串,这个字串在源码中出现时,将被替换为3.14。例如:
int main()
{
printf("圆周率: %f\n", PI);
// 此语句将被替换为:printf("圆周率: %f\n", 3.14);
}
-
宏的作用:
- 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
- 使得程序修改更容易:修改宏定义,即修改了所有该宏替换的表达式。
- 提高程序的运行效率:程序的执行不再需要函数切换开销,而是就地展开。
#include <stdio.h> // 定义宏,注意没有分号 // 编译之前可以确认PI的数据内容以及类型,所以编译的时候不会报错 // 方便管理数据 #define PI 3.14 // typedef 的作用是将数据类型取别名 // 注意typedef是c语法,所以需要用;结尾 // 编译的时候才能调用typdef,所以typdef修饰常量的时候会报错 typedef int (*ptr)(float,int); int func(float a, int b) { printf("%.2f\n",a+b); } int main(int argc, char const *argv[]) { int (*p)(float,int); p = func; p(PI,10); ptr fun = func; fun(PI,30); return 0; }
无参宏
无参宏意味着使用宏的时候,无需指定任何参数,比如:
#define PI 3.14
#define SCREEN_SIZE 800*480*4
int main()
{
// 在代码中,可以随时使用以上无参宏,来替代其所代表的表达式:
printf("圆周率: %f\n", PI);
mmap(NULL, SCREEN_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, ...);
}
注意到,上述代码中,除了有自定义的宏,还有系统预定义的宏:
// 自定义宏:
#define PI 3.14
#define SCREEN_SIZE 800*480*4
// 系统预定义宏
#define NULL ((void *)0)
#define PROT_READ 0x1 /* Page can be read. */
#define PROT_WRITE 0x2 /* Page can be written. */
#define MAP_SHARED 0x01 /* Share changes. */
宏的最基本特征是进行直接文本替换,以上代码被替换之后的结果是:
int main()
{
printf("圆周率: %f\n", 3.14);
mmap(((void *)0), 800*480*4, 0x1|0x2, 0x01, ...);
}
带参宏
带参宏意味着宏定义可以携带“参数”,从形式上看跟函数很像,例如:
#define MAX(a, b) a>b ? a : b
#define MIN(a, b) a<b ? a : b
// 多个变量要用{}
#define A(a,b,c) ({a=1;b+=1;c=a+b;})
以上的MAX(a,b) 和 MIN(a,b) 都是带参宏,不管是否带参,宏都遵循最初的规则,即宏是一段待替换的文本,例如在以下代码中,宏在预处理阶段都将被替换掉:
int main()
{
int x = 100, y = 200;
printf("最大值:%d\n", MAX(x, y));
printf("最小值:%d\n", MIN(x, y));
// 以上代码等价于:
// printf("最大值:%d\n", x>y ? x : y);
// printf("最小值:%d\n", x<y ? x : y);
}
-
带参宏的特点:
- 直接文本替换,不做任何语法判断,更不做任何中间运算。
- 宏在编译的第一个阶段就被替换掉,运行中不存在宏。
- 宏将在所有出现它的地方展开,这一方面浪费了内存空间,另一方面有节约了切换时间。
-
typedef与宏区别
#include <stdio.h> // 宏是预编译的时候进行替换,无消耗编译时间 // 宏是可以修改任意数据的别名 // 一般数据该别名用宏 #define int_t int #define PI 3.14 // typedef : 编译的时候替换,消耗编译时间 // typedef 只能用于关键字改别名 // 一般数据类型改别名用typedef typedef int int32_t; //typedef 3.14 pi; int main(int argc, char const *argv[]) { int32_t a = 10; int_t b = 20; return 0; }
带参宏的副作用
由于宏仅仅做文本替换,中间不涉及任何语法检查、类型匹配、数值运算,因此用起来相对函数要麻烦很多。例如:
#define MAX(a, b) a>b ? a : b
int main()
{
int x = 100, y = 200;
printf("最大值:%d\n", MAX(x, y==200?888:999));
}
直观上看,无论 y 的取值是多少,表达式 y==200?888:999 的值一定比 x 要大,但由于宏定义仅仅是文本替换,中间不涉及任何运算,因此等价于:
printf("最大值:%d\n", x>y==200?888:999 ? x : y==200?888:999);
可见,带参宏的参数不能像函数参数那样视为一个整体,整个宏定义也不能视为一个单一的数据,事实上,不管是宏参数还是宏本身,都应被视为一个字串,或者一个表达式,或者一段文本,因此最基本的原则是:
- 将宏定义中所有能用括号括起来的部分,都括起来,比如:
#define MAX(a, b) ((a)>(b) ? (a) : (b))
#include <stdio.h>
#define PI 3.14
#define HELLO "hello world"
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define A(a,b,c) ({a=1;b+=1;c=a+b;})
int main(int argc, char const *argv[])
{
printf("%f\n",PI);
printf("%s\n",HELLO);
int a = 10,b = 20,c;
A(a,b,c);
printf("%d,%d,%d\n",a,b,c);
printf("max:%d\n",MAX(a,b));
int x = 100, y = 200;
printf("最大值:%d\n", MAX(x, y==200?888:999));
int z = x > y==200?888:999;
printf("%d\n",z);
return 0;
}
宏定义中的符号粘贴
有些时候,宏参数中的符号并非用来传递数据,而是用来形成多种不同的字串,例如在某些系统函数中,系统本身规范了函数接口的部分标准,形如:
void __zinitcall_service_1(void)
{
...
wifi
}
void __zinitcall_service_2(void)
{
...
有线网
}
void __zinitcall_feature_1(void)
{
...
zigbee(无线网)
}
void __zinitcall_feature_2(void)
{
...
4G
}
此时,若需要向用户提供一个方便整合字串的宏定义,可以这么写:
#define LAYER_INITCALL(num, layer) __zinitcall_##layer##_##num
用户的调用如下:
LAYER_INITCALL(service, 1);
LAYER_INITCALL(service, 2);
LAYER_INITCALL(feature, 1);
LAYER_INITCALL(feature, 2);
// demo
#include <stdio.h>
#define INTERNET_CALL(layer,num) __zinitcall_##layer##_##num
void __zinitcall_service_1(void)
{
printf("切换到wifi模式\n");
}
void __zinitcall_service_2(void)
{
printf("切换到飞行模式\n");
}
void __zinitcall_feature_1(void)
{
printf("切换到5g模式\n");
}
void __zinitcall_feature_2(void)
{
printf("切换到4g模式\n");
}
int main()
{
// 编译出错的时候查找问题,需要观察预编译的结果
// gcc 4_粘贴宏.c -o a.i -E
INTERNET_CALL(service,1)();
INTERNET_CALL(service,2)();
INTERNET_CALL(feature,1)();
INTERNET_CALL(feature,2)();
}
#include <stdio.h>
// 定义粘贴宏
#define CAL_FUNC(layer) CAL_##layer##_func
int CAL_add_func(int a,int b)
{
return a+b;
}
int CAL_sub_func(int a, int b)
{
return a-b;
}
int CAL_mul_func(int a, int b)
{
return a * b;
}
int CAL_div_func(int a, int b)
{
return a / b;
}
int main(int argc, char const *argv[])
{
int ret = CAL_FUNC(sub)(20,5);
printf("%d\n",ret);
return 0;
}
注意:
在书写非字符串的字串时(如上述例子),使用两边双井号来粘贴字串,并且要注意如果字串出现在最末尾,则最后的双井号必须去除,例如上述代码不可写成:
#define LAYER_INITCALL(num, layer) __zinitcall_##layer##_##num##
但如果粘贴的字串并非出现在最末尾,则前后都必须加上双井号:
#define LAYER_INITCALL(num, layer) __zinitcall_##layer##_##num##end
注意:
另外,如果字串本身拼接为字符串,那么只需要使用一个井号即可,比如:
#define domainName(a, b) "www." #a "." #b ".com"
int main()
{
printf("%s\n", domainName(vstc, lab));
}
执行打印如下:
gec@ubuntu:~$ ./a.out
www.vstc.lab.com
gec@ubuntu:~$
#include <stdio.h>
// 字符串粘贴宏
#define COMPANAMECOM(a) "www."#a".com"
int main()
{
printf("%s\n",COMPANAMECOM(yueqian));
}
无值宏定义
定义无参宏的时候,不一定需要带值,无值的宏定义经常在条件编译中作为判断条件出现,例如:
#define BIG_ENDIAN
#define __cplusplus
条件编译
- 概念:有条件的编译,通过控制某些宏的值,来决定编译哪段代码。
- 形式:
- 形式1:判断表达式 MACRO 是否为真,据此决定其所包含的代码段是否要编译
- 注意:#if形式条件编译需要有值宏
#define A 0
#define B 1
#define C 2
#if A
... // 如果 MACRO 为真,那么该段代码将被编译,否则被丢弃
#endif
// 二路分支
#if A
...
#elif B
...
#endif
// 多路分支
#if A
...
#elif B
...
#elif C
...
#endif
- 形式:
- 形式2:判断宏 MACRO 是否已被定义,据此决定其所包含的代码段是否要编译
// 单独判断
#ifdef MACRO
...
#endif
// 二路分支
#ifdef MACRO
...
#else
...
#endif
- 形式:
- 形式3:判断宏MACRO是否未被定义,据此决定其所包含的代码段是否要编译
// 单独判断
#ifndef MACRO
...
#endif
// 二路分支
#ifndef MACRO
...
#else
...
#endif
- 总结:
- #ifdef 此种形式,判定的是宏是否已被定义,这不要求宏有值。
- #if 、#elif 这些形式,判定的是宏的值是否为真,这要求宏必须有值。
条件编译的使用场景
控制调试语句:在程序中,用条件编译将调试语句包裹起来,通过gcc编译选项随意控制调试代码的启停状态。例如:
gcc example.c -o example -DMACRO
以上语句中,-D意味着 Define,MACRO 是程序中用来控制调试语句的一个宏,如此一来就可以在完全不需要修改源代码的情况下,通过外部编译指令选项非常方便地控制调试信息的启停。
选择代码片段:在一些大型项目中(例如 Linux 内核),某个相同功能的模块往往有不同的实现,需要用户根据具体的情况来“配置”,这个所谓的配置的过程,就是对代码中不同的宏的选择的过程。
例如:
#define A 0 // 网卡1
#define B 1 // 网卡2 √
#define C 0 // 网卡3
// 多路分支
#if A
...
#elif B
...
#elif C
...
#endif
取消宏
#ifdef 宏名
#undef 宏名 // 取消该宏
...
#endif
#ifdef B
#undef B
printf("BBBB\n");
#endif
#include <stdio.h>
#define A
int main(int argc, char const *argv[])
{
#ifdef A // 如果定义宏A
#undef A // 取消宏A
printf("A\n");
#ifdef A // 如果定义宏A
printf("aaa\n");
#endif
return 0;
}
定义宏
#ifndef 宏名
...
#else
...
endif
#ifndef B
#define B
printf("BBBBBB\n");
#endif
标准预定义宏
1.预定义日期和时间
__DATE__ __TIME__
2.函数名和当前行号
__FUNCTION__ ___LINE__
3.文件名
__FILE__
printf("日期:%s 时间:%s 当前文件:%s 当前行数:%d 函数名:%s\n",__DATE__,__TIME__,__FILE__,__LINE__,__FUNCTION__);
#include <stdio.h>
int main(int argc, char const *argv[])
{
// 打印行号
printf("__%d__\n",__LINE__);
// 打印日期
printf("日期:%s",__DATE__);
// 打印时间
printf("时间 : %s\n",__TIME__);
// 打印函数名
printf("函数名 : %s\n",__FUNCTION__);
// 打印文件名
printf("文件名 : %s\n",__FILE__);
return 0;
}
宏有这么多用途 同志们都学费了吗?宏无处不在,如果有兴趣的同志可以去看看c语言底层代码,加油!