C语言预处理和宏

C语言预处理和宏

C语言宏的定义和宏的使用方法(#define) (biancheng.net)

C/C++ 宏编程的艺术 - 知乎 (zhihu.com)

预处理命令

预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。这些在编译之前对源文件进行简单加工的过程,就称为预处理

预处理阶段的工作:把代码当成普通文本,根据设定的条件进行一些简单的文本替换,将替换以后的结果再交给编译器处理。

预处理命令

  • #号开头
  • 放在所有函数之外,而且一般都放在源文件的前面
指令 描述
#define 定义宏
#include 包含一个源代码文件
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的分支
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

使用#if defined(x) 则可以和前面的#ifdef x 作用相当,典型的应用是判断当前平台和编译器:

#ifdef _WIN32
/* Windows -------------------------------------------------- */
#elif __linux__
/* Linux ---------------------------------------------------- */
#else
#error unsupported platform
#endif

#if defined(__clang__)
/* Clang/LLVM. ---------------------------------------------- */
#elif defined(__GNUC__) || defined(__GNUG__)
/* GNU GCC/G++. --------------------------------------------- */
#elif defined(_MSC_VER)
/* Microsoft Visual Studio. --------------------------------- */
#else
#error unsupported compiler
#endif

预定义宏

ANSI C 定义了许多宏(预定义宏 | Microsoft Learn)。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
__DATE__ 当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。
__TIME__ 当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。
__FILE__ 当前文件名,一个字符串常量。
__LINE__ 当前行号,一个十进制常量。
__FUNCTION__ 当前所在的函数。__PRETTY_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

宏的作用域

宏可以在任何文件,任何位置开始定义,且不受命名空间影响。只要在预处理阶段#include后生成那个新的大大的源文件(即一个翻译单元)中,宏的使用总在定义之后就可以。

具体来说,宏的作用范围是从#define开始:

  • 到预处理#include后生成的新源文件的结尾
  • #undef该宏为止

宏与typedef的区别

typedef存在作用域和命名空间限制:

  • 如果放在所有函数之外,它的作用域就是从它定义开始直到文件尾(这个文件也是预处理后的文件);
  • 如果放在某个函数内,定义域就是从定义开始直到该函数结尾;

宏中操作符的用法

#@字符化操作符

把一个宏参数变成对应的字符字面量(用''括起来)

#define ToChar(x) #@x

#@x只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。

char a = ToChar(1);
     ==> char a='1';

做个越界试验

char a = ToChar(123);
     ==> char a='3';

但是如果你的参数超过四个字符,编译器就给给你报错了

!error C2015: too many characters in constant :P

#字符串化操作符

把一个宏参数变成对应的字符串字面量(用""括起来)

 char* str = ToString(123132);
 ==> char* str="123132";
#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
}

##连接操作符

把位于它两边的宏参数合成一个。 它允许宏定义从分离的文本片段创建标识符

int n = Conn(123,456);
     ==> int n=123456;
char* str = Conn("asdf", "adf");
     ==> char* str = "asdfadf";
#define Conn(x, y) x##y
int main() {
    int workhard = 100;
    printf("%d\n", Conn(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 xstr(s) str(s)
#define str(s) #s
#define foo 4
str (foo)
     ==> "foo"
xstr (foo)
     ==> xstr (4)
     ==> str (4)
     ==> "4"

s参数在str宏中被字符串化,所以它不是优先被宏展开。然而s参数是xstr宏的一个普通参数,在被传递到str宏之前已经被宏展开。

\行继续操作符

宏跨行延续

一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下(导致有些行没有#号),则使用宏延续运算符(\)。

#define getsymdo        \
    if (-1 == getsym()) \
    return -1

注意最后一行不要加续行符(写成多行时,换行后的行首不能出现空格,反斜杠后不能有空格,否则编译器(ARM或VC)会报错!)

定义宏

参考:C语言宏定义、宏函数、内置宏与常用宏_宏定义多个函数-CSDN博客

#define机制包括了一个规定,允许把参数替换到文本中,实现类函数宏【相比于函数不需要建立栈帧】

#define MALLOC(num,type) (type*)malloc(num*sizeof(type)) // 一个简写malloc的宏
#define HELLO "hello \ 使用
the world"

不支持对宏重复定义,编译器会直接报错。使用#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__:用在内容中,表示是一个可变参数的宏。 (C/C++标准)
  3. args...:表示可变参数列表且可用args来表示这个可变参数列表。args换成随便什么名字都可以。(GNU扩展)
  4. __VA_OPT__ :用于与 __VA_ARGS__ 搭配,表示在没有可变参数时不存在,有可变参数时才存在。(C++20起)

二、基础应用:

#define LOG1(...)  func1(__VA_ARGS__) // 这种写法不需要指明可变参数列表的名字,__VA_ARGS__直接就表示这个可变参数列表
#define LOG2(args...) func1(args) // 这种写法是表明使用args来指代可变参数列表

__VA_ARGS__作用: 将左边宏中的...的内容原样抄到右边__VA_ARGS__所占用的位置。 以上两宏等价。

#define CALL_PRIVATELY(ret_type, func_full_signature, ...) \
	ret_type func_full_signature \
	{ \
		return func##_private(table, __VA_ARGS__)); \
	}

CALL_PRIVATELY(int, table_add_record(table_handle_t table, void* record), record) // OK
CALL_PRIVATE(void, table_record_count(table_handle_t table)) // 报错。无法适用于没有可变参数的情况
// 但是下面这个版本可以
#define CALL_PRIVATELY(ret_type, func_full_signature, ...) \
	ret_type func_full_signature \
	{ \
		return func##_private(table __VA_OPT__(,) __VA_ARGS__)); \
	}

__VA_OPT__作用:在 __VA_ARGS__ 为空时,不会生成内容。注意必须和 __VA_ARGS__ 搭配使用。

三、实现格式化输出:

#define LOG1(fmt, ...)  printf(fmt, ##__VA_ARGS__)
#define LOG2(fmt, args...) printf(fmt, ##args)

##的作用:当可变参数的个数为0时(即没有传入可变参数),##起到把前面多余的","去掉的作用,否则会编译出错(这个是gcc和clang的私货,不是标准C++,C++20以后这么写:__VA_OPT__(, ), __VA_ARGS__)。

一个内核日志输出的示例:

#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

关于宏二次展开

#define TO_STRING(x) #x
#define EXPAND_AND_STRINGIFY(x) TO_STRING(x)

假设有 -D__HAHA__=hello ,则由于预处理器不会对 ### 运算符之后的宏参数进行二次展开,所以对于 TO_STRING(__HAHA__) 需要在 TO_STRING 的参数里就得到 __HAHA__ 的宏展开后的值,从而再交给 TO_STRING 才能得到字符串字面量。

于是提供 EXPAND_AND_STRINGIFY(__HAHA__) 宏,因为 EXPAND_AND_STRINGIFY 宏没有携带 ### ,因此会展开 __HAHA__,从而传给 TO_STRING 的参数是 __HAHA__ 的替换后的值。

宏的缺陷

C++宏孩儿?差不多得了!_哔哩哔哩_bilibili

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的。
  3. 宏虽然有 #undef 来卸载宏,但是在意外出现的宏很多时,一个一个卸载非常麻烦
  4. 宏由于类型无关,没有参数检查。
  5. 宏可能会带来运算符优先级、多次求值、意外生成语句块等副作用。
posted @ 2023-10-13 23:35  3的4次方  阅读(70)  评论(1)    收藏  举报