cJSON Note(3):字符串解析

引言(Introduction)

写在文章开头的一句话,不积硅步,无以致千里;不积小流,无以成江海。

第二部分应用篇对于普通的应用场景已经足够了,但是还是缺少了字符串解析的功能,并且字符串解析也是cJSON中非常重要的模块。解析字符串只有几个函数,并且函数内容也比较简单,所以本文更注重其源码的实现。

1. 实例(Example)

在分析源码之前,先使用cJSON提供的接口来解析字符串,下面是一个实例:

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

// {
//     "name": "Zhang San",    // string
//     "sex": 1,               // boolen
//     "height": 1.8
//     "family": [
//         {
//             "name": "Zhang Si",
//             "relationship": "Father"
//         },
//         {
//             "name": "Li Si",
//             "relationship": "Mother"
//         }
//     ],
//     "birthday": {
//         "year": 2000,
//         "month": 1,
//         "day":1
//     },
// }

int CJSON_CDECL main(void){
    
    const char *json = "{\"name\":\"Zhang San\", \"sex\":true, \"height\":1.8,\
        \"family\":[{\"name\":\"Zhang Si\",\"relationship\":\"Father\"},\
        {\"name\":\"Li Si\",\"relationship\":\"Mother\"}],\
        \"brithday\":{\"year\":2000, \"month\":1, \"day\":1}}";
    char *output = NULL;
    cJSON *root = NULL;

    root = cJSON_Parse(json);

    output = cJSON_Print(root);
    printf("%s\n", output);

    free(output);
    cJSON_Delete(root);
    return 0;
}

上面例子中只使用到了接口"cJSON_Parse",其将json指向的字符串解析到cJSON指针root指向的结构体中。被解析的字符串需要使用一定的格式,这边根据源码直接给出注意事项:

  • 解析的对象是字符串,即C/C++中的字符串;
  • 一个json结构需要以 ’ { ’ 为开头,以 ’ } ’ 结束;
  • 一个json数组需要以 ’ [ ’ 为开头,以 ’ ] ’ 结束;
  • 键值对之间用 ’ : ’ 表示附属关系;
  • 字符串的值需要以 \" 为开头,同时以 \" 结束
  • 取消转义字符需要在转义字符前面添加 \\ ;
  • 数字型的值不需要使用双引号等特别注明,只需要写明数字即可。

2. 源码(Source code)

cJSON提供给用户的接口有"cJSON_Parse",“cJSON_ParseWithLength”,“cJSON_ParseWithOpts”,“cJSON_ParseWithLengthOpts”。不管调用哪个接口,最终都会调用到"cJSON_ParseWithLengthOpts"函数。

在这里插入图片描述

下面直接给出前三者的函数声明以及函数定义部分:

  • “cJSON_Parse()”

    //	cJSON.h
    //	该函数为默认参数版本,即不返回解析末端以及不检查'\0'是否是作为结尾。
    CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
    
    //	cJSON.c
    CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value)
    {
        return cJSON_ParseWithOpts(value, 0, 0);
    }
    
  • “cJSON_ParseWithOpts()”

    //	cJSON.h
    //	该函数为选项解析版本,可以返回解析末端的字符,以及是否检查解析结果的结尾是否为'\0'
    CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
    
    //	cJSON.c
    CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated)
    {
        // buffer_length用于存储解析结果的长度
        size_t buffer_length;
    
        if (NULL == value)
        {
            return NULL;
        }
    
    	// 在结尾添加空字符的大小
        buffer_length = strlen(value) + sizeof("");
    
        return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated);
    }
    
  • “cJSON_ParseWithLength()”

    //	cJSON.h
    //	该函数为按照给定的长度对字符串进行解析
    CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
    
    //	cJSON.c
    CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length)
    {
        return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0);
    }
    

