手写JSON库学习记录 By miloyip/json-tutorial

Github传送门:https://github.com/miloyip/json-tutorial

记录自己做这个项目的过程

是的,在很久以前就开始想做这个东西,但是鸽了很久..终于又重新开始学了:p

Part1

较为简单,略。

Part2

task

1.重构合并 lept_parse_null()、lept_parse_false()、lept_parse_true() 为 lept_parse_literal()。
2.加入 维基百科双精度浮点数 的一些边界值至单元测试,如 min subnormal positive double、max double 等。
3.去掉 test_parse_invalid_value() 和 test_parse_root_not_singular() 中的 #if 0 ... #endif,执行测试,证实测试失败。按 JSON number 的语法在 lept_parse_number() 校验,不符合标准的情况返回 LEPT_PARSE_INVALID_VALUE 错误码。
4.去掉 test_parse_number_too_big 中的 #if 0 ... #endif,执行测试,证实测试失败。仔细阅读 strtod(),看看怎样从返回值得知数值是否过大,以返回 LEPT_PARSE_NUMBER_TOO_BIG 错误码。(提示:这里需要 #include 额外两个标准库头文件。)
一个一个来

1.

我的写法:

static int lept_parse_literal(lept_context* c,lept_value* v,int op) {
    char word[3][5] = {
        "null","false","true"
    };
    int lenOfType[3] = { 4,5,4 };
    EXPECT(c, word[op][0]);
    int checkFlag = 1;
    for (int i = 0;i < lenOfType[op];i++) {
        if (c->json[i] != word[op][i+1]) checkFlag = 0;
    }
    if(!checkFlag) return LEPT_PARSE_INVALID_VALUE;
    c->json += lenOfType[op]-1;
    if (op == 0) v->type = LEPT_NULL;
    if (op == 1) v->type = LEPT_FALSE;
    if (op == 2) v->type = LEPT_TRUE;
    return LEPT_PARSE_OK;
}

2.

