cJson之parse_number(二)

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

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

c 复制代码
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 名字 文本内容

scss 复制代码
#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宏来设置数值

ini 复制代码
int main(void) 
{
    cJSON* item = cJSON_CreateNumber(20);
    cJSON_SetNumberValue(item, 10);
    return 0;
}

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

cpp main.c main.i

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

arduino 复制代码
# 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框架提供的,用来断言的
scss 复制代码
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')才结束转换,并将结果返回。

c 复制代码
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
ini 复制代码
int a = 10;
int *a_pointer = &a;
int **a_double_pointer = &apointer

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

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

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

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

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

scss 复制代码
TEST_ASSERT_TRUE(parse_number(item, &buffer));
TEST_ASSERT_EQUAL_INT(integer, item->valueint);
TEST_ASSERT_EQUAL_DOUBLE(real, item->valuedouble);

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

arduino 复制代码
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");
}

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

相关推荐
奔跑吧邓邓子3 小时前
【C语言实战(8)】C语言循环结构(do-while):解锁编程新境界
c语言·实战·do-while
小莞尔3 小时前
【51单片机】【protues仿真】基于51单片机温度测量系统
c语言·单片机·嵌入式硬件·物联网·51单片机
晓风凌殇3 小时前
单片机按键检测与长短按识别实现
c语言·单片机
。。。9044 小时前
mit6s081 lab8 locks
操作系统·c
坚持编程的菜鸟5 小时前
LeetCode每日一题——螺旋矩阵
c语言·算法·leetcode·矩阵
机器视觉知识推荐、就业指导6 小时前
C语言中的预编译是什么?何时需要预编译?
c语言·开发语言
GISBox10 小时前
GISBox如何让GeoTIFF突破Imagery Provider加载限制?
react.js·json·gis
一碗绿豆汤10 小时前
C语言-函数
c语言
闭着眼睛学算法10 小时前
【双机位A卷】华为OD笔试之【模拟】双机位A-新学校选址【Py/Java/C++/C/JS/Go六种语言】【欧弟算法】全网注释最详细分类最全的华子OD真题题解
java·c语言·javascript·c++·python·算法·华为od
草莓工作室10 小时前
AT指令解析:TencentOS Tiny AT指令解析源码分析1-TencentOS Tiny 简介
c语言·物联网·嵌入式·at指令·4g模组