c印记(七): ini file解析


title: c印记(七): ini file解析
tags: ini文件, c语言, 轮子
grammar_cjkRuby: true

目录

一、 写在前面的话

ini 文件并不是什么新东西,所以解析ini文件的算法实现也不少,这里写一个实现也基本上算是造轮子了,但换个角度看,更准确一点的说法应该是深度定制属于自己的轮子,因为网上虽算法不少,但要么就是过于复杂,喜欢简洁的我是无法忍受的,除了复杂的版本之外,还有一个极端,就是过度简洁,要么是需要的API没有,要么就是解析的不够完善,于是乎就萌生了自己写一个的想法,目前还只有解析读取的功能,并没有写入和输出ini文件的功能(就目前来说,还用不到那些,为了简洁,就暂未实作)

二、 ini文件格式

1. 简介

ini 文件是Initialization File的缩写,即初始化文件,是windows的系统配置文件所采用的存储格式 ,当然这类文件也可以用来初始化或配置其他的应用程序(不管是windows,linux还是android系统),因为它比较简单,没有xml那么复杂和庞大,不过它也有缺点,就是能够表达的内容相对单一,无法向xml可以嵌套语句,但就一般的简单配置数据而言,ini文件已经足够了。

2. 格式

ini文件一般是由节, 键,值组成,其中节并不是必须的,一个ini文件可以只由(键,值)这样的参数对组成。

节
     [section]
     
参数(键=值)
     key=value

注解