加入维基百科双精度浮点数的一些边界值至单元测试,如 min subnormal positive double、max double 等。
这个资料没看懂,所以也没测试。不过tutorial.md里说了加了也是能通过测试的,所以没做就原谅我吧(

3.

这问是tutorial 2里最困难的一部分,主要考察怎么校验数字格式。我的做法很naive,面向测试样例编程,分多种情况一个个判过去。

static int lept_parse_number(lept_context* c, lept_value* v) {
    // ①check First letter must <='9'&&>='0' || =='-'
    char* p = c->json;
    if (*p < '0' || *p>'9') {
        if (*p != '-') return LEPT_PARSE_INVALID_VALUE;
    }
    //②after zero should be '.' , 'E' , 'e' or nothing 
    if (*p == '0') {
        p++;
        if (*p != '.' && *p != 'E' && *p != 'e' && *p != '\0')
        {
            return LEPT_PARSE_ROOT_NOT_SINGULAR;
        }
    }
    //③ at least one digit after '.'
    p = c->json;
    char* point = NULL;
    while (*p != '\0') {
        if (*p == '.' && point == NULL) {
            point = p;
            break;
        }
        p++;
    }
    if (point != NULL) {
        point++;
        if (*point >= '0' && *point <= '9') {//do nothing
        }
        else return LEPT_PARSE_INVALID_VALUE;
    }
    char* end;
    /* \TODO validate number */
    errno = 0;
    v->n = strtod(c->json, &end);
    if (c->json == end)
        return LEPT_PARSE_INVALID_VALUE;
    //④ check number too big
    if (errno == ERANGE && (v->n == HUGE_VAL || v->n == -HUGE_VAL ||
        v->n == HUGE_VALF || v->n == -HUGE_VALF ||
        v->n == HUGE_VALL || v->n == -HUGE_VALL)){
        return LEPT_PARSE_NUMBER_TOO_BIG;
    }
    c->json = end;
    v->type = LEPT_NUMBER;
    return LEPT_PARSE_OK;
}

4.

这题纯纯阅读理解,可惜我没理解出来。贴一个看完答案后自己写的能通过测试的代码:

errno = 0;
v->n = strtod(c->json, &end);
if (c->json == end)
    return LEPT_PARSE_INVALID_VALUE;
//④ check number too big
if (errno == ERANGE && (v->n == HUGE_VAL || v->n == -HUGE_VAL ||
    v->n == HUGE_VALF || v->n == -HUGE_VALF ||
    v->n == HUGE_VALL || v->n == -HUGE_VALL)){
    return LEPT_PARSE_NUMBER_TOO_BIG;
}

最开始没有把 errno 设为0,这不符合校验的原则(初值应该设置为一个不是目标值的值);其次没懂文档里“会返回HUGE_VAL”是什么意思,查完才知道在math.h库里,所有太大的数都会被设置成HUGE_VAL。

part3

task

1.编写 lept_get_boolean() 等访问函数的单元测试,然后实现。
2.实现除了 \u 以外的转义序列解析,令 test_parse_string() 中所有测试通过。
3.解决 test_parse_invalid_string_escape() 和 test_parse_invalid_string_char() 中的失败测试。
4.思考如何优化 test_parse_string() 的性能,那些优化方法有没有缺点。

1.

这问让我们写单元测试,主要测能否正常get、set,仿照函数test_access_null()写了一份

static void test_access_number() {
    lept_value v;
    lept_init(&v);
    lept_set_number(&v, 123.5);//测试的话主要是测set这个函数
    EXPECT_EQ_DOUBLE(123.5, lept_get_number(&v));
    lept_set_number(&v, 1000000007);
    EXPECT_EQ_DOUBLE(1000000007, lept_get_number(&v));
    lept_set_number(&v, -0.00005);
    EXPECT_EQ_DOUBLE(-0.00005, lept_get_number(&v));
    lept_free(&v);
}

思路简单,既然要测试,就将v设置成一个指定值,调用宏去判断是否符合预期,多写了几个边界数据
完成整份练习后看标程校验了一下,发现作者做得更好的一点是:"故意先把值设为字符串,那么做可以测试设置其他类型时,有没有调用 lept_free() 去释放内存"。
简单学了如何检测内存泄漏:Windows下用CRT库,Linux下用valgrind工具。

2.

简单枚举一下

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 '\\':
                ch = *p++;
            //Updated begin
                switch (ch) {
                    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;
                }
                break;
            //Updated over ......

思路就是枚举所有的转义序列,因为'\'能直接读取到,所以用switch语句先判断是否为'\',若是,再处理各种情况

3.

观察失败测试因何失败。第一类失败测试形如""\v""、""\x12"",问题出在"\"后字符非法,在上面新增的case语句后加一句默认值即可

default: 
    return LEPT_PARSE_INVALID_STRING_ESCAPE;

第二类失败测试形如""\x01""、""\x1F"",超出了ascii码语法的码点(<0x20都是非法的),因此再加个判断

static int lept_parse_string(lept_context* c, lept_value* v) {
    //...
            case '\\':
                ch = *p++;
                switch (ch) {
                    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: 
                        return LEPT_PARSE_INVALID_STRING_ESCAPE;
                }
                break;
            default:
                if (ch < 0x20) {  // 检测非法控制字符
                    c->top = head;               // 恢复栈
                    return LEPT_PARSE_INVALID_STRING_CHAR; // 返回错误
                }
                PUTC(c, ch);
        }
    }
}

4.

这里实在是没什么idea了,作者提供的答案是用某个库去优化,以及最坏情况下要把字符串复制一遍,2倍常数。
如果能在转义字符前直接一遍扫过,则能优化一些空间。这个思路我在思考怎么判断转义时想过,但实在是太难写了,相当于手写一个大模拟来字符串匹配,还有太多的
边界情况,真正执行起来不见得快,不如直接用缓冲区复制。

学习笔记

这部分讲到了内存管理、堆栈等,触动很大。接下来以罗列知识点的形式记录自己的收获:

  • 不同的布局
    神秘的布局,能从组成原理的角度节省内存,原来声明变量时如何设计,也是值得推敲的。
  • 对内存管理有初步认识,通过lept_init()和lept_free()两个函数,前者用来初始化,清除可能的脏数据,确保结构体处于安全的初始状态;后者释放一个 lept_value 对象中可能动态分配的内存,防止内存泄漏。内存泄漏指的是"动态分配的内存未被释放,导致这些内存变得不可用,同时又无法被系统重新分配和使用的现象",粗糙地说就是占着茅坑不拉屎还不走。往往出现在new或者malloc后,没有delete或者free。让我想起学操作系统时的"内部碎片"、"外部碎片",内存泄漏对应的应该是"不可用碎片"。
  • 在实现字符串时,作者用了堆栈来当缓冲区。这类模拟题写过很多了,无非就是手写栈来存储过程性的东西,选择栈是因为"先进后出"的需求。惊艳我的地方1.是扩展时每次以1.5倍而非2倍,并且很多STL源码都是这么设计的。2.是不明白为何"ret = c->stack + c->top;"此处可以直接拿stack和top相加?
