cJSON使用問題記錄

人工晶振發表於2020-12-14

cJSON庫使用筆記

JSON:: 輕量級的資料交換語言,該語言以易於讓人閱讀的文字為基礎,用來傳輸由屬性值或者序列性的值組成的資料物件。cJSON是一個輕量級的開源JSON解析器,能夠實現使用C語言生成或解析json格式的物件,本文主要記錄使用該解析器的過程中,遇到的一些問題。

New

通過了解cJSON庫中用於定義cJSON物件的結構體與cJSON_New_Item函式:

typedef struct cJSON
{
    /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
    struct cJSON *next;
    struct cJSON *prev;
    /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
    struct cJSON *child;

    /* The type of the item, as above. */
    int type;

    /* The item's string, if type==cJSON_String  and type == cJSON_Raw */
    char *valuestring;
    /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
    int valueint;
    /* The item's number, if type==cJSON_Number */
    double valuedouble;

    /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
    char *string;
} cJSON;

/* Internal constructor. */
static cJSON *cJSON_New_Item(void)
{
	cJSON* node = (cJSON*)cJSON_malloc(sizeof(cJSON));
	if (node) memset(node, 0, sizeof(cJSON));
	return node;
}

不難發現,本質上可以將cJSON物件理解為一個雙向連結串列。在新建cJSON物件時,需要對其進行初始化操作,不然會造成存在野指標訪問記憶體的問題。

//例項一個新的cJSON物件
cJSON *json_new = cJSON_CreateObject();

//解析一串符合json格式的字串,並新建一個cJSON指標指向其解析結果
char *str = "{\"Title\":\"Hello World!\"}";
cJSON *json_parse = cJSON_Parse(str);

Delete

既然cJSON物件的空間是被動態分配出來的,在使用完該物件後自然也需要在記憶體中釋放掉相關資源,cJSON中提供了cJSON_Delete(cJSON *obj);函式便於開發者釋放cJSON物件的空間,但是需要注意,在釋放空間時只需要釋放最上層的父節點,例如以下cJSON物件被新建後:

cJSON *json_root = cJSON_CreateObject();
cJSON *json_array = cJSON_CreateArray();
cJSON *json_item1 = cJSON_CreateObject();
cJSON *json_item2 = cJSON_CreateObject();

cJSON_AddStringToObject(json_item1, "name" , "張三");
cJSON_AddNumberToObject(json_item1, "age" , 15);
cJSON_AddStringToObject(json_item2, "name" , "李四");
cJSON_AddNumberToObject(json_item2, "age" , 20);

cJSON_AddItemToArray(json_array, json_item1);
cJSON_AddItemToArray(json_array, json_item2);

cJSON_AddNumberToObject(json_root, "index" , 0);
cJSON_AddItemToObject(json_root, "list", json_array);

char *str_json = cJSON_Print(json_root);
printf("%s", str_json);
/*
{
    "index":1,
    "list":[
        {
            "name":"張三",
            "age":15
        },
        {
            "name":"李四",
            "age":20
        }
    ]
}
*/

cJSON_Delete(json_root);
free(str_json);

可以看到,在程式的起始部分,新建了四個cJSON物件,但在後續的操作中,json_item1與json_item2先後被新增至json陣列json_list中,最後json_list被新增至json_root中,此時根據cJSON物件的結構體成員與cJSON_AddXxxToYyy函式的實現方法,能夠看出通過訪問json_root->child的子節點便能夠遍歷其他三個子節點,這時只需要對父節點呼叫cJSON_Delete()函式即可將子節點一同釋放,若在Delete父節點後又對子節點進行了Delete操作,則會引發釋放野指標的錯誤。

上述的物件需要Delete的問題其實有用過cJSON的程式猿應該都知道,接下來這個問題才是讓我真正想記錄這篇文章的原動力。。。

cJSON *json_root = cJSON_CreateObject();
cJSON_AddNumberToObject(json_root, "index" , 0);
printf(cJSON_Print(json_root));
cJSON_Delete(json_root);

上面這幾行程式碼看著一點毛病也沒有,增加、修改、輸出、釋放一氣呵成,實際執行起來也的確成功輸出了"{“index”:0}",那麼問題點在哪呢?
將cJSON_Print“剝開”,一層一層往裡面看,可以發現當需要輸出資料型別為整型時,呼叫瞭如下函式:

/* Render the number nicely from the given item into a string. */
static char *print_number(cJSON *item, printbuffer *p)
{
	char *str = 0;
	double d = item->valuedouble;
	if (d == 0)
	{
		if (p)	str = ensure(p, 2);
		else	str = (char*)cJSON_malloc(2);	/* special case for 0. */
		if (str) strcpy(str, "0");
	}
	else if (fabs(((double)item->valueint) - d) <= DBL_EPSILON && d <= INT_MAX && d >= INT_MIN)
	{
		if (p)	str = ensure(p, 21);
		else	str = (char*)cJSON_malloc(21);	/* 2^64+1 can be represented in 21 chars. */
		if (str)	sprintf(str, "%d", item->valueint);
	}
	else
	{
		if (p)	str = ensure(p, 64);
		else	str = (char*)cJSON_malloc(64);	/* This is a nice tradeoff. */
		if (str)
		{
			if (fabs(floor(d) - d) <= DBL_EPSILON && fabs(d)<1.0e60)sprintf(str, "%.0f", d);
			else if (fabs(d)<1.0e-6 || fabs(d)>1.0e9)			sprintf(str, "%e", d);
			else												sprintf(str, "%f", d);
		}
	}
	return str;
}

這也就直接導致了,每列印輸出一次,記憶體中就會對應著產生一段被開闢而不被佔用的記憶體(其實看返回的char*就能夠判斷出會有這個現象了,本質上還是自己太菜+寫程式碼太不嚴謹),在電腦上可能用得好好的,但偶然有一次在STM32上用了cJSON,沒跑一會直接給我報了個HardFault。。。。急得我把程式碼拷到VS上看了波記憶體佔用情況,廢了半天功夫才找到問題。

Edit

對Json物件中的某部分內容進行修改是一項極為正常的操作,當修改的資料為整型的時候,可能大概看了一遍cJSON的結構體定義,就決定直接使用json_obj->valueint = i的方法來賦值,而但凡看一遍了"cJSON.h",就能夠發現在標頭檔案的最底部有這麼一個巨集定義:

/* When assigning an integer value, it needs to be propagated to valuedouble too. */
#define cJSON_SetIntValue(object,val)			((object)?(object)->valueint=(object)->valuedouble=(val):(val))
#define cJSON_SetNumberValue(object,val)		((object)?(object)->valueint=(object)->valuedouble=(val):(val))

由註釋可知,當Json物件中的整型資料被賦值時,需要同時設定下double值。雖然沒有遇到過,但是看網上不少老哥踩到了這個坑。而在修改string型別的值時,可以使用cJSON_ReplaceItemInObject函式對物件進行操作。

相關文章