注解使用分号表示(;)。 在分号后面的文字,直到该行结尾都全部为注解。除此之外,本ini文件解析 算法还增加了类似 shell脚本中的注释符号(#)。

; 这是一个注释
# 这是一个注释
  • 节,

ini文件中节 可以用来表示某一个项目的参数,或者某一类型,某一组参数,一般发生在多个项目共用一个ini文件,或者一个项目中有不同种类的参数(如,各个模块有各个模块的参数)。 一般来说这种节 的名字不应该重复,在本文的算法实现中也是不支持 重复节名字的。

  • 键-值 参数对

可以重复的是 键-值 这样的参数对,比如,ini文件中有一个节表示项目需要的插件列表,就可以以如下的方式表示:

[plugin.list]
plugin = liba.so
plugin = libb.so
plugin = libc.so
  • 注解

在本文算法实现中,即支持注解独占一行,也支持在键-值 参数对之后,形如:

;显示模块配置
[display.cfg]
#控制显示开关
enable_display=true ;true:开启显示,false:关闭显示
enable_hdmi = true #true: 开启hdmi,false:关闭hdmi
  • 空格

在本文的实现中,支持 键-值 之间存在 空格,值 和 行末尾注释之间存在空格。

三、ini文件解析 API

1. 声明基础数据类型

在本问的实现中,并没有有直接使用 int,unsigned int之类的基本数据类型,均使用typedef进行了 重定义,以便类型名字简洁,长短统一,在使用的时候能够对齐,这样看起来更美观一些。

//general signed integer type
typedef int                GS32;  //32 bits

//general unsigned integer type
typedef unsigned int       GU32; //32 bits

如上面所示,基础数据类型的定义,都是以 ‘G’开始的,表示通用(general),然后跟着的是 S(有符号)或U(无符号),最后是数据长度。这里指定义了 32位的有符号和无符号类型,因为本文当中只使用到这两个数据类型,当然,还有 void,和 char,这两个也可以进行重定义,但因为其本来就是四个字符宽度了,所以本文实现中并未对其进行重定义。除此之外,其他的基础数据类型也可以以此方式进行重定义,如 8位无符号,可重定义为 GU08,16位有符号,可重定义为 GU16,float,可重定义为 GFLT等。

因为本文是使用c语言实现的解析算法,所以需要定义 布尔 类型的基础数据类型。

//boolean type declaration
typedef enum GBOL
{
    GFALSE = 0,
    GTRUE = !GFALSE,
}GBOL;

除了基础数据类型之外,为了更好的调试或遇到异常时,能更好的知道异常原因,所以需要定义错误类型。

//general error type define
typedef enum general_error_e
{
    G_OK = 0,

    /** There were insufficient resources to perform the requested operation */
    G_ErrorInsufficientResources = (GS32) 0x80001000,
    /** There was an error, but the cause of the error could not be determined */
    G_ErrorUndefined = (GS32) 0x80001001,
    /** One or more parameters were not valid */
    G_ErrorBadParameter = (GS32)0x80001004,
    G_ErrorInvalidOperation = (GS32)0x8000101C, /** invalid operation */

    /** No target with the specified name string was found */
    G_ErrorNotFound = (GS32)0x80001003,
}general_error_t;

2. API 声明

这里就不挨个介绍API了,直接将将整个 头文件的实现都贴出来。

#ifndef __TINY_INI_FILE_H__
#define __TINY_INI_FILE_H__

#ifdef __cplusplus
extern "C"{
#endif

/**
 * ini file 
 * syntax:
 * []     <   section lable,eg. [log.conf]
 * #      <   one line comment
 * ;      <   one line comment
 * =      <   link key(at left) and value(at right),eg. level = debug
 *
 * @note  1. the key in a section is not unique, it mean that, maybe have
 *           several same key in one section.
 *
 * for example:
 *
 * [log.conf] #log print configure
 * level = debug
 * enable_line_number = true
 *
 * ;pluing list
 * [plugin.list]
 * plugin = libfdk_aac_enc.so
 * plugin = libx264_enc.so
 *
 */


/***************************************************************************
 *
 * macro declaration
 *
 ***************************************************************************/
/**
* if ini file havan't any section,then parser will add a default section
* user cann't use this section name,it is reserve section name.
*/

#define DEFAULT_SECTION_NAME "default"

#define SECTION_MAX_COUNT  32  /** maximu section count in one ini file */

#define LINE_BUF_MAX_LEN   1024 /** the expression line maximum length */


/** compat the keyword 'inline' */
#if (_MSC_VER > 1200) && !defined (__cplusplus)
#define inline    _inline  /** to compat keyword 'inline' */
#endif

/***************************************************************************
 *
 * data structure declaration
 *
 ***************************************************************************/

/** base data type define */

//general signed integer type
typedef int                GS32;  //32 bits

//general unsigned integer type
typedef unsigned int       GU32; //32 bits

//boolean type declaration
typedef enum GBOL
{
    GFALSE = 0,
    GTRUE = !GFALSE,
}GBOL;

//general error type define
typedef enum general_error_e
{
    G_OK = 0,

    /** There were insufficient resources 
     * to perform the requested operation 
     */
    G_ErrorInsufficientResources = (GS32) 0x80001000,
    /** There was an error, but the cause of 
     * the error could not be determined 
     */
    G_ErrorUndefined = (GS32) 0x80001001,
    /** One or more parameters were not valid */
    G_ErrorBadParameter = (GS32)0x80001004,
    /** invalid operation */
    G_ErrorInvalidOperation = (GS32)0x8000101C, 

    /** No target with the specified name string was found */
    G_ErrorNotFound = (GS32)0x80001003,
}general_error_t;



/** ini file data structure define */

typedef struct key_value_pair_s key_value_pair_t;

typedef struct ini_section_s //一个节的数据结构声明
{
    int pair_count; /**the conut of pairs in current section */   
    key_value_pair_t* pairs;
    char *name; /** section name */
    void* opaque; /** private data, user cann't modify it */
}ini_section_t;


/** todo, add more function, 
 * for example add key-value, add section, save ini 
 */
typedef struct tiny_ini_file_s //ini 文件解析的API声明
{
	/** GTRUE: already loaded one file, GFALSE: doesn't */
    GBOL is_loaded; 
    void* opaque;
    /*
     *@brief load a ini file
     *
     *@param ini_file  [in] ini file instance pointer
     *@param file_name [in] the name of ini file
     *
     *@return sucess: FRE_OK, fail: error code.
     *
     **/
     //加载一个ini文件
    GS32(*load)(struct tiny_ini_file_s* ini_file, 
			    const char* file_name);

    /*
     *@brief get the value of a key which is first key in the section.
     *
     *@param ini_file [in]  ini file instance pointer
     *@param section  [in]  the name of section
     *@param key      [in]  the name of key
     *@param value    [out] the value of the key
     *
     *@return sucess: FRE_OK, fail: error code.
     *
     *@note maybe have many same key in one section, here just get we 
     *      find the first value with "key"
     *
     **/
     //获取一个 键 对应的 值
    GS32(*getValue)(struct tiny_ini_file_s* ini_file, const char* section, 
				    const char* key, char** value);

    /*
     *@brief get all values of a key in the section.
     *
     *@param ini_file [in]  ini file instance pointer
     *@param section  [in]  the name of section
     *@param key      [in]  the name of key
     *@param values   [out] the all values of the key
     *@param count    [out] the count of values
     *
     *@return sucess: FRE_OK, fail: error code.
     *
     *@note 
     *
     **/
     //获取一个键 对应的所有值(主要是针对在一个节中有多个相同 键 名的参数对)
    GS32(*getValues)(struct tiny_ini_file_s* ini_file, 
				    const char* section, const char* key, 
				    char** values[], GU32* count);


    /*
     *@brief free values
     *
     *@param ini_file [in] ini file instance pointer
     *@param values   [in] the values which need to free
     *@param count    [in] the count of values 
     *
     *@return none.
     *
     *@note 
     *
     **/
     //释放获取到的键-值 参数对, 对应 getValues
    void(*freeValues)(struct tiny_ini_file_s* ini_file, 
				      char* values[], GU32 count);

    /*
     *@brief get a section with section name.
     *
     *@param ini_file [in]  ini file instance pointer
     *@param name     [in]  the name of section
     *@param key      [in]  the name of key
     *@param section  [out] required section pointer
     *
     *@return sucess: FRE_OK, fail: error code.
     *
     *@note 
     *
     **/
     //获取一个节
    GS32(*getSection)(struct tiny_ini_file_s* ini_file, 
				      const char* name, ini_section_t** section);

    /*
     *@brief detect if there is a key in the current section
     *
     *@param ini_file [in] ini file instance pointer
     *@param section  [in] the name of section
     *@param key      [in] the name of key
     *
     *@return have: GTRUE, haven't: GFALSE
     *
     *@note 
     *
     **/
     //判断ini文件中是否存在某个 键
    GBOL(*hasKey)(struct tiny_ini_file_s* ini_file, 
			      const char* section, const char* key);

    /*
     *@brief detect if there is a section
     *
     *@param ini_file [in] ini file instance pointer
     *@param section  [in] the name of section
     *
     *@return have: GTRUE, haven't: GFALSE
     *
     *@note 
     *
     **/
     //判断ini文件中是否存在某个 节
    GBOL(*hasSection)(struct tiny_ini_file_s* ini_file, 
					  const char* section);

    /*
     *@brief print all data in ini file
     *
     *@param ini_file [in] ini file instance pointer
     *
     *@return none
     *
     *@note 
     *
     **/
    //打印ini文件中的所有内容(除了注解之外)
    void(*dump)(struct tiny_ini_file_s* ini_file);


    /*
     *@brief release ini parser
     *
     *@param ini_file  [in]  ini file instance pointer
     *
     *@return none.
     *
     *@note 
     *
     **/
    //销毁ini 文件解析API实例 
    void(*destroy)(struct tiny_ini_file_s* ini_file);
}tiny_ini_file_t;

/***************************************************************************
 *
 * API declaration
 *
 ***************************************************************************/

//创建一个ini文件解析API的实例
GS32 TinyIniFileCreate(tiny_ini_file_t** ini_file);

#ifdef __cplusplus
}
#endif


#endif //end of __TINY_INI_FILE_H__
  • 注意

如上所示,可以看出ini文件的API都是在一个结构体中的函数指针, 之所以如此声明,其一,仿c++的类的继承和多态中的虚基类,在必要的时候完全可以重新实现这些API,其二,以这样的形式声明,使用者和实现者之间不必有编译上的依赖,比如,library core当中是一些基础功能实现的library(其中包含 ini解析),有一个library a当中,需要读取ini配置文件中的某些参数,这个时候,可以在host executable Application中创建一个ini 文件解析API的实例,然后透过load() API 去加载目标ini文件,成功之后,再透过library a的某个API(eg. a_init(tiny_ini_file_t* ini_file) ),传入到library a当中,在这个例子中,在编译阶段library a就可以完全不依赖 library core, 只需要依赖头文件 tiny_ini_file.h。

四、ini 文件解析API实现

这里主要说明其中解析过程中的几个核心函数,

1. ini文件解析:iniFileParseFile

ini file的API,load() 前半部分就只判断参数,以及打开传入的ini 文件,没什么好说的,这里要说 的第一个函数就是 iniFileParseFile,这是ini 文件解析的最顶层的函数,被 API load()调用。

函数声明为:

\\mif:ini文件API的数据结构指针
\\fp: 已经打开了一个ini文件的文件指针
\\成功:返回 G_OK, 失败: 返回相应的错误号
static GS32 iniFileParseFile(my_ini_file_t* mif, FILE* fp);

函数实现为:

static GS32 iniFileParseFile(my_ini_file_t* mif, FILE* fp)
{
    GS32 ret = G_OK;
   
   /**表示 ini文件中的行号,解析一行,就自加一次,当ini文件有错时可以用以定位 */
    GU32 line_num = 0; 
    
    /**暂存一行数据的buffer,其中LINE_BUF_MAX_LEN 在tiny_ini_file.h中定义 */
    char line_buf[LINE_BUF_MAX_LEN] = { 0 };

    while (fgets(line_buf, LINE_BUF_MAX_LEN, fp)) /** 从文件中读取一行数据 */
    {
        line_num++; /**因为行号从 1 开始,故在这里先自加,然后再解析 */
        /** 解析行 */
        ret = iniFileParseLine(mif, line_buf, strlen(line_buf), line_num);

        if (G_OK != ret)
        {
            LOGE("parse line failed\n");
            break;
        }
    }

    return ret;
}

2. 行解析:iniFileParseLine

函数声明为:

\\mif: ini文件API的数据结构指针
\\line:一行数据的起始指针
\\length:行的长度
\\line_num:行号
\\成功: 返回G_OK, 失败:返回对应的错误号
static GS32 iniFileParseLine(my_ini_file_t* mif,
							 char* line, GU32 length, GU32 line_num);

函数的实现为:

static GS32 iniFileParseLine(my_ini_file_t* mif, 
							 char* line, GU32 length, GU32 line_num)
{
    char* ptr = line;
    char* pEnd = line + length;
    GS32 ret = G_OK;

    while (ptr < pEnd)
    {
        /**跳过行首的 white space */
        while (isspace(*ptr)) ptr++; /** skip white-space  */

        char ch = *ptr;

        if (isCommentTag(ch) == GTRUE)
        {/** 判断是否为 注解 起始关键字(';'或'#'),如果是,就直接跳过这一整行 */
            /** skip component */
            ptr = pEnd;
        }
        else if (ch == '[')
        {/** 判断是否为 节 的起始关键字符,如果是解析 节,主要是解析节的名字 */
            /** ++ indicate skip character '[' */
            ptr = iniFileParseSection(mif, ++ptr, pEnd, line_num, &ret);
        }
        else if (ch == '\0') /** line end */
        {/** 如果是遇到'\0',表示行结束,什么也不用做 */
            /**do nothing */
        }
        else
        {
        /** 这里就是开始解析 参数 对 */
            if (mif->section_count == 0)
            {/**如果在解析第一个参数对的时候,还没有遇到 节 ,那有可能就是没有 
              * 节或者,后面才有节,所以在这里将创建一个默认的节来包含所有不在 
              * 节范围来的参数对。其中DEFAULT_SECTION_NAME是在头文件中定义的,
              * 需要说明的是:不能在ini文件中出现和默认节名相同的节名,这是不被
              * 允许的。
              */
                /** alloc default section */
                ini_section_t* isec = iniFileNewSection(DEFAULT_SECTION_NAME);

                if (isec)
                {
                    /** 将节添加到节列表中 */
                    ret = iniFileAddSection(mif, isec);

                    if (G_OK != ret)
                    {
                        LOGE("add section to list failed(0x%08x)\n", ret);
                        free(isec);
                        break;
                    }
                }
                else
                {
                    ret = G_ErrorInsufficientResources;
                    break;
                }
            }
           
           /** 解析参数对,其中iniFileGetNewestSection(mif),
             * 表示参数对,都是解析到就近节当中
             */
           /**
            * current section aways is the newest section ?
            * TODO, this case is aways right?
            */
            ptr = iniFileParseParameter(iniFileGetNewestSection(mif),
                                        ptr, pEnd, line_num, &ret);
        }
    }

    return ret;
}

3. 节(名)解析:iniFileParseSection

函数声明为:

\\mif: ini文件API的数据结构指针
\\line:一行数据的起始指针
\\line_end:一行数据的末尾指针
\\line_num:行号
\\ret:函数的返回值,成功:G_OK。失败:错误号

\\函数返回 节解析之后的,行数据指针,如 == line_end,表示当前行解析结束

static char* iniFileParseSection(my_ini_file_t* mif, char* line, 
                                 char* line_end, GU32 line_num, GS32* ret);

函数实现:

static char* iniFileParseSection(my_ini_file_t* mif, char* line, 
                                 char* line_end, GU32 line_num, GS32* ret)
{
    char* ptr = line;
    char* pEnd = line_end;
    char* section = strchr(ptr, ']'); /**寻找结束标识符 */
    /** process section name */

    if (section) /**不为空,表示是有效的节,为空,语法错误,将结束解析 */
    {
        //char temp = *section;
        //*section = '\0';

        /** 判断列表中是否已经存在当前节,如果存在,
          * 语法错误,因为语法规定不能存在名字相同的节 
          */
        if (iniFileHasSection_l(mif, ptr) == GTRUE)
        {
            LOGE("syntax error[line:%d]: section(%s) already "
                 "exist, end parse \n", line_num, ptr);
            ptr = pEnd;
            *ret = G_ErrorUndefined;
        }
        else
        {
            char* pLeft = ptr;
            char* pRight = section - 1;

            /** trim left white-space */
            while (isspace(*pLeft) && *pLeft) pLeft++;

            /** trim right white-space */
            while (isspace(*pRight) && pRight >= pLeft) pRight--;

            *(pRight + 1) = '\0';
            
            //创建 节
            ini_section_t* isec = iniFileNewSection(pLeft);

            if (isec)
            {
                //将节加入到列表当中
                GS32 result = iniFileAddSection(mif, isec);

                if (G_OK == result)
                {
                    ptr = section + 1; /** + 1 indicate skip character ']' */
                }
                else
                {
                    free(isec); /** release resource */
                    LOGE("add secition(%s) to list failed(0x%08x)\n", 
	                    pLeft, *ret);
                    ptr = pEnd;
                    *ret = result;
                }
            }
            else
            {
                LOGE("create section(%s) failed\n", pLeft);
                ptr = pEnd;
                *ret = G_ErrorInsufficientResources;
            }
        }
    }
    else
    {
        LOGE("syntax error[line:%d]: section doesn't end with ']'\n", 
	        line_num);
        ptr = pEnd;
        *ret = G_ErrorUndefined;
    }

    return ptr;
}

4. 键-值 参数对解析:iniFileParseParameter

函数声明为:


\\mif: ini文件API的数据结构指针
\\line:一行数据的起始指针
\\line_end:一行数据的末尾指针
\\line_num:行号
\\ret:函数的返回值,成功:G_OK。失败:错误号

\\函数返回 节解析之后的,行数据指针,如 == line_end,表示当前行解析结束

static char* iniFileParseParameter(ini_section_t* section, char* line,

                                   char* line_end,GU32 line_num, GS32* ret)

函数实现为:

static char* iniFileParseParameter(ini_section_t* section, char* line,
                                   char* line_end,GU32 line_num, GS32* ret)
{
    char* ptr = line;
    char* pEnd = line_end;

	/**寻找 参数对中 键与值之间的各个标识符 '=' */
    char* key_end = strchr(ptr, '='); 

    if (key_end)/** 如果存在,就分别解析'='两边的键和值 */
    {
        char* pRight = key_end - 1;

        /**去除,键 与 '=' 之间的white-space */
        /** strip right white-space */
        while (isspace(*pRight) && pRight >= ptr) pRight--;

        GU32 key_len = (pRight + 1) - ptr;
        char* key = ptr; /** 获取键的起始指针以及长度 */

        char* value = key_end + 1; /** + 1 indicate skip character '=' */

        /** 去除 值 与 '=' 之间的white-space */
        /** strip left white-space */
        while (isspace(*value) && value < pEnd) value++;


        if (value < pEnd) 
        {/** 如果值的指针 < line end,表示可能有有效的"值",否则语法错误 */
            /** find value end */
            char* value_end = value;
  
            /** 寻找 值的 结束位置,条件是遇到 white-space或者
             * 注解标识符或者line end就截止 
             */
            /** strip ritght white-space and comment 
              * after the value of parameter 
              */
            while ((!isspace(*value_end) && 
		           (isCommentTag(*value_end) == GFALSE)) &&
                   (value_end < pEnd))
            {
                value_end++;
            }

            if (value_end > value)
            {/** 如果 值的 结束位置 > 值的起始位置,表示有有效的 值 */
                GU32 value_len = value_end - value;
                /** 创建一个键-值参数对的item */
                key_value_pair_t* pair = iniFileNewParameter(key, 
											                key_len, 
											                value, 
											                value_len);

                if (pair)
                {
                    /** 将键-值 参数对加入到列表中 */
                    iniFileAddParameter(section, pair);

                    ptr = value_end;
                }
                else
                {
                    ptr = pEnd;
                    *ret = G_ErrorInsufficientResources;
                }
            }
            else
            {
                LOGE("syntax error: the value of parameter "
                     "is empty(right of \'=\')\n");
                ptr = pEnd;
                *ret = G_ErrorUndefined;
            }

        }
        else
        {
            LOGE("syntax error[line:%d]: the value of parameter "
                 "is empty(right of \'=\')\n", line_num);
            ptr = pEnd;
            *ret = G_ErrorUndefined;
        }

      
    }
    else
    {
        LOGE("syntax error[line:%d]: not complete key, doesn't "
             "find end character \'=\' \n", line_num);
        ptr = pEnd;
        *ret = G_ErrorUndefined;
    }

    return ptr;
}

五、使用例子

  • 建立一个名为my_test.ini的ini文件,其内容如下:
#这是一个测试用的ini文件
;这个ini文件包含默认 节,自定义节,参数对等
app_name = ini_file_parse_test #程序的名字
app_version = 0.0.1 ;程序版本


[test_section]
test_key = test_value1
test_key = test_value2
other_key= something_else
test_key = test_value3
test_key = test_value4

  • 编写测试程序
#include <stdio.h>
#include "tiny_ini_file.h"

#define LOGD printf
#define LOGE printf

#define TEST_INI_FILE_NAME "my_test.ini"

int main(int argc, char *argv[])
{
	GS32 ret = G_OK;
	tiny_ini_file_t* ini_file = NULL;
	
	ret = TinyIniFileCreate(&ini_file);
	
	if (G_OK == ret)
	{
		ret = ini_file->load(ini_file, TEST_INI_FILE_NAME);
		
		if (G_OK == ret)
		{
			char* value = NULL;
			
			ini_file->dump(ini_file); /** print the content of ini file */
			
			LOGD("=========this is cut-off rule===============\n\n");
			LOGD("start test get value and get values:\n\n");
			ret = ini_file->getValue(ini_file, 
									DEFAULT_SECTION_NAME, 
									"app_name", 
									&value);
			
			if (G_OK == ret)
			{
				LOGD("section: %s, key: %s, value:%s\n", 
				     DEFAULT_SECTION_NAME, "app_name", value);
			}
			
			{
				GU32 i;
				char** values = NULL;
				GU32  values_cnt = 0;
				
				ret = ini_file->getValues(ini_file, 
										 "test_section", 
										 "test_key", 
										 &values, &values_cnt);
				
				if (G_OK == ret)
				{
					for (i = 0; i < values_cnt; i++)
					{
						LOGD("section: %s, key: %s, value[%d]:%s\n", 
							"test_section", "test_key", i, values[i]);
					}
					
					/** when use up, valuse, 
					 *  need do free with API freeValues() 
					 */
					 ini_file->freeValues(ini_file, values, values_cnt);
				}
				
			}			
			
		}
		else
		{
			LOGE("load ini file(%s) failed(0x%08x)\n", 
				 TEST_INI_FILE_NAME, ret);
		}
	}
	else
	{
		LOGE("create ini file instance failed(0x%08x)\n", ret);
	}
	
	if (ini_file)
	{
		ini_file->destroy(ini_file);
	}   

    return 0;
}


六、完整源代码

https://gitee.com/xuanwolanxue/Tiny-ini-file-parser

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值