EPICS libCom库(7) -- epicsString.h

这篇具有很好参考价值的文章主要介绍了EPICS libCom库(7) -- epicsString.h。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

epicsString.h当前描述了: 

1) int epicsStrnRawFromEscaped(char *dst, size_t dstlen, const char *src, size_t srclen);

epicsStrnRawFromEscaped从字符串src最多复制strlen个字符到尺寸dstlen的缓存dst中,转换C风格转义序列为它们的二进制形式。一个zero字节终结输入字符串。只要dstlen非零,产生的字符串将是zero结尾的。返回值是实际写入dst的字符数目,不计算不何时或者zero终止符的字符。由于输出字符串不能长于源,src和dst指向相同缓存是合法的,并且strlen和dstlen有相同值,因而执行原地字符转换。

int epicsStrnRawFromEscaped(char *dst, size_t dstlen, const char *src, size_t srclen)
{
    int rem = dstlen;
    int ndst = 0;

    while (srclen--) {
        // 从源读取一个字符,如果源字符为0,则退出循环
        int c = *src++;
        #define OUT(chr) if (--rem > 0) ndst++, *dst++ = chr

        if (!c) break;

    input:
        // 源字符是不为\,则输出计数加1,将\添加到目标缓存
        if (c != '\\') {
            OUT(c);
            continue;
        }

        // 源长度变为0了或者源中当前为0,退出循环
        if (!srclen-- || !(c = *src++)) break;

        字符串表示的'\\a'->'\a'等
        switch (c) {
        case 'a':  OUT('\a'); break;
        case 'b':  OUT('\b'); break;
        case 'f':  OUT('\f'); break;
        case 'n':  OUT('\n'); break;
        case 'r':  OUT('\r'); break;
        case 't':  OUT('\t'); break;
        case 'v':  OUT('\v'); break;
        case '\\': OUT('\\'); break;
        case '\'': OUT('\''); break;
        case '\"': OUT('\"'); break;
        
        // 把字符串形式的数值转成二进制形式的数值:'\\012' '\\120'
        case '0' :case '1' :case '2' :case '3' :
        case '4' :case '5' :case '6' :case '7' :
            { /* \ooo */
                unsigned int u = c - '0';

                if (!srclen-- || !(c = *src++)) {// 源已经到末尾了,则调至结束
                    OUT(u); goto done;
                }
                if (c < '0' || c > '7') {//不在八进制范围内
                    OUT(u); goto input;
                }
                u = u << 3 | (c - '0'); // 计数数值

                if (!srclen-- || !(c = *src++)) {
                    OUT(u); goto done;
                }
                if (c < '0' || c > '7') {
                    OUT(u); goto input;
                }
                u = u << 3 | (c - '0');

                if (u > 0377) {
                    /* Undefined behaviour! */
                }
                OUT(u);
            }
            break;

        case 'x' : '\x12A'
            { /* \xXXX... */
                unsigned int u = 0;

                if (!srclen-- || !(c = *src++ & 0xff))
                    goto done;

                while (isxdigit(c)) {
                    u = u << 4 | ((c > '9') ? toupper(c) - 'A' + 10 : c - '0');
                    if (u > 0xff) {
                        /* Undefined behaviour! */
                    }
                    if (!srclen-- || !(c = *src++ & 0xff)) {
                        OUT(u);
                        goto done;
                    }
                }
                OUT(u);
                goto input;
            }

        default:
            OUT(c);
        }
        #undef OUT
    }
done:
    if (dstlen)
        *dst = '\0';
    return ndst;
}

2) int epicsStrnEscapedFromRaw(char *outbuf, size_t outsize, const char *inbuf, size_t inlen);

epicsStrnEscapedFromRaw做的事情与epicsStrnRawFromEscaped相反:它尝试从字符串src中复制strlen个字符到尺寸为dstlen的缓存中,转换非打印字符为C风格的转义序列。一个zero字节将不终结这个输入字符串。只要dstlen为非零,输出字符串将是zero终结的。虽然将读取输入字符串中所有的字符,但实际写入到输出缓存的字符不多于dstlen。如果输出缓存足够大,返回值将是在它中存储的字符数目,或者如果dst=src,返回值是一个负数。由于转义结果加你个通常大于输入字符串,原位转换不被允许。在输出中使用的允许的转义字符常数:\a \b \f \n \r \t \v \\ \' \"。所有其它不可打印的字符将以格式\ooo形式显示为八进制转义,此处ooo为3位八进制数字(0-7)。不可打印字符有C运行时库函数isprint()确定。

