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

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

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

#define 宏名 宏定义字符串

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

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

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

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

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

宏定义

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

#define name( parament-list ) stuff
//name和左括号要紧密相邻
#define PI 3.14159
#define SQUARE(x) ((x) * (x))

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

#define NUMBER 123;
//打印出来为以下:
//123;;

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

if(condition)
 max = MAX;
else
 max = 0;

宏替换

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

#define MAX(x, y) ((x) > (y) ? (x) : (y))

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

int a = 10;
int b = 20;
int max_num = MAX(a, b);

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

int a = 10;
int b = 20;
int max_num = ((a) > (b) ? (a) : (b));

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

#define SQUARE(x) x * x
#define SQUARE(x) (x) * (x)
 

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

得到的两种结果:

3//得到的表达式是:a+1*a+1
4//得到的表达式是:(a+1)*(a+1)

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

#define SQUARE(x) ((x) * (x))

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

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

宏与函数

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

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

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

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

宏的缺陷

  • 可能引起宏展开后的代码过长,影响可读性。
  • 可能导致宏的滥用,使代码变得难以理解和维护。
  • 宏无法调试,不能很好的检索错误
  • 宏无法像函数那样递归,不能嵌套宏
  • 宏展开可能导致意外的副作用,如参数多次计算等。
x+1;//不带副作⽤
x++;//带有副作⽤,会被多次计算

预处理运算符

#

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

#include <stdio.h>
 
#define STRINGIFY(x) #x
 
int main() {
    int num = 10;
    const char* str = STRINGIFY(num);
    
    printf("String representation of num: %s\n", str);
    
    return 0;
}

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

String representation of num: 10

##

作用:连接前后两个符号

#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: %d\n", result);
    
    return 0;
}

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

Result: 30

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

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

得到的结果就是

Result: 0

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

条件编译

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

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

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

#if define 宏名以及条件

#ifdef-----前者的简写形式

用于条件编译定义

#if !define 宏名以及条件

#ifndef-----前者的简写形式

用于否定的条件编译定义

#elif

#else

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

#endif

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

#include <stdio.h>
 
#define DEBUG 1
 
int main() {
    int num = 10;
    
    #if DEBUG
        printf("Debug mode is enabled\n");
        printf("Number: %d\n", num);
    #else
        printf("Debug mode is disabled\n");
    #endif
    
    return 0;
}

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

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

Debug mode is enabled
Number: 10

头文件包含

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

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

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

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

预处理指令

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

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

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

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

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

目录
相关文章
|
1月前
|
编译器 C语言
C语言--预处理详解(1)
【10月更文挑战第3天】
|
1月前
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
|
15天前
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
19 2
|
1月前
|
编译器 Linux C语言
【C语言篇】编译和链接以及预处理介绍(下篇)
【C语言篇】编译和链接以及预处理介绍(下篇)
32 1
【C语言篇】编译和链接以及预处理介绍(下篇)
|
1月前
|
C语言
C语言--预处理详解(2)
【10月更文挑战第3天】
|
1月前
|
编译器 C语言
C语言预处理详解
C语言预处理详解
|
1月前
|
存储 C语言
【C语言篇】编译和链接以及预处理介绍(上篇)2
【C语言篇】编译和链接以及预处理介绍(上篇)
36 0
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
32 3
|
C语言 编译器
《C语言编程初学者指南》一1.6 使用指令
本节书摘来自华章出版社《C语言编程初学者指南》一书中的第1章,第1.6节,作者【美】Keith Davenport(达文波特) , M1ichael Vine(维恩),更多章节内容可以访问云栖社区“异步社区”公众号查看 1.6 使用指令 下面再看看本章一开始给出的示例程序。
1492 0
|
4天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
19 6