答:c->stack 和 c->top 的单位是一致的:c->stack 是一个 char*,单位是字节。c->top 是一个 size_t,也表示字节数,此时指针运算直接以字节为单位。c语言是支持对指针运算的。

Part4

task

1.实现 lept_parse_hex4(),不合法的十六进位数返回 LEPT_PARSE_INVALID_UNICODE_HEX。
2.按第 3 节谈到的 UTF-8 编码原理,实现 lept_encode_utf8()。这函数假设码点在正确范围 U+0000 ~ U+10FFFF(用断言检测)。
3.加入对代理对的处理,不正确的代理对范围要返回 LEPT_PARSE_INVALID_UNICODE_SURROGATE 错误。

1.

我的写法较标答繁琐,先判后四位是否是十六进制位数再转换

static const char* lept_parse_hex4(const char* p, unsigned* u) {
    //如果 `\u` 后不是 4 位十六进位数字,则返回 `LEPT_PARSE_INVALID_UNICODE_HEX` 错误。
    char* to = p;
    int cntOfNumber = 0;
    to++;
    if (((*to) <= 'F' && (*to) >= 'A') || ((*to) <= '9' && (*to) >= '0') ||
        ((*to) <= 'f' && (*to) >= 'a')) {
        cntOfNumber++;
        while (((*to) <= 'F' && (*to) >= 'A') || ((*to) <= '9' && (*to) >= '0') ||
        ((*to) <= 'f' && (*to) >= 'a')){
            to++;
            cntOfNumber++;
            if (cntOfNumber == 4) break;
        }
    }
    if (cntOfNumber < 4) return NULL;
    *u = 0;
    for (int i = 0;i < 4;i++) {
        *u <<= 4;
        if ((*p) <= 'F' && (*p) >= 'A') {
            *u += ((*p) - 'A') + 10;
        }
        else if ((*p) <= 'f' && (*p) >= 'a') {
            *u += ((*p) - 'a') + 10;
        }
        else if ((*p) <= '9' && (*p) >= '0') {
            *u += ((*p) - '0');
        }
        else {
            return NULL;
        }
        p++;
    }
    return p;
}

2.

只要把UTF-8的编码翻译出来就好,写代码时根据字节数声明了对应变量,这么做是为了方便自己调试。

static void lept_encode_utf8(lept_context* c, unsigned u) {
    //把码点编码成 UTF-8,写进缓冲区
    assert(u >= 0x0000 && u <= 0x10FFFF);
    if (u >= 0x0000 && u <= 0x007F) {
        unsigned bit1;
        bit1 = u & 0xFF;
        PUTC(c,bit1);
    }
    else if (u >= 0x0080 && u <= 0x07FF) {
        unsigned bit1,bit2;
        bit1 = (0xC0 | ((u >> 6) & 0xFF) );
        bit2 = (0x80 | (u & 0x3F));
        PUTC(c,bit1);
        PUTC(c, bit2);
    }
    else if (u >= 0x0800 && u <= 0xFFFF) {
        unsigned bit1, bit2, bit3;
        bit1 = (0xE0 | ((u >> 12) & 0xFF));
        bit2 = (0x80 | ((u >> 6) & 0x3F));
        bit3 = (0x80 | (u & 0x3F));
        PUTC(c, bit1);
        PUTC(c, bit2);
        PUTC(c, bit3);
    }
    else {
        unsigned bit1, bit2, bit3,bit4;
        bit1 = (0xF0 | ((u >> 18) & 0xFF));
        bit2 = (0x80 | ((u >> 12) & 0x3F));
        bit3 = (0x80 | ((u >> 6) & 0x3F));
        bit4 = (0x80 | (u  & 0x3F));
        PUTC(c, bit1);
        PUTC(c, bit2);
        PUTC(c, bit3);
        PUTC(c, bit4);
    }
}

