一、预处理基础认知

预处理是C语言编译的第一步,由预处理器完成,核心作用是“文本替换”——在代码编译前,对指定的预处理指令(以#开头)进行处理,生成新的源文件后再进入编译阶段。

常见预处理指令:#define(宏定义)、#include(头文件包含)、#undef(移除宏)、#if/#elif/#else/#endif(条件编译)等,以下按“基础→进阶”的顺序,结合全新示例讲解。

二、#define 指令(最基础、最常用)

#define 的核心功能是“文本替换”,无需编译,预处理阶段直接替换,分为「定义常量」和「定义带参数的宏」两种用法,重点提醒:定义末尾一定不要加分号(加分号会被当作替换内容,导致语法错误)。

1. 定义常量(无参数)

作用:将代码中重复使用的固定值,用一个标识符(常量名)代替,便于修改和阅读(常量名建议全大写,符合编码规范)。

格式:#define 常量名 常量值

全新示例1(定义整数常量):

#define STUDENT_NUM 50 // 定义常量,代表学生人数为50
#include<stdio.h>
int main()
{
    // 使用常量,预处理后会替换为50
    printf("班级总人数:%d\n", STUDENT_NUM);
    return 0;
}

运行结果:班级总人数:50(修改人数时,只需改#define后的50,无需修改所有使用处)。

全新示例2(长内容分行定义,用续行符\):

如果常量/宏内容过长,一行写不下,可在每行末尾(最后一行除外)加反斜杠\,预处理会自动合并为一行。

#define STUDENT_INFO "姓名:张三\n性别:男\n年龄:18\n班级:高一(1)班" \
"学号:2026001" // 分行定义长字符串
#include<stdio.h>
int main()
{
    printf("%s", STUDENT_INFO); // 替换后为完整字符串
    return 0;
}

2. 定义带参数的宏(类似简易函数)

作用:比常量更灵活,可接收参数,实现简单的计算或操作,预处理时直接替换参数和内容(不检查参数类型、不计算结果,仅做文本替换)。

格式:#define 宏名(参数) 替换内容(注意:宏名和左括号之间不能有空格)

(1)基础示例(无运算,简单替换)

#define PRINT(num) printf("数字:%d\n", num) // 带参数的宏
#include<stdio.h>
int main()
{
    PRINT(10); // 替换后:printf("数字:%d\n", 10);
    PRINT(20); // 替换后:printf("数字:%d\n", 20);
    return 0;
}

运行结果:数字:10 数字:20(简化重复的printf语句)。

(2)易错点:必须加小括号(避免运算优先级错误)

宏仅做文本替换,不会自动调整运算优先级,涉及运算时,必须给“每个参数”和“整个替换内容”加小括号,否则会出错。

全新反例(未加括号,出错):

#define MUL(a,b) a*b // 未加括号
#include<stdio.h>
int main()
{
    int result = MUL(2+3, 4); // 替换后:2+3*4 = 14(预期:5*4=20)
    printf("结果:%d", result);
    return 0;
}

全新正例(加括号,正确):

#define MUL(a,b) ((a)*(b)) // 内外都加括号
#include<stdio.h>
int main()
{
    int result = MUL(2+3, 4); // 替换后:((2+3)*(4)) = 20(符合预期)
    printf("结果:%d", result);
    return 0;
}

(3)易错点:避免带副作用的参数

副作用:参数使用后,自身值会改变(如i++、++i),若参数在宏中使用多次,会导致结果异常。

全新示例(带副作用参数,结果异常):

#define SUM(a,b) ((a)+(b))
#include<stdio.h>
int main()
{
    int i = 3;
    // i++是带副作用参数,宏中使用2次,i会自增2次
    int result = SUM(i++, i++); // 替换后:((3)+(4))=7,i最终变为5
    printf("结果:%d,i的值:%d", result, i);
    return 0;
}

运行结果:结果:7,i的值:5(预期i自增1次,实际自增2次,因参数被使用2次)。

结论:尽量不用i++、++i这类带副作用的参数作为宏参数。

(4)宏与函数的简单对比(易懂版)

  • 宏:预处理替换,无调用开销,速度快;不检查参数类型,不严谨,无法调试。

  • 函数:编译后执行,有调用开销,速度稍慢;检查参数类型,严谨,可调试。

示例(宏 vs 函数,实现相同功能):

#include<stdio.h>
// 宏实现求和
#define SUM_MACRO(a,b) ((a)+(b))
// 函数实现求和
int sum_func(int a, int b)
{
    return a + b;
}
int main()
{
    printf("宏求和:%d\n", SUM_MACRO(2.5, 3.5)); // 宏支持浮点型(不检查类型)
    // printf("函数求和:%d\n", sum_func(2.5, 3.5)); // 函数报错(参数类型不匹配)
    printf("函数求和:%d\n", sum_func(2, 3)); // 函数仅支持整型
    return 0;
}

三、预定义符号(C语言自带,直接使用)

C语言内置了5个预定义符号,无需#define,直接使用,主要用于调试(定位错误、记录编译信息),全部为大写形式。

  • FILE:当前编译的源文件名(字符串),比如“test.c”。

  • LINE:当前代码的行号(整数),调试时快速定位报错位置。

  • DATE:代码编译的日期(字符串),格式如“Mar 11 2026”。

  • TIME:代码编译的时间(字符串),格式如“15:30:20”。

  • STDC:判断编译器是否遵循ANSI C标准,遵循则为1,否则未定义。

全新示例(调试常用,打印错误位置):

#include<stdio.h>
// 结合宏和预定义符号,实现调试打印
#define DEBUG_PRINT printf("文件:%s  行号:%d  编译时间:%s\n", __FILE__, __LINE__, __TIME__)
int main()
{
    printf("开始执行程序\n");
    DEBUG_PRINT; // 打印调试信息
    int a = 10;
    if (a != 20)
    {
        DEBUG_PRINT; // 报错时,可定位到当前行
        printf("a的值不等于20\n");
    }
    return 0;
}

四、#undef 指令(移除已定义的宏)

作用:取消(移除)之前用#define定义的宏,使该宏在#undef之后不再生效,后续使用会报错。

全新示例:

#include<stdio.h>
#define NUM 100 // 定义宏NUM
int main()
{
    printf("NUM:%d\n", NUM); // 正常使用,替换为100
    #undef NUM // 移除宏NUM的定义
    // printf("NUM:%d\n", NUM); // 报错:NUM未定义(取消注释会编译失败)
    return 0;
}

注意:#undef只移除宏定义,不影响之前已经替换好的代码(替换在预处理阶段完成)。

五、#与## 操作符(宏的进阶用法,仅在宏中生效)

两个特殊操作符,仅用于宏定义中,辅助实现字符串和符号的操作,预处理阶段生效。

1. # 操作符:字符串化(将参数转为字符串)

作用:把宏参数的“原始文本”转为字符串,不计算参数的值,只保留文本。

全新示例:

#include<stdio.h>
// #name 把参数name转为字符串
#define PRINT_NAME(name) printf("姓名:"#name"\n")
int main()
{
    PRINT_NAME(李四); // 替换后:printf("姓名:""李四""\n"),输出“姓名:李四”
    PRINT_NAME(王五); // 输出“姓名:王五”
    return 0;
}

2. ## 操作符:符号拼接(将两个符号合并为一个)

作用:把##两边的标识符(参数、变量名)合并为一个新的标识符,实现动态命名。

全新示例(动态生成变量名):

#include<stdio.h>
// 拼接前缀和数字,生成变量名(如num1、num2)
#define CREATE_NUM(prefix, num) prefix##num
int main()
{
    int CREATE_NUM(num, 1) = 10; // 替换后:int num1 = 10;
    int CREATE_NUM(num, 2) = 20; // 替换后:int num2 = 20;
    printf("num1:%d,num2:%d", num1, num2);
    return 0;
}

运行结果:num1:10,num2:20(通过宏动态生成变量名,简化代码)。

六、命令行定义(编译时定义宏)

无需在代码中写#define,编译时通过命令行直接定义宏,适用于灵活调整参数(如数组大小),无需修改代码。

常用格式(GCC编译器):gcc -D 宏名=宏值 源文件名.c -o 可执行文件名

全新示例(命令行定义数组大小):

#include<stdio.h>
int main()
{
    // ARRAY_LEN未在代码中定义,需通过命令行指定
    int arr[ARRAY_LEN];
    // 给数组赋值并打印
    for (int i = 0; i < ARRAY_LEN; i++)
    {
        arr[i] = i + 1;
        printf("%d ", arr[i]);
    }
    return 0;
}

编译命令:gcc -D ARRAY_LEN=6 test.c -o test(定义ARRAY_LEN=6,数组大小为6)

运行结果:1 2 3 4 5 6(修改数组大小,只需修改命令行中的数值,无需改代码)。

七、条件编译(根据条件决定是否编译代码)

作用:预处理阶段,根据指定条件(宏是否定义、表达式真假),决定某段代码是否被编译,常用于跨平台、调试开关。

1. 基础用法:#if … #endif

格式:#if 条件(非0为真,0为假) … #endif,条件为真则编译中间代码,否则不编译。

全新示例(调试开关,控制代码是否执行):

#include<stdio.h>
#define DEBUG 1 // 1:开启调试,0:关闭调试
int main()
{
    #if DEBUG
        printf("调试模式开启,打印详细信息\n"); // 条件为真,编译执行
        printf("变量初始化完成\n");
    #endif
    printf("程序正常执行\n"); // 无论条件真假,都编译执行
    return 0;
}

说明:把DEBUG改为0,调试相关的printf会被屏蔽(不编译),相当于“注释”代码。

2. 多条件分支:#if … #elif … #else … #endif

类似if-else if-else,多条件判断,仅编译满足条件的代码块。

全新示例(根据宏的值,执行不同代码):

#include<stdio.h>
#define GRADE 85 // 定义成绩宏
int main()
{
    #if GRADE >= 90
        printf("优秀\n");
    #elif GRADE >= 80
        printf("良好\n");
    #elif GRADE >= 60
        printf("及格\n");
    #else
        printf("不及格\n");
    #endif
    return 0;
}

运行结果:良好(GRADE=85,满足第二个条件)。

3. 检查宏是否定义:#ifdef / #ifndef

最常用的条件编译,判断某个宏是否被定义,无需写复杂表达式。

  • #ifdef 宏名:宏被定义,则编译代码(等价于#if defined(宏名))。

  • #ifndef 宏名:宏未被定义,则编译代码(等价于#if !defined(宏名))。

全新示例(判断宏是否定义,避免重复定义):

#include<stdio.h>
// 模拟头文件中的判断,避免重复定义
#ifndef STUDENT_H
#define STUDENT_H
    #define STUDENT_NAME "张三"
#endif

int main()
{
    #ifdef STUDENT_NAME
        printf("学生姓名:%s\n", STUDENT_NAME); // 宏已定义,编译执行
    #endif

    #ifndef TEACHER_NAME
        printf("教师姓名未定义\n"); // 宏未定义,编译执行
    #endif
    return 0;
}

4. 嵌套条件编译(复杂场景)

条件编译可嵌套,适用于跨平台开发(不同系统执行不同代码)。

#include<stdio.h>
#define OS_WINDOWS 1 // 定义当前系统为Windows(可切换为OS_LINUX=1)
// #define OS_LINUX 1

int main()
{
    #if defined(OS_WINDOWS)
        printf("当前系统:Windows,执行Windows相关操作\n");
        #ifdef DEBUG
            printf("Windows调试模式开启\n");
        #endif
    #elif defined(OS_LINUX)
        printf("当前系统:Linux,执行Linux相关操作\n");
    #else
        printf("未知系统\n");
    #endif
    return 0;
}

八、#include 头文件包含

作用:将其他文件(头文件.h、源文件.c)的内容,完整插入到当前文件的#include位置,预处理阶段完成替换,用于复用代码(如函数声明、宏定义)。

两种包含格式,查找路径不同,用法有明确区分,新手必记:

1. 双引号格式:#include “文件名”

查找路径:先在当前源文件所在目录查找 → 找不到再去系统标准路径查找。

适用场景:包含自己编写的头文件(如myfunc.h)。

全新示例:

#include "myfunc.h" // 自己编写的头文件,先查当前目录
#include<stdio.h>
int main()
{
    my_print(); // 调用myfunc.h中声明的函数
    return 0;
}

2. 尖括号格式:#include <文件名>

查找路径:直接在系统标准头文件路径查找(不查当前目录),效率更高。

适用场景:包含系统自带的头文件(如stdio.h、stdlib.h)。

全新示例:

#include <stdio.h> // 系统头文件,直接查系统路径
#include <stdlib.h>
int main()
{
    printf("系统头文件使用示例\n");
    int* p = (int*)malloc(4); // 调用stdlib.h中的malloc函数
    free(p);
    return 0;
}

新手注意事项

  • 系统头文件用尖括号<>,自定义头文件用双引号"",避免查找效率低。

  • 避免头文件重复包含:同一头文件被多次包含,会导致变量、函数重复定义(可用品3中的#ifdef判断解决)。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