在正式介绍"cJSON_ParseWithLengthOpts"的源码之前,需要先介绍使用到的结构体:

  • “parse_buffer”

    // 用于存放解析过程中的信息的结构体
    typedef struct
    {
        const unsigned char *content;		// 被解析的字符串
        size_t length;					   // 被解析的字符串长度
        size_t offset;					   // 记录当前解析的位置
        size_t depth;					   // 记录当前解析结果的深度
        internal_hooks hooks; 			    // 用于分配内存与释放内存的钩子
    } parse_buffer;
    
  • “internal_hooks”

    // 用于分配内存与释放内存的钩子
    typedef struct internal_hooks
    {
        void *(CJSON_CDECL *allocate)(size_t size);						// 内存分配的函数指针
        void (CJSON_CDECL *deallocate)(void *pointer);					// 内存析构的函数指针
        void *(CJSON_CDECL *reallocate)(void *pointer, size_t size);	 // 内存重新分配的函数指针
    } internal_hooks;
    static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc };
    															    // "cJSON.c"文件使用的全局变量,用于对cJSON实例分配内存
    
  • “error”

    // 包含错误信息的结构体
    typedef struct {
        const unsigned char *json;				// 指向错误信息的字符串指针
        size_t position;					   // 错误信息的位置
    } error;
    static error global_error = { NULL, 0 };	 // "cJSON.c"文件使用的全局变量,用于存储各个函数发生错误的信息
    

最后终于可以介绍本文的重头戏了,"cJSON_ParseWithLengthOpts"的源码如下:

//	cJSON.h
//	该函数为提供了所有选项的字符串解析版本,包括解析长度,返回解析末端信息,使用'\0'作为结尾
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);

//	cJSON.c
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated)
{
    //  param: value, the string to be parsed;
    //  param: buffer_length, the length of the string to be parsed;
    //  param: return_parse_end, the char* store the end character of the result;
    //  param: require_null_terminated, check for a null terminator
    parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } };
    cJSON *item = NULL;

    // 重置错误信息
    global_error.json = NULL; 
    global_error.position = 0;

    if (value == NULL || 0 == buffer_length)
    {
        goto fail;
    }
    
    // 将内容存放到buffer
    buffer.content = (const unsigned char*)value;
    buffer.length = buffer_length; 
    buffer.offset = 0;
    buffer.hooks = global_hooks;

    // 创建一个新对象
    item = cJSON_New_Item(&global_hooks);
    if (item == NULL)
    {
        goto fail;
    }

    // 对buffer开始进行解析,下文会对此进行详细介绍
    if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer))))
    {
        goto fail;
    }

    // 如果设置了require_null_terminated,那么需要检查末端字符是否为'\0'
    if (require_null_terminated)
    {
        buffer_skip_whitespace(&buffer);
        if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0')
        {
            goto fail;
        }
    }
    if (return_parse_end)
    {
        // 将末端字符存储到return_parse_end中
        *return_parse_end = (const char*)buffer_at_offset(&buffer);
    }

    return item;

fail:
    if (item != NULL)
    {
        cJSON_Delete(item);
    }

    if (value != NULL)
    {
        error local_error;
        local_error.json = (const unsigned char*)value;
        local_error.position = 0;

        if (buffer.offset < buffer.length)
        {
            local_error.position = buffer.offset;
        }
        else if (buffer.length > 0)
        {
            local_error.position = buffer.length - 1;
        }

        // 将错误信息存储到return_parse_end
        if (return_parse_end != NULL)
        {
            *return_parse_end = (const char*)local_error.json + local_error.position;
        }

        global_error = local_error;
    }

    return NULL;
}

可以看到,"cJSON_ParseWithLengthOpts()“的核心函数是"parse_value()”,在此之前,还调用了"buffer_skip_whitespace()"与"skip_utf8_bom()"函数:

  • “buffer_skip_whitespace()”

    // 跳过buffer中ASI码小于等于空格的字符,这里是只跳过位于buffer开端处的字符
    static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer)
    {
        if ((buffer == NULL) || (buffer->content == NULL))
        {
            return NULL;
        }
        
        // cannot_access_at_index检查某个是否无法访问
        if (cannot_access_at_index(buffer, 0))
        {
            return buffer;
        }
    
        while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32))
        {
           // 持续跳过buffer中ASI码小于等于空格的字符
           buffer->offset++;
        }
    
        if (buffer->offset == buffer->length)
        {
            buffer->offset--;
        }
    
        return buffer;
    }
    
  • “skip_utf8_bom()”

    // 跳过缓冲区开头的UTF-8 字节顺序标注
    static parse_buffer *skip_utf8_bom(parse_buffer * const buffer)
    {
        if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0))
        {
            return NULL;
        }
    
        if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0))
        {
            // 跳过buffer前端的"\xEF\xBB\xBF"三个字符
            buffer->offset += 3;
        }
    
        return buffer;
    }
    

