C语言预处理和宏
预处理命令
预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。这些在编译之前对源文件进行简单加工的过程,就称为预处理。
预处理阶段的工作:把代码当成普通文本,根据设定的条件进行一些简单的文本替换,将替换以后的结果再交给编译器处理。
预处理命令
- 以
#
号开头 - 放在所有函数之外,而且一般都放在源文件的前面
指令 | 描述 |
---|---|
#define |
定义宏 |
#include |
包含一个源代码文件 |
#undef |
取消已定义的宏 |
#ifdef |
如果宏已经定义,则返回真 |
#ifndef |
如果宏没有定义,则返回真 |
#if |
如果给定条件为真,则编译下面代码 |
#else |
#if 的替代方案 |
#elif |
如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif |
结束一个 #if……#else 条件编译块 |
#error |
当遇到标准错误时,输出错误消息 |
#pragma |
使用标准化方法,向编译器发布特殊的命令到编译器中 |
预定义宏
ANSI C 定义了许多宏(预定义宏 | Microsoft Learn)。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏。
宏 | 描述 |
---|---|
__DATE__ |
当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。 |
__TIME__ |
当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。 |
__FILE__ |
当前文件名,一个字符串常量。 |
__LINE__ |
当前行号,一个十进制常量。 |
__FUNCTION__ |
当前所在的函数。 |
__STDC__ |
当编译器以 ANSI 标准编译时定义为 1,否则未定义。 |
这些宏常用于调试和输出日志。
#include <stdio.h>
#ifdef _DEBUG
#define DEBUGMSG(msg, date) \
printf(msg); \
printf("%d%d%d", date, __LINE__, __FILE__)
#else
#define DEBUGMSG(msg, date)
#endif
int main() {
printf("%s\n", __FILE__); // G:\Code\test.cpp
printf("%d\n", __LINE__); // 4
printf("%s\n", __DATE__); // Oct 13 2023
printf("%s\n", __TIME__); // 23:01:14
printf("%s\n", __FUNCTION__); // main
printf("%d\n", __STDC__); // 1
return 0;
}
条件编译
条件编译可以嵌套。
注意:未满足条件编译指令的代码,在预处理阶段将被编译器自动删除,不参与后面的代码编译过程。
(1)分支判断
#if 表达式
//TODO
#elif 表达式
//TODO
#else 表达式
//TODO
#endif
(2)判断是否有#define定义
//第一种的正面
#if defined(表达式)
//TODO
#endif
//第一种的反面
#if !defined(表达式)
//TODO
#endif
//第二种的正面
#ifdef 表达式
//TODO
#endif
//第二种的反面
#ifndef 表达式
//TODO
#endif
宏中#和##的用法
#
:把一个宏参数变成对应的字符串。
#define print(val, fmt) \
printf("The value of " #val " is " fmt "\n", val)
int main() {
int age = 18;
print(age, "%d"); //输出The value of age is 18
}
##
:把位于它两边的宏参数合成一个。 它允许宏定义从分离的文本片段创建标识符。
#define CAT(x, y) x##y
int main() {
int workhard = 100;
printf("%d\n", CAT(work, hard)); // 输出100
}
注意:凡宏定义里有用#
或##
的地方,宏参数不会再展开。
#define A (2)
#define STR(s) #s
#define CONS(a, b) int(a##e##b)
printf("max: %sn", STR(INT_MAX)); // 展开为printf("max: %s\n", #INT_MAX);
printf("%s\n", CONS(A,A)); // 展开为printf("%s\n", int(AeA)); //编译错误
/
:宏跨行延续。
一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下(导致有些行没有#
号),则使用宏延续运算符(\
)。
#define getsymdo \
if (-1 == getsym()) \
return -1
定义宏
#define
机制包括了一个规定,允许把参数替换到文本中,实现类似函数的功能【相比于函数不需要建立栈帧】,这种实现通常称为宏(macro)或定义宏(define macro)。
#define MALLOC(num,type) (type*)malloc(num*sizeof(type)) // 一个简写malloc的宏
使用#undef
来移除一个#define
定义的宏。
避免边界效应
由于宏展开的特性,需要避免边界效应,常用的解决方案:
(1)为参数添加()
包裹
#define MAX( x, y ) ( ((x) > (y)) ? (x) : (y) )
#define MIN( x, y ) ( ((x) < (y)) ? (x) : (y) )
(2)使用do {} while(0)
来包裹整个内容
#define DO(a,b) do { a+b; a++; } while(0)
(3)谨慎使用宏,小心副作用(side effect)
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int a = 10, b = 20;
int c = MAX(a++, b++);
/*实际上展开得到:
int c = ((a++)>(b++)?(a++):(b++));
显然是错误的,会产生副作用*/
(4)宏一般不写末尾的;
号,以此让用户强制书写;
避免宏出现递归
宏不能递归地展开:
#define FAC(x) (x) * FAC(x - 1) // error
可变参数宏
一、概念介绍:
1、...
:表示可变参数列表。
2、__VA_ARGS__
:表示是一个可变参数的宏。
3、args...
:表示可变参数列表,表示后续的args可能会有多个。
4、args
:表示是一个可变参数的宏。
二、基础应用:
#define LOG1(...) func1(__VA_ARGS__)
#define LOG2(args...) func1(args)
__VA_ARGS__
作用: 将左边宏中的...
的内容原样抄到右边__VA_ARGS__
所占用的位置。 以上两宏等价。
三、实现格式化输出:
#define LOG1(fmt, ...) printf(fmt, ##__VA_ARGS__)
#define LOG2(fmt, args...) printf(fmt, ##args)
##
的作用:当可变参数的个数为0时,##
起到把前面多余的","去掉的作用,否则会编译出错。
一个内核日志输出的示例:
#define MODULENAME "helloworld"
#define LOG_INFO(fmt, ...)\
printk (KERN INFO MODULENAME ":" fmt, ##__VA_ARGS__)
// 使用
LOG_INFO("This is a log message. Value of x is %d\n", x);
// 输出
[ 12.345678] INFO helloworld: This is a log message. Value of x is 123
宏的缺陷
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的。
- 宏由于类型无关,没有参数检查。
- 宏可能会带来运算符优先级的问题。