C/C++ 全部预处理指令大全(完整+实用,单片机开发高频版)
预处理指令是C/C++编译器的「编译前处理命令」,所有指令都以#开头,在代码编译阶段之前执行,不参与程序运行、不占用单片机的ROM/RAM资源,是嵌入式(51/STM32/单片机)开发中最常用、最核心的语法之一。
✅ 核心特点:所有预处理指令行尾不需要加分号;,这是新手最容易犯的错误!
✅ 适用场景:单片机开发中,预处理指令的使用频率远超PC端编程,几乎所有工程都会用到,必须吃透。
一、先明确:预处理指令的执行时机
程序的编译执行分3步,预处理指令在第一步完成,执行后编译器才会开始真正的编译、链接:预处理阶段(执行所有#指令)→编译阶段(把C代码转汇编/机器码)→链接阶段(生成可执行文件)
二、最全预处理指令分类讲解(按「使用频率优先级」排序,重点标★,嵌入式必掌握)
所有预处理指令分为6大类,我把单片机开发高频用的标红加粗,新手必学的标★,完全贴合你的单片机开发场景,学完直接能用!
✅ 第一类:【宏定义相关】★★★★★ 最高频(嵌入式每天都用)
核心作用:定义宏、取消宏,是单片机里定义引脚、常量、简化代码的核心指令,没有之一。
1.#define 宏名 [内容]定义宏(无参宏)
最基础、最常用的指令,用来定义符号常量、引脚别名、功能标识。
// 单片机经典用法1:定义引脚(51单片机)#defineLED_PINP1_0// 经典用法2:定义常量(避免魔法数字,改值只需改一处)#defineDELAY_TIME800#defineBAUD_RATE9600// 经典用法3:定义功能开关(配合你学的#ifdef使用)#defineDEBUG1#defineUSE_UART12.#define 宏名(参数1,参数2) 表达式带参数宏(宏函数)
相当于「简化版的函数」,编译前直接替换代码,无函数调用开销,单片机中用来写简单的运算/操作,极致节省资源。
// 例子1:取两个数的最大值#defineMAX(a,b)(a)>(b)?(a):(b)// 例子2:数码管消影的空指令,简化代码#defineNOP()_nop_()✔️ 注意:带参宏的参数必须加括号,避免运算优先级错误!
3.#undef 宏名取消宏定义
作用:删除已经被#define定义的宏,取消后,该宏就视为「未定义」状态,常和#ifdef配合使用。
#defineTEST1#ifdefTESTLED_PIN=1;// 这段会编译#endif#undefTEST// 取消TEST的宏定义#ifdefTESTLED_PIN=0;// 这段不会编译,因为TEST已被取消#endif✅ 第二类:【条件编译相关】★★★★★ 最高频(你问的#ifdef就在这)
核心作用:选择性编译代码—— 满足条件就编译某段代码,不满足就直接剔除,被剔除的代码不会生成机器码,完全不占存储空间。
这是单片机开发的「灵魂指令」,解决硬件适配、功能裁剪、调试开关、跨平台移植所有核心问题,你之前学的#ifdef只是其中一个,完整全家桶如下,全部要掌握:
1.#ifdef 宏名+#endif「判断宏是否被定义」
✅ 你已掌握:如果宏名被#define定义过(不管值是1/0/任意内容),就编译中间的代码。
当代码需要根据不同的操作系统或环境编译不同部分时,可以用 #if 配合 #elif 来实现多路分支
#ifdefined(_WIN32)// Windows 平台的代码#include<windows.h>#elifdefined(__linux__)// Linux 平台的代码#include<unistd.h>#else// 其他未知平台的代码#error"不支持的操作系统"#endif2.#ifndef 宏名+#endif「判断宏是否未被定义」 ⭐高频
▷ 含义:和#ifdef完全相反→if not defined
▷ 核心用途:头文件保护(头文件卫士),单片机项目中100%必用,解决「头文件重复包含导致的变量/函数重定义报错」!
// 写在所有头文件(.h)的最开头和结尾,固定模板,背下来!#ifndef__LED_H__// 如果 __LED_H__ 未被定义#define__LED_H__// 就定义这个宏// 这里写头文件的内容:变量声明、函数声明、宏定义sbit LED=P1^0;voidLED_Init(void);#endif// 结束条件编译3.#else条件分支
配合#ifdef/#ifndef/#if使用,实现「二选一」编译,之前讲#ifdef时已经用过。
4.#elif多条件分支 ⭐高频
▷ 含义:else if的预处理版本,实现「多选一」编译,搭配#if使用效果最好。
▷ 单片机常用场景:根据不同的硬件版本/功能等级,编译不同代码。
#defineDEVICE_VERSION3// 设备版本:1=低配版,2=标准版,3=高配版#ifDEVICE_VERSION==1#defineLED_NUM1// 低配版:1个LED#elifDEVICE_VERSION==2#defineLED_NUM4// 标准版:4个LED#else#defineLED_NUM8// 高配版:8个LED#endif5.#if 表达式⭐⭐ 重中之重
▷ 含义:判断「表达式的值是否为真(非0)」,满足则编译代码。
▷ 区别你之前学的#ifdef:
#ifdef 宏名→ 只判断「宏有没有被定义」,不管宏的值是多少;#if 宏名→ 只判断「宏的值是不是非0」,宏必须有值;
✅ 这是新手最容易混淆的点,用一个经典例子彻底分清(嵌入式调试开关必用):
#defineDEBUG0// 定义宏DEBUG,值为0#ifdefDEBUG// ✔️ 结果:真 → 编译(因为DEBUG被定义了)printf("调试信息1\r\n");#endif#ifDEBUG// ❌ 结果:假 → 不编译(因为DEBUG的值是0)printf("调试信息2\r\n");#endif6.#elif defined(宏名)组合用法 ⭐高频
#elif不能直接跟宏名,需要配合defined()函数,实现「多宏判断」,比如:
#ifdefSTM32F103#include"stm32f1xx.h"#elifdefined(STM32F407)#include"stm32f4xx.h"#elifdefined(AT89C51)#include<reg51.h>#else#error"未定义目标单片机型号!"#endif7.#error 提示文本强制报错指令 ⭐⭐ 超实用
▷ 含义:预处理阶段,如果执行到这条指令,编译器会直接报错并终止编译,同时显示你写的「提示文本」。
▷ 单片机用途:做「强制校验」,比如忘记定义硬件宏、参数配置错误时,主动报错提醒,避免程序烧录后出问题。
8.#warning 提示文本警告指令
▷ 含义:和#error类似,但只弹出警告,不会终止编译,适合做「友好提醒」,比如:
#ifndefBAUD_RATE#warning"未定义波特率,默认使用9600"#defineBAUD_RATE9600#endif✅ 第三类:【文件包含指令】★★★★★ 最高频 没有之一
#include <文件名>或#include "文件名"
▷ 核心作用:把「指定文件的全部内容」,原封不动复制粘贴到当前代码的这个位置,单片机中用来包含「头文件(.h)」。
▷ 两种写法的区别(嵌入式必知,面试也常考):
#include <xxx.h>→ 包含系统头文件(编译器自带的库文件),比如#include <reg51.h>、#include <stdio.h>;编译器会去「系统库路径」查找文件。#include "xxx.h"→ 包含自定义头文件(自己写的.h文件),比如#include "led.h"、#include "uart.h";编译器会先去「当前项目路径」查找,找不到再去系统库路径查找。
✅ 单片机开发建议:所有自定义头文件用双引号,系统头文件用尖括号,规范且不易出错。
✅ 第四类:【行号/文件名控制】低频但实用
这类指令平时用的少,但在调试排错、生成日志时特别好用,都是编译器内置的指令,无需手动定义。
1.#line 行号 [文件名]
强制修改当前代码的「行号」和「文件名」,预处理阶段生效,比如:
#line100"main_test.c"printf("当前行号:%d\n",__LINE__);// 输出:1002. 编译器内置预定义宏⭐ 调试神器
这些宏是编译器自带的,无需#define定义,直接用!单片机调试时,用来打印「出错位置、编译时间」,定位BUG效率翻倍,重点记4个最常用的:
__LINE__:当前代码的行号(数字)__FILE__:当前代码的文件名(字符串)__DATE__:程序编译的日期(字符串,格式:月 日 年)__TIME__:程序编译的时间(字符串,格式:时:分:秒)
✅ 单片机调试经典用法:
#ifdefDEBUG#defineLOG(fmt,...)printf("[%s][%d] "fmt,__FILE__,__LINE__,##__VA_ARGS__)#else#defineLOG(fmt,...)#endif// 使用:LOG("数码管扫描初始化完成\r\n");LOG("当前温度:%d℃\r\n",temp);✅ 第五类:【编译器控制指令】★★★ 嵌入式高频(#pragma)
#pragma 指令内容编译控制指令
▷ 核心特点:编译器专属指令,不同编译器(KEIL5、MDK、GCC)的#pragma用法不同,但功能强大,是嵌入式开发中「优化代码、配置编译规则」的核心指令。
▷ 单片机开发必学3个常用用法(KEIL/MDK编译器):
#pragma once→ 替代「头文件卫士(#ifndef)」,作用是让当前头文件只被包含一次,写法更简洁!
✅ 二选一即可:要么写#ifndef的头文件卫士,要么写#pragma once,效果一样。#pragma optimize=0→ 关闭编译器的代码优化,单片机调试时必用!因为编译器优化后,会删减一些「看似无用」的代码,导致调试时变量值看不到、断点失效。#pragma anon_unions→ 允许使用匿名联合体,单片机中定义寄存器结构体时常用,节省代码量。
✅ 第六类:【冷门但有用的指令】低频了解即可
这类指令在单片机开发中几乎用不到,知道就行,不用深究:
#pragma pack(n):设置结构体的内存对齐方式,比如#pragma pack(1)表示按1字节对齐,嵌入式中定义硬件寄存器时偶尔用。#undefine:和#undef功能一致,部分编译器的别名写法。#import:和#include类似,主要在C++/Objective-C中使用,C语言(单片机)不用。
三、【重中之重】嵌入式开发 高频预处理指令「组合用法模板」(直接套用)
所有指令中,90%的开发场景都是几个指令组合使用,我整理了单片机开发中最常用的5个模板,都是现成的,背下来直接写到你的项目里,效率翻倍!
模板1:头文件保护(二选一,必写!)
// 方式1:传统写法(兼容性最好,所有编译器都支持)#ifndef__UART_H__#define__UART_H__// 头文件内容#endif// 方式2:简洁写法(KEIL/MDK推荐,代码少)#pragmaonce// 头文件内容模板2:调试开关(万能日志宏,必写!)
#defineDEBUG1// 1=开调试,0=关调试#ifdefDEBUG#include<stdio.h>#defineLOG(fmt,...)printf("[%s][%d] "fmt,__FILE__,__LINE__,##__VA_ARGS__)#else#defineLOG(fmt,...)#endif模板3:跨硬件平台适配(51/STM32通用)
#defineTARGET_STM321// #define TARGET_51 1#ifdefTARGET_STM32#include"stm32f1xx_hal.h"#defineLED_GPIO_PORTGPIOA#defineLED_GPIO_PINGPIO_PIN_0#elifdefined(TARGET_51)#include<reg51.h>#defineLED_PINP1_0#else#error"请定义目标单片机型号!"#endif模板4:功能模块裁剪(低配/高配版)
#defineHIGH_CONFIG1// 1=高配版,0=低配版voidDevice_Init(void){// 基础功能,必编译LED_Init();UART_Init();// 高配版专属功能,条件编译#ifdefHIGH_CONFIGI2C_Init();SPI_Init();WIFI_Init();#endif}模板5:宏定义引脚+常量(简化代码)
// 数码管引脚定义#defineSEG_PORTP0#defineBIT_PORTP2// 延时常量定义#defineSCAN_DELAY800#defineKEY_DELAY20// 功能开关#defineUSE_SMG1#defineUSE_KEY1#defineUSE_BEEP0四、【新手避坑】最容易搞错的「预处理指令误区」(必看!)
✅ 误区1:预处理指令行尾加了分号→ 编译报错!比如#define LED P1^0;❌,正确是#define LED P1^0✔️。
✅ 误区2:把typedef当成预处理指令 →typedef是C语言的关键字,不是预处理指令,它是编译阶段执行的,用来定义类型别名。
✅ 误区3:#ifdef判断宏的值 →#ifdef只判断「宏是否存在」,不管值是多少,判断值用#if。
✅ 误区4:头文件重复包含 → 一定要加#ifndef或#pragma once,否则会报「重定义」错误。
✅ 误区5:带参宏不加括号 → 比如#define MUL(a,b) a*b,调用MUL(1+2,3)会变成1+2*3,正确写法是#define MUL(a,b) (a)*(b)✔️。
五、【速查表】所有预处理指令 汇总(按重要性排序,方便查阅)
| 指令分类 | 指令名称 | 核心作用 | 优先级 |
|---|---|---|---|
| 宏定义 | #define | 定义宏/宏函数 | ★★★★★ |
| 宏定义 | #undef | 取消宏定义 | ★★★★☆ |
| 文件包含 | #include | 包含头文件 | ★★★★★ |
| 条件编译 | #ifdef | 判断宏已定义 | ★★★★★ |
| 条件编译 | #ifndef | 判断宏未定义 | ★★★★★ |
| 条件编译 | #endif | 结束条件编译 | ★★★★★ |
| 条件编译 | #else | 条件分支 | ★★★★☆ |
| 条件编译 | #elif | 多条件分支 | ★★★★☆ |
| 条件编译 | #if | 判断表达式真 | ★★★★★ |
| 编译控制 | #pragma | 编译器配置 | ★★★☆☆ |
| 报错提醒 | #error | 强制编译报错 | ★★★☆☆ |
| 警告提醒 | #warning | 编译警告 | ★★☆☆☆ |
| 行号控制 | #line | 修改行号文件名 | ★☆☆☆☆ |
总结
C/C++的预处理指令就这么多,对单片机开发来说,你只需要吃透「宏定义、条件编译、文件包含」这三类核心指令,就能解决99%的开发场景,剩下的指令用到时再查即可。
以下是 C 语言(基于 C99 / C11 / C17 标准)中所有预处理指令的完整整理,附带简要说明和示例。同时标注了 C23 标准新增的少数指令,供参考。
一、宏定义与取消
指令 说明 示例
#define 定义宏(可带参数) #define PI 3.14 #define MAX(a,b) ((a)>(b)?(a):(b))
#undef 取消宏定义 #undef PI
二、条件编译
指令 说明 示例
#if 条件为真时编译后续代码 #if defined(DEBUG)
#ifdef 如果宏已定义则编译 #ifdef _WIN32
#ifndef 如果宏未定义则编译 #ifndef HEADER_H
#elif 否则如果(与 #if 搭配) #elifSTDC_VERSION>= 201112L
#else 否则 #else
#endif 结束条件编译块 #endif
defined() 运算符,在 #if / #elif 中判断宏是否已定义 #if defined(DEBUG) && DEBUG==1
C23 新增:#elifdef 和 #elifndef
例如:
#ifdef A
#elifdef B // 相当于 #elif defined(B)
#endif
三、文件包含
指令 说明 示例
#include <…> 在系统头文件路径中搜索并包含 #include <stdio.h>
#include “…” 先在用户路径(当前目录)搜索,再搜索系统路径 #include “myheader.h”
四、错误与警告
指令 说明 示例
#error 产生编译错误,并输出指定消息 #error “Unsupported platform”
#warning 产生编译警告(非标准,但多数编译器支持) #warning “This feature is deprecated”
五、行号控制
指令 说明 示例
#line 修改编译器认为的当前行号和文件名 #line 100 “newfile.c”
六、编译器特定控制
指令 说明 示例
#pragma 提供编译器特定功能或行为 #pragma once(防止头文件重复包含) #pragma pack(1)(字节对齐)
七、空指令
指令 说明 示例
(单独一个 #) 空指令,无任何效果,有时用于提高可读性 # (单独一行)
注意:单独一个 # 后不加任何预处理词,是合法的预处理行(C99 起)。
八、宏中的特殊运算符(属于预处理器但并非独立指令)
运算符 说明 示例
(字符串化) 将宏参数转换为字符串字面量 #define STR(x) #x → STR(hello) 展开为 “hello”
(粘贴/连接) 连接两个记号,形成新的预处理记号 #define CONCAT(a,b) a##b → CONCAT(var,1) 展开为 var1
九、C23 新增的预处理指令(简要)
C23 标准引入了几个新的预处理功能:
指令 说明
#elifdef 等同 #elif defined(…)
#elifndef 等同 #elif !defined(…)
#embed 在编译时将二进制文件内容直接嵌入(以字节序列形式)
常用组合示例(头文件保护)
#ifndefMYHEADER_H#defineMYHEADER_H// 内容...#endif// MYHEADER_H