C语言-宏的基础、进阶、高级、内置宏的用法

C语言宏(Macro)完全指南:从基础到高级全解析

宏是C语言预处理阶段的核心特性,通过`#define`指令实现文本替换,能简化代码、提升复用性。你在学习C语言宏时,先吃透基础用法,再逐步掌握进阶和高级技巧,能更清晰地理解其核心逻辑。本文按**基础、进阶、高级**分层讲解,其中基础部分仅聚焦入门核心用法,不混入任何进阶内容,帮你打好扎实的基础。

一、宏的基础用法

在学习具体用法前,先明确宏的核心基础特性:

  • 宏在**预处理阶段**完成文本替换,编译器编译前就已完成替换,不会对宏做语法检查;
  • 宏不占用运行时内存(区别于变量/函数),替换后的代码直接嵌入到源文件中;
  • 基础宏分为两类:**对象宏**(无参数)和**函数宏**(带参数)。

1.1 基础用法1:对象宏(无参数宏)------ 定义常量/文本片段

对象宏是最常用的基础宏,核心作用是用"宏名"替代固定的常量、字符串或重复代码片段,提升代码可读性和可维护性。
语法

复制代码
#define 宏名 替换文本

示例1:定义数值/字符串常量

复制代码
#include <stdio.h>

// 定义数值常量(替换魔法数字)
#define PI 3.1415926
// 定义字符串常量
#define APP_NAME "C语言宏入门"
// 定义整型常量
#define MAX_NUM 100

int main() {
    // 预处理阶段,PI会被直接替换为3.1415926
    double circle_area = PI * 5 * 5;
    printf("应用名称:%s\n", APP_NAME);
    printf("圆周率:%f\n", PI);
    printf("半径为5的圆面积:%f\n", circle_area);
    printf("最大数值限制:%d\n", MAX_NUM);
    return 0;
}

输出结果

复制代码
应用名称:C语言宏入门
圆周率:3.141593
半径为5的圆面积:78.539815
最大数值限制:100

示例2:替换重复代码片段

复制代码
#include <stdio.h>

// 替换重复的打印语句(基础文本替换)
#define PRINT_HELLO printf("Hello, Macro!\n")

int main() {
    // 多次调用宏,避免重复写printf语句
    PRINT_HELLO;
    PRINT_HELLO;
    return 0;
}

输出结果

复制代码
Hello, Macro!
Hello, Macro!

基础注意事项

  • 宏定义末尾**不要加分号**(否则替换后会多出分号,导致语法错误);
  • 常量宏建议全大写(编程规范),方便区分普通变量;
  • 宏名一旦定义,预处理阶段所有出现该宏名的位置都会被替换。

1.2 基础用法2:函数宏(带参数宏)------ 简单参数替换

函数宏允许传入参数,实现"类似函数"的文本替换,核心是简化重复的表达式计算。
语法

复制代码
#define 宏名(参数列表) 表达式

示例1:简单计算表达式

复制代码
#include <stdio.h>

// 带参数宏:计算两个数的和
#define ADD(a, b) a + b
// 带参数宏:计算圆的面积(基础写法)
#define CIRCLE_AREA(r) PI * r * r
// 复用之前定义的PI常量宏
#define PI 3.1415926

int main() {
    // 预处理阶段替换为:10 + 20
    int sum = ADD(10, 20);
    printf("10+20=%d\n", sum);
    
    // 预处理阶段替换为:3.1415926 * 3 * 3
    double area = CIRCLE_AREA(3);
    printf("半径为3的圆面积:%f\n", area);
    return 0;
}

输出结果

复制代码
10+20=30
半径为3的圆面积:28.274333

示例2:简化重复的逻辑判断

复制代码
#include <stdio.h>

// 带参数宏:判断数值是否为正数
#define IS_POSITIVE(x) x > 0

int main() {
    int num1 = 15, num2 = -5;
    // 替换为:15 > 0 → 真(1)
    printf("15是否为正数:%d\n", IS_POSITIVE(num1));
    // 替换为:-5 > 0 → 假(0)
    printf("-5是否为正数:%d\n", IS_POSITIVE(num2));
    return 0;
}

输出结果

复制代码
15是否为正数:1
-5是否为正数:0

