cJson之parse_number(二)

1,405 阅读6分钟

cJson之环境搭建(一) 快速搭建cJson项目环境

对Json数据的解析其实就是对字符串的处理,根据类型将它们变成cJson结构体,这个数据结构是项目的核心,它是如何表达Json数据的将在后面介绍,当前只了解如何将解析的值写到cJson中即可

typedef struct cJSON
{
    // array、object类型的cJson可能有前后节点
    struct cJSON *next;
    struct cJSON *prev;
    // array、object类型的cJson可能有子节点
    struct cJSON *child;

    // 这个cJson的类型,数字、字符串、布尔值、数组等
    int type;

    // 当类型是cJSON_String 或 cJSON_Raw 时的字符串值
    char *valuestring;
    // int类型值 最好使用cJSON_SetNumberValue设置 */
    int valueint;
    //当类型是cJSON_Number的浮点值,也同时会给valueint赋值 */
    double valuedouble;

    // Json对象中子项的名字,如 {"width": 1280,"height": 720}中的with,height
    char *string;
} cJSON;

在项目搭建的时候,也建立了测试环境,其好处在于,所有人可以补充测试用例去验证这个功能。前面使用了tests目录下的parse_number.c文件,我们就从这个开始

define 宏替换

无论是common.h头文件还是cJson.h头文件中使用define都定义了很多宏

#define 名字 文本内容

#define assert_has_no_const_string(item) TEST_ASSERT_BITS_MESSAGE(cJSON_StringIsConst, 0, item->type, "Item should not have a const string.")

#define assert_has_valuestring(item) TEST_ASSERT_NOT_NULL_MESSAGE(item->valuestring, "Valuestring is NULL.")

#define assert_has_no_valuestring(item) TEST_ASSERT_NULL_MESSAGE(item->valuestring, "Valuestring is not NULL.")

#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))

这个和函数调用是不一样的,我们调用这些宏其实是一种文本替换,在预编译的时候完成,比如我们调用了cJSON_SetNumberValue宏来设置数值

int main(void) 
{
    cJSON* item = cJSON_CreateNumber(20);
    cJSON_SetNumberValue(item, 10);
    return 0;
}

之前通过cmake编译的时候,其实中间有很多命令,最后只生成了可执行文件,看不到预编译的这个中间文件,所以需要我们手动执行一下,通过c预处理器(cpp)命令将源文件转换成ASCII码的中间文件

cpp main.c main.i

image.png 打开main.i文件查看main函数部分,去除一些注释,结果就是文本替换加传递参数

# 5 "main.c"
int main(void) {
    cJSON* item = cJSON_CreateNumber(20);
    ((item != ((void *)0)) ? cJSON_SetNumberHelper(item, (double)10) : (10));
    return 0;
}

当你对宏定义有疑惑的时候,这个cpp命令很有用,让你可以对预编译的结果查看验证

parse_number

整个parse_number.c文件都是在测试parse_number这个函数,从其中各种测试用例我们也能大致知道可能遇到的数字类型:整数、小数、科学计数,下面是核心代码,两点说明一下

  • parse_buffer用content来存储要解析的数据,length来存储其长度,其它不用管
  • TEST_ASSERT_XXX函数是unity框架提供的,用来断言的
static void assert_parse_number(const char *string, int integer, double real)
{
    parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } };
    buffer.content = (const unsigned char*)string;
    buffer.length = strlen(string) + sizeof("");

    TEST_ASSERT_TRUE(parse_number(item, &buffer));
    // 对cJson的综合判断 可以先忽略
    assert_is_number(item);
    TEST_ASSERT_EQUAL_INT(integer, item->valueint);
    TEST_ASSERT_EQUAL_DOUBLE(real, item->valuedouble);
}
// 以下是部分测试用例
static void parse_number_should_parse_positive_integers(void)
{
    assert_parse_number("1", 1, 1);
    assert_parse_number("32767", 32767, 32767.0);
    assert_parse_number("2147483647", (int)2147483647.0, 2147483647.0);
}