"parse_value()“中根据解析对象的类型,调用了不同的子函数如"parse_string”,“parse_number”,“parse_array"与"parse_object”,这几个函数分别解析字符串,数字,json数组以及json对象。这可以根据"parse_value()"的实现方法看到:

// 字符串解析的核心公式,将buffer里面的content解析到item中去
static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer)
{
    if ((input_buffer == NULL) || (input_buffer->content == NULL))
    {
        return false;
    }

    /* 解析不同类型的对象,每次解析都要改变buffer中offset,使其指向当前解析的位置 */
    /* null */
    if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0))
    {
        // null对象需要使用字符串"null"
        item->type = cJSON_NULL;
        input_buffer->offset += 4;
        return true;
    }
    /* false */
    if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0))
    {
        // false对象需要使用字符串"false"
        item->type = cJSON_False;
        input_buffer->offset += 5;
        return true;
    }
    /* true */
    if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0))
    {
        // ture对象需要使用字符串"true"
        item->type = cJSON_True;
        item->valueint = 1;
        input_buffer->offset += 4;
        return true;
    }
    /* string */
    if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"'))
    {
        // string对象需要以'\"'为开头,以'\"'为结束
        return parse_string(item, input_buffer);
    }
    /* number */
    if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9'))))
    {
        // number对象需要以'-'或者'0'~'9'为开头
        return parse_number(item, input_buffer);
    }
    /* array */
    if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '['))
    {
        // array对象需要以'['开头,以']'结束
        return parse_array(item, input_buffer);
    }
    /* object */
    if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{'))
    {
        // object对象需要以'{'开头,以'}'结束
        return parse_object(item, input_buffer);
    }
    return false;
}