但此处有个困惑,为什么这里可以直接写入unsigned?PUT函数接受的不是char吗?以及lept_context_push()函数好像也没实际写入数据,只做了扩容,到底在哪完成的写入?
答:

来看PUTC的宏定义:#define PUTC(c, ch) do { *(char*)lept_context_push(c, sizeof(char)) = (ch); } while(0)
这里用了"解引用"将函数传回的指针所指向的位置赋值成ch,也就是说lept_context_push()函数里并没有写入,只是扩容并返回指针。
可以直接写入unsigned是因为C语言有隐式转换:将unsighed类型直接转为char。
如果unsigned类型的值在char类型能表示的范围(有符号char-128127,无符号char0255)内,那会被正确转换。
但可能会丢失超出char表示范围的高位数据(比如对于无符号char,如果unsigned值大于255,会只保留低8位)。

3.

代理对可以在完成一次lept_parse_hex4后检测,如果解析出来的数在[0xD800,0xDBFF]范围内,说明后面应该跟着\uXXXX。
逐个判断即可。

case 'u':
//codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)
    if (!(p = lept_parse_hex4(p, &u)))
        STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);
    if (u >= 0xD800 && u <= 0xDBFF) {
        if (*p != '\\') return LEPT_PARSE_INVALID_UNICODE_SURROGATE;
        p++;
        if (*p != 'u') return LEPT_PARSE_INVALID_UNICODE_SURROGATE;
        p++;
        unsigned uLow;
        if (!(p = lept_parse_hex4(p, &uLow)))
            STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);
        if (uLow > 0xDFFF || uLow < 0xDC00) return LEPT_PARSE_INVALID_UNICODE_SURROGATE;
        unsigned codePoint;
        codePoint = 0x10000 + (u - 0xD800) * 0x400 + (uLow - 0xDC00);
        lept_encode_utf8(c, codePoint);
    }
    else lept_encode_utf8(c, u);
    break;
default:
    STRING_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE);

学习笔记

了解了码点、UTF-8的编码原理,印象最深的是代理对和字节序。
代理对原理:

正常来说如果用一个数字表示一个码点的话,能表示的数目就是O(n)的。
但需要用少量的数字范围表示更多的码点,且同时保留低位和ASCII码一致时,就可以选择开辟部分区间作为特别的"入口"。
从某些特定的数字(入口)出发,后面再跟着一个数字,就能用原先的范围表示o(n^2)的数据。
非常巧妙的代理对。

字节序原理:

UTF-8之所以通用的另一个原因是UTF - 8 不存在字节序的概念。
它不像UTF-16/32将一个字符的编码均匀分布在固定长度的多个字节中,且字节顺序会变化。
组成原理里有大端、小端的概念:这好比编程语言里面的阅读顺序,我们读现代文是从左往右,读古文时是从右往左。
那编译时放在大端的到底是低位还是高位?还好UTF-8没有这种困扰,它的编码是从高位到低位依次分布在字节中的。

Part5

这次和之前不同的是在开始写代码前记录了读tutorial.md的笔记:

比较了数组和链表,数组支持快速访问和高缓存一致性,但无法快速插入;
链表只能O(n)访问和容易缓存不命中,但可以快速插入。

typedef struct lept_value lept_value;

struct lept_value {
    union {
        struct { lept_value* e; size_t size; }a; /* array */
        struct { char* s; size_t len; }s;
        double n;
    }u;
    lept_type type;
};

union 的特点是其内部所有成员共用同一块内存空间
在同一时刻只能有一个成员被使用
因为它们共享内存,对其中一个成员赋值会覆盖其他成员对应内存的数据
这也很合理,显然我们只会用到union的某一部分,json对象不可能既是字符又是数字

自身类型指针:指向自身结构体的指针,该指针可以用来实现
遍历数组、动态分配或释放数组内存等操作
前向声明:真正定义类型前,先通知编译器它存在,稍后再完整定义。
为什么需要前向声明:当结构体包含自身指针时,编译器又是按顺序读代码的,
就会尝试去找自身指针的完整定义——但此时还没定义好,因为它内部有一个指向自身类型的指针,完整定义还在后面
。就需要前向声明安抚一下编译器别找了,后面有,这里先放着。

作者反复强调了:C 语言的数组大小应该使用 size_t 类型。但受语言标准
的影响,打印size_t各有写法,所以if和endif这套可以加上条件:

#if defined(_MSC_VER)
...
#else
...
#endif

task

1.编写 test_parse_array() 单元测试,解析以下 2 个 JSON。由于数组是复合的类型,不能使用一个宏去测试结果,请使用各个 API 检查解析后的内容。
2.现时的测试结果应该是失败的,因为 lept_parse_array() 里没有处理空白字符,加进合适的 lept_parse_whitespace() 令测试通过。
3.使用第三单元解答篇介绍的检测内存泄漏工具,会发现测试中有内存泄漏。很明显在 lept_parse_array() 中使用到 malloc() 分配内存,但却没有对应的 free()。应该在哪里释放内存?修改代码,使工具不再检测到相关的内存泄漏。
4.开启 test.c 中两处被 #if 0 ... #endif 关闭的测试,本来 lept_parse_array() 已经能处理这些测试。然而,运行时会发现 Assertion failed: (c.top == 0) 断言失败。这是由于,当错误发生时,仍然有一些临时值在堆栈里,既没有放进数组,也没有被释放。修改 lept_parse_array(),当遇到错误时,从堆栈中弹出并释放那些临时值,然后才返回错误码。
5.第 4 节那段代码为什么会有 bug?

1.

起初没懂"调用api",看了下标程,才知道是调access类的函数检查返回了什么,再用宏去检测是否与预期的一致

static void test_parse_array() {
    lept_value v;
    lept_init(&v);
    EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "[ ]"));
    EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(&v));
    EXPECT_EQ_SIZE_T(0, lept_get_array_size(&v));
    lept_free(&v);
   
    lept_init(&v);
    EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "[ null , false , true , 123 , \"abc\" ]"));
    EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(&v));
    EXPECT_EQ_SIZE_T(5, lept_get_array_size(&v));
    EXPECT_EQ_INT(LEPT_NULL, lept_get_type(lept_get_array_element(&v,0)));
    EXPECT_EQ_INT(LEPT_FALSE, lept_get_type(lept_get_array_element(&v, 1)));
    EXPECT_EQ_INT(LEPT_TRUE, lept_get_type(lept_get_array_element(&v, 2)));
    EXPECT_EQ_INT(LEPT_NUMBER, lept_get_type(lept_get_array_element(&v, 3)));
    EXPECT_EQ_INT(LEPT_STRING, lept_get_type(lept_get_array_element(&v, 4)));
    EXPECT_EQ_STRING("abc", lept_get_string(lept_get_array_element(&v, 4)),3);
    EXPECT_EQ_DOUBLE(123.0, lept_get_number(lept_get_array_element(&v, 3)));
    lept_free(&v);
    
    lept_init(&v);
    EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "[ [ ] , [ 0 ] , [ 0 , 1 ] , [ 0 , 1 , 2 ] ]"));
    EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(&v));
    EXPECT_EQ_SIZE_T(4, lept_get_array_size(&v));
    
    _Bool checkFlag = 1;
    for (int i = 0;i < 4;i++) {
        lept_value* e=lept_get_array_element(&v,i);
        EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(e));
        if (lept_get_array_size(e) != i) {
            checkFlag = 0;
            break;
        }
        for (int j = 0;j < i;j++) {
            int actualNumber = lept_get_number(lept_get_array_element(e, j));
            if (actualNumber != j) checkFlag = 0;
        }
    }
    EXPECT_EQ_INT(1, checkFlag);
    lept_free(&v);
    
}

2.

static int lept_parse_array(lept_context* c, lept_value* v) {
    size_t size = 0;
    int ret;
    EXPECT(c, '[');
    //比如 [ abc , 123 ],"["后就有空格了
    lept_parse_whitespace(c);
    ...
    for (;;) {
        lept_value e;
        lept_init(&e);
        //比如 [ abc , 123 ],开始解析一个元素前会有空格
        lept_parse_whitespace(c);
        if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK) {
            for (int i = 0; i < size; i++)
                lept_free(lept_context_pop(c, sizeof(lept_value)));
        // lept_context_pop(c, sizeof(lept_value) * size);
            return ret;
        }
        // 比如 [ abc , 123 ],解析完一个元素后会有空格
        lept_parse_whitespace(c);
        memcpy(lept_context_push(c, sizeof(lept_value)), &e, sizeof(lept_value));
        size++;
        if (*c->json == ',')
            c->json++,
            //比如 [ abc , 123 ],逗号后也会有
            lept_parse_whitespace(c);
        ...
}