static void parse_number_should_parse_positive_reals(void)
{
    assert_parse_number("0.001", 0, 0.001);
    assert_parse_number("10e-10", 0, 10e-10);
    assert_parse_number("10E-10", 0, 10e-10);
    assert_parse_number("10e10", INT_MAX, 10e10);
    assert_parse_number("123e+127", INT_MAX, 123e127);
    assert_parse_number("123e-128", 0, 123e-128);
}

上面的测试用例你可能发现,既有整数也有小数部分,这是因为parse_number统一将字符串解析成小数,同时保存小数和整数,函数主要内容如下:

  • 将要解析的字符串挨个复制到数组中,长度上限是64,遇到非数字字符终止复制过程;之所以挨个复制而不是整体复制是因为:在整个Json中,数字字符串是其中一部分,还有其它内容,不像测试用例那样全是数字字符串
  • 虽然字面上的小数点是一个点号".",但有的语言,如法语、南非语、丹麦语、德语、希腊语、部分西班牙语中,小数点是逗号。所以需要替换成本地化的小数点,方便库函数解析
  • 转换的方法调用的是strtod函数,将字符串转换为double类型浮点值

double strtod(const char *nptr, char **endptr);

strtod()会扫描参数nptr字符串,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,到出现非数字或字符串结束时('\0')才结束转换,并将结果返回。

static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer)
{
    double number = 0;
    unsigned char *after_end = NULL;
    unsigned char number_c_string[64];
    unsigned char decimal_point = get_decimal_point();
    size_t i = 0;

    if ((input_buffer == NULL) || (input_buffer->content == NULL))
    {
        return false;
    }
    // can_access_at_index用来判断是否会访问越界
    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';
    // 第二个参数是二级指针,用来给after_end赋值
    number = strtod((const char*)number_c_string, (char**)&after_end);
    // 如果解析失败,第一个参数的值就会赋给after_end
    if (number_c_string == after_end)
    {
        return false; 
    }

    item->valuedouble = number;

   // 处理int值 考虑极端情况,这部分内容被抽取到cJSON_SetNumberValue函数中了,但这里没有直接掉用
    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;
    // 记录偏移量,完整的Json解析还要往后继续解析,测试用例用不上
    input_buffer->offset += (size_t)(after_end - number_c_string);
    return true;
}

strtod里面涉及到二级指针,先说一下指针,

  • 指针可以理解为内存地址
  • 一级指针存放的是普通类型数据,如int,如下图a_pointer就是一级指针,存放了整数10

改变a的值有两种方法,

  • 直接改就是a = 20
  • 间接改就是*a_pointer = 20,意思是将地址0x4521存储的值改成20
int a = 10;
int *a_pointer = &a;
int **a_double_pointer = &apointer

image.png

二级指针存放的其它指针的地址,a_double_pointer就是二级指针,其放的就是指针a_pointer的地址。

after_end是char类型指针,after_end声明后如下,虽然此时指向的指针为空,但储存这个NULL的空间地址0x2233是存在的,就像房子是空的和没房子是两码事

image.png

所以传给strtod的第二个参数就是0x2233,有个这个地址就好办了,比如解析"1.2"结束后,就可以把结束位置0x1237存在0x2233这个存储地址,如果解析失败了把首地址0x1234放进去

image.png

如果解析成功,after_end的值就是number_c_string的结束位置0x1237 image.png

测试方法是简单直白的,基本就那几个模板,比较容易使用。

TEST_ASSERT_TRUE(parse_number(item, &buffer));
TEST_ASSERT_EQUAL_INT(integer, item->valueint);
TEST_ASSERT_EQUAL_DOUBLE(real, item->valuedouble);

测试用例如果需要时可以自己添加的,比如源码中就没有对解析失败的用例,可以自己尝试添加测试

static void assert_parse_non_number(const char *string)
{
    parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } };
    buffer.content = (const unsigned char*)string;
    buffer.length = strlen(string) + sizeof("");

    TEST_ASSERT_FALSE(parse_number(item, &buffer));
}

 static void parse_number_fail_parse(void)
{
    assert_parse_non_number("abc");
    assert_parse_non_number("a1234");
    assert_parse_non_number("oxff");
}

image.png

写测试用例也是对代码的另一个维度思考,尤其是异常场景的用例会让代码更健壮,也能体现思维的广度