int epicsStrnEscapedFromRaw(char *dst, size_t dstlen, const char *src,
    size_t srclen)
{
    int rem = dstlen;
    int ndst = 0;

    if (dst == src)
        return -1;

    while (srclen--) {
        // 从源取一个字符
        int c = *src++;
        #define OUT(chr) ndst++; if (--rem > 0) *dst++ = chr

        // 如果c为不可打印字符,则进行转义:'\a'->'\\a'
        switch (c) {
        case '\a': OUT('\\'); OUT('a');  break;
        case '\b': OUT('\\'); OUT('b');  break;
        case '\f': OUT('\\'); OUT('f');  break;
        case '\n': OUT('\\'); OUT('n');  break;
        case '\r': OUT('\\'); OUT('r');  break;
        case '\t': OUT('\\'); OUT('t');  break;
        case '\v': OUT('\\'); OUT('v');  break;
        case '\\': OUT('\\'); OUT('\\'); break;
        case '\'': OUT('\\'); OUT('\''); break;
        case '\"': OUT('\\'); OUT('\"'); break;
        default:// c为可打印字符,则直接将c输出到目标
            if (isprint(c & 0xff)) {
                OUT(c);
                break;
            }
            \\ 此处为不可打印字符的数值的处理,从高位到低位,可处理的最大数值为八进制0377
            \\ 转成字符串表示的数值
            OUT('\\');
            OUT('0' + ((c & 0300) >> 6));
            OUT('0' + ((c & 0070) >> 3));
            OUT('0' +  (c & 0007));
        }
        #undef OUT
    }\\ 末尾添加结束符
    if (dstlen)
        *dst = '\0';
    return ndst;
}

 3) size_t epicsStrnEscapedFromRawSize(const char *src, size_t srclen);

 epicsStrnEscapedFromRawSize最多扫描可能包含不可打印字符的字符串src中strlen个字符,并且返回需要转义那个字符串的输出缓存的大小。在输出缓存中需要的进行终止的zero字节未被算入,因而由调用者允许。这个例程快于用一个0长度输出缓存调用epicsStrnEscapedFromRaw,二者应该返回相同结果。

size_t epicsStrnEscapedFromRawSize(const char *src, size_t srclen)
{
    size_t ndst = srclen;

    while (srclen--) {
        int c = *src++;

        switch (c) {
        case '\a': case '\b': case '\f': case '\n':
        case '\r': case '\t': case '\v': case '\\':
        case '\'': case '\"':
            ndst++;
            break;
        default:
            if (!isprint(c & 0xff))
                ndst += 3;
        }
    }
    return ndst;
}

4) int epicsStrCaseCmp(const char *s1, const char *s2); 

int epicsStrnCaseCmp(const char *s1, const char *s2, int n); 

epicsStrCaseCmp和epicsStrnCaseCmp各种实现了strcasecmp和strncasecmp函数,它们不是在所有操作系统上能够获取的。它们操作类似strcmp和stncmp,但不区分大小写。

int epicsStrCaseCmp(const char *s1, const char *s2)
{
    while (1) {
        // 两个字符串中取相同索引的字符,转成大写字母后比较
        int ch1 = toupper((int) *s1);
        int ch2 = toupper((int) *s2);

        if (ch2 == 0) return (ch1 != 0);
        if (ch1 == 0) return -1;
        if (ch1 < ch2) return -1;
        if (ch1 > ch2) return 1;
        s1++;
        s2++;
    }
}
/* 比较两个字符串中前len个字符 */
int epicsStrnCaseCmp(const char *s1, const char *s2, size_t len)
{
    size_t i = 0;

    while (i++ < len) {
        int ch1 = toupper((int) *s1);
        int ch2 = toupper((int) *s2);

        if (ch2 == 0) return (ch1 != 0);
        if (ch1 == 0) return -1;
        if (ch1 < ch2) return -1;
        if (ch1 > ch2) return 1;
        s1++;
        s2++;
    }
    return 0;
}

