• 深度剖析 —— 预处理


    系列文章目录

    深度剖析:数据
    深度剖析:递归
    深度剖析:结构体
    深度剖析:动态内存管理
    深度剖析:文件操作
    深度剖析:预处理



    前言

    C预处理器在程序执行之前查看程序(故称之为预处理器)。本文将围绕C语言的预处理器,深度剖析预处理指令。


    一、预定义符号

    符号作用
    FILE进行编译的源文件
    LINE文件当前的行号
    DATE文件被编译的日期
    TIME文件被编译的时间
    STDC如果编译器遵循ANSI C,其值为1,否则未定义

    这些预定义符号都是语言内置的

    printf("file:%s line:%d\n", __FILE__, __LINE__);
    
    • 1

    二、预定义

    1. #define 定义标识符

    1.语法结构

    #define name stuff
    
    • 1

    2.调用方式

    调用作用
    #define MAX 1000定义一个全局常量MAX,大小为1000
    #define reg register为 register这个关键字,创建一个简短的名字
    #define do_forever for( ; ; )用更形象的符号来替换一种实现
    #define CASE break; case在写case语句的时候自动把 break写上

    3.注意事项

    在define定义标识符的时候不建议加上;

    #define MAX 100;
    #define MIN 10;
    
    • 1
    • 2
    int ret = MAX * MIN;
    
    • 1

    由于 #define 定义的内容是在预处理时将其替换到代码中,此时此刻就会出问题。因此强烈不建议在定义标识符时加上;

    2. #define 定义宏

    1.语法结构

    #define name( parament-list ) stuff
    
    • 1

    #define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)

    2.调用示例

    #define SQUARE(x) x * x
    
    • 1
    传参替换结果
    SQUARE( 5 );5 * 525
    SQUARE( 5 + 1 )5 + 1 * 5 + 111
    SQUARE( 5 + 2 + 1 )5 + 2 + 1 * 5 + 2 + 115
    #define SQUARE(x) (x) * (x)
    
    • 1
    传参替换结果
    SQUARE( 5 );( 5 ) * ( 5 )25
    SQUARE( 5 + 1 )( 5 + 1 ) * ( 5 + 1 )36
    SQUARE( 5 + 2 + 1 )( 5 + 2 + 1 ) * ( 5 + 2 + 1 )64
    #define SQUARE(x) (x) + (x)
    
    • 1
    传参替换结果
    2 * SQUARE( 5 );2 * ( 5 ) + ( 5 )15
    2 * SQUARE( 5 + 1 )2 * ( 5 + 1 ) + ( 5 + 1 )18
    2 * SQUARE( 5 + 2 + 1 )2 * ( 5 + 2 + 1 ) + ( 5 + 2 + 1 )24
    #define SQUARE(x) ((x) + (x))
    
    • 1
    传参替换结果
    2 * SQUARE( 5 );2 * ( ( 5 ) + ( 5 ) )20
    2 * SQUARE( 5 + 1 )2 * ( ( 5 + 1 ) + ( 5 + 1 ) )24
    2 * SQUARE( 5 + 2 + 1 )2 * ( ( 5 + 2 + 1 ) + ( 5 + 2 + 1 ) )32

    3.注意事项

    所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或 邻近操作符之间不可预料的相互作用

    3. #define 使用规则

    1.替换规则

    在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤:

    1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
    2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
    3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

    2.命名约定

    一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:把宏名全部大写 函数名不要全部大写

    3.注意事项

    在程序中使用 #define 定义符号和宏时,需要注意:

    1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
    2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

    三、宏和函数对比

    1.宏的优势

    宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。

    #define MAX(a, b) ((a)>(b)?(a):(b))
    
    • 1

    宏相比函数优势的地方:

    1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序 的规模和速度方面更胜一筹。
    2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可 以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。

    2.宏的劣势

    宏相比函数劣势的地方:

    1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
    2. 宏无法进行调试的。
    3. 宏由于类型无关,也就不够严谨。
    4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
    5. 宏有时候可以做函数做不到的事情。

    3.详细对比表

    属 性#define定义宏函数
    代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏 之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使 用这个函数时,都调用那个地方的同 一份代码
    执行速度更快存在函数的调用和返回的额外开销, 所以相对慢一些
    操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加 上括号,否则邻近操作符的优先级可能会产生不可预料的后 果,所以建议宏在书写的时候多些括号。函数参数只在函数调用的时候求值一 次,它的结果值传递给函数。表达式 的求值结果更容易预测。
    带有副作用的参数参数可能被替换到宏体中的多个位置,所以带有副作用的参 数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次, 结果更容易控制。
    参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可 以使用于任何参数类型。函数的参数是与类型有关的,如果参 数的类型不同,就需要不同的函数, 即使他们执行的任务是不同的。
    调试宏是不方便调试的函数是可以逐语句调试的
    递归宏是不能递归的函数是可以递归的

    四、命令行定义

    许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要 编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果 机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)

    #include 
    
    int main() {
        int array[ARRAY_SIZE];
        int i = 0;
        for (i = 0; i < ARRAY_SIZE; i++) {
            array[i] = i;
        }
        for (i = 0; i < ARRAY_SIZE; i++) {
            printf("%d ", array[i]);
        }
        printf("\n");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    编译指令:

    gcc -D ARRAY_SIZE=10 programe.c
    
    • 1

    五、条件编译

    在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

    调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

    #include 
    
    #define __DEBUG__
    
    int main() {
    
        int i = 0;
        int arr[10] = {0};
        for (i = 0; i < 10; i++) {
            arr[i] = i;
            #ifdef __DEBUG__
            printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
            #endif //__DEBUG__ }
            return 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    常见的条件编译指令

    1.单分支

    #if  常量表达式 
    	//...
    #endif //常量表达式由预处理器求值。
    
    • 1
    • 2
    • 3

    2.多分支

    #if 常量表达式 
    	//... 
    #elif 常量表达式 
    	//... 
    #else 
    	//... 
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.判断是否被定义

    #if defined(symbol) 
    #ifdef symbol
    
    #if !defined(symbol) 
    #ifndef symbol
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4.嵌套指令

    #if defined(OS_UNIX) 
    	#ifdef OPTION1 
    		unix_version_option1(); 
    	#endif 
    	#ifdef OPTION2 
    		unix_version_option2(); 
    	#endif 
    #elif defined(OS_MSDOS) 
    	#ifdef OPTION2 
    		msdos_version_option2(); 
    	#endif 
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    总结

    C语言的重要附件之一就是C预处理器。C预处理器遵循预处理器指令,在编译源代码之前调整源代码。它提供了更多更便捷的方式去编写程序,是C语言中非常重要的一环。

  • 相关阅读:
    深入理解Spring注解机制(一):注解的搜索与处理机制
    别再到处乱放配置文件了!我司使用 7 年的这套解决方案,稳的一秕
    计算机组成原理习题课第一章-2(唐朔飞)
    游戏报错d3dcompiler_47.dll缺失怎么修复,总结多种修复方法
    基于ClickHouse的近实时数据更新方案
    【云原生】Java设计模式8,校验、审批流程改善神器,责任链模式
    量子AI取得突破性进展:大大减少所需训练数据量
    【vue项目部署】Linux+Nginx实现项目部署及跨域
    ARM 虚拟化简介
    基于FPGA的VGA图像显示
  • 原文地址:https://blog.csdn.net/qq_64589446/article/details/126026830