開源項目cJSON具體實現5(數組的解析)

5.1 數組語法與解釋

先來看看 JSON 數組的語法:

JSON-text = ws value ws
value = null / false / true / number / string / array
array = %x5B ws [ value *( ws %x2C ws value ) ] ws %x5D

其中:

  • %x5B 是左中括號 [
  • %x2C 是逗號 ,
  • %x5D 是右中括號 ]
  • ws 是空白字符

一個數組可以包含零至多個值,而這些值可以字符,數字,也可以是數組;值與值之間以逗號分隔,例如 []、[1,2,true]、[[1,2],[3,4],“abc”] 都是合法的數組。但注意 JSON數組 不接受末端額外的逗號,例如 [1,2,] 是不合法的。

JSON 數組的語法很簡單,解析實現的難點不在語法上,而是怎樣管理內存。我們應該如何設計JSON數組類型的數據結構呢?有兩個選擇:

  1. C語言的數組

數組最大的好處是能以 O(1) 用索引訪問任意元素,次要好處是內存佈局緊湊,省內存之餘還有高緩存一致性(cache coherence)。但數組的缺點是不能快速插入元素,而且我們在解析 JSON 數組的時候,還不知道應該分配多大的數組才合適。

  1. 鏈表

它的最大優點是可快速地插入元素(開端、末端或中間),但需要以O(n) 時間去經索引取得內容。如果我們只需順序遍歷,那麼是沒有問題的。還有一個小缺點,就是相對數組而言,鏈表在存儲每個元素時有額外內存開銷(存儲下一節點的指針),而且遍歷時元素所在的內存可能不連續,令緩存不命中(cache miss)的機會上升。

5.2 設計頭文件

我們的實現選擇數組。我們將會通過之前在解析字符串時實現的堆棧,來解決解析 JSON 數組時未知數組大小的問題。

首先是要給 lept_value 的 union 裏面加入數組的結構:

typedef struct lept_value lept_value;//由於 lept_value 內使用了自身類型的指針,我們必須前向聲明(forward declare)此類型。

struct lept_value {
    union {
        struct { lept_value* e; size_t size; }a; /* 數組(動態數組) */
        struct { char* s; size_t len; }s;
        double n;
    }u;
    lept_type type;
};

注意這裏 size 是數組元素的個數,不是字節單位。

在頭文件裏面還增加了一個lept_parse函數的返回值錯誤類型:LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET表示不合法的數組類型。

我們在leptjson.h文件裏面增加兩個 API 去訪問 JSON 數組類型的值:

size_t lept_get_array_size(const lept_value* v); //獲得數組元素的個數

lept_value* lept_get_array_element(const lept_value* v, size_t index); //獲得數組的元素

並在 leptjson.c文件裏面實現這兩個 API。

size_t lept_get_array_size(const lept_value* v) {
    assert(v != NULL && v->type == LEPT_ARRAY);
    return v->u.a.size;
}

lept_value* lept_get_array_element(const lept_value* v, size_t index) {
    assert(v != NULL && v->type == LEPT_ARRAY);
    assert(index < v->u.a.size);
    return &v->u.a.e[index];
}

8.3 test.c的設計

我們寫一個單元測試 test_parse_array() 去測試這些 API,先寫一個簡單的。

#if defined(_MSC_VER)    //_MSC_VER:Microsoft的C編譯器的版本
#define EXPECT_EQ_SIZE_T(expect, actual) EXPECT_EQ_BASE((expect) == (actual), (size_t)expect, (size_t)actual, "%Iu")
#else
#define EXPECT_EQ_SIZE_T(expect, actual) EXPECT_EQ_BASE((expect) == (actual), (size_t)expect, (size_t)actual, "%zu")
#endif

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);
 }

拓展:
C 語言的數組大小應該使用 size_t 類型。因爲我們要驗證 lept_get_array_size() 返回值是否正確,所以再爲單元測試框架添加一個宏 EXPECT_EQ_SIZE_T。麻煩之處在於,ANSI C(C89)並沒有的 size_t打印方法,在 C99 則加入了 “%zu”,但 VS2015 中才有,之前的 VC 版本使用非標準的 “%Iu”。因此,上面的代碼使用條件編譯去區分 VC 和其他編譯器。雖然這部分不跨平臺也不是 ANSI C 標準,但它只在測試程序中,不太影響程序庫的跨平臺性。

8.4 數組解析函數lept_parse_array();的編寫。

我們上面也提到過,如果我們使用數組來實現JSON數組,有個問題是不知道應該分配多大的數組才合適。我們得通過解析字符串時實現的堆棧,來解決解析 JSON 數組時未知數組大小的問題。