5) char *epicsStrDup(const char *s); 

char * epicsStrnDup(const char *s, size_t len)

epicsStrDup实现了strup,它不是在所有操作系统上可以获取的。它位字符串分配足够的内存,复制它并且返回指向这个新副本的指针。这个指针应该最终被传递给free()。如果内存不足,调用cantProceed()。

char * epicsStrDup(const char *s)
{
    return strcpy(mallocMustSucceed(strlen(s)+1, "epicsStrDup"), s);
}
char * epicsStrnDup(const char *s, size_t len)
{
    char *buf = mallocMustSucceed(len + 1, "epicsStrnDup");

    strncpy(buf, s, len);
    buf[len] = '\0';
    return buf;
}

6) int epicsStrPrintEscaped(FILE *fp, const char *s, int n); 

epicsStrPrintEscaped打印输入缓存的内容,用转义序列替代不可打印字符。

int epicsStrPrintEscaped(FILE *fp, const char *s, size_t len)
{
   int nout = 0;

   while (len--) {
       char c = *s++;

       switch (c) {
       case '\a':  nout += fprintf(fp, "\\a");  break;
       case '\b':  nout += fprintf(fp, "\\b");  break;
       case '\f':  nout += fprintf(fp, "\\f");  break;
       case '\n':  nout += fprintf(fp, "\\n");  break;
       case '\r':  nout += fprintf(fp, "\\r");  break;
       case '\t':  nout += fprintf(fp, "\\t");  break;
       case '\v':  nout += fprintf(fp, "\\v");  break;
       case '\\':  nout += fprintf(fp, "\\\\"); break;
       case '\'':  nout += fprintf(fp, "\\'");  break;
       case '\"':  nout += fprintf(fp, "\\\"");  break;
       default:
           if (isprint(0xff & (int)c))
               nout += fprintf(fp, "%c", c);
           else
               nout += fprintf(fp, "\\%03o", (unsigned char)c);
           break;
       }
   }
   return nout;
}

7) int epicsStrGlobMatch(const char *str, const char *pattern); 

epicsStrGlobMatch如果str匹配这个shell通配符模式,返回非0.

int epicsStrGlobMatch(const char *str, const char *pattern)
{
    const char *cp = NULL, *mp = NULL;

    // 从字符串开头和模式字符串开头逐字节依次比较,循环结束条件:遇到字符串末尾或者pattern中
    // 出现了*字符
    while ((*str) && (*pattern != '*')) {
        // 逐字节比较,两处字符不相同并且模式串中对应字符不为?时,匹配失败,直接返回0
        if ((*pattern != *str) && (*pattern != '?'))
            return 0;
        pattern++;//取模式串后一个字节地址
        str++;    // 取字符串后一个字节地址
    }

    // str为到结尾,初始进入这个while循环,表示当前pattern处字符一定为*
    while (*str) {
        if (*pattern == '*') {
            if (!*++pattern)//模式字符串*之后是结束符,则表示匹配上了,返回1
                return 1;
            mp = pattern;  // 记下*之后的字符
            cp = str+1;   // cp记下字符串下一个字符位置
        }
        else if ((*pattern == *str) || (*pattern == '?')) {
            pattern++;
            str++;
        }
        else {// 当前字符与模式匹配字符不同,并且pattern不为?也不为*
            pattern = mp;
            str = cp++;
        }
    }
    while (*pattern == '*')
        pattern++;
    return !*pattern;
}

8) char *epicsStrtok_r(char *s, const char *delim, char **lasts); 

epicsStrtok_r 实现了 strtok_r, 不是在所有操作系统上可以获取。

