C语言(16)----预处理中的宏以及预处理指令

2024-06-18 15:02:04 浏览数 (1)

预处理作为编译的预先准备阶段,其中的宏是一种由预处理器处理的指令或代码片段。宏的基本定义由#define来完成。通常为了区分变量名和函数,宏名通常使用大写字母串来书写。

代码语言:javascript复制
#define 宏名 宏定义字符串

对于宏用途简单的描述包括以下几点:

1.符号常量,用来增加程序的灵活性

2.简单的函数功能实现,但局限于一行之内完成

3.提供需要多次书写时的方便。可以使用一行宏来简写。

针对宏的特点和用途,接下来详细介绍。

宏定义

一般来说宏可以定义常量也可以定义变量。

代码语言:javascript复制
#define name( parament-list ) stuff
//name和左括号要紧密相邻
代码语言:javascript复制
#define PI 3.14159
#define SQUARE(x) ((x) * (x))

注意:一般在语句的最后不会加上分号;,因为这也属于语句的一部分,否则会出现以下情况:

代码语言:javascript复制
#define NUMBER 123;
//打印出来为以下:
//123;;

而对于某些函数语句例如if、while,更有可能出现语法错误。 

代码语言:javascript复制
if(condition)
 max = MAX;
else
 max = 0;

 宏替换

在预处理阶段,预处理器会将代码中的宏调用替换为宏定义的内容。

代码语言:javascript复制
#define MAX(x, y) ((x) > (y) ? (x) : (y))

 现在我们在代码中使用这个宏来比较两个数的大小,例如:

代码语言:javascript复制
int a = 10;
int b = 20;
int max_num = MAX(a, b);

 在预处理阶段,预处理器会将宏调用MAX(a, b)替换为其定义的内容,即((a) > (b) ? (a) : (b))。因此,上面的代码在编译之前会被替换为:

代码语言:javascript复制
int a = 10;
int b = 20;
int max_num = ((a) > (b) ? (a) : (b));

注意:由于运算符优先级的问题,定义宏不要吝啬括号。比如以下的例子

代码语言:javascript复制
#define SQUARE(x) x * x
#define SQUARE(x) (x) * (x)

这两种表达方式得到的结果有可能都不相同。比如我们代入a 1,而当a=1时

得到的两种结果:

代码语言:javascript复制
3//得到的表达式是:a 1*a 1
4//得到的表达式是:(a 1)*(a 1)

所以我们最保险的方法就是给整个宏都加上括号:

代码语言:javascript复制
#define SQUARE(x) ((x) * (x))

宏替换的过程可以简单概括为以下几个步骤:

  1. 宏定义:在代码中使用#define指令定义宏,为常量、函数等起一个易记的名字。
  2. 宏调用:在代码中使用定义好的宏,传入参数(如果有的话)。
  3. 预处理阶段:在编译之前的预处理阶段,预处理器会扫描代码中的宏调用,并将其替换为宏定义的内容。
  4. 宏展开:预处理器将宏调用展开为其定义的内容,包括参数的替换。
  5. 编译阶段:展开后的代码会被编译器处理,生成可执行代码。

宏与函数

经过上述的介绍可以发现,宏和函数实际上有很多相似之处。实际上对于它们的使用也有很大的相似之处,但是它们之间的差异也是显而易见的。

    • 预处理阶段替换:宏是在预处理阶段被替换为其定义的内容,只需要直接运算,而不是像函数那样需要先调用再运算再返回。
    • 无类型检查:宏没有参数类型检查,因此在宏中使用参数时需要特别小心,确保类型匹配。(这实际上既是优点也是缺点,增加了自由性但是舍弃了严谨性)
    • 效率:宏展开时会直接替换文本,没有函数调用的开销,运行所需时间也会大幅减少,因此在一些情况下可能更高效。
    • 代码复杂性:宏可以包含更复杂的代码逻辑,如条件判断等。
  1. 函数
    • 运行时调用:函数是在程序运行时被调用执行的,具有独立的作用域和参数传递机制。
    • 类型安全:函数具有参数类型检查,可以避免一些潜在的错误。
    • 可读性:函数提供了更结构化和模块化的代码组织方式,增强了代码的可读性和维护性。
    • 调试:函数调用可以更方便地进行调试和跟踪。

在选择使用宏还是函数时,可以根据具体情况来决定:

  • 如果需要高效的代码替换和更复杂的宏逻辑,可以选择宏。
  • 如果需要类型安全、可读性强和更好的代码组织,可以选择函数。

 宏的缺陷

可能引起宏展开后的代码过长,影响可读性。

可能导致宏的滥用,使代码变得难以理解和维护。

宏无法调试,不能很好的检索错误