回憶一下,在解析字符串的時候,我們是這樣設計的,當 c->json 中保存的是字符串文件時,我們通過lept_parse_string函數來解析字符串,並把解析好的字符串通過lept_context_push函數保存到c->stack中,並用 c->size和c->top來記錄保存的數據。當遇到字符串結束標識時,我們用lept_context_pop函數將c->stack裏面的內容保存到 lept_value v 裏面。

對於 JSON 數組,我們也可以用相同的方法,只需要把每個解析好的元素壓入堆棧,解析到數組結束時,再一次性把所有元素彈出,複製至新分配的內存之中。

但是注意:和字符串不一樣的是,數組是複合型數據類型,它的元素可以是數組,也可以是字符串。所以如果把 JSON 當作一棵樹的數據結構,JSON 字符串是葉節點;而 JSON 數組是中間節點,它是可以繼續向下拓展的。在字符串的解析函數中,我們怎樣使用那個堆棧都可以,只要在解析完字符串後還原就好了。但對於數組這樣的,如果我們等到解析完整個數組後還原的話,如果數組中間的值是數組或是字符串,他的解析還要用到這個堆棧,怎麼辦?所以並不能等到最後再還原這個堆棧。

解決辦法是:在解析數組時,只要在任何解析函數結束時還原堆棧的狀態,就沒有問題。在這篇文章裏,有具體的圖解:知乎

即:在解析字符串的時候,實際上是要調用三個函數的:lept_parse -> lept_parse_value -> lept_parse_string,同理在解析數組的時候也是要調用三個函數的:lept_parse -> lept_parse_value -> lept_parse_array();,當我們解析數組的時候,lept_parse_array()會生成一個臨時的 lept_value e,我們在解析數組的任何一個元素時,會在lept_parse_array()中就應該調用 lept_parse_value() 函數, 當 lept_parse_value() 函數在解析過程中就可能會用到臨時的堆棧來幫助解析,當lept_parse_value() 函數解析完成後,我們將臨時堆棧的內容進行彈出工作並保存在臨時的 lept_value e中,並且將這個lept_value push 到臨時堆棧中,並將size++;依次循環直到數組整個完成解析,再將臨時堆棧裏的內容複製到 lept_value *v裏,並free臨時堆棧。

static int lept_parse_value(lept_context* c, lept_value* v);/*前向聲明*/

/*

函數目的:解析數組
思路: 把解析好的元素壓入堆棧,直到數組結束,再一次性把所有元素彈出。
1. 進入lept_parse_array中 生成一個臨時的lept_value e用於存儲之後的元素。
2. 遇到數組中的第一個值,調用lept_parse_value去解析,並把解析後的結果保存在臨時的lept_value e中,每次lept_parse_value函數返回的時候,要將臨時堆棧也復原了
3. 解析值完成後,調用push函數將臨時lept_value e中的結果複製到臨時堆棧中,並將size++
4. 如果數組中還有值,接着循環2.3。
5. 如果數組結束,則一次性將堆棧裏面的內容彈出。
*/
static int lept_parse_array(lept_context* c, lept_value* v) {
    size_t size = 0;//用來記錄數組中有多少個元素。

    int ret;
    EXPECT(c, '[');
    if (*c->json == ']') {
        c->json++;
        v->type = LEPT_ARRAY;
        v->u.a.size = 0;
        v->u.a.e = NULL;
        return LEPT_PARSE_OK;
    }
    for (;;) {
        lept_value e;
        lept_init(&e);
        if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK)
            return ret;
        memcpy(lept_context_push(c, sizeof(lept_value)), &e, sizeof(lept_value));
        size++;
        if (*c->json == ',')
            c->json++;
        else if (*c->json == ']') {
            c->json++;
            v->type = LEPT_ARRAY;
            v->u.a.size = size;
            size *= sizeof(lept_value);
            memcpy(v->u.a.e = (lept_value*)malloc(size), lept_context_pop(c, size), size);
            return LEPT_PARSE_OK;
        }
        else
            return LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET;
    }
}

static int lept_parse_value(lept_context* c, lept_value* v) {
    switch (*c->json) {
        /* ... */
        case '[':  return lept_parse_array(c, v);
    }
}

8.5 完善代碼1

進行到這裏,就算初步完成數組的解析了,按照TDD的編程思路,我們先寫測試代碼,再進行完善。

還記得這個測試的的代碼嗎?如果我們把下面的[] 改爲 [ ] ,再運行代碼就會報錯,報錯的原因在於我們沒有處理數組的空白,從數組的語法我們可以知道一共有三處空白:array = %x5B ws [ value *( ws %x2C ws value ) ] ws %x5D,所以相應的需要加入 3 個 lept_parse_whitespace() 調用,分別是解析 [ 之後,元素之後,以及 , 之後:

//test.c
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);
 }