1.3 基础宏的核心特性总结(纯基础)

  1. **无类型检查**:宏的参数和替换文本不区分类型,比如`ADD(10, 3.5)`也能执行(替换为`10+3.5`);

  2. **无调用开销**:宏是文本替换,不像函数调用需要栈帧创建/销毁,执行效率高;

  3. **作用域全局**:宏定义后,从定义位置到文件结束都有效,可通过`#undef 宏名`提前终止。
    示例:终止宏的作用域

    #include <stdio.h>

    #define TEST 100
    printf("TEST=%d\n", TEST); // 正常替换,输出100

    #undef TEST // 终止TEST宏的作用域
    // printf("TEST=%d\n", TEST); // 报错:TEST未定义

二、宏的进阶用法

进阶用法聚焦解决基础宏的缺陷,并扩展宏的能力边界,以下内容均为基础用法的补充和优化。

2.1 带参数宏的陷阱修复:运算符优先级问题

基础带参数宏(如`CIRCLE_AREA(r)`)存在隐藏问题:参数为表达式时,运算符优先级会导致计算错误。
问题示例(基础写法的缺陷)

复制代码
#include <stdio.h>
#define PI 3.1415926
#define CIRCLE_AREA(r) PI * r * r

int main() {
    // 期望:PI*(2+3)*(2+3)=78.539815
    // 实际替换:PI*2+3*2+3=15.283185
    printf("面积:%f\n", CIRCLE_AREA(2+3));
    return 0;
}

解决方案:给参数和整体加括号

复制代码
#include <stdio.h>
#define PI 3.1415926
// 优化:每个参数加括号,整体表达式加括号
#define CIRCLE_AREA(r) (PI * (r) * (r))

int main() {
    // 替换为:(3.1415926 * (2+3) * (2+3))=78.539815
    printf("面积:%f\n", CIRCLE_AREA(2+3));
    return 0;
}

