提示:本博客作为学习笔记,有错误的地方希望指正
文章目录
参考文章:
#define用法详解
C语言中define的用法详解
C语言中define的用法
C语言中的“宏”是指什么?
C语言 | 预处理 | 宏定义 | #define | 定义函数
define 的高级用法
#define的高级用法
前言:
最近在看ESP32 log的时候,看到一个很有意思的#define宏定义结合vprintf的使用,查找了资料特意整理下,不然时间久了容易忘记。具体的代码如下所示,我的理解就是宏定义不断的套娃,实现一些巧妙的运用,精简整体的代码,但是阅读起来需要对C语言的功底有些要求。
预处理的指令:
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
预处理运算符:
符号 | 描述 |
---|---|
\ | 宏延续运算符。一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\) |
# | 字符串常量化运算符。在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。 |
## | 标记粘贴运算符。宏定义内的标记粘贴运算符(##)会合并两个参数。 |
defined | 预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。 |
#if CONFIG_LOG_COLORS
#define LOG_COLOR_BLACK "30"
#define LOG_COLOR_RED "31"
#define LOG_COLOR_GREEN "32"
#define LOG_COLOR_BROWN "33"
#define LOG_COLOR_BLUE "34"
#define LOG_COLOR_PURPLE "35"
#define LOG_COLOR_CYAN "36"
#define LOG_COLOR(COLOR) "\033[0;" COLOR "m"
#define LOG_BOLD(COLOR) "\033[1;" COLOR "m"
#define LOG_RESET_COLOR "\033[0m"
#define LOG_COLOR_E LOG_COLOR(LOG_COLOR_RED)
#define LOG_COLOR_W LOG_COLOR(LOG_COLOR_BROWN)
#define LOG_COLOR_I LOG_COLOR(LOG_COLOR_GREEN)
#define LOG_COLOR_D
#define LOG_COLOR_V
#else //CONFIG_LOG_COLORS
#define LOG_COLOR_E
#define LOG_COLOR_W
#define LOG_COLOR_I
#define LOG_COLOR_D
#define LOG_COLOR_V
#define LOG_RESET_COLOR
#endif //CONFIG_LOG_COLORS
#define LOG_FORMAT(letter, format) LOG_COLOR_ ## letter #letter " (%u) %s: " format LOG_RESET_COLOR "\n"
#define LOG_SYSTEM_TIME_FORMAT(letter, format) LOG_COLOR_ ## letter #letter " (%s) %s: " format LOG_RESET_COLOR "\n"
#define ESP_LOG_EARLY_IMPL(tag, format, log_level, log_tag_letter, ...) do { \
if (_ESP_LOG_EARLY_ENABLED(log_level)) { \
esp_rom_printf(LOG_FORMAT(log_tag_letter, format), esp_log_timestamp(), tag, ##__VA_ARGS__); \
}} while(0)
一、认识#define宏定义
对于C语言的编译处理,需要经过以下几个步骤,预编译 —> 编译 —> 汇编 —> 链接,在预编译过程中会将我们的代码中宏定义全部换掉。其中对于#define的详细介绍推荐看以下几篇文章讲的挺不错的。在宏定义使用的时候我们需要注意的是越界的问题,就是有参数的时候可能需要添加一些括号,这样省的编译的时候出一些问题。
下面是一些关于#define中的使用的一些说明:
①宏名一般用大写
②宏定义末尾不加分好;
③可以用#undef命令终止宏定义的作用域
④宏定义可以嵌套
⑤字符串“”中永远不包含宏
⑥宏替换在编译前进行,不分配内存,变量定义分配内存,函 数调用在编译后程序运行时进行,并且分配内存
⑦预处理是在编译之前的处理,而编译工作的任务之一就是语 法检查,预处理不做语法检查
⑧使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。
需要解释的是字符串汇中永远不要包含宏的意思是啥。举例如下:
#define NAME_STR "WSP"
char *str = "My name is NAME_STR"; //其中NAME_STR不会在预编译过程中替换成WSP
char *str1 = NAME_STR; //NAME_STR 会被替换成WSP
1.1、#define无参数宏定义
其中无参宏定义可能是我们最常用的,例如我们在调试一个参数的时候值可能是某个不固定的参数,多个地方同时使用到这个变量,适配不同的环境的时候,这个时候我们就可以使用无参数宏定义就比较方便了。
#define无参数宏定义例如:
#define MAX_NUMBER 168 //统一替代在函数中引用到MAX_NUMBE R
1.2、#define有参数宏定义
有参数的宏定义可以实现一些灵活的定义,但是要值得注意的是注意边界问题,宏定义的括号问题,如果不加上括号可能会发生意想不到的错误。
#define有参数宏定义例如:
#define SUM(a,b) (a+b) //实现一个累积和的灵活运用
#define MAX(a,b) (a>b)?a:b //比较两个数中的最大一个值输出,这里使用到一个三目运算操作
1.3、#define定义函数
宏定义函数也是一个比较有意思的,宏定义换行的时候使用的是斜线来实现的。例如下面这个例子就是上述的代码中的内容。
#define ESP_LOG_EARLY_IMPL(tag, format, log_level, log_tag_letter, ...) do { \
if (_ESP_LOG_EARLY_ENABLED(log_level)) { \
esp_rom_printf(LOG_FORMAT(log_tag_letter, format), esp_log_timestamp(), tag, ##__VA_ARGS__); \
}} while(0)
do……while(0)的作用主要有以下几个方面:
- 空的宏定义避免warning: #define foo() do{}while(0)
- 存在一个独立的block,可以用来进行变量定义,进行比较复杂的实现。
- 如果出现在判断语句过后的宏,这样可以保证作为一个整体来是实现:
示例:
void Printf_One(int value)
{
printf("Printf_One:%d",value);
}
void Printf_Two(int value)
{
printf("Printf_Two:%d",value);
}
//1、未使用do{}while(0)的情况 单独编译宏定义可以通过,但是调用的时候就编译出错。
#define PRINTF_FUNCTION_ONE(value) \
Printf_One(value); \
Printf_Two(value);
//函数中调用 当然了这样的使用我在使用vscode编译的时候直接报错了
if(value)
PRINTF_FUNCTION_ONE(value);
//2、使用do{}while(0)的情况 测试使用没问题
#define PRINTF_FUNCTION_TWO(value) \
do{ \
Printf_One(value); \
Printf_Two(value); \
}while(0)
if(value)
PRINTF_FUNCTION_TWO(value);
1.4、#define处理头文件被包涵或者源文件包含的情况
在嵌入式开发中我们常会在每个.h文件中使用条件编译,这样防止出现代码中相同。
#ifndef _LED_H_ //如果没有定义_LED_H_
#define _LED_H_ //则定义_LED_H_
//code
#endif //与ifndef配对出现
1.6、#define条件编译
首先宏定义Printf_vlaue,正常执行PRINTF_FUNCTION_TWO(value);当使用#undef Printf_vlaue之后,就不再执行printf(“not define Printf_vlaue”);
#define Printf_vlaue
#ifdef Printf_vlaue
PRINTF_FUNCTION_TWO(value);
#elif
printf("not print PRINTF_FUNCTION_TWO function");
#endif
#undef Printf_vlaue
// 使用 undef之后不打印 not define Printf_vlaue
#ifdef Printf_vlaue
printf("not define Printf_vlaue");
#endif
1.5、#define中#、#@和##的用法
- #(参数x) : 将参数x变成字符串
- #@(参数x) : 将参数x变成字符
- (参数x) ## (参数y) : 将参数x与y链接成一个数,x、y的数据类型一定要相同,不然会报错
值的注意的是上述的几种方法中#、#@、##与输入的参数之间可以有空隔,空格不会被编译进入最后的结果中
#define TO_CONNECT(x,y) x##y // 将输入的参数 xy链接在一起,注意x、y变量的数据类型一定要相同
#define TO_STR(x) #@x // 将输入的x变成字符
#define TO_STRING(x) #x // 将输入的x变成一个字符串
// 我使用Vscode编译的时候一直会出现 '#' is not followed by a macro parameter 这里可能是编译器的问题
char to_str = TO_STR(1);
printf("to_str:%c",to_str);
printf("TO_CONNECT:%d TO_STRING:%s",TO_CONNECT(1,68),TO_STRING(WSP));
1.6、#define与const的区别
- define在预处理阶段进行替换,const常量在编译阶段使用
- 宏不做类型检查,仅仅进行替换,const常量次有数据类型,会执行类型检查
- define不能调试,const常量可以调试
- define定义的常量在替换后运行过程中会不断的占用内存,而const定义的常量存储在数据段,只有一份copy,效率更高。
- define可以定义一些简单的函数,const不可以。
1.7、#define与typedef的区别
(1)原理不同
(2)功能不同
(3)作用域不同
(4)对指针的操作不同
#define和typedef的作用看起来很像,但是实质上是差别很大。#define仅在预处理时对代码进行简单的字符串替换处理,不作正确性检查,不关含义是否正确照样替换;而typedef是在编译时处理,它建立了一个新的数据类型别名。一般来说,最好使用typedef定义用户类型,部分原因是它能正确处理指针类型。
详细的区别可以参考这篇文章:typedef和define有什么区别、关于typedef void (*sighandler_t)(int)的理解
1.8、一些特殊的宏的用法
__VA_ARGS__. :总体来说就是将左边宏中 ... 的内容原样抄写在右边 __VA_ARGS__ 所在的位置
__FILE__ :宏在预编译时会替换成当前的源文件名L
__LINE__ :宏在预编译时会替换成当前的行号
__FUNCTION__ :宏在预编译时会替换成当前的函数名称
__TIME__ :该源文件最近一次编译的时间和日期
__STDC__ :当编译器以 ANSI 标准编译时,则定义为 1;判断该文件是不是标准 C 程序。
__TIMESTAMP__:C中的时间戳,精度为毫秒
//示例demo
printf("ile:%s line%d function:%s time:%s time_ms:%s ANSI:%d",__FILE__,__LINE__,__FUNCTION__,__TIME__,__TIMESTAMP__,__STDC__);
//输出结果
File:./main/blink_example_main.c line58 function:app_main time:22:43:01 time_ms:Mon Oct 3 22:43:00 2022 ANSI:1
二、认识printf
函数原型:
int printf ( const char * format, ... );
返回值:
正确返回输出的字符总数,错误返回负值。与此同时,输入输出流错误标志将被置值,可由指示器函数 ferror(FILE *stream) 来检查输入输出流的错误标志,如果 ferror() 返回一个非零值,表示出错。
这里我找到一篇非常详细的文章:C printf() 详解之终极无惑
三、认识vprintf
vprintf的用法与sprintf类似,他们两个都类似,为啥还需要vprintf呢?这里主要使用vprintf作为自定义类似printf输出的效果。可以实现自定义的输出格式。
函数原型:
vsprintf(char *buffer, char *format, va_list param);
既然谈到vprintf可以自定义输出格式的话,那么我们怎么自定义呢,这里正是我主要想写这篇文章的主要原所在了。在文章最开始的的宏定义函数中出现esp_rom_printf的函数原型如下:
int esp_rom_printf(const char *fmt, ...)
{
va_list list;
va_start(list, fmt);
int result = esp_rom_vprintf(s_esp_rom_putc, fmt, list);
va_end(list);
return result;
}
void esp_log_write(esp_log_level_t level,
const char *tag,
const char *format, ...)
{
va_list arg;
va_start(arg, format);
vprintf(format, arg);
va_end(arg);
}
其中esp_rom_vprintf是乐鑫重写了,功能与vprintf一样,内部做了一些优化,专门给ESP_LOG打印这一大类使用。esp_log_write也可以在乐鑫的demo中搜索到,对esp_log_write的分析还是要的对va_list、va_start、va_end几个变量和函数的认识,它们几个一般使用在不定长参数的函数中使用。
它们几个的原型:
typedef char* va_list;
void va_start ( va_list ap, prev_param ); /* ANSI version */
type va_arg ( va_list ap, type );
void va_end ( va_list ap );
1)va_list:一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
2)va_start:对ap进行初始化,让ap指向可变参数表里面的第一个参数。第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
3)va_arg: 获取参数。它的第一个参数是ap,第二个参数是要获取的参数的指定类型。按照指定类型获取当前参数,返回这个指定类型的值,然后把 ap 的位置指向变参表中下一个变量的位置;
4)va_end:释放指针,将输入的参数 ap 置为 NULL。通常va_start和va_end是成对出现。
新版本中的代码 ,其实内部都是一样的功能,只是做了进一步的封装
#ifndef _VA_LIST
typedef __builtin_va_list va_list;
#define _VA_LIST
#endif
#define va_start(ap, param) __builtin_va_start(ap, param)
#define va_end(ap) __builtin_va_end(ap)
#define va_arg(ap, type) __builtin_va_arg(ap, type)
#if (__GNUC__ > 2)
typedef __builtin_va_list __darwin_va_list; /* va_list */
#else
typedef void * __darwin_va_list; /* va_list */
#endif
#define配合vprintf的使用 具体的代码:
uint32_t esp_log_timestamp(void);
void esp_log_write(esp_log_level_t level, const char* tag, const char* format, ...) __attribute__ ((format (printf, 3, 4)));
#define LOG_FORMAT(letter, format) LOG_COLOR_ ## letter #letter " (%d) %s: " format LOG_RESET_COLOR "\n"
#define ESP_LOGE( tag, format, ... ) if (LOG_LOCAL_LEVEL >= ESP_LOG_ERROR) { esp_log_write(ESP_LOG_ERROR, tag, LOG_FORMAT(E, format), esp_log_timestamp(), tag, ##__VA_ARGS__); }
void esp_log_write(esp_log_level_t level,
const char *tag,
const char *format, ...)
{
va_list arg;
va_start(arg, format);
vprintf(format, arg);
va_end(arg);
}
分析实现过程:其中宏定义 #define LOG_FORMAT(letter, format)的可变参数的字符串中多了" (%d) %s: "的vprintf的输出类型的控制符号,这个按照#define的语法编译的时候会报错的,但是我们还的要从esp_log_write函数和宏定义ESP_LOGE来结合研究它这样使用为啥不会出错,其中可以看到esp_log_write 函数中有esp_log_timestamp(), tag,这两个参数,然后在结合前面的LOG_FORMAT(E, format)的参数一看,LOG_FORMAT(E, format)在编译之后就会替换成:
LOG_COLOR_E"E"" (%d) %s: " format LOG_RESET_COLOR "\n"
经一步将替换
"\033[0;" "31" "m""E"" (%d) %s: " "\033[0m" "\n"
将替换输入法esp_log_write中
esp_log_write(ESP_LOG_ERROR,tag,"\033[0;" "31" "m""E"" (%d) %s: " "\033[0m" "\n",esp_log_timestamp(), tag, ##__VA_ARGS__);
//这样就比较清楚的认识到 #define中的输出控制符号 (%d) %s: 的具体作用了,用于控制esp_log_timestamp(), tag两个参数的输出类型。
//##__VA_ARGS__ 宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的","去掉的作用,否则会编译出错
前面替换可以和下类似
ESP_LOGI(TAG,"Hello" "Esp" "32 “My name is“ ”(%d)%s",168,"WSP");
这样一分析就感觉特别清楚到底是啥回事,就是宏定义和vprintf的巧妙的配合使用。颜色的控制通过ESC字符(\033)加”[“加颜色代码加”m”实现。ESC的ASCII码是十进制的27,八进制的033(\033)。\033[0m 关闭所有属性。[\0330;30m 之超级终端的字体背景和颜色显示等
四、认识sprintf
函数原型:
int sprintf(char *string, char *format [,argument,...]);
其中的功能就是需要整理、格式化字符串,这样我们可以给我们先要的字符串类型打印在一个字符串中,当使用printf输出的时候就可以得到我们想要的格式的字符串。具体详细了解可以参考:sprintf 函数详解
OK,整体关于宏定义以及printf、vprintf、sprintf的几个的介绍就结束了,我想给后面这三个一起将的原因就是区分不同,方便记住。