C语言预处理(通俗易懂)
一、预处理基础认知
预处理是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判断解决)。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)