3.

这问有意思,起初简单在lept_free里改动了:

void lept_free(lept_value* v) {
    assert(v != NULL);
    if (v->type == LEPT_STRING)
        free(v->u.s.s);
    if (v->type == LEPT_ARRAY) {
        free(v->u.a.e);
    }
    v->type = LEPT_NULL;
}

还是泄漏,发现把新增的两个复杂样例删去就没有了,说明是能free的,但解决不了多个、嵌套多层的情况,猜测要用dfs手动释放。
写个递归即可:

void lept_free(lept_value* v) {
    assert(v != NULL);
    if (v->type == LEPT_STRING)
        free(v->u.s.s);
    // 想清楚它是用链表形式(有很多的next)还是数组形式
    // 实质上构成一棵树,每个节点都是一个指针数组
    if (v->type == LEPT_ARRAY) {
        size_t sizeOfArray = lept_get_array_size(v);//这里和直接用v->u.a.size是一样的
        for (int i = 0;i < sizeOfArray;i++) {
            lept_free(&(v->u.a.e[i]));
        }
        free(v->u.a.e);
    }
    v->type = LEPT_NULL;
}

这里有多处free,注意free传入的是指针,所以释放叶子节点时直接用 v->u.a.e 或者 v->u.s.s 即可,但递归去下一层时,应该对数组里的元素取地址。

4.

static int lept_parse_array(lept_context* c, lept_value* v) {

    ......
    
    for (;;) {
        lept_value e;
        lept_init(&e);
        lept_parse_whitespace(c);
        if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK) {
            for (int i = 0; i < size; i++)
                lept_free(lept_context_pop(c, sizeof(lept_value)));
            return ret;
        }
        ......
        else {
        // pop what?
        // 不是size,显然size并不等同于缓冲区大小,还和占多少字节有关系,毕竟堆栈其实是按字节算的
        // 是sizeof(lept_value)),但不能只pop一次,应该重复做size次
        // 所以可以for一遍,也可以直接算出一共需要pop多少
        // 法一:for一遍
            for (int i = 0; i < size; i++)
                lept_free(lept_context_pop(c, sizeof(lept_value)));
        // 法二:直接算    
        // lept_context_pop(c, sizeof(lept_value)*size);
        // 法一和法二的共性在于都能改变c.top的值,使得代码通过测试
        // 但直接算的话就没法逐个手动free,经crt工具测试,会导致内存泄漏,所以手动循环更优
            return LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET;
        }
    }
}

5.

Q:第 4 节那段代码为什么会有 bug?

A:用指针去获取空间时,如果能直接写入数据倒还好,但如果在写入数据前调用了realloc()等可能会使当前指针地址失效的函数,
就会出现"悬空指针"问题,即:地址还是那个地址,但已经没有意义了。

为什么realloc()会导致失效?realloc的原理是能扩展时直接往下扩展,不能扩展的话另找一块足够的内存把数据复制过去,并扩展。
那么,当空间不够时,所有数据就会搬到另外一个地址去,此时再往指针指向的地方写入数据就没意义了——它既不是连续的,我们也无法从某个数组的首地址出发去访问到它。

总结:注意指针的生命周期。

Part6

学习笔记.

Q:lept_parse_string_raw(lept_context* c, char** str, size_t* len)
为什么重构时要用二级指针?

A:

函数的传递参数机制。如果只传char* str,收到的是该变量的副本,而不是变量本身。
但传递指针的地址(二级指针char**)就可以通过解引用操作(*str)来修改原始指针的值。

Q:为什么涉及到从"缓冲区搬运东西"、"写入结构体"等操作时,都用memcpy而非简单的指针赋值?比如

size_t s = sizeof(lept_member) * size;
memcpy(v->u.o.m = (lept_member*)malloc(s), lept_context_pop(c, s), s);

A:

1.简单的指针赋值不能真正写入数据,而memcpy可以处理任意类型的数据(基本数据类型、结构体、数组)。
它可以将这些数据从一个内存位置复制到另一个内存位置,实现数据的完整复制。
2.简单的指针赋值会导致指针指向同一块内存,一切会很混乱

Q:以下代码为什么要引入lept_member m?

