cJSON專案解析

2020-08-10 16:12:10

首先簡單瞭解一下JSON。它是一種輕量級的數據交換格式。以一定的格式儲存數據。看一個簡單的例子就明白了:

{
	"name": "gao",
	"age": 21,
	"friends": ["張三", "李四"],
	"more": {
		"city": "湖北",
		"country": "中國"
	}
}

[]表示陣列,{}表示物件,物件中包含鍵值對,值可以是物件、陣列、數位、字串或者三個字面值(false、null、true)中的一個,也說明了可以陣列和物件可以巢狀使用。關於JSON詳細介紹可以查閱相關資料,下面 下麪着重介紹cJSON專案。
cJSON是國外大神用c語言寫的非常簡單的生成和解析JSON數據格式的工具。只包含cJSON.h和cJSON.c兩個檔案,用一個main函數去呼叫相應的方法即可。
介紹一個JSON數據(生成解析均以下例爲例)

{
	"奔馳":	{
		"factory":	"一汽大衆",
		"last":	31,
		"price":83,
		"sell":	49,
		"sum":	80,
		"other":[123, 1, "hello world", 
					{
						"梅賽德斯":"心所向"
					}
				],
		"color":{
			"顏色":["紅","綠","黑"]
		}
	}
}

下面 下麪用程式碼的方式生成這樣一個格式的數據,並列印在控制檯上:
數據包含四組{}物件、兩組陣列。最外層是一個大的物件obj,建立物件:

cJSON* obj = cJSON_CreateObject();

物件中包含奔馳鍵值對,值對應的又是一個物件subObj,subObj物件中包含一些鍵值對,物件中新增鍵值對:

cJSON_AddItemToObject(subObj, "factory", cJSON_CreateString("一汽大衆"));
...

other鍵對應的值是一個數組array1,陣列中包含一些元素和物件subsub1,陣列中新增元素和物件:

//建立陣列
cJSON* array1 =  cJSON_CreateArray();
cJSON_AddItemToArray(array1, cJSON_CreateNumber(123));
...
cJSON_AddItemToArray(array1, subsub1);

color鍵對應的值是一個物件subsub2,物件中顏色鍵對應的值是一個數組array2,因此需要建立array2陣列,將該陣列新增到subsub2物件中,color鍵值對再新增到奔馳物件中,最後將奔馳鍵值對新增到最大的obj物件中。
程式碼如下:

int main()
{
	//建立物件
	cJSON* obj = cJSON_CreateObject();

	//建立子物件(奔馳)
	cJSON* subObj = cJSON_CreateObject();

	//向奔馳物件中新增key-value
	cJSON_AddItemToObject(subObj, "factory", cJSON_CreateString("一汽大衆"));
	cJSON_AddItemToObject(subObj, "last", cJSON_CreateNumber(31));
	cJSON_AddItemToObject(subObj, "price", cJSON_CreateNumber(83));
	cJSON_AddItemToObject(subObj, "sell", cJSON_CreateNumber(49));
	cJSON_AddItemToObject(subObj, "sum", cJSON_CreateNumber(80));

	//建立json陣列(other)
	cJSON* array1 = cJSON_CreateArray();
	//向other陣列中新增元素
	cJSON_AddItemToArray(array1, cJSON_CreateNumber(123));
	cJSON_AddItemToArray(array1, cJSON_CreateNumber(1));
	cJSON_AddItemToArray(array1, cJSON_CreateString("hello world"));

	//建立other中的物件
	cJSON* subsub1 = cJSON_CreateObject();
	cJSON_AddItemToObject(subsub1, "梅賽德斯", cJSON_CreateString("心所向"));

	//向other陣列中新增物件
	cJSON_AddItemToArray(array1, subsub1);

	//向奔馳物件中新增other
	cJSON_AddItemToObject(subObj, "other", array1);

	//建立color中的subsub2物件
	cJSON* subsub2 = cJSON_CreateObject();
	//建立color中顏色陣列
	cJSON* array2 = cJSON_CreateArray();

	//向顏色陣列array2新增元素
	cJSON_AddItemToArray(array2, cJSON_CreateString("紅"));
	cJSON_AddItemToArray(array2, cJSON_CreateString("綠"));
	cJSON_AddItemToArray(array2, cJSON_CreateString("黑"));

	//新增顏色陣列到subsub2物件中
	cJSON_AddItemToObject(subsub2, "顏色", array2);

	//向奔馳物件中新增color key-value
	cJSON_AddItemToObject(subObj, "color", subsub2);

	//向obj中新增奔馳key-value
	cJSON_AddItemToObject(obj, "奔馳", subObj);

	//列印數據
	printf("帶格式輸出:\n");
	char* data = cJSON_Print(obj);
	printf("%s\n", data);
	printf("不帶格式輸出:\n");
	char* data1 = cJSON_PrintUnformatted(obj);
	printf("%s\n", data1);

	return 0;
}

在这里插入图片描述
下面 下麪將字串解析成帶格式的輸出,需要注意把字串中的雙引號跳脫

