【C语言】预处理器

本文详细介绍了C语言预处理器的工作原理,包括#include指令的使用,宏定义的功能和规则,以及条件编译(#if、#ifdef、ifndef、#endif)在代码组织中的应用。特别关注了如何避免头文件重复加载的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

C 语言编译器在编译程序之前,会先使用预处理器(preprocessor)处理代码。

预处理器首先会清理代码,进行删除注释、宏展开、多行语句合成一个逻辑行等工作。然后,执行#开头的预处理指令。

预处理指令可以出现在程序的任何地方,但是习惯上,往往放在代码的开头部分。它的主要好处是,会使得程序的可读性更好,也更容易修改。

每个预处理指令都以#开头,放在一行的行首,指令前面可以有空白字符(比如空格或制表符)。#和指令的其余部分之间也可以有空格,但是为了兼容老的编译器,一般不留空格。

所有预处理指令都是一行的,除非在行尾使用反斜杠\,将其折行。指令结尾处不需要分号。

#include

#include指令用于编译时将其他源码文件,加载进入当前文件,可以是头文件(通常以**.h**结尾)或其他源文件。

  • #include <foo.h> // 加载系统提供的文件

    形式一,文件名写在尖括号里面,表示该文件是系统提供的,通常是标准库的库文件,不需要写路径。因为编译器会到系统指定的安装目录里面,去寻找这些文件。

  • #include "foo.h" // 加载用户提供的文件

    形式二,文件名写在双引号里面,表示该文件由用户提供,具体的路径取决于编译器的设置,可能是当前目录,也可能是项目的工作目录。如果所要包含的文件在其他位置,就需要指定路径,下面是一个例子。

    #include "/usr/local/lib/foo.h"
    

GCC 编译器的-I参数,也可以用来指定include命令中用户文件的加载路径。

$ gcc -I include/ -o hello hello.c

常常用于模块化开发,将功能分割到不同的文件中,以便于维护和管理。避免在头文件中直接包含其他源文件,以免导致编译时间增加和重复定义的问题。当一个文件被包含多次时,编译器可能会产生重复定义的错误,可以通过预处理器指令 #ifndef#define#endif 来避免这种情况

// 假设有一个头文件 example.h
// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H

		void printHello(); // 声明一个函数

#endif // EXAMPLE_H// 在另一个源文件中使用 example.h
// main.c
#include <stdio.h> // 包含标准输入输出库的头文件
#include "example.h" // 包含自定义的头文件int main() {
    printf("Hello, world!\n");
    printHello(); // 使用 example.h 中声明的函数
    return 0;
}

// 在另一个源文件中实现 example.h 中声明的函数
// example.c
#include <stdio.h>
#include "example.h"
void printHello() {
    printf("Hello from printHello function!\n");
}

#define

#define指令用于创建宏定义,可以将一段代码或值定义为一个标识符,以后在程序中可以直接使用该标识符来代替这段代码或值。这样可以用于简化代码、提高可读性、增加代码的灵活性。(宏可以用于替换整型、浮点型、字符型等任何类型的表达式)

  • 宏的命名规则:宏的名称不允许有空格,而且必须遵守 C 语言的变量命名规则(只能使用字母、数字、下划线,且首字符不能是数字)
    同名的宏可以重复定义,只要定义是相同的,就没有问题。如果定义不同,就会报错。

  • 宏定义的值可以是任意的表达式,但是建议将复杂的表达式封装在括号中,以确保优先级和结合性正确。宏定义是在预处理阶段进行替换,因此不涉及类型检查和作用域的问题。宏定义是简单的文本替换,可能导致副作用和不可预见的行为。因此在定义宏时要小心,避免与程序中已有的标识符冲突。

    #include <stdio.h> // 简单的宏定义,用于定义常量
    #define PI 3.14159
    #define MAX(a, b) ((a) > (b) ? (a) : (b))
    
    int main() {
        double radius = 5.0;
        double area = PI * radius * radius; // 使用宏定义 PI
        printf("Area of circle: %f\n", area);
    
        int x = 10, y = 20;
        int max_value = MAX(x, y); // 使用带参数的宏定义 MAX
        printf("Max value: %d\n", max_value);
    
        return 0;
    }
    
  • 如果宏出现在字符串里面(即出现在双引号中),或者是其他标识符的一部分,就会失效,并不会发生替换。

    #define FOO 100
    
    char *s = "FOO...";
    printf("FOO\n");    // FOO
    printf("%s\n", s);  // FOO...
    
  1. 不带参数的宏

    不带参数的宏是 C 语言中一种简单的文本替换机制。它允许你在代码中定义一个标识符来代表一个特定的值或一段代码,当编译器遇到该标识符时,会将其替换为预先定义的值或代码片段。

    #define PI 3.14
    

    在上面的例子中,PI 是一个宏,它被定义为 3.14159。在代码中使用 PI 时,编译器会将其替换为 3.14159

  2. 带参数的宏

    带参数的宏允许你定义一个带有参数的代码模板,在宏被调用时,实际参数会替换模板中的参数,并展开为相应的代码。

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

    在上面的例子中,MAX 是一个宏,它接受两个参数 xy,并返回其中较大的一个。在代码中使用 MAX 时,编译器会将其展开为相应的代码。

  3. 不定参数的宏

    不定参数的宏允许你定义一个可以接受任意数量参数的宏。在宏定义中,... 表示不定数量的参数。你可以使用 __VA_ARGS__ 来引用传递给宏的参数列表。

    #define PRINT(...) printf(__VA_ARGS__)
    
    

    在上面的例子中,PRINT 是一个不定参数的宏,它接受任意数量的参数,并将它们传递给 printf 函数。

  4. #运算符 ##运算符

    • # 运算符:宏可以用于替换整型、浮点型、字符型等任何类型的表达式,而**#** 运算符将参数转换为字符串。例如:

      #define STR(x) #x
      printf("%s\n", STR(hello)); // 输出 "hello"
      
    • ## 运算符:在宏定义中,## 运算符用于连接两个标记(tokens),使它们成为一个单独的标记。

      #define CONCAT(x, y) x##y
      int n = CONCAT(10, 20); // 1020
      