2.2 字符串化运算符(#)

`#`是宏的特殊运算符,作用是将宏参数**转换为字符串常量**,常用于调试打印。
语法与示例

复制代码
#include <stdio.h>

// 字符串化:将参数转为字符串
#define STR(x) #x
// 组合使用:打印变量名和值
#define PRINT_VAR(x) printf("变量%s = %d\n", #x, x)

int main() {
    printf(STR(hello world));  // 替换为:printf("hello world");
    printf("\n");
    
    int num = 100;
    PRINT_VAR(num);  // 替换为:printf("变量%s = %d\n", "num", num);
    return 0;
}

输出结果

复制代码
hello world
变量num = 100

2.3 令牌连接运算符(##)

##(令牌粘贴 / 连接运算符)的核心作用是将两个标识符(变量名、函数名、宏名等)拼接成一个新的合法标识符,是批量定义代码的核心技巧,以下是完整用法:
核心语法

复制代码
#define 宏名(参数1, 参数2) 参数1##参数2

示例 1:基础拼接(变量 / 函数名)

复制代码
#include <stdio.h>

// 基础拼接:两个参数拼接成新标识符
#define CONCAT(a, b) a##b

int main() {
    // 1. 拼接变量名
    int student1 = 90, student2 = 85;
    printf("学生1成绩:%d\n", CONCAT(student, 1));  // 替换为student1
    printf("学生2成绩:%d\n", CONCAT(student, 2));  // 替换为student2
    
    // 2. 拼接函数名
    void print_info1() { printf("信息1:C语言基础\n"); }
    void print_info2() { printf("信息2:宏的用法\n"); }
    CONCAT(print_info, 2)();  // 替换为print_info2()
    return 0;
}

输出结果

复制代码
学生1成绩:90
学生2成绩:85
信息2:宏的用法

示例 2:批量定义变量 / 函数(实用场景)

复制代码
#include <stdio.h>

// 批量定义变量:prefix_数字
#define DEFINE_VAR(prefix, num, value) int prefix##num = value;

// 批量定义函数:func_数字()
#define DEFINE_FUNC(num) void func##num() { printf("调用func%d\n", num); }

// 批量定义变量
DEFINE_VAR(data, 1, 100);
DEFINE_VAR(data, 2, 200);

// 批量定义函数
DEFINE_FUNC(1);
DEFINE_FUNC(2);

int main() {
    printf("data1 = %d, data2 = %d\n", data1, data2);
    func1();
    func2();
    return 0;
}

输出结果

复制代码
data1 = 100, data2 = 200
调用func1
调用func2

示例 3:结合其他宏拼接(进阶场景)

复制代码
#include <stdio.h>

#define PREFIX "msg_"
#define NUM 3
// 拼接后:msg_3
#define FULL_NAME CONCAT(PREFIX, NUM)  // 嵌套宏

// 注意:字符串拼接无需##,编译器会自动合并相邻字符串
#define MSG(num) "msg_"#num

int main() {
    printf("%s\n", MSG(3));  // 输出msg_3(#字符串化+字符串自动合并)
    return 0;
}

输出结果

复制代码
msg_3

注意事项

  • ##只能拼接标识符(字母、数字、下划线组成),不能拼接字符串 / 数值常量;
  • 拼接后的标识符必须是合法的(如不能以数字开头);
  • ##右侧为空时,部分编译器需用##__VA_ARGS__兼容(变参宏场景)。

2.4 连接运算符()

``用于将两个标识符**拼接成一个新的标识符**(令牌粘贴),常用于批量定义变量/函数。
语法与示例

复制代码
#include <stdio.h>

// 拼接标识符
#define CONCAT(a, b) ab

int main() {
    // 1. 拼接变量名
    int num1 = 10, num2 = 20;
    printf("num1 = %d\n", CONCAT(num, 1));  // 替换为:num1
    
    // 2. 拼接函数名
    void func1() { printf("调用func1\n"); }
    void func2() { printf("调用func2\n"); }
    CONCAT(func, 2)();  // 替换为:func2();
    return 0;
}

输出结果

复制代码
num1 = 10
调用func2

2.5 变参宏(VA_ARGS

`VA_ARGS`实现**可变参数的宏**,模拟`printf`的可变参数特性,适用于自定义日志打印。
语法与示例

复制代码
#include <stdio.h>

// 自定义日志宏:带级别+可变参数
#define LOG(level, ...) printf("[%s] " __VA_ARGS__, level)

int main() {
    LOG("INFO", "程序启动,版本:%.1f\n", 1.0);
    LOG("ERROR", "数值错误:%d\n", 500);
    return 0;
}

输出结果

复制代码
[INFO] 程序启动,版本:1.0
[ERROR] 数值错误:500

2.6 条件编译宏

用于控制代码的编译范围,常见于跨平台开发、调试模式开关。
核心指令与示例

复制代码
#include <stdio.h>

// 定义调试模式宏
#define DEBUG 1

int main() {
    int a = 10;
    
    // 调试模式下打印变量
    #ifdef DEBUG
        printf("调试模式:a = %d\n", a);
    #endif
    
    // 跨平台编译示例
    #ifdef _WIN32
        printf("Windows系统\n");
    #elif __linux__
        printf("Linux系统\n");
    #else
        printf("其他系统\n");
    #endif
    
    return 0;
}

2.7 **#if条件宏**

条件编译通过预处理指令控制代码是否被编译,核心包括#if、#elif、#else、#endif,其中#if是数值条件判断 ,区别于#ifdef(仅判断宏是否定义),以下是完整用法:
#if核心语法

复制代码
#if 数值表达式
    // 表达式为非0时编译此部分代码
#elif 数值表达式
    // 否则如果表达式为非0
#else
    // 以上都不满足时编译
#endif

关键区别

|------------|------------------|----------------------|
| 指令 | 作用 | 示例 |
| #ifdef 宏名 | 判断宏是否 已定义 (不关心值) | #ifdef DEBUG |
| #ifndef 宏名 | 判断宏是否 未定义 | #ifndef WINDOWS |
| #if 表达式 | 判断数值表达式是否为 非 0 | #if DEBUG_LEVEL == 2 |

示例 1:#if基础数值判断

复制代码
#include <stdio.h>

// 定义调试级别
#define DEBUG_LEVEL 2

int main() {
    // #if判断数值表达式
    #if DEBUG_LEVEL == 1
        printf("基础调试:仅打印关键信息\n");
    #elif DEBUG_LEVEL == 2
        printf("详细调试:打印所有信息\n");
    #else
        printf("无调试:不打印任何信息\n");
    #endif
    
    return 0;
}

输出结果

复制代码
详细调试:打印所有信息

示例 2:#if结合defined()(判断宏是否定义)

复制代码
#include <stdio.h>

#define OS_WINDOWS 0
#define OS_LINUX 1

int main() {
    // 判断宏是否定义,且值为1
    #if defined(OS_LINUX) && OS_LINUX == 1
        printf("当前系统:Linux\n");
    #elif defined(OS_WINDOWS) && OS_WINDOWS == 1
        printf("当前系统:Windows\n");
    #else
        printf("当前系统:未知\n");
    #endif
    
    // 禁用某段代码
    #if 0
        printf("这段代码不会被编译\n");
    #endif
    
    return 0;
}

输出结果

复制代码
当前系统:Linux

示例 3:#if实现代码版本控制

复制代码
#include <stdio.h>

#define VERSION 3

int main() {
    #if VERSION >= 3
        printf("V3版本:支持新功能\n");
    #elif VERSION == 2
        printf("V2版本:基础功能\n");
    #else
        printf("V1版本:测试版\n");
    #endif
    return 0;
}

输出结果

复制代码
V3版本:支持新功能

注意事项

  • #if后的表达式只能是常量表达式(预处理阶段可计算),不能使用变量;
  • defined(宏名)可嵌套在#if表达式中,比#ifdef更灵活;
  • #if 0可快速注释大段代码(比/* */更安全,避免嵌套注释冲突)。

三、宏的高级用法

高级用法结合多个宏特性,实现函数难以完成的功能,需谨慎使用(可读性会降低)。

3.1 宏函数:模拟多返回值

C语言函数只能返回一个值,通过宏可模拟"多返回值"(本质是文本替换)。
示例代码

复制代码
#include <stdio.h>

// 宏函数:计算和与积,通过参数返回
#define CALC_SUM_PROD(a, b, sum, prod) \
    do { \
        sum = (a) + (b); \
        prod = (a) * (b); \
    } while(0)  // 保证宏作为独立语句执行

int main() {
    int a = 5, b = 6;
    int sum, prod;
    CALC_SUM_PROD(a, b, sum, prod);
    printf("和:%d,积:%d\n", sum, prod);
    return 0;
}

输出结果

复制代码
和:11,积:30

3.2 嵌套宏:宏中调用宏

宏支持嵌套定义和调用,组合多个宏实现复杂逻辑。
示例代码

复制代码
#include <stdio.h>

// 基础宏:求绝对值
#define ABS(x) ((x) < 0 ? -(x) : (x))
// 嵌套宏:求两个数的绝对值之和
#define ABS_SUM(a, b) ABS(a) + ABS(b)

int main() {
    printf("|-3| + |5| = %d\n", ABS_SUM(-3, 5));
    printf("|2| + |-7| = %d\n", ABS_SUM(2, -7));
    return 0;
}

输出结果

复制代码
|-3| + |5| = 8
|2| + |-7| = 9

3.3 宏实现类型泛型(C11)

结合`_Generic`关键字,实现"泛型函数"(类似C++模板)。
示例代码

复制代码
#include <stdio.h>

// 泛型宏:打印任意类型变量
#define PRINT(x) _Generic((x), \
    int: printf("int: %d\n", x), \
    float: printf("float: %.2f\n", x), \
    char*: printf("string: %s\n", x), \
    default: printf("未知类型\n") \
)

int main() {
    PRINT(10);          // int类型
    PRINT(3.14f);       // float类型
    PRINT("hello");     // 字符串类型
    return 0;
}

输出结果

复制代码
int: 10
float: 3.14
string: hello

3.4 编译期计算:宏实现常量表达式

宏可在预处理阶段完成计算,避免运行时开销。
示例代码

复制代码
#include <stdio.h>

// 编译期计算数组大小
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))