int main()
{
	char* value = "{\"奔馳\":{\"factory\":\"一汽大衆\",\"last\":31,\"price\":83, \
		\"sell\":49,\"sum\":80,\"other\":[123,1,\"hello world\",{\"梅賽德斯\":   \
		\"心所向\"}],\"color\":{\"顏色\":[\"紅\",\"綠\",\"黑\"]}}}";	

	cJSON* obj1 = cJSON_Parse(value);

	char* data1 = cJSON_Print(obj1);

	printf("%s", data1);
}

在这里插入图片描述
下面 下麪具體說一下程式是如何執行的。
程式是採用雙向鏈表的數據結構

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

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

	char *valuestring;			/* The item's string, if type==cJSON_String */
	int valueint;				/* The item's number, if type==cJSON_Number */
	double valuedouble;			/* The item's number, if type==cJSON_Number */

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

以下例爲例看看數據在鏈表中究竟是如何儲存的:

{
	"奔馳":	{
		"factory":	"一汽大衆",
		"last":	31,
		"price":83,
		"color":{
			"顏色":["紅","綠","黑"]
		}
	}
}

鏈表以逗號分隔結點:
在这里插入图片描述
提供了查詢陣列大小、刪除結點等一系列方法:

int    cJSON_GetArraySize(cJSON *array);
void   cJSON_DeleteItemFromArray(cJSON *array,int which);
void   cJSON_DeleteItemFromObject(cJSON *object,const char *string);
...

程式最重要的是對字串的解析,下面 下麪看一下程式是如何解析變換的。
跟蹤原始碼可以發現:
首先呼叫cJSON *cJSON_Parse(const char *value)傳入壓縮的字串,再呼叫cJSON *cJSON_ParseWithOpts(const char *value,...)在該函數內呼叫static const char *parse_value(cJSON *item,const char *value)

/* Parser core - when encountering text, process appropriately. */
static const char *parse_value(cJSON *item,const char *value)
{
	if (!value)						return 0;	/* Fail on null. */
	if (!strncmp(value,"null",4))	{ item->type=cJSON_NULL;  return value+4; }
	if (!strncmp(value,"false",5))	{ item->type=cJSON_False; return value+5; }
	if (!strncmp(value,"true",4))	{ item->type=cJSON_True; item->valueint=1;	return value+4; }
	if (*value=='\"')				{ return parse_string(item,value); }
	if (*value=='-' || (*value>='0' && *value<='9'))	{ return parse_number(item,value); }
	if (*value=='[')				{ return parse_array(item,value); }
	if (*value=='{')				{ return parse_object(item,value); }

	ep=value;return 0;	/* failure. */
}

判斷傳入的value值第一個符號是什麼,再分別呼叫相應的解析函數,因此要去除value中開頭的空格,防止識別錯誤,這裏用到一個函數,去除字串開頭的空格。

//去除字串開頭的空格
static const char *skip(const char *in) 
{	
	while (in && *in && (unsigned char)*in<=32) 
		in++; 
	return in;
}

這裏認爲ASCII爲32之前的都是非法字元(空格的ASCII十進制爲32),不得不讚嘆程式碼的簡潔啊。同時這也是學習原始碼的好處。
再回到parse_value函數中,這裏JSON字串是物件"{「開始,因此呼叫parse_object,在parse_object中通過parse_string函數解析鍵、parse_value解析值,遇到」,"說明不止一個鍵值對繼續進行。對於進入parse_value如果值還爲物件則繼續呼叫parse_object如此遞回回圈直到結束。解析陣列也是同樣的道理。根據字串建立雙向鏈表,完成後返回鏈表的頭結點。

static const char *parse_string(cJSON *item,const char *str);
static const char *parse_number(cJSON *item,const char *num);
static const char *parse_array(cJSON *item,const char *value);
static const char *parse_object(cJSON *item,const char *value);

列印有兩種方式,按格式列印和沒有格式列印

/* Render a cJSON item/entity/structure to text. */
char *cJSON_Print(cJSON *item)				{return print_value(item,0,1,0);}
char *cJSON_PrintUnformatted(cJSON *item)	{return print_value(item,0,0,0);}

區別就在於print_value中的第三個參數。

static char *print_value(cJSON *item,int depth,int fmt,printbuffer *p)
{
        switch ((item->type)&255)
		{
			case cJSON_NULL:	out=cJSON_strdup("null");	break;
			case cJSON_False:	out=cJSON_strdup("false");break;
			case cJSON_True:	out=cJSON_strdup("true"); break;
			case cJSON_Number:	out=print_number(item,0);break;
			case cJSON_String:	out=print_string(item,0);break;
			case cJSON_Array:	out=print_array(item,depth,fmt,0);break;
			case cJSON_Object:	out=print_object(item,depth,fmt,0);break;
		}
}

事實上也就是根據該參數判斷是否列印換行符和製表符,如果按格式列印,則輸出相應的換行符和製表符,看起來更加直觀。如果沒有格式,則只是字串。(效果見圖一)

static char *print_number(cJSON *item,printbuffer *p);
static char *print_string(cJSON *item,printbuffer *p);
static char *print_array(cJSON *item,int depth,int fmt,printbuffer *p);
static char *print_object(cJSON *item,int depth,int fmt,printbuffer *p);

剩下的工作就是在相應的函數,處理字串了。最終返回一個char*型別的字串,列印輸出即可。

Linux命令:
gdb偵錯中,偵錯帶參數的程式
gdb --args ./filname arg1 arg2