#undef

#undef指令用来取消已经使用#define定义的宏,使得该宏在之后的代码中不再起作用。

  • 使用 #undef 取消宏定义后,该宏在之后的代码中将不再起作用,应确保取消宏定义不会影响到后续代码的正确性,因此要谨慎使用 #undef
  • 同名的宏如果两次定义不一样,会报错,而#undef后跟不存在的宏,并不会报错。
#include <stdio.h>
#define DEBUG_MODE // 定义一个调试模式的宏
int main() {
    #ifdef DEBUG_MODE
        printf("Debug mode is enabled\n");
    #endif
    #undef DEBUG_MODE // 现在取消 DEBUG_MODE 宏定义

    #ifdef DEBUG_MODE
        printf("This line won't be printed because DEBUG_MODE is undefined\n");
    #endif
    return 0;
}

GCC 的-U选项可以在命令行取消宏的定义,相当于#undef

$ gcc -U MAXSIZE hello.c

上面示例中的-U参数,取消了宏LIMIT,相当于源文件里面的#undef LIMIT

#if #elif #else #endif

#if#endif 用于标记条件编译的起始和结束位置,条件(可以是表达式)为真时,起始位置和结束位置之间的内码就会编译,否则不会编译。

如果#if后紧跟着的是一个宏,那么效果就与#ifdef等同。

  • 条件编译可以通过定义预处理器宏来控制条件编译的行为,从而实现灵活的代码组织和构建。也可以根据不同的条件在编译时决定是否包含某段代码,从而实现针对不同平台、不同环境或不同配置的代码控制。

  • #if#endif 之间可以包含其他预处理指令,如 #define#include#ifdef#ifndef

    #include <stdio.h>
    #define DEBUG 1 // 定义一个调试模式的宏,值为 1 表示开启调试模式
    int main() {
    
    #if DEBUG
            printf("Debug mode is enabled\n");
            // 这里可以放置调试相关的代码
    #else
    		    printf("Debug mode is disabled\n");
            // 这里放置非调试模式下的代码
    #endif
    		    return 0;
    }
    

GCC 的-D参数可以在编译时指定宏的值,因此可以很方便地打开调试开关。

$ gcc -DDEBUG=1 foo.c

上面示例中,-D参数指定宏DEBUG1,相当于在代码中指定#define DEBUG 1

#ifdef #endif

#ifdef#endif 用于标记条件编译的起始和结束位置。#ifdef 后面跟随一个标识符(通常是一个宏),如果该标识符已经被定义,则编译 #ifdef#endif 之间的代码;否则,编译器会忽略这段代码。

实际上#ifdef就是#if defined

  • defined

    defined 是一个预处理运算符(不是函数和宏),用于检查指定的宏是否已经被定义过(并不能检查宏的值)。它可以用于 #if#elif 指令中,用于执行条件编译。

    #define DEBUG_MODE  // 定义一个宏
    
    #if defined(DEBUG_MODE)
        // 如果 DEBUG_MODE 宏已经被定义,则执行此处代码
        printf("Debug mode is enabled.\n");
    #endif
    
  • 使用 #ifdef 时应注意所检查的标识符是否已经在合适的位置被定义,避免出现编译错误或者意外的代码被包含。

    #include <stdio.h>
    #define def1
    
    int main(){
    #ifdef def1
        printf("def1在条件编译语句之上定义, 可以输出");
    #endif
    
    #ifdef def2
        printf("def1在条件编译语句之下定义, 不可以输出")
    #endif
    
    #define def2
        return 0;
    }
    
  • 在编写库或跨平台代码时,常常会使用 #ifdef 来检查特定平台或编译器下的宏定义,以实现代码的兼容性。

  • 这段文本讨论的是在编写库文件时,有时会出现重复加载的情况。为了避免这种情况,可以在库文件中使用 #define 定义一个空的宏。通过检查这个宏是否已经被定义,可以判断库文件是否已经被加载。

#ifndef #endif

#ifndef#endif 用于标记条件编译的起始和结束位置。#ifndef 后面跟随一个标识符(通常是一个宏),如果该标识符尚未被定义,则编译 #ifndef#endif 之间的代码;否则,编译器会忽略这段代码。

这和上面讲述的#ifdef #endif是完全相反的情况

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值