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");
}
写测试用例也是对代码的另一个维度思考,尤其是异常场景的用例会让代码更健壮,也能体现思维的广度