//leptison.h
static int lept_parse_array(lept_context* c, lept_value* v) {
    /* ... */
    EXPECT(c, '[');
    lept_parse_whitespace(c);
    /* ... */
    for (;;) {
        /* ... */
        if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK)
            return ret;
        /* ... */
        lept_parse_whitespace(c);
        if (*c->json == ',') {
            c->json++;
            lept_parse_whitespace(c);
        }
        /* ... */
    }
}

處理完空白字符後,又出現新的問題,比如說我現在想用下面兩個數組來測試我們解析函數,那麼測試代碼應該怎麼寫?
[ null , false , true , 123 , “abc” ]
[ [ ] , [ 0 ] , [ 0 , 1 ] , [ 0 , 1 , 2 ] ]

//test.c
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);
    
    size_t i, j;
    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_INT(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_DOUBLE(123.0, lept_get_number(lept_get_array_element(&v, 3)));
    EXPECT_EQ_STRING("abc", 
lept_get_string(lept_get_array_element(&v,4)),lept_get_string_length(lept_get_array_element(&v,4)) );
    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_INT(4, lept_get_array_size(&v));

    for (i = 0; i < lept_get_array_size(&v);i++)
    {
           lept_value* a = lept_get_array_element(&v, i);
           EXPECT_EQ_INT(LEPT_ARRAY, lept_get_type(a));
           EXPECT_EQ_INT(i, lept_get_array_size(a));
           for (j = 0; j < i; j++)
           {
                   lept_value* e = lept_get_array_element(a, j);
                   EXPECT_EQ_INT(LEPT_NUMBER,lept_get_type(e));
                   EXPECT_EQ_DOUBLE((double)j,lept_get_number(e));
           }
    }
    lept_free(&v);
}

8.6 內存泄漏

成功測試那 3 個 JSON 後,使用內存泄漏檢測工具會發現測試中有內存泄漏。很明顯在 lept_parse_array() 中使用到malloc() 分配內存,但卻沒有對應的 free()。應該在哪裏釋放內存?

很明顯,有 malloc() 就要有對應的 free()。正確的釋放位置應該放置在 lept_free(),當值被釋放時,該值擁有的內存也在那裏釋放。之前字符串的釋放也是放在這裏。

對於數組,我們應該先把數組內的元素通過遞歸調用 lept_free() 釋放,然後才釋放本身的 v->u.a.e:

void lept_free(lept_value* v) {
        //字符串的釋放
        /*assert(v != NULL);
        if (v->type == LEPT_STRING)
               free(v->u.s.s);
        v->type = LEPT_NULL;*/

  //數組的釋放
        size_t i;
        assert(v != NULL);
        switch (v->type) {
               case LEPT_STRING:
                       free(v->u.s.s);
                       break;
               case LEPT_ARRAY:
                       for (i = 0; i < v->u.a.size; i++)
                              lept_free(&v->u.a.e[i]);
                       free(v->u.a.e);
                       break;
               default: break;
        }
        v->type = LEPT_NULL;
}

8.7 完善代碼2

除了上面的測試,還有錯誤的測試:

//test.c
static void test_parse_invalid_value() {
       /* ....*/
	#if 0  
	 /* invalid value in array */  
	TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "[1,]");    
	TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "[\"a\", nul]");
	#endif
}

static void test_parse_miss_comma_or_square_bracket() {
	#if 0    
	TEST_ERROR(LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET, "[1");
	TEST_ERROR(LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET, "[1}");
	TEST_ERROR(LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET, "[1 2");
	TEST_ERROR(LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET, "[[]");
	#endif
}

在測試上面的代碼時,會報出錯誤問題。

8.8 解析錯誤時的內存處理

遇到解析錯誤時,我們可能在之前已壓入了一些值在自定堆棧上。如果沒有處理,最後會在 lept_parse() 中發現堆棧上還有一些值,做成斷言失敗。所以,遇到解析錯誤時,我們必須彈出並釋放那些值。

在 lept_parse_array 中,原本遇到解析失敗時,會直接返回錯誤碼。我們把它改爲 break 離開循環,在循環結束後的地方用 lept_free() 釋放從堆棧彈出的值,然後才返回錯誤碼。

static int lept_parse_array(lept_context* c, lept_value* v) {
    /* ... */
    for (;;) {
        /* ... */
        if ((ret = lept_parse_value(c, &e)) != LEPT_PARSE_OK)
            break;
        /* ... */
        if (*c->json == ',') {
            /* ... */
        }
        else if (*c->json == ']') {
            /* ... */
        }
        else {
            ret = LEPT_PARSE_MISS_COMMA_OR_SQUARE_BRACKET;
            break;
        }
    }
    /* Pop and free values on the stack */
    for (i = 0; i < size; i++)
        lept_free((lept_value*)lept_context_pop(c, sizeof(lept_value)));
    return ret;
 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章