int main() {
    int arr[] = {1,2,3,4,5};
    // 预处理阶段计算:sizeof(arr)=20 / sizeof(arr[0])=4 → 5
    printf("数组长度:%d\n", ARRAY_SIZE(arr));
    return 0;
}

输出结果

复制代码
数组长度:5

四、内置宏

内置宏是指C语言内置的一部分宏,有许多,常用的有五个_FILE__、DATETIMELINEfunc,因为常常在代码中看到,所以这里加入讲解一下

4.1 基础用法 1:定位类宏 ------ 精准标记代码位置

定位类宏是调试场景的核心工具,核心作用是用宏名替代 "文件名、行号、函数名",帮你快速定位代码执行位置,解决调试时 "不知道哪行代码出问题" 的痛点。
语法(直接使用,无需定义)

|----------|--------------|-------|
| 宏名 | 替换文本 | 类型 |
| FILE | 当前代码所在的源文件路径 | 字符串常量 |
| LINE | 当前代码所在的行号 | 整数常量 |
| func | 当前代码所在的函数名 | 字符串常量 |

示例 1:基础定位 ------ 打印代码位置

复制代码
#include <stdio.h>

// 直接使用预定义宏,无需手动定义
int main() {
    // 预处理阶段,宏会被替换为实际的文件名、行号、函数名
    printf("当前文件:%s\n", __FILE__);
    printf("当前行号:%d\n", __LINE__);
    printf("当前函数:%s\n", __func__);
    return 0;
}

