lept_json库的学习4
这一章我们来讲字符串的解析Parse_String
Problem
首先,我们先来探讨一下解析字符串可能会遇到的问题:
Problem one:小明到底说了什么?
比如说,我想要在字符串里包含""双引号,这是很正常的,举个例子:
"//这是context识别的头双引号
小明对小红说:"长大后,我就要娶做我老婆。"
"//这是context识别的尾双引号
如果我是用json保存一段剧本(文本信息)那么包含双引号就是很自然的事情,但是对于电脑来说,如果我们不特别指名,电脑可能会解析为:
"//这是context识别的头双引号
小明对小红说:
"//这是context识别的尾双引号
长//解析错误
大//解析错误
后//解析错误
,//解析错误
/*...*/
如果真是这样,那么小明和小红之间可就太可惜了。
所以我们要解决这个问题,总的来说,就是我们要解决转义字符的问题。
转义序列有一下9种:
%x22 / ; " 双引号 U+0022
%x5C / ; \ 反斜杠 U+005C
%x2F / ; / 斜杠 U+002F
%x62 / ; b 退格符 U+0008
%x66 / ; f 换页符 U+000C
%x6E / ; n 换行符 U+000A
%x72 / ; r 回车符 U+000D
%x74 / ; t 制表符 U+0009
%x75 4HEXDIG ) ; uXXXX不知道什么符 U+XXXX
因为这一块我也不是很懂,就直接看项目原话吧:
简单翻译一下,JSON 字符串是由前后两个双引号夹着零至多个字符。字符分为无转义字符或转义序列。转义序列有 9 种,都是以反斜线开始,如常见的 \n 代表换行符。比较特殊的是 \uXXXX,当中 XXXX 为 16 进位的 UTF-16 编码。
Problem two:JSON字符串
众所周知,c/c++中的字符串都是以空字符(‘\0’)结尾的,但是在json的语法里,字符串中是允许包含空字符(指显示\0两个字符)的。那么我们呢要如何将解析空字符呢?
假设我们有一段字符串如下:
“\\\\\000000000///”
那么在解析的时候,如果只是字符串读取的话,只会解析成这样:
“\\\\”
’\0‘之后的都会丢失,这肯定不是我们所希望的。
以及我们需要在value的数据结构中加入字符串类型(这个在第一节里应该有提到)
Problem three:字符串的不定长问题
我们要解析的字符串长度是不固定的,字符串不像数字那样,数字再长我们也已经设置了上限和下限。所以数字可以看作是整体的读取。而字符串则可能有很多很多个字符,需要我们一个一个读取。所以我们要自己考虑如何动态的去分配字符串的长度以及它的内存。
Problem four:生命周期
字符串的创建和释放
Solution
寻找答案,尝试理解
字符串解析这一节其实我也没搞太懂,但是当所有代码拿给我看时,又觉得一目了然,所以这里我在写的时候,实际上就是帮助自己在过一遍这个过程。
以下开始解决上面提出的三个问题:
Solution one
首先我们思考一下,如果我们要让字符串中显示”(双引号),而不被计算机理解为字符串尾的双引号,我们应该怎么输入呢?
我们应该在其前面加一个\(反斜杠)。如果我们呢想要让字符串显示\(反斜杠),那么需要在其前面再加一个\(反斜杠),所以显示"和显示\的方法如下:
\\\" (\")
\\\\ (\\)
而显示\n、\t、\b之类的则可以少一个反斜杠,因为n、t、b之类的本身只是无转义字符,显示方法如下:
\\b (\b)
\\t (\t)
\\n (\n)
...
那么总结一下规律,我们可以写出一个switch判断语句:
switch (ch) {
case '\"': //如果ch是",说明字符串中止
len = c.top - head; //栈顶的移动值为len
str = (char*)lept_context_pop(c, len);
c.json = p;
return LEPT_PARSE_OK;
case '\0':
c.top = head;
return LEPT_PARSE_MISS_QUOTATION_MARK;
case '\\':
switch (*p++) {
case '\"': PUTC(c, '\"'); break;/* 显示" */
case '\\': PUTC(c, '\\'); break;/* 显示\ */
case '/': PUTC(c, '/'); break; /* 显示/ */
case 'b': PUTC(c, '\b'); break;/* 转义 */
case 'f': PUTC(c, '\f'); break;/* 转义 */
case 'n': PUTC(c, '\n'); break;/* 转义 */
case 'r': PUTC(c, '\r'); break;/* 转义 */
case 't': PUTC(c, '\t'); break;/* 转义 */
case 'u':{/*这里我不是很清楚*/}
break;
default:
c.top = head;
return LEPT_PARSE_INVALID_STRING_ESCAPE;
}
break;
default:
if ((unsigned char)ch < 0x20) {
c.top = head;
return LEPT_PARSE_INVALID_STRING_CHAR;
}
PUTC(c, ch);
}
大致的框架就是一个switch里套一个switch,其中还有一些涉及内存分配的函数我们接着讲。
Solution two
关于空字符’\0’的问题在上一个solution中其实就已经解决了。我们来探讨一下字符串数据结构的问题:
首先我们肯定需要一个存储字符串的类型,比如char *
其次,为了之后能更好的控制字符串,我们加入一个size_t 类型来表示字符串的长度。
那么结构就可以很快的写出来了:
typedef struct {
char* s; size_t len; /* string */
double n; /* number */
lept_type type; /* type */
}lept_value;
但是我们可以想到,一个数据类型不可能既是number,又是string,因为在parse_value中我们使用switch语句来进行分治,所以这里我们可以用union来节省内存(union:实例化时只存储其中被实例化的数据类型)
typedef struct {
union {
struct { char* s; size_t len; }m_str; /* string */
double m_num; /* number */
}u;
lept_type type;
}lept_value;
Solution three
关于字符串不定长,我们可以想到很多数据结构来使其长度可变化,比如std::vector它是一个动态数组,通过push在尾部压入元素,pop弹出尾部元素。
我引用一下项目里的原话,然后说说自己的理解:
如果每次解析字符串时,都重新建一个动态数组,那么是比较耗时的。我们可以重用这个动态数组,每次解析 JSON
时就只需要创建一个。而且我们将会发现,无论是解析字符串、数组或对象,我们也只需要以先进后出的方式访问这个动态数组。换句话说,我们需要一个动态的堆栈(stack)数据结构。
也就是说如果我们引入一个vector来控制我们的context的话,那么每次context里解析到一个字符串,我们都会重建一个vector,而引入堆栈,我们可以直接在context中加入堆栈,直接用context来控制。
typedef struct {
const char* json; /* context 主要结构 */
char* stack; /* 栈 */
size_t size, top; /* 栈顶和元素数量(大小) */
}lept_context;
而且对于之后的数组Array和对象Object都可以很好的控制。
而后还有context_push和context_pop两个函数用来更改stack的大小(内存分配)
//固定扩容值
#ifndef LEPT_PARSE_STACK_INIT_SIZE
#define LEPT_PARSE_STACK_INIT_SIZE 256
static void* lept_context_push(lept_context& c, size_t size) {
void* ret; //空指针ret
assert(size > 0); //你总不能push一个size<0的东西进去吧
if (c.top + size >= c.size) {//如果栈顶加入这个东西的size 比原size要大(需要重新分配 内存
if (c.size == 0) //如果还是空栈
c.size = LEPT_PARSE_STACK_INIT_SIZE;//给他个256
while (c.top + size >= c.size)//如果还不够
c.size += c.size >> 1; /* c->size * 1.5 */ //给他扩大个1.5倍
c.stack = (char*)realloc(c.stack, c.size);
}
ret = c.stack + c.top;//直接指针值加上top值,所以top不是指针也没问题
c.top += size;//栈顶移动
return ret;
}
static void* lept_context_pop(lept_context& c, size_t size) {
assert(c.top >= size);
return c.stack + (c.top -= size);
}
这里返回void*是c语言的经典操作,我的理解是它返回的都是stack,至于其它加加减减其实都是stack的容量大小变化。
Solution four
其实关于生命周期,前面我们提到过init函数(写成了宏)和free函数(这里特指lept_free)这两个其实就是干这个事的,但是为什么之前没细讲呢?之前也说了,NULL和BOOL实际上没有用到新数据,只改变了type,number实际存储了一个double变量,而double变量是union几个元素中内存最小的,且大小固定,所以内存不需要我们来控制。但是string、array、object三者大小不固定,而且存储大小可能比double大很多,需要我们合理的去管理。
之前提到Init函数时我们说过,init就是将value_type置空,而free也是将value_type置空。现在我们就需要往我们的free函数里加东西。
void lept_free(lept_value & v){
/* ... */
switch (v.type) {
case LEPT_STRING:
free(v.u.m_str.s);//释放string中的内容
break;
default: break;
}
/* ... */
}
之后我们在set_string中调用:
void lept_set_string(lept_value& v, const char* s, size_t len) {
assert(&v != NULL && (s != NULL || len == 0));//数据类型不为空,且字符串不为空或长度为0
lept_free(v); //先释放v的string 内存
v.u.m_str.s = (char*)malloc(len + 1); //重新分配内存
memcpy(v.u.m_str.s, s, len); //内存值copy过去
v.u.m_str.s[len] = '\0'; //字符串尾置为空字符
v.u.m_str.len = len; //字符串长赋值
v.type = LEPT_STRING; //类型改为string类
}
这里直接在最开始释放内存,就不需要一开始初始化Init,最后释放内存free这种重复操作了。
可能有人会问,为什么这里我们用的是set_string而不是和前三个类型一样使用parse_string。如果您观察足够仔细可以发现,这个set_string函数前面是没有static 的,也就是说这其实是一个对外接口,使用json的客户可以通过set_string来给value赋值为string。我们再来看看parse_string函数
static int lept_parse_string(lept_context& c, lept_value& v) {
int ret;
char* s;
size_t len;
if ((ret = lept_parse_string_raw(c, s, len)) == LEPT_PARSE_OK)
lept_set_string(v, s, len);
return ret;
}
可以看到这里将解析和赋值区分了开来,区分的原因这里可以先说一下,是因为之后我们解析object时,object里是键值对存储,也就是说一个key对应一个value,其中key是字符串类型存储的。当我们解析object里的key时,就会用到parse_string_raw。所以其实在最初编写parse_string时是没有parse_string_raw的,这个是后期重构的结果。
所以最后我们来看看parse_string_raw的全貌:
#define PUTC(c, ch) do { *(char*)lept_context_push(c, sizeof(char)) = (ch); } while(0)
static int lept_parse_string_raw(lept_context & c, char* & str, size_t & len) {
unsigned u, u2;
size_t head = c.top; //设置一个top的copy
const char* p; //来一个const char * p
EXPECT(c, '\"'); //如果字符串第一个是"则跳过
p = c.json; //从"后开始记录json字符串
for (;;) {
char ch = *(p++); //记录当前字符为ch,而后p++
switch (ch) {
case '\"': //如果ch是",说明字符串中止
len = c.top - head; //栈顶的移动值为len
str = (char*)lept_context_pop(c, len);
c.json = p;
return LEPT_PARSE_OK;
case '\0':
c.top = head;
return LEPT_PARSE_MISS_QUOTATION_MARK;
case '\\':
switch (*p++) {
case '\"': PUTC(c, '\"'); break;
case '\\': PUTC(c, '\\'); break;
case '/': PUTC(c, '/'); break;
case 'b': PUTC(c, '\b'); break;
case 'f': PUTC(c, '\f'); break;
case 'n': PUTC(c, '\n'); break;
case 'r': PUTC(c, '\r'); break;
case 't': PUTC(c, '\t'); break;
case '0': PUTC(c, '\0'); break;
//case 'u':
// if (!(p = lept_parse_hex4(p, u)))
// STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);
// if (u >= 0xD800 && u <= 0xDBFF) { /* surrogate pair */
// if (*p++ != '\\')
// STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
// if (*p++ != 'u')
// STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
// if (!(p = lept_parse_hex4(p, u2)))
// STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);
// if (u2 < 0xDC00 || u2 > 0xDFFF)
// STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
// u = (((u - 0xD800) << 10) | (u2 - 0xDC00)) + 0x10000;
// }
// lept_encode_utf8(c, u);
// break;
default:
c.top = head;
return LEPT_PARSE_INVALID_STRING_ESCAPE;
}
break;
default:
if ((unsigned char)ch < 0x20) {
c.top = head;
return LEPT_PARSE_INVALID_STRING_CHAR;
}
PUTC(c, ch);
}
}
}
这里来稍微讲讲被注释的代码吧(其实这部分我是真的一窍不通)
之前的转义序列里的第九个,是\uXXXX,项目原话:
而对于 JSON字符串中的 \uXXXX 是以 16 进制表示码点 U+0000 至 U+FFFF,我们需要:
1.解析 4 位十六进制整数为码点;
2.由于字符串是以 UTF-8 存储,我们要把这个码点编码成 UTF-8。
然后项目里花了一章来详细介绍unicode以及编码转换,对这部分感兴趣的可以直接去github上看看叶老师的讲解。
总之就是解析\uXXXX那一大段代码里就是一些编码转换(我一点也不懂,虽然能看懂),我就不多讲了。
单元测试
又到了单元测试环节。
首先我先在这里说一下,因为我个人的理解,我会在主体部分把parse和stringify全部讲了之后,再来讲api,这是适合我的理解方法,所以我就这么讲了,可以看到我每个部分的api都只是提一嘴,最后所有的api我会一块讲,就像现在我也还没有讲stringify一样。但是单元测试里肯定会用到api,而且实际编程过程中,也肯定是把api一块写了,再来测试的,所以我只贴代码,但不讲解(其实我也讲不出个所以然)
#define EXPECT_EQ_STRING(expect, actual, alength) \
EXPECT_EQ_BASE(sizeof(expect) - 1 == (alength) && memcmp(expect, actual, alength) == 0, expect, actual, "%s")
#define TEST_STRING(expect, json)\
do {\
lept_value v;\
lept_init(v);\
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(v, json));\
EXPECT_EQ_INT(LEPT_STRING, lept_get_type(v));\
EXPECT_EQ_STRING(expect, lept_get_string(v), lept_get_string_length(v));\
lept_free(v);\
} while(0)
还是老样子,先来一个string专用的相等测试宏EXPECT_EQ_STRING。检测string的长度以及string存储的内容是否和输入值相等。
而TEST_STRING则检测parse是否成功,value_type是否更改成功,以及EXPECT_EQ_STRING
而后是示例测试代码:(我之所以觉得测试示例代码有必要贴,是因为如果我以后要用,我可以直接ctrl+c,v)
static void test_parse_string() {
TEST_STRING("", "\"\"");
TEST_STRING("Hello", "\"Hello\"");
TEST_STRING("Hello\nWorld", "\"Hello\\nWorld\"");
//转义序列测试
TEST_STRING("\" \\ / \b \f \n \r \t", "\"\\\" \\\\ \\/ \\b \\f \\n \\r \\t\"");
}
//错误测试
static void test_parse_missing_quotation_mark() {
TEST_ERROR(LEPT_PARSE_MISS_QUOTATION_MARK, "\"");
TEST_ERROR(LEPT_PARSE_MISS_QUOTATION_MARK, "\"abc");
}
static void test_parse_invalid_string_escape() {
TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\v\"");
TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\'\"");
TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\0\"");
TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\x12\"");
}
static void test_parse_invalid_string_char() {
TEST_ERROR(LEPT_PARSE_INVALID_STRING_CHAR, "\"\x01\"");
TEST_ERROR(LEPT_PARSE_INVALID_STRING_CHAR, "\"\x1F\"");
}
以及我不了解的编码转换测试:
static void test_parse_invalid_unicode_hex() {
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u01\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u012\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u/000\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\uG000\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0/00\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0G00\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0/00\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u00G0\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u000/\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u000G\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u 123\"");
}
static void test_parse_invalid_unicode_surrogate() {
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uDBFF\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\\\\\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\\uDBFF\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\\uE000\"");
}
大致如此,感觉我自己讲起来也没什么好讲的,讲了的也没讲很清楚,不过我自己能看懂,这就足够了。
内存泄漏检测
这也是原项目里的加餐,帮助我们了解了内存检测的方法。直接贴代码:(windows下的,详情可见github原项目)
#ifdef _WINDOWS
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#endif
int main() {
#ifdef _WINDOWS
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
/* ... */
}
以上
lept_json Github:https://github.com/miloyip/json-tutorial
本人流星画魂第四次在csdn上做笔记,有什么错误或者是需要改进的地方请即时提出
我只是一个对编程感兴趣的人,但懒得要死,学得又不认真,希望读者能骂就骂两句,真的太懒了