当前位置 博文首页 > CW_qian的博客:8月22日笔记C语言基础(补3)宏定义与条件编译1

    CW_qian的博客:8月22日笔记C语言基础(补3)宏定义与条件编译1

    作者:[db:作者] 时间:2021-08-25 21:43

    预处理

    ????????在C语言程序源码中,凡是以井号(#)开头的语句被称为预处理语句,这些语句严格意义上并不属于C语言语法的范畴,它们在编译的阶段统一由所谓预处理器(cc1)来处理。所谓预处理,顾名思义,指的是真正的C程序编译之前预先进行的一些处理步骤,这些预处理指令包括:

    1. 头文件:#include
    2. 定义宏:#define
    3. 取消宏:#undef
    4. 条件编译:#if、#ifdef、#ifndef、#else、#elif、#endif
    5. 显示错误:#error
    6. 修改当前文件名和行号:#line
    7. 向编译器传送特定指令:#progma
    • 基本语法
      • 一个逻辑行只能出现一条预处理指令,多个物理行需要用反斜杠连接成一个逻辑行
      • 预处理是整个编译全过程的第一步:预处理 - 编译 - 汇编 - 链接
      • 可以通过如下编译选项来指定来限定编译器只进行预处理操作:

    gcc example.c -o example.i -E


    宏的概念

    ????????宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。例如:

    #define?PI 3.14

    此处,PI 就是宏(宏一般习惯用大写字母表达,以区分于变量和函数,但这并不是语法规定,只是一种习惯),是一段特定的字串,这个字串在源码中出现时,将被替换为3.14。例如:

    int?main()

    {

    ????printf("圆周率: %f\n", PI);

    ????// 此语句将被替换为:printf("圆周率: %f\n", 3.14);

    }

    • 宏的作用:
      • 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
      • 使得程序修改更易行:修改宏定义,即修改了所有该宏替换的表达式。
      • 提高程序的运行效率:程序的执行不再需要函数切换开销,而是就地展开。

    无参宏

    ????????无参宏意味着使用宏的时候,无需指定任何参数,比如:

    #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

    以上的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);

    }

    • 带参宏的特点:
      1. 直接文本替换,不做任何语法判断,更不做任何中间运算。
      2. 宏在编译的第一个阶段就被替换掉,运行中不存在宏。
      3. 宏将在所有出现它的地方展开,这一方面浪费了内存空间,另一方面有节约了切换时间。

    带参宏的副作用

    ????????由于宏仅仅做文本替换,中间不涉及任何语法检查、类型匹配、数值运算,因此用起来相对函数要麻烦很多。例如:

    #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))


    无值宏定义

    ????????定义无参宏的时候,不一定需要带值,无值的宏定义经常在条件编译中作为判断条件出现,例如:

    #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