输出结果(示例)

复制代码
当前文件:test.c
当前行号:8
当前函数:main

示例 2:函数内定位 ------ 追踪代码执行路径

复制代码
#include <stdio.h>

void calculate(int a, int b) {
    // 函数内打印定位信息,明确哪行代码执行
    printf("[%s:%d %s] 开始计算:a=%d, b=%d\n", __FILE__, __LINE__, __func__, a, b);
    int sum = a + b;
    printf("[%s:%d %s] 计算结果:sum=%d\n", __FILE__, __LINE__, __func__, sum);
}

int main() {
    printf("[%s:%d %s] 程序启动\n", __FILE__, __LINE__, __func__);
    calculate(10, 20);
    printf("[%s:%d %s] 程序结束\n", __FILE__, __LINE__, __func__);
    return 0;
}

输出结果(示例)

复制代码
[test.c:12 main] 程序启动
[test.c:6 calculate] 开始计算:a=10, b=20
[test.c:8 calculate] 计算结果:sum=30
[test.c:14 main] 程序结束

基础注意事项

  • func__是 C99 标准引入,老编译器可替换为__FUNCTION(GCC 扩展,功能一致);
  • __LINE__是整数,打印时需用%d格式符,FILE/__func__是字符串,需用%s格式符;
  • 定位类宏的替换结果是 "代码编写 / 编译时的位置",不是运行时动态变化的位置。

4.2 基础用法 2:编译信息类宏 ------ 记录程序编译时间

编译信息类宏的核心作用是用宏名替代 "编译日期、编译时间",帮你记录程序的 "诞生时间",解决 "分不清代码编译版本" 的痛点。
语法(直接使用,无需定义)

|----------|----------------------|-------|
| 宏名 | 替换文本 | 类型 |
| DATE | 编译日期(格式:MMM DD YYYY) | 字符串常量 |
| TIME | 编译时间(格式:HH:MM:SS) | 字符串常量 |

示例 1:基础用法 ------ 打印编译信息

复制代码
#include <stdio.h>

// 直接使用编译信息类宏
int main() {
    printf("程序编译文件:%s\n", __FILE__);
    printf("程序编译日期:%s\n", __DATE__);
    printf("程序编译时间:%s\n", __TIME__);
    return 0;
}

输出结果(示例)

复制代码
程序编译文件:version.c
程序编译日期:Dec 20 2025
程序编译时间:16:20:35

示例 2:结合常量 ------ 记录版本编译信息

复制代码
#include <stdio.h>

// 定义版本常量(基础对象宏)
#define APP_VERSION "v1.0.0"
#define APP_AUTHOR "C语言学习者"

int main() {
    printf("=== 程序版本信息 ===\n");
    printf("版本号:%s\n", APP_VERSION);
    printf("作者:%s\n", APP_AUTHOR);
    printf("编译文件:%s\n", __FILE__);
    printf("编译时间:%s %s\n", __DATE__, __TIME__);
    return 0;
}

输出结果(示例)

复制代码
=== 程序版本信息 ===
版本号:v1.0.0
作者:C语言学习者
编译文件:version_info.c
编译时间:Dec 20 2025 16:25:10

基础注意事项

  • DATE/__TIME__记录的是编译时刻 的时间,不是程序运行时间(运行时间需用time()函数);
  • 编译信息类宏都是字符串常量,打印时需用%s格式符;
  • 重新编译代码后,DATE/__TIME__的替换结果会更新为新的编译时间。
相关推荐
moxiaoran57532 小时前
Go语言的递归函数
开发语言·后端·golang
水坚石青2 小时前
Java+Swing+Mysql实现物业管理系统
java·开发语言·数据库·mysql·swing
尼古拉斯·纯情暖男·天真·阿玮2 小时前
[JavaEE初阶] Thread类的基本用法
java·开发语言
特立独行的猫a2 小时前
C++开发中的构建工具:现代CMake实战速成
开发语言·c++·cmake·入门教程
朝花不迟暮2 小时前
Go基础-闭包
android·开发语言·golang
Wpa.wk2 小时前
自动化测试(java) - PO模式了解
java·开发语言·python·测试工具·自动化·po模式
进阶的猪2 小时前
stm32 GPIO输出-使用固件库点亮LED灯 Q&A
c语言·笔记·stm32·单片机
徐先生 @_@|||2 小时前
Java/Maven 对比 Python/PyPI
开发语言·python
编程猪猪侠2 小时前
手写js轮播图效果参考
开发语言·javascript·ecmascript