C语言序列之(1)#define宏定义字符串结合vprintf、sprintf的使用

提示:本博客作为学习笔记,有错误的地方希望指正


参考文章:
#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的几个的介绍就结束了,我想给后面这三个一起将的原因就是区分不同,方便记住。

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值