C —— 宏
- 基本宏定义
- 符号常量
- 函数式宏
- 宏的特殊用法
- 多行宏
- 字符串化操作符
- 连接操作符
- 可变参数宏
- 基本语法
- 省略可变参数(C99 和 GNU C 扩展)
- 计算可变参数的个数(GNU C 扩展)**
- `_Generic` + 可变参数宏(C11)
- 可变参数宏的常见用途
- 注意事项
- 总结
- 常用预定义宏
- 1. `__LINE__` - 当前行号
- 2. `__FILE__` - 当前文件名
- 3. `__DATE__` - 编译日期(格式 "MMM DD YYYY")
- 4. `__TIME__` - 编译时间(格式 "HH:MM:SS")
- 5. `__STDC__` - 是否遵循 ANSI C 标准
- 6. `__func__` - 当前函数名(C99)
- 综合示例:调试日志宏
- 关键点总结
- 条件编译
- C语言条件编译详解
- 基本条件编译指令
- 1. `#ifdef` / `#ifndef` / `#endif`
- 2. `#if` / `#elif` / `#else` / `#endif`
- 常见应用场景
- 1. 跨平台开发
- 2. 调试代码
- 3. 功能开关
- 预定义宏与条件编译
- 实用技巧
- 1. 宏定义检查
- 2. 防止头文件重复包含
- 3. 编译器特定指令
- 注意事项
- 高级用法
- 1. 宏参数的条件编译
- 2. 静态断言(C11之前)
我们在学习C语言时,很早就接触过了宏的概念,今天我们就对宏这一用法进行详细的梳理:
基本宏定义
符号常量
这种用法我们最熟悉,就是简单定义,然后放到代码段中去用:
#define PI 3.14159
#define MAX_SIZE 100
#define AUTHOR "John Doe"
这里我们就简单介绍一下,不做过多展开。
函数式宏
除了我们自己可以写函数之外,宏也可以定义函数:
这里给所有参数都带上括号,是为了保证运算顺序的正确性。如果不带括号,运算结果可能就会大相径庭:
#define FUCNTION(a,b)(a * b)int main()
{printf("%d\n", FUCNTION(10 + 10,20 + 20));
}
但是如果我加上括号:
#define FUCNTION(a,b)((a) * (b))int main()
{printf("%d\n", FUCNTION(10 + 10,20 + 20));
}
因为,宏不对本身做任何处理,它只会傻瓜式的展开,我们来看看两个函数展开之后长啥样子:
所以在编写宏时,如果运算顺序很重要,记得打上括号哦~。
宏的特殊用法
多行宏
使用反斜杠\可以定义多行宏:
# define SWAP(a,b,type){ \type temp = a; \a = b; \b = temp; \
}int main()
{int x = 10;int y = 20;printf("%d %d\n", x, y);SWAP(x, y, int)printf("%d %d\n", x, y);
}
字符串化操作符
将宏参数转换为字符串:
#define STR(x) #xint main()
{printf("%s\n", STR(123456));
}
连接操作符
将两个标记连接成一个:
#define CONCAT(a,b) a##bint main()
{int xy = 10;printf("%d\n", CONCAT(x, y));
}
可变参数宏
接下来就是一些比较高级的用法了,在 C 语言中,可变参数宏(Variadic Macros)允许宏接受 可变数量的参数,类似于 printf() 这样的可变参数函数。它使用 … 和 VA_ARGS 来实现,是 C99 标准引入的特性。
基本语法
#define MACRO_NAME(fixed_args, ...) replacement_text __VA_ARGS__
... 表示可变参数部分。__VA_ARGS__ 在宏展开时会被替换为传入的可变参数。
#define LOG(fmt,...) printf(fmt,__VA_ARGS__)int main()
{LOG("Hello, %s! Your score is %d.\n", "Alice", 95);
}
省略可变参数(C99 和 GNU C 扩展)
如果可变参数可能为空,C99 要求至少有一个参数,但 GNU C 允许完全省略:
// C99 标准写法(必须至少有一个可变参数)
#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)// GNU C 扩展(允许完全省略可变参数)
#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)int main() {LOG("Hello, world!\n"); // ✅ GNU C 允许,C99 会报错return 0;
}
说明:
## 是 标记粘贴运算符,如果 __VA_ARGS__ 为空,它会删除前面的逗号,避免语法错误。
计算可变参数的个数(GNU C 扩展)**
#define COUNT_ARGS(...) sizeof((int[]){__VA_ARGS__}) / sizeof(int)int main() {printf("Number of args: %zu\n", COUNT_ARGS(1, 2, 3, 4)); // 输出 4return 0;
}
注意:
- 这种方法仅适用于 相同类型的参数(如
int
)。 - 不是标准 C,仅适用于 GCC/Clang。
_Generic
+ 可变参数宏(C11)
结合 _Generic
实现类型安全的可变参数宏:
#include <stdio.h>#define PRINT_TYPE(x) _Generic((x), \int: printf("%d\n", x), \float: printf("%f\n", x), \char*: printf("%s\n", x) \
)#define PRINT_ALL(...) do { \int arr[] = {__VA_ARGS__}; \for (size_t i = 0; i < sizeof(arr)/sizeof(arr[0]); i++) { \PRINT_TYPE(arr[i]); \} \
} while(0)int main() {PRINT_ALL(42, 3.14, "Hello"); // 需要更复杂的实现return 0;
}
说明:
- 这个例子只是示意,实际实现需要更复杂的宏技巧。
可变参数宏的常见用途
- 日志系统
#define LOG_INFO(fmt, ...) fprintf(stderr, "[INFO] " fmt, ##__VA_ARGS__)
- 调试输出
#define DBG_PRINT(...) printf("[DEBUG] %s:%d: ", __FILE__, __LINE__); \printf(__VA_ARGS__)
- 泛型容器操作
#define FOREACH(item, ...) \for (int keep = 1, count = 0; keep && count != sizeof((int[]){__VA_ARGS__})/sizeof(int); \keep = !keep, count++) \for (item = ((int[]){__VA_ARGS__})[count]; keep; keep = !keep)
注意事项
- 标准 C (C99) 要求至少一个可变参数,但 GNU C 允许零个。
- 避免参数副作用(如
LOG("%d", i++)
可能导致多次求值)。 - 调试困难,因为宏在预处理阶段展开,错误信息可能难以理解。
- 可读性差,复杂的可变参数宏可能难以维护。
总结
特性 | 说明 |
---|---|
... | 定义可变参数 |
__VA_ARGS__ | 引用可变参数 |
##__VA_ARGS__ | 允许省略可变参数(GNU C) |
_Generic + 可变宏 | 类型安全可变参数(C11) |
do-while(0) | 包裹多语句宏 |
最佳实践:
- 优先使用 函数 替代复杂宏。
- 如果必须用宏,确保 可读性 和 安全性。
- 在跨平台代码中,避免依赖 GNU C 扩展(如
##__VA_ARGS__
)。
可变参数宏非常强大,但需要谨慎使用!
常用预定义宏
__LINE__ // 当前行号
__FILE__ // 当前文件名
__DATE__ // 编译日期
__TIME__ // 编译时间
__STDC__ // 如果编译器符合ANSI C标准则为1
__func__ // 当前函数名 (C99)
下面是对这些预定义宏的具体用法示例,每个宏都附带一个完整的代码示例和输出说明:
1. __LINE__
- 当前行号
#include <stdio.h>int main() {printf("This line is at: %d\n", __LINE__); // 输出当前行号printf("Now we're at line: %d\n", __LINE__); // 行号+1return 0;
}
输出示例:
This line is at: 4
Now we're at line: 5
2. __FILE__
- 当前文件名
#include <stdio.h>int main() {printf("This code is in file: %s\n", __FILE__);return 0;
}
输出示例:
This code is in file: example.c
3. __DATE__
- 编译日期(格式 “MMM DD YYYY”)
#include <stdio.h>int main() {printf("This program was compiled on: %s\n", __DATE__);return 0;
}
输出示例:
This program was compiled on: Jun 15 2023
4. __TIME__
- 编译时间(格式 “HH:MM:SS”)
#include <stdio.h>int main() {printf("Compilation time: %s\n", __TIME__);return 0;
}
输出示例:
Compilation time: 14:30:45
5. __STDC__
- 是否遵循 ANSI C 标准
#include <stdio.h>int main() {#if __STDC__printf("This compiler is ANSI C compliant\n");#elseprintf("This compiler is NOT ANSI C compliant\n");#endifreturn 0;
}
典型输出(现代编译器):
This compiler is ANSI C compliant
6. __func__
- 当前函数名(C99)
#include <stdio.h>void my_function() {printf("Current function: %s\n", __func__);
}int main() {printf("Entering %s\n", __func__);my_function();return 0;
}
输出:
Entering main
Current function: my_function
综合示例:调试日志宏
#include <stdio.h>#define LOG(msg) printf("[%s] %s (Line %d in %s): %s\n", \__TIME__, __func__, __LINE__, __FILE__, msg)int main() {LOG("Program started");if (1) {LOG("Inside conditional block");}return 0;
}
输出示例:
[14:30:45] main (Line 7 in example.c): Program started
[14:30:45] main (Line 9 in example.c): Inside conditional block
关键点总结
宏 | 作用 | 输出示例 | 标准支持 |
---|---|---|---|
__LINE__ | 当前行号 | 42 | C89 |
__FILE__ | 源文件名 | "example.c" | C89 |
__DATE__ | 编译日期 | "Jun 15 2023" | C89 |
__TIME__ | 编译时间 | "14:30:45" | C89 |
__STDC__ | ANSI C 合规 | 1 | C89 |
__func__ | 函数名 | "main" | C99 |
注意事项:
__func__
是 C99 引入的,不是字符串字面量(不能直接拼接)- 所有带下划线的宏由编译器预定义,不可修改
- 在调试和日志系统中特别有用
条件编译
C语言条件编译详解
条件编译是C语言预处理器提供的一项重要功能,它允许程序员在编译时根据不同的条件选择性地包含或排除代码段。条件编译在跨平台开发、调试代码、功能开关等场景中非常有用。
基本条件编译指令
1. #ifdef
/ #ifndef
/ #endif
#ifdef MACRO_NAME// 如果定义了MACRO_NAME,则编译这部分代码
#endif#ifndef MACRO_NAME// 如果没有定义MACRO_NAME,则编译这部分代码
#endif
示例:
#define DEBUG // 定义DEBUG宏#ifdef DEBUGprintf("Debug information\n");
#endif#ifndef RELEASEprintf("Not in release mode\n");
#endif
2. #if
/ #elif
/ #else
/ #endif
#if 表达式// 如果表达式为真,编译这部分代码
#elif 其他表达式// 如果前面的表达式为假且这个表达式为真,编译这部分代码
#else// 如果所有表达式都为假,编译这部分代码
#endif
示例:
#define VERSION 3#if VERSION == 1printf("Version 1\n");
#elif VERSION == 2printf("Version 2\n");
#elif VERSION == 3printf("Version 3\n");
#elseprintf("Unknown version\n");
#endif
常见应用场景
1. 跨平台开发
#ifdef _WIN32// Windows平台专用代码#include <windows.h>
#elif __linux__// Linux平台专用代码#include <unistd.h>
#elif __APPLE__// macOS平台专用代码#include <TargetConditionals.h>
#endif
2. 调试代码
#define DEBUG_MODE 1#if DEBUG_MODE#define DEBUG_PRINT(fmt, ...) printf("[DEBUG] " fmt, ##__VA_ARGS__)
#else#define DEBUG_PRINT(fmt, ...) // 定义为空,不产生任何代码
#endifint main() {DEBUG_PRINT("This is debug info: %d\n", 42);return 0;
}
3. 功能开关
#define FEATURE_A_ENABLED 1
#define FEATURE_B_ENABLED 0#if FEATURE_A_ENABLEDvoid feature_a() {printf("Feature A is enabled\n");}
#endif#if FEATURE_B_ENABLEDvoid feature_b() {printf("Feature B is enabled\n");}
#endifint main() {#if FEATURE_A_ENABLEDfeature_a();#endif#if FEATURE_B_ENABLEDfeature_b();#endifreturn 0;
}
预定义宏与条件编译
C语言提供了一些预定义宏,常用于条件编译:
#if __STDC_VERSION__ >= 201112L// C11或更高版本代码
#elif __STDC_VERSION__ >= 199901L// C99代码
#else// C89代码
#endif
实用技巧
1. 宏定义检查
#if defined(MACRO1) && !defined(MACRO2)// 如果定义了MACRO1且没有定义MACRO2
#endif
2. 防止头文件重复包含
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H// 头文件内容#endif // MYHEADER_H
3. 编译器特定指令
#ifdef __GNUC__// GCC编译器特有代码#define likely(x) __builtin_expect(!!(x), 1)#define unlikely(x) __builtin_expect(!!(x), 0)
#else// 其他编译器#define likely(x) (x)#define unlikely(x) (x)
#endif
注意事项
- 条件编译指令必须在函数体外使用
- 表达式必须是常量表达式
- 过度使用条件编译会使代码难以维护
- 确保所有条件分支都有正确的
#endif
匹配
高级用法
1. 宏参数的条件编译
#define CONFIG(opt) (defined(CONFIG_##opt) && CONFIG_##opt)#if CONFIG(DEBUG)printf("Debug mode enabled\n");
#endif
2. 静态断言(C11之前)
#define STATIC_ASSERT(expr, msg) \typedef char static_assertion_##msg[(expr) ? 1 : -1]STATIC_ASSERT(sizeof(int) == 4, int_size_must_be_4_bytes);
条件编译是C语言中非常强大的功能,合理使用可以大大提高代码的灵活性和可移植性。