核心概念:
#
开头。1. #include
:包含文件
.h
),这些头文件通常包含函数声明、宏定义、类型定义等。#include
:用于包含标准库头文件或系统级的头文件。预处理器会在预定义的系统路径下查找文件 (例如
,
,
)。#include "filename.h"
:用于包含用户自定义的头文件。预处理器通常会先在当前源文件所在的目录查找,如果找不到,再去系统路径或其他指定的包含路径下查找。2. #define
:定义宏
#define PI 3.14159
#define BUFFER_SIZE 1024
#define DEVICE_ID "SENSOR_01"
float circumference = 2 * PI * radius;
char buffer[BUFFER_SIZE];
printf("Device: %s\n", DEVICE_ID);
预处理器会把代码中出现的 PI
替换为 3.14159
,BUFFER_SIZE
替换为 1024
等等。
#define
处)。const
变量安全(const
有类型,作用域更明确)。#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define IS_ODD(n) ((n) % 2 != 0)
int largest = MAX(x, y);
if (IS_ODD(value)) { ... }
预处理器会把 MAX(x, y)
替换为 ((x) > (y) ? (x) : (y))
。
(a)
,(b)
。((a) > (b) ? (a) : (b))
。这可以防止因为运算符优先级问题导致的意外结果。
#define SQUARE(x) x*x
。如果调用 SQUARE(a + b)
会被替换成 a + b*a + b
,而不是 (a + b)*(a + b)
。正确写法:#define SQUARE(x) ((x)*(x))
。++
, --
),可能会被执行多次。例如 MAX(i++, j++)
,i++
或 j++
可能会执行两次。#undef
: 用于取消一个已经定义的宏。#undef MACRO_NAME
。3. 条件编译指令 (#if
, #ifdef
, #ifndef
, #else
, #elif
, #endif
)
#ifdef MACRO_NAME
:如果 MACRO_NAME
这个宏已经被定义了 (通过 #define
),则编译后续代码块,直到遇到 #else
, #elif
或 #endif
。#ifndef MACRO_NAME
:如果 MACRO_NAME
这个宏没有被定义,则编译后续代码块。#if constant_expression
:如果后面的常量表达式(在预处理阶段求值)为真(非零),则编译后续代码块。表达式可以包含整数常量、#define
定义的常量、算术运算、位运算、逻辑运算以及 defined(MACRO_NAME)
操作符(defined
用来检查宏是否已定义)。#else
:如果前面的 #if
, #ifdef
, #ifndef
, #elif
条件不满足,则编译 #else
后面的代码块。#elif constant_expression
:“else if” 的缩写。如果前面的条件不满足,则检查这个新的常量表达式。#endif
:必须用来结束一个 #if
, #ifdef
或 #ifndef
代码块。#ifndef
最经典的用法: C // my_driver.h
#ifndef MY_DRIVER_H // 如果 MY_DRIVER_H 还没有被定义过...
#define MY_DRIVER_H // ...就定义它
// --- 头文件的实际内容 ---
struct device_config {
int baud_rate;
char parity;
};
void init_device(struct device_config *config);
// --- 头文件内容结束 ---
#endif // MY_DRIVER_H // 结束 #ifndef 块
当第一次包含此文件时,MY_DRIVER_H
未定义,于是 #define
和文件内容被处理。如果再次包含,MY_DRIVER_H
已经定义,#ifndef
条件为假,整个文件内容被跳过。#ifdef STM32F4_SERIES
// STM32F4 特有的初始化代码
uart_init_stm32f4();
#elif defined(ESP32_PLATFORM)
// ESP32 特有的初始化代码
uart_init_esp32();
#else
#error "Unsupported hardware platform!" // 如果都不支持,编译时报错
#endif
#ifdef DEBUG_MODE
printf("Debug: Variable x = %d\n", x);
#endif
// 在编译时可以通过编译器选项定义 DEBUG_MODE (例如 gcc -DDEBUG_MODE main.c)
#define USE_ADVANCED_FEATURE 1 // 或者 0
#if USE_ADVANCED_FEATURE
// 包含高级功能的代码
setup_advanced_feature();
#endif
#if 0 // 条件为 0 (假),所以内部代码不会被编译
... 大段暂时不用的代码 ...
#endif
4. 其他常用指令 (了解)
#error message
:让预处理器输出一个错误信息 message
并停止编译。常用于条件编译中,当配置无效或不支持时报错。#warning message
:让预处理器输出一个警告信息 message
,但不停止编译。用于提示潜在问题。#pragma directive
:提供了一种向编译器传递特定于编译器的指令或信息的方法。例如:
#pragma once
:许多现代编译器支持这个指令作为头文件保护的一种替代方案,效果与 #ifndef/#define/#endif
类似,但更简洁(不过不是所有编译器都支持)。#pragma
可能用于控制优化级别、内存对齐、中断处理等,具体取决于编译器。小测验:
想象一下,你正在为一个嵌入式项目编写代码,需要根据目标硬件是 BOARD_V1
还是 BOARD_V2
来设置不同的 LED 引脚号。你会如何使用预处理指令来实现这个目标?假设 V1 板的 LED 在引脚 5,V2 板的 LED 在引脚 12。
思考一下需要用到哪些指令?