• C语言程序的编译(预处理) —— 下


    3. 接上篇 #define

    3.2.2 #define 定义宏

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

    那么写法我们可以跟函数类似,但是不能与函数的概念混淆。下面是宏的声明方式:

    #define name(parament-list) stuff

    需要特别注意的是:参数列表的左括号必须与 name 紧邻

    我们现在来看一下用法:

    1. #include
    2. #define ADD(X) X+X
    3. int main()
    4. {
    5. printf("%d\n", ADD(3));
    6. return 0;
    7. }

     这就是一个非常简单的计算加法的宏,我们一定要搞清楚宏与函数的区别。函数传递的是参数,但是宏完成的是文本替换。我们可以在代码上加上注释方便阅读。

    1. #include
    2. #define ADD(X) X+X //那么 X 就被 3 替代 ---> ADD(3) 3+3
    3. int main()
    4. {
    5. printf("%d\n", ADD(3));// 给宏传递了一个 3
    6. return 0;
    7. }

    既然完成的只是文本替换,那么就会存在下面这种歧义:

    1. #include
    2. #define SUB(X) X*X
    3. int main()
    4. {
    5. printf("%d\n", SUB(3+2));
    6. return 0;
    7. }

    按我们的直觉来说,宏完成的计算应该是 (3+2)*(3+2) = 25 。事实上宏只是会完成文本替换,并不会去运算参数表达式

    上面这个答案的计算过程是 3+2*3+2 = 11 。 

     

    那么既然我们要避免歧义,就需要手动添置括号。

    1. #include
    2. #define SUB(X) ((X)*(X))
    3. int main()
    4. {
    5. printf("%d\n", SUB(3+2));
    6. return 0;
    7. }

    不仅仅是将文本替换对象添上括号,也必须将宏的表达式也添上括号,构成一个整体。这样可以避免诸多问题,这里不再一一列举。

    3.2.3 #define 替换规则

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

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

    需要注意的是:

    1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。也就是说,使用宏时,可以调用其他宏,但绝不能调用自己。
    2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。这条我们用代码来解释:
    1. #include
    2. #define TEST(X) ((X)+(X))
    3. int main()
    4. {
    5. char* str = "TEST";//字符串常量与宏名重复,但预处理器并不会查找
    6. printf("%d", TEST(3));
    7. return 0;
    8. }

    3.2.4 # 的使用

    首先我们来看以下 printf 的非常规用法:

    1. #include
    2. int main()
    3. {
    4. printf("hello " "world" "\n");
    5. return 0;
    6. }

     这段代码是会正常输出 hello world 的。

    那我们到底想表达什么呢?

    例如我们写一个函数:

    1. #include
    2. void print(int n)
    3. {
    4. printf("a = %d", n);
    5. }
    6. int main()
    7. {
    8. int a = 3;
    9. print(a);
    10. return 0;
    11. }

     这段代码会输出 a = 3,但是我们把 a 的变量名改为 b ,我们想让这个程序输出 b = 3,是明显不方便的,因为我们需要手动修改。

    1. #include
    2. void print(int n)
    3. {
    4. printf("a = %d", n);
    5. }
    6. int main()
    7. {
    8. int b = 3;
    9. print(b);
    10. return 0;
    11. }

    但是在宏里面,就可以使用 # 和 printf 相互配合来达到这个效果。

     

    1. #include
    2. // # 的作用是将参数变成字符串
    3. #define PRINT(value,format) printf("the value of " #value " is " format "\n",value)
    4. // == printf("the value of " "a" " is " " %d " "\n",value )
    5. int main()
    6. {
    7. int a = 3;
    8. PRINT(a, "%d");
    9. return 0;
    10. }

    3.2.5 带副作用的宏参数

    当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。就好比说:

    1. x+1;//此表达式的 x 值不变,不带副作用
    2. x++;//此表达式 x=x+1; x 的值会变,带副作用

     我们来看看使用带有副作用参数的宏会出现什么问题:

    1. #include
    2. #define MAX(a,b) ((a)>(b)?(a):(b))
    3. int main()
    4. {
    5. int x = 5;
    6. int y = 8;
    7. int z = MAX(x++, y++);
    8. printf("x=%d y=%d z=%d\n", x, y, z);
    9. return 0;
    10. }

    我们来分析分析:

     3.2.6 宏和函数对比

    宏通常被应用于执行简单的计算。那为什么不用函数来完成简单的计算呢?

    原因有二:

    1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更过。所以宏比函数在程序的规模和速度方面更胜一筹。
    2. 更为重要的时函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。但是宏的参数是不需要声明类型的。

    当然,既然存在函数和宏,就说明二者是缺一不可的,宏的缺点有下:

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

    3.2.7 命名约定

    一般来讲函数的宏的使用语法很近似。所以语言本身没法帮我们区分二者。但我们有一个习惯:宏名全大写,函数名全小写。说到这里就不得不提到 offsetof 这个宏,乍一看它是一个函数,事实上它是一个宏。它的作用是求出结构体成员相对于 0 地址的偏移量。如果大家忘了结构体内存对齐,可以复习以下。在这里,我们来模拟实现以下 offsetof 宏。 

    1. #include
    2. #define OFFSETOF(type,member) ((int)(&(((type*)0)->member)))
    3. struct S
    4. {
    5. char a;
    6. int b;
    7. char c;
    8. };
    9. int main()
    10. {
    11. printf("%d\n", OFFSETOF(struct S, a));
    12. printf("%d\n", OFFSETOF(struct S, b));
    13. printf("%d\n", OFFSETOF(struct S, c));
    14. return 0;
    15. }

    我们来拆分一下我们的实现原理。

     

    3.3 #undef

    这条指令用于移除一个宏定义。

    1. #include
    2. #define OFFSETOF(type,member) ((int)(&(((type*)0)->member)))
    3. struct S
    4. {
    5. char a;
    6. int b;
    7. char c;
    8. };
    9. int main()
    10. {
    11. #undef OFFSETOF //将 OFFSETOF 取消定义
    12. printf("%d\n", OFFSETOF(struct S, a));
    13. printf("%d\n", OFFSETOF(struct S, b));
    14. printf("%d\n", OFFSETOF(struct S, c));
    15. return 0;
    16. }

     

     

     3.4 文件包含

    我们要知道,#include 这条指令是使头文件在编译时可以被编译。

    我们同样也知道包含头文件的方式有两种:

    1. #include
    2. #include "stdio.h"

    这两种包含方式有什么区别呢?

    • 对于第一种来说,查找的头文件直接去标准路径下去查找,也就是直接去编译器带的头文件库里面查找。
    • 对于第二种来说,查找的头文件会先在项目目录下查找,如果找不到,再去标准路径下查找。

    也就是说,库文件可以使用 "" 包含,但是这样做的效率会比较低。所以在使用头文件时有一个不成文的规定:在包含库文件时,使用 <> ,包含自己的头文件时,使用 "" 。

  • 相关阅读:
    C#结合OpenCVSharp4使用直方图算法比较图片相似度
    N种实用功能,助力企业智破服务难题
    io概述及其分类
    Docker 启动容器报错:cannot allocate memory: unknown
    TI Sitara系列 AM64x开发板——TSN通信测试手册(下)
    Java 面向对象编程
    红队|域渗透重要漏洞总结
    CenterNet根据自己的数据训练模型
    Matlab:多变量数据
    10.26ALP论文原代码请稿
  • 原文地址:https://blog.csdn.net/weixin_59913110/article/details/125875837