lept_json库的学习之parse_string

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上做笔记,有什么错误或者是需要改进的地方请即时提出
我只是一个对编程感兴趣的人,但懒得要死,学得又不认真,希望读者能骂就骂两句,真的太懒了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值