char * epicsStrtok_r(char *s, const char *delim, char **lasts)
{
   const char *spanp;
   int c, sc;
   char *tok;

   if (s == NULL && (s = *lasts) == NULL)
      return NULL;

   /*
    * 跳过开头的分隔符字符串中所有分隔符
    */
cont:
   c = *s++;
   for (spanp = delim; (sc = *spanp++) != 0;) {
      if (c == sc)
         goto cont;
   }

   if (c == 0) {      /* 已经到字符串末尾了 */
      *lasts = NULL;
      return NULL;
   }
   // 字符串中开头分隔符中的索引最大的分隔符, s是非分隔符的位置
   tok = s - 1;

   /*
    * 扫描token(扫描分隔符:xxxaaabbb->aaabbb,x表示分隔符,s指向xxxaaabbb变为s指向aaabbb)
    */
   for (;;) {
      c = *s++; // 取当前字符,s指向下一个字符
      spanp = delim;
      do {
         if ((sc = *spanp++) == c) {// 取分隔符串中所有分隔符与c比较,c是否是分隔符
            if (c == 0) // 到字符串末尾了
               s = NULL;  
            else
               s[-1] = 0; //当前字符为分隔符,则当前字符替换为NULL
            *lasts = s; // 记住下一次开始的位置
            return tok; // 跳过开头分隔符后的第一个字符
         }
      } while (sc != 0);
   }
}

9) unsigned int epicsStrHash(const char *str, unsigned int seed); unsigned int epicsMemHash(const char *str, size_t length, unsigned int seed); 

epicsStrHash计算一个zero终结字符串str的哈希值,而epicsMemHash对可能包含zero字节的固定长度内存缓存使用相同算法。在两种情况中,提供一个初始种子值,它使得多个字符串或者缓存被合并成单个哈希结果。最终结果应该被掩码计算来实现哈希值中所需比特数。

// 计算str字符串的哈希值
unsigned int epicsStrHash(const char *str, unsigned int seed)
{
    unsigned int hash = seed;
    char c;

    // 逐字节计算
    while ((c = *str++)) {
        hash ^= ~((hash << 11) ^ c ^ (hash >> 5));
        if (!(c = *str++)) break;
        hash ^= (hash << 7) ^ c ^ (hash >> 3);
    }
    return hash;
}

 // 计算str中length个字符长度的哈希值
unsigned int epicsMemHash(const char *str, size_t length, unsigned int seed)
{
    unsigned int hash = seed;

   
    while (length--) {
        hash ^= ~((hash << 11) ^ *str++ ^ (hash >> 5));
        if (!length--) break;
        hash ^= (hash << 7) ^ *str++ ^ (hash >> 3);
    }
    return hash;
}

以下是一个用于演示以上库函数用法及测试的程序:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "epicsString.h"

