前言
学习了Json字面量解析和数值解析后,本节我们将完成Json的单个字符串解析。Json中的字符串是以""
修饰的。
代码设计
1. 编写 lept_get_boolean()
等访问函数的单元测试,然后实现。
要解决这个问题,首先我们得弄明白lept_get_xxx(const lept_value* v)
函数是用来干什么的。顾名思义,就是将Json Value v
的值转化为C语言的内置变量。略微观察一下这一节的lept_value结构跟上一节有什么区别。
typedef struct {
union {
struct { char* s; size_t len; }s; /* string: null-terminated string, string length */
double n; /* number */
}u;
lept_type type;
}lept_value;
这里有一个union
的结构体,用法跟struct
差不多,但是struct
的各个成员在结构中都具有单独的内存位置,union
成员则共享同一个内存位置,也就是说联合中的所有成员都是从相同的内存地址开始。这里将字符串值s
和数值n
放在一个union结构中,可以节省内存,因为一个Json Value不可能同时属于字符串和数值类型。下图左边struct
的内存结构,右边是union
的内存结构。
所以,letp_get_xxx
和lept_set_xxx
的实现分别如下:
int lept_get_boolean(const lept_value* v) {
assert(v != NULL && (v->type == LEPT_FALSE || v->type == LEPT_TRUE));
/*0 for false, 1 for true*/
return v->type == LEPT_TRUE;
}
void lept_set_boolean(lept_value* v, int b) {
lept_free(v);
v->type = (b ? LEPT_TRUE : LEPT_FALSE);
}
double lept_get_number(const lept_value* v) {
assert(v != NULL && v->type == LEPT_NUMBER);
return v->u.n;
}
void lept_set_number(lept_value* v, double n) {
lept_free(v);
v->type = LEPT_NUMBER;
v->u.n = n;
}
这里要注意的是,在set
方法的开始,必须要调用一下lept_free
函数,对v进行reset,该初始化的初始化,该释放的资源也得释放(堆上的char[]),否则会有资源泄漏的问题。
2. 实现除了 \u 以外的转义序列解析,令test_parse_string()
中所有测试通过
test_parse_string()
主要涉及的函数是lept_parse_string(lept_context* c, lept_value* v)
。先来分析一下它的结构:
static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 't': return lept_parse_literal(c, v, "true", LEPT_TRUE);
case 'f': return lept_parse_literal(c, v, "false", LEPT_FALSE);
case 'n': return lept_parse_literal(c, v, "null", LEPT_NULL);
default: return lept_parse_number(c, v);
case '"': return lept_parse_string(c, v);
case '\0': return LEPT_PARSE_EXPECT_VALUE;
}
}
static int lept_parse_string(lept_context* c, lept_value* v) {
size_t head = c->top, len;
const char* p;
EXPECT(c, '\"');
p = c->json;
for (;;) {
char ch = *p++;
switch (ch) {
case '\"':
len = c->top - head;
lept_set_string(v, (const char*)lept_context_pop(c, len), len);
c->json = p;
return LEPT_PARSE_OK;
case '\0':
c->top = head;
return LEPT_PARSE_MISS_QUOTATION_MARK;
default:
PUTC(c, ch);
}
}
}
首先判断要解析的字符串的第一个字符,如果是"
的话,我们就认为这是一个Json字符串的起始标志。注意,单引号内部使用双引号,不需要加上转义字符\
。然后,在lept_parse_string内部对后续的字符串序列做一个循环,如果:
- 是双引号。认为一个字符串结束,将栈内暂存的解析完的字符全部
pop
出来到v
内。 - 是结束符
'\0'
。此时,说明已经遍历到字符串最后了,并且从没遇到过"
,因为一旦遇到就会return,不会走到这个分支。 - default。其他情况就将字符串入栈。
这里为什么要用一个栈结构来存字符串呢?
我们解析字符串(以及之后的数组、对象)时,因为有可能会解析失败,所以需要把解析的结果先储存在一个临时的缓冲区,最后再用 lept_set_string() 把缓冲区的结果设进值之中。在完成解析一个字符串之前,这个缓冲区的大小是不能预知的。因此,我们可以采用动态数组(dynamic array)这种数据结构,即数组空间不足时,能自动扩展。C++ 标准库的 std::vector 也是一种动态数组。
如果每次解析字符串时,都重新建一个动态数组,那么是比较耗时的。我们可以重用这个动态数组,每次解析 JSON 时就只需要创建一个。而且我们将会发现,无论是解析字符串、数组或对象,我们也只需要以先进后出的方式访问这个动态数组。换句话说,我们需要一个动态的堆栈(stack)数据结构。
回看lept_parse_string
函数,没有考虑到转义字符的情况,我们所要做的就是补上两种情况:
- 转义正确
- 转义错误
转义正确的情况一共有tutorial所说的以下几种(escape):
string = quotation-mark *char quotation-mark
char = unescaped /
escape (
%x22 / ; " quotation mark U+0022
%x5C / ; \ reverse solidus U+005C
%x2F / ; / solidus U+002F
%x62 / ; b backspace U+0008
%x66 / ; f form feed U+000C
%x6E / ; n line feed U+000A
%x72 / ; r carriage return U+000D
%x74 / ; t tab U+0009
%x75 4HEXDIG ) ; uXXXX U+XXXX
escape = %x5C ; \
quotation-mark = %x22 ; "
unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
如果不包括\u开头的Unicode编码,我们只需要考虑前面8种转义字符。首先判断\\
字符,如果首次出现则表示下一个字符是转义字符。再判断下一个字符如果在合法的转义字符范围内,则入栈,否则,返回ESCAPE错误码。
static int lept_parse_string(lept_context* c, lept_value* v) {
size_t head = c->top, len;
const char* p;
EXPECT(c, '\"');
p = c->json;
for (;;) {
char ch = *p++;
switch (ch) {
case '\"':
len = c->top - head;
lept_set_string(v, (const char*)lept_context_pop(c, len), 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;
default:
/* Parse failed, move top to the original position and return. */
c->top = head;
return LEPT_PARSE_INVALID_STRING_ESCAPE;
}
break;
default:
PUTC(c, ch);
}
}
}
3. 解决 test_parse_invalid_string_escape()
和 test_parse_invalid_string_char()
中的失败测试。
我们来看这俩测试用例:
static void test_parse_invalid_string_escape() {
#if 1
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\"");
#endif
}
static void test_parse_invalid_string_char() {
#if 1
TEST_ERROR(LEPT_PARSE_INVALID_STRING_CHAR, "\"\x01\"");
TEST_ERROR(LEPT_PARSE_INVALID_STRING_CHAR, "\"\x1F\"");
#endif
}
对于"\"\\v\""
这样的字符串,Json在解析时,会将\"
、\\
都分别看作一个字符,而不是两个。举个例子,解析字符串"\"\\v\""
时,只会解析3个字符。所以,在解析完\\
字符后,C语言程序读取的实际上只是一个\
符号,此时已经进入了转义语义的解析,由于我们只有8个转义字符是合法的(不算\u
),这里的v
将被认为是非法,所以直接返回ESCAPE错误码。这个测试用例在上一小节已经被修复了:
对于"\"\x01\""
这样的字符串,我们在解析时如果在双引号内部遇到了\x _ _
这样的形式,则说明这是一个16进制的ASCII字符,\x01转换成16进制其实就是1,与ASCII码表对应可以发现,这并不是一个可读字符,所以认为是非法INVALID_STRING_CHAR
。
从ASCII码表中来看,小于32的值都是非法的,正好对应了tutorial中unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
的范围。我们在本节只需要判断小于\x20
就可以。完整的字符串解析函数如下:
static int lept_parse_string(lept_context* c, lept_value* v) {
size_t head = c->top, len;
const char* p;
EXPECT(c, '\"');
p = c->json;
for (;;) {
char ch = *p++;
switch (ch) {
case '\"':
len = c->top - head;
lept_set_string(v, (const char*)lept_context_pop(c, len), 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;
default:
/* Parse failed, move top to the original position and return. */
c->top = head;
return LEPT_PARSE_INVALID_STRING_ESCAPE;
}
break;
default:
/* 0x20 is the smallest ASCII symbol(space) -- 32
* This chapter is only refer to ASCII symbols,
* so a char (8bit) is enough to describe a symbol.*/
if ((unsigned char)ch < 0x20) {
c->top = head;
return LEPT_PARSE_INVALID_STRING_CHAR;
}
PUTC(c, ch);
}
}
}
4. 栈设计
顺便来看一下这里栈的设计,有两个关键函数:lept_context_pop
和lept_context_push
。入栈时可以直接用宏PUTC
。
#define PUTC(c, ch) do { *(char*)lept_context_push(c, sizeof(char)) = (ch); } while(0)
static void* lept_context_push(lept_context* c, size_t size) {
void* ret;
assert(size > 0);
if (c->top + size >= c->size) {
if (c->size == 0)
// 初始化栈容量
c->size = LEPT_PARSE_STACK_INIT_SIZE;
while (c->top + size >= c->size)
/*如果字符串大小超过了栈容量,扩容至原容量的1.5倍,不够就再扩*/
c->size += c->size >> 1; /* c->size * 1.5 */
/*realloc重新分配内存*/
c->stack = (char*)realloc(c->stack, c->size);
}
ret = c->stack + c->top;
c->top += size;
/*本次入栈的字符串:[ret, c->top]*/
return ret;
}
typedef struct {
const char* json;
/*字符栈*/
char* stack;
/*栈大小、栈顶位置*/
size_t size, top;
}lept_context;
static void* lept_context_pop(lept_context* c, size_t size) {
assert(c->top >= size);
/*本次出栈的字符串:[c->stack + (c->top -= size), ]*/
return c->stack + (c->top -= size);
}
方法的含义基本上都在注释中写明白了,需要强调的一点是,这里的栈所说的先进后出指的是整个字符串的进出,而不是单个字符的进出。
标签:case,return,lept,top,Tutorial03,Json,LEPT,字符串,size From: https://www.cnblogs.com/muuu520/p/17021655.html