下面将分别分析"parse_string",“parse_number”,"parse_array"与"parse_object"的源码:

  • “parse_string”

    // 用于解析字符串作为键或者值,这部分可以分成两部分来看,一部分用于找到有效字符并分配内存,一部分用于在该内存中复制字符
    static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer)
    {
        // input_pointer用于指向当前复制字符的位置
        const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1;
        // input_end用于指向当前字符串的结尾
        const unsigned char *input_end = buffer_at_offset(input_buffer) + 1;
        // output_pointer用于指向当前复制字符的目的地地址
        unsigned char *output_pointer = NULL;
        // output用于指向复制的字符串目的地的其实地址
        unsigned char *output = NULL;
    
        // 字符串需要以 '\"' 为开头,同时以 '\"' 为结束
        if (buffer_at_offset(input_buffer)[0] != '\"')
        {
            goto fail;
        }
    
        {
            // 第一部分,估计有效字符串需要的内存大小,为预估计
            size_t allocation_length = 0;
            size_t skipped_bytes = 0;
            while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"'))
            {
                // 使用input_end遍历input_buffer,找到字符串的终点
                if (input_end[0] == '\\')
                {
                    // 跳过取消转义字符
                    if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length)
                    {
                        goto fail;
                    }
                    skipped_bytes++;
                    input_end++;
                }
                input_end++;
            }
            if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"'))
            {
                // 如果input_end读进来的长度长于缓冲区,或者字符串不以 '\"' 结束,则发生错误
                goto fail;
            }
    
            // allocation_length是最少应该分配的字节长度,为字符串长度减去转义字符的数量,此值是往大的估计
            allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes;
            // output指向分配的内存块
            output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof(""));
            if (output == NULL)
            {
                goto fail;
            }
        }
    
        output_pointer = output;
        while (input_pointer < input_end)
        {
            // 第二部分,循环进行复制赋值
            if (*input_pointer != '\\')
            {
                // 如果不是取消转义字符
                *output_pointer++ = *input_pointer++;
            }
            else
            {
                unsigned char sequence_length = 2;
                if ((input_end - input_pointer) < 1)
                {
                    goto fail;
                }
    
                switch (input_pointer[1])
                {
                    case 'b':
                        *output_pointer++ = '\b';
                        break;
                    case 'f':
                        *output_pointer++ = '\f';
                        break;
                    case 'n':
                        *output_pointer++ = '\n';
                        break;
                    case 'r':
                        *output_pointer++ = '\r';
                        break;
                    case 't':
                        *output_pointer++ = '\t';
                        break;
                    case '\"':
                    case '\\':
                    case '/':
                        *output_pointer++ = input_pointer[1];
                        break;
                    case 'u':
                        // 将utf16字面值转化为utf8字面值
                        sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer);
                        if (sequence_length == 0)
                        {
                            goto fail;
                        }
                        break;
    
                    default:
                        goto fail;
                }
                input_pointer += sequence_length;
            }
        }
    
        // 字符串后面添加 '\0' 作为结束
        *output_pointer = '\0';
    
        item->type = cJSON_String;
        item->valuestring = (char*)output;
    
        input_buffer->offset = (size_t) (input_end - input_buffer->content);
        input_buffer->offset++;
    
        return true;
    
    fail:
        if (output != NULL)
        {
            input_buffer->hooks.deallocate(output);
        }
    
        if (input_pointer != NULL)
        {
            // 将buffer中的偏移量重置
            input_buffer->offset = (size_t)(input_pointer - input_buffer->content);
        }
    
        return false;
    }
    
  • “parse_number”

    // 解析字符串中的数字
    static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer)
    {
        double number = 0;                                  // 存储数字字符串转化之后的数字
        unsigned char *after_end = NULL;                    // 指向number_c_string中字符数字的最后一位
        unsigned char number_c_string[64];                  // 存储有效数字的数组,可见数字最大为10E63,但后续还有个clip函数
        unsigned char decimal_point = get_decimal_point();	// 获取小数点的字符
        size_t i = 0;
    
        if ((input_buffer == NULL) || (input_buffer->content == NULL))
        {
            return false;
        }
        for (i = 0; (i < (sizeof(number_c_string) - 1)) && can_access_at_index(input_buffer, i); i++)
        {
            // 逐个字节读取数字
            switch (buffer_at_offset(input_buffer)[i])
            {
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '+':
                case '-':
                case 'e':
                case 'E':
                    number_c_string[i] = buffer_at_offset(input_buffer)[i];
                    break;
    
                case '.':
                    number_c_string[i] = decimal_point;
                    break;
    
                default:
                    goto loop_end;
            }
        }
    loop_end:
        number_c_string[i] = '\0';
    
        // 将字符串数字转化为浮点型数字类型
        number = strtod((const char*)number_c_string, (char**)&after_end);
        if (number_c_string == after_end)
        {
            return false;
        }
    
        item->valuedouble = number;
    
        // 将整形数字转化为规定范围
        if (number >= INT_MAX)
        {
            item->valueint = INT_MAX;
        }
        else if (number <= (double)INT_MIN)
        {
            item->valueint = INT_MIN;
        }
        else
        {
            item->valueint = (int)number;
        }
    
        item->type = cJSON_Number;
    
        input_buffer->offset += (size_t)(after_end - number_c_string);
        return true;
    }
    
  • “parse_array”

    // 解析json数组,该数组以链表的形式实现
    static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer)
    {
        // array字符串的格式为: "[" + array + "]"
        cJSON *head = NULL;
        cJSON *current_item = NULL;
    
        if (input_buffer->depth >= CJSON_NESTING_LIMIT)
        {
            // 解析的深度大于约束值
            return false;
        }
        input_buffer->depth++;
    
        if (buffer_at_offset(input_buffer)[0] != '[')
        {
            goto fail;
        }
    
        input_buffer->offset++;
        buffer_skip_whitespace(input_buffer);	// 跳过左中括号后面的空格
        if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']'))
        {
            // 如果该array为空
            goto success;
        }
    
        if (cannot_access_at_index(input_buffer, 0))
        {
            input_buffer->offset--;
            goto fail;
        }
    
        input_buffer->offset--;
        do
        {
            // 创建新的cJSON节点
            cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks));
            if (new_item == NULL)
            {
                goto fail;
            }
    
            if (head == NULL)
            {
                // 如果头节点为空,将新节点当作头节点
                current_item = head = new_item;
            }
            else
            {
                // 如果头节点不为空,将新节点当作当前节点
                current_item->next = new_item;
                new_item->prev = current_item;
                current_item = new_item;
            }
    
            input_buffer->offset++;
            buffer_skip_whitespace(input_buffer);
            // 递归调用parse_value
            if (!parse_value(current_item, input_buffer))
            {
                goto fail;
            }
            buffer_skip_whitespace(input_buffer);
        }
        while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ','));
    
        if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']')
        {
            goto fail;
        }
    
    success:
        input_buffer->depth--;
    
        if (head != NULL) {
            head->prev = current_item;
        }
    
        item->type = cJSON_Array;
        item->child = head;
    
        input_buffer->offset++;
    
        return true;
    
    fail:
        if (head != NULL)
        {
            cJSON_Delete(head);
        }
    
        return false;
    }
    
    
  • “parse_object”

    // 解析一个json对象,里面的元素也是以链表的形式实现
    static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer)
    {
        // object的字符串格式为: "{" + array + "}"
        cJSON *head = NULL;
        cJSON *current_item = NULL;
    
        if (input_buffer->depth >= CJSON_NESTING_LIMIT)
        {
            // 解析的深度大于约束值
            return false;
        }
        input_buffer->depth++;
    
        if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{'))
        {
            goto fail;
        }
    
        input_buffer->offset++;
        buffer_skip_whitespace(input_buffer);
        if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}'))
        {
            goto success;
        }
    
        if (cannot_access_at_index(input_buffer, 0))
        {
            input_buffer->offset--;
            goto fail;
        }
    
        input_buffer->offset--;
        do
        {
            // 创建新的节点
            cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks));
            if (new_item == NULL)
            {
                goto fail;
            }
    
            if (head == NULL)
            {
                // 如果头节点为空,将新节点当作头节点
                current_item = head = new_item;
            }
            else
            {
                // 如果头节点不为空,将新节点当作当前节点
                current_item->next = new_item;
                new_item->prev = current_item;
                current_item = new_item;
            }
    
            // 解析字符串作为该item的键名
            input_buffer->offset++;
            buffer_skip_whitespace(input_buffer);
            if (!parse_string(current_item, input_buffer))
            {
                goto fail;
            }
            buffer_skip_whitespace(input_buffer);
    
    	    // 将解析的结果当作该item的键名
            current_item->string = current_item->valuestring;
            current_item->valuestring = NULL;
    
            // 键值对之间需要用":"
            if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':'))
            {
                goto fail;
            }
    
            // 解析值
            input_buffer->offset++;
            buffer_skip_whitespace(input_buffer);
            if (!parse_value(current_item, input_buffer))
            {
                goto fail;
            }
            buffer_skip_whitespace(input_buffer);
        }
        while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ','));
    
        if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}'))
        {
            goto fail;
        }
    
    success:
        input_buffer->depth--;
    
        if (head != NULL) {
            head->prev = current_item;
        }
    
        item->type = cJSON_Object;
        item->child = head;
    
        input_buffer->offset++;
        return true;
    
    fail:
        if (head != NULL)
        {
            cJSON_Delete(head);
        }
    
        return false;
    }
    

3. 总结(Conclusion)

cJSON解析字符串的源码并不复杂,语法也比较简单,主要难点在于字符的原子处理,并且还需要考虑不同的编码方式(UTF-16 to UTF-8)。如果单纯为了使用的话,使用作者提供的接口就可以了,但是通过阅读源码,可以发现被解析的字符串的格式要求,也能学习到作者的编程思维与技巧,这些都是非常宝贵的。

参考资料(Reference)

cjson-sourceforge

cjson-github

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值