int main()
{
        char src[100];
        char dst[100];

        printf("Test epicsStrnRawFromEscaped:\n");
        strcpy(src,   "hello\\tworld\\n");
        epicsStrnRawFromEscaped(dst, sizeof(dst),src,strlen(src));

        printf("src = %s\n", src);
        printf("dst = %s\n", dst);
        printf("\n\n");

        printf("Test epicsStrnEscapedFromRaw:\n");
        strcpy(src, dst);
        int srclen = strlen(src);
        int dstlen = 0;

        dstlen = epicsStrnEscapedFromRaw(dst, sizeof(dst), src, srclen);
        printf("src = %s, len = %d\n", src, srclen);
        printf("dst = %s, len = %d\n", dst, dstlen);
        printf("\n\n");

        printf("Test epcisStrnEscapedFromRAwSize:\n");
        size_t len = epicsStrnEscapedFromRawSize(src, srclen);
        printf("scr = %s, raw size: %d ---> escaped size: %ld\n", src, srclen, len);
        printf("\n\n");


        printf("Test epicsStrCaseCmp\n");
        strcpy(src, "Compare String");
        strcpy(dst, "comPare string hello");

        if (epicsStrCaseCmp(src, dst) == 0){
                printf("%s and %s are same (case insensitive)\n", src, dst);
        }
        else{
                printf("%s and %s are not the same\n", src, dst);
        }
        printf("\n\n");

        printf("Test epicsStrinCaseCmp\n");
        if (epicsStrnCaseCmp(src, dst, strlen(src)) == 0){
                printf("%s and %s are same before the %ld character(case insensitive)\n", src, dst, strlen(src));
        }
        else{
                printf("%s and %s are not the same\n", src, dst);
        }
        printf("\n\n");

        printf("Test epicsStrDup:\n");
        strcpy(src, "Hello World!");
        char * newchar = epicsStrDup(src);
        printf("newchar = %p: %s, src = %p: %s\n", newchar, newchar, src, src);
        free(newchar);
        newchar = NULL;
        printf("\n\n");

        printf("Test epicsStrnDup:\n");
        newchar = epicsStrnDup(src, 5);
        printf("newchar = %p: %s, src = %p: %s\n", newchar, newchar, src, src);
        free(newchar);
        newchar = NULL;
        printf("\n\n");

        printf("Test epicsStrPrintEscaped:\n");
        strcpy(src, "\t\thello\n");
        epicsStrPrintEscaped(stdout, src, strlen(src));
        printf("\n\n");

        printf("Test epicsStrGlobMatch:\n");
        strcpy(src, "Hello World");
        char * patterns[4] = {"H?l*", "Hell?", "*ld", "?ell*p"};
        int i;
        for (i = 0; i < 4; i++){
                if(epicsStrGlobMatch(src, patterns[i])){
                        printf("%s and %s match\n", src, patterns[i]);
                }
                else{
                        printf("%s and %s not match\n", src, patterns[i]);
                }
        }
        printf("\n\n");

        char delims[30];
        printf("Test *epicsStrtok_r:\n");
        strcpy(src, "Hello\tworld good,morning?Next;day\nComputer");
        strcpy(dst, src);
        strcpy(delims, "\t ,?;\n");
        char * lasts;
        char * start;

        newchar = epicsStrtok_r(src, delims, &lasts);
        printf("newchar = %s\n", newchar);
        printf("left chars = %s\n", lasts);

        while (lasts){
                start = lasts;
                newchar = epicsStrtok_r(start, delims, &lasts);

                if (newchar){
                        printf("newchar = %s\n", newchar);
                }
        }
        printf("\n\n");

        printf("Test epicsStrHash:\n");
        strcpy(src, "Hello World");
        unsigned int hash1 = epicsStrHash(src, 1001);
        unsigned int hash2 = epicsMemHash(src, 5, 1001);

        printf("src = %s's hash value %u\n", src, hash1);
        printf("src = %s's first 5 charachters's hash value %u\n",src, hash2);

        return 0;
}

编译并且运行以上测试程序:文章来源地址https://www.toymoban.com/news/detail-679829.html

orangepi@orangepi5:~/epics/hostApp$ O.linux-aarch64/test_epicsString
Test epicsStrnRawFromEscaped:
src = hello\tworld\n
dst = hello     world



Test epicsStrnEscapedFromRaw:
src = hello     world
, len = 12
dst = hello\tworld\n, len = 14


Test epcisStrnEscapedFromRAwSize:
scr = hello     world
, raw size: 12 ---> escaped size: 14


Test epicsStrCaseCmp
Compare String and comPare string hello are not the same


Test epicsStrinCaseCmp
Compare String and comPare string hello are same before the 14 character(case insensitive)


Test epicsStrDup:
newchar = 0x5579f9ef40: Hello World!, src = 0x7fe9a0c3b8: Hello World!


Test epicsStrnDup:
newchar = 0x5579f9ef40: Hello, src = 0x7fe9a0c3b8: Hello World!


Test epicsStrPrintEscaped:
\t\thello\n

Test epicsStrGlobMatch:
Hello World and H?l* match
Hello World and Hell? not match
Hello World and *ld match
Hello World and ?ell*p not match


Test *epicsStrtok_r:
newchar = Hello
left chars = world good,morning?Next;day
Computer
newchar = world
newchar = good
newchar = morning
newchar = Next
newchar = day
newchar = Computer


Test epicsStrHash and epicsMemHash:
src = Hello World's hash value 4022895612
src = Hello World's first 5 charachters's hash value 1976639928