static int lept_parse_object(lept_context* c, lept_value* v) {
    size_t i, size = 0;
    lept_member m;
    ...
    m.k = NULL;
    for (;;) {
        ...
        lept_init(&m.v);
        ...
        if ((ret = lept_parse_string_raw(c, &str, &m.klen)) != LEPT_PARSE_OK)
            break;
        memcpy(m.k = (char*)malloc(m.klen + 1), str, m.klen);
        m.k[m.klen] = '\0';
        ...
        if ((ret = lept_parse_value(c, &m.v)) != LEPT_PARSE_OK)
            break;
        memcpy(lept_context_push(c, sizeof(lept_member)), &m, sizeof(lept_member));
        ...
        m.k = NULL; 
        ...
        if (*c->json == '}') {
            size_t s = sizeof(lept_member) * size;
            memcpy(v->u.o.m = (lept_member*)malloc(s), lept_context_pop(c, s), s);
        }
        ...
    }
    free(m.k);
    for (i = 0;i < size;i++) {
        lept_member* m = lept_context_pop(c, sizeof(lept_member));
        free(m->k);
        //此处传给lept_free的是指针,应该对m->v取地址。
        lept_free(&(m->v));
    }
}

A:

一般引入中间变量都是为了方便。
1.起到集成的作用,复制数据、清理内存都只要关心m就好,因为它集成了这些信息。
2.封装起来方便单独处理每个成员,否则可能需引入计数器啥的。

Q:什么时候用 -> 什么时候用 . ?

A:

结构体或类的实例对象,用 . 来访问其成员。
指向结构体或类的指针,用 -> 运算符来访问指针所指向对象的成员。
并且其实->是(*指针).成员的简写形式。

Q:
没看到有声明数组,只有指针啊,为什么可以用 m[i] 来访问?比如:

void lept_free(lept_value* v) {
    size_t i;
    assert(v != NULL);
    switch (v->type) {
        ......
        case LEPT_OBJECT:
            for (i = 0;i < v->u.o.size;i++) {
                lept_free(&((v->u.o.m[i]).v));
                free((v->u.o.m[i]).k);
            }
    }
}

A:

malloc分配指定大小的连续内存块。当分配的内存大小是某个结构体类型大小的整数倍时,这块内存就可看作该结构体类的数组。
数组名在大多数表达式中会被隐式转换为指向数组首元素的指针。
同样,当指针指向一块连续的内存,且这块内存可看作数组时,就能用数组下标运算符 [] 来访问。
所以看起来是指针,但用了malloc,实际上指向的是数组。

Part7

学习笔记

这章完成得挺顺利,可能因为也做到尾声了,对于工程代码的写法、思路有了初步的了解。
比较有意思的两点是:

json语法的转义
case '\\': PUTS(c, "\\\\", 2);break;
case '\b': PUTS(c, "\\b", 2);break;
case '\f': PUTS(c, "\\f", 2);break;
case '\n': PUTS(c, "\\n", 2);break;

假设有一个包含换行符的字符串,在将其转换为JSON串时,要把换行符转为\n。
例如,原始字符串为 "Hello\nWorld",经上述处理后,变成"Hello\nWorld",这就符合JSON要求。
同理,解析时是一个相反的过程:

case '\\':
    switch (*p++) {
        case '\\': PUTC(c, '\\'); break;
        case '/':  PUTC(c, '/' ); break;
        case 'b':  PUTC(c, '\b'); break;
        case 'f':  PUTC(c, '\f'); break;

如果读到一个"\"说明后面跟着转义字符,要特别处理。
那么,如果我就是想表达作为文本的'\b'呢?特别处理一下''。
比如串"abc\\bdef"中的"\\b"就会被解释成普通的文本"\b",因为对反斜杠进行额外的转义,确保它不会被解释为退格符的转义序列
解析成JSON串则是"abc\\\\bdef",此处"\\\\"对应了"\\"
(此处又被markdown语法转义了一层,所以为了表达四个"",在后台打了八个""..)

优化方式

既然每次都检查会浪费,不如一次性空间拉满,用指针赋值。这是空间换时间的做法,但没关系空间不值钱。
此外小优化是自行编写十六进位输出,避免调用函数printf()的开销,具体快在:
1.位运算
2.避免调用函数,若调用,需要保存寄存器状态、传递参数、跳转到函数入口地址
3.直接从数组中获取十六进制字符,数组索引操作也很快

posted @   liyishui  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示