宏无法像函数那样递归,不能嵌套宏

宏展开可能导致意外的副作用,如参数多次计算等。

代码语言:javascript复制
x 1;//不带副作⽤
x  ;//带有副作⽤,会被多次计算

预处理运算符

#

作用:将宏参数转换为字符串

代码语言:javascript复制
#include <stdio.h>

#define STRINGIFY(x) #x

int main() {
    int num = 10;
    const char* str = STRINGIFY(num);
    
    printf("String representation of num: %sn", str);
    
    return 0;
}

STRINGIFY宏使用了#运算符,将传入的参数num转换为字符串。在main函数中,我们将num的字符串表示打印出来。

代码语言:javascript复制
String representation of num: 10

## 

作用:连接前后两个符号

代码语言:javascript复制
#include <stdio.h>

#define CONCAT(a, b) a##b

int main() {
    int num1 = 10;
    int num2 = 20;
    
    int result = CONCAT(num, 1)   CONCAT(num, 2);
    
    printf("Result: %dn", result);
    
    return 0;
}

CONCAT宏使用了##运算符,将num和后面的数字连接在一起,形成新的符号。在main函数中,我们使用CONCAT宏将num1num2连接在一起,并将它们相加。

代码语言:javascript复制
Result: 30

这表明##运算符成功将num1num2连接在一起,并进行了相加操作。

而如果我们不使用##运算符,宏参数和其他文本会被简单地拼接在一起,而不会进行连接操作。

得到的结果就是

代码语言:javascript复制
Result: 0

#和##在实际运用中其实很少,所以只作介绍。

条件编译

条件编译允许根据条件来选择性地编译代码。如果我们要将某语句临时放弃或者更改,就可以用到条件编译。

理论上条件编译的功能和条件语句十分相像,只不过一个是在预处理过程中一个是在具体的代码程序中。

在C语言中,条件编译通常使用预处理指令#if#ifdef#ifndef#elif#else#endif来实现。

#if define 宏名以及条件 #ifdef-----前者的简写形式 用于条件编译定义

#if !define 宏名以及条件 #ifndef-----前者的简写形式 用于否定的条件编译定义

#elif #else 两者多用于多个分支的条件编译

#endif 条件编译预处理指令的结束标记,与前面几个指令配对使用,用于结束条件编译的代码块。

代码语言:javascript复制
#include <stdio.h>

#define DEBUG 1

int main() {
    int num = 10;
    
    #if DEBUG
        printf("Debug mode is enabledn");
        printf("Number: %dn", num);
    #else
        printf("Debug mode is disabledn");
    #endif
    
    return 0;
}

在上面的示例中,我们定义了一个宏DEBUG并赋值为1。在main函数中,使用条件编译指令#if DEBUG来判断是否启用了调试模式。如果DEBUG宏被定义且值为非零,则会编译#if DEBUG#else之间的代码;否则,会编译#else#endif之间的代码。

当程序编译时,由于DEBUG宏被定义为1,所以会编译#if DEBUG#else之间的代码。因此,输出结果为:

代码语言:javascript复制
Debug mode is enabled
Number: 10

头文件包含

头文件包含的方式为以下两种

  1. #include <header.h>
    • 使用尖括号<>包含头文件时,编译器会在系统默认的目录中查找头文件。如果找不到就提示编译错误。
    • 这种方式通常用于包含标准库头文件或系统提供的头文件。
  2. #include "header.h"
    • 使用双引号""包含头文件时,编译器会先在当前源文件所在目录中查找头文件,如果找不到再去系统默认目录中查找。如果找不到就提示编译错误。
    • 这种方式通常用于包含自定义的头文件。

我们可以发现,""的形式似乎较<>的泛用性更大,那为何不直接全部使用前者来包含头文件呢?

这样做确实可以,但是我们需要时刻注意优秀的代码是需要保持高效性的,这样做会增加查找的时间,并且它并不能用于查找库文件,所以在某些时刻二者区分使用是有好处的。

预处理指令

除了上述已经基本介绍完毕的预处理指令,

常见的预处理指令还包括这些:

  1. #undef:取消宏定义
  2. #error:生成错误消息
  3. #warning:生成警告消息
  4. #pragma:编译器指令
  5. #line:修改行号和文件名信息
  6. #ifdef#ifndef#else#elif#endif:条件编译
  7. #pragma once:头文件只包含一次

这些预处理指令在源代码编译之前起作用,用于对源代码进行预处理、条件编译、头文件包含等操作,可以在一定程度上提高代码的灵活性和可维护性。

而在实际编程中,合理使用预处理指令可以简化代码逻辑、提高代码的可读性和可维护性,从而帮助程序员更好地编写代码。 

0 人点赞