到了这里,关于EPICS libCom库(7) -- epicsString.h的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • mysql在EPICS IOC过程变量数据存储中的应用

    此处使用的IOC中含有3个温度值,2个二进制输出,2个二进制输入,如下所示: 三个温度值的过程变量名: THREETS:T1:Temperarure THREETS:T2:Temperarure THREETS:T3:Temperarure 两个二进制输出: TEST:Bo1 TEST:Bo2 两个二进制输入: TEST:Bi1 TEST:Bi2 在mysql数据库中建立一个名为temperature的数据库,并

    2024年02月13日
    浏览(34)
  • C语言入门教程,C语言学习教程(第三部分:C语言变量和数据类型)二

    前面我们多次提到了字符串,字符串是多个字符的集合,它们由 \\\" \\\" 包围,例如 \\\"http://c.biancheng.net\\\" 、 \\\"C语言中文网\\\" 。字符串中的字符在内存中按照次序、紧挨着排列,整个字符串占用一块连续的内存。 当然,字符串也可以只包含一个字符,例如 \\\"A\\\" 、 \\\"6\\\" ;不过为了操作方

    2024年01月17日
    浏览(49)
  • 1 Go语言开发环境搭建详细教程+go常见bug合集【Go语言教程】

    官网地址:golang.org,因为一些原因国内可能无法访问。可以使用下面第二个链接。 国内地址访问:https://golang.google.cn/dl或者https://www.golangtc.com/download 根据自己操作系统版本,下载安装即可,目录尽量选择全英文且没有空格和其他其他特殊字符。 2.1 Windows下 GOPATH:即默认的w

    2024年02月05日
    浏览(46)
  • 【Go】Go 语言教程--语言结构(二)

    Go 语言教程–介绍(一) Go 语言的基础组成有以下几个部分: 包声明 引入包 函数 变量 语句 表达式 注释 接下来让我们来看下简单的代码,该代码输出了\\\"Hello World!\\\": 实例 让我们来看下以上程序的各个部分: 第一行代码 package main 定义了包名。你必须在源文件中非注释的第

    2024年02月12日
    浏览(53)
  • 最新Visual Studio下载安装以及C语言环境搭建教程(含C语言入门教程)

    最新Visual Studio下载安装以及C语言环境搭建教程来啦!一起来看看吧~ C语言是一种高级编程语言,由美国贝尔实验室的Dennis Ritchie于1972年发明,它是Unix操作系统的核心语言。C语言以其简洁、高效和可移植性在计算机编程领域得到广泛应用,成为了当今最为流行的编程语言之一

    2024年02月02日
    浏览(60)
  • 【C语言学习教程---1】VC++6.0的安装和创建简单C语言工程文件教程

    事物的难度远远低于对事物的恐惧 在学习C语言之前,首先需要安装编译器软件,学习完理论知识及时动手操作是才能印象深刻,切勿纸上谈兵,这里选择安装的是一款比较经典的并且运行相对比较稳定的VC++6.0软件。 把软件资源下载到电脑上并进行解压,下载地址: 链接:

    2024年02月08日
    浏览(51)
  • 【Go】Go 语言教程--GO语言数组(十一)

    往期回顾: Go 语言教程–介绍(一) Go 语言教程–语言结构(二) Go 语言教程–语言结构(三) Go 语言教程–数据类型(四) Go 语言教程–语言变量(五) Go 语言教程–GO语言常量(六) Go 语言教程–GO语言运算符(七) Go 语言教程–GO条件和循环语句(八) Go 语言教程

    2024年02月15日
    浏览(37)
  • 【Go】Go 语言教程--Go 语言接口(十九)

    往期回顾: Go 语言教程–介绍(一) Go 语言教程–语言结构(二) Go 语言教程–语言结构(三) Go 语言教程–数据类型(四) Go 语言教程–语言变量(五) Go 语言教程–GO语言常量(六) Go 语言教程–GO语言运算符(七) Go 语言教程–GO条件和循环语句(八) Go 语言教程

    2024年02月16日
    浏览(46)
  • 【Go】Go 语言教程--GO语言结构体(十三)

    往期回顾: Go 语言教程–介绍(一) Go 语言教程–语言结构(二) Go 语言教程–语言结构(三) Go 语言教程–数据类型(四) Go 语言教程–语言变量(五) Go 语言教程–GO语言常量(六) Go 语言教程–GO语言运算符(七) Go 语言教程–GO条件和循环语句(八) Go 语言教程

    2024年02月16日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包