一. cJSON简介 cJSON格式是一种用于表示和交换数据的轻量级数据格式,常用于网络通信和数据存储。以下是一些常见的cJSON使用场景:
网络通信:cJSON常用于客户端和服务器之间的数据传输。客户端可以将数据转换为cJSON格式,并通过网络发送给服务器,服务器收到数据后可以使用cJSON解析库解析数据。同样地,服务器也可以将数据转换为cJSON格式返回给客户端。
数据存储:cJSON可以用于将数据以JSON格式存储在文件或数据库中。由于cJSON的简单性和易用性,它是一种常见的选择来存储和读取结构化数据。
API接口:很多Web服务的API接口返回的数据格式是JSON。使用cJSON可以方便地解析和处理这些API返回的数据。
配置文件:cJSON可以用于存储应用程序的配置信息。将配置信息以JSON格式存储在文件中,可以方便地读取和修改配置。
总的来说,cJSON格式的使用场景非常广泛,适用于各种需要表示和交换数据的情况。它的简单性、轻量性和易用性使得它成为处理JSON数据的常用工具之一。
二. cJSON结构体 若想在使用cJSON库的时候不犯一些内存方面的错误,理解cJSON结构体是很关键的。
从数据结构的角度来看,struct cJSON
是一颗孩子兄弟树 。
child
字段指向孩子节点
next
和prev
字段是这颗树的同一深度下的兄弟节点的双向链表实现。
1 2 3 4 5 6 7 8 9 10 11 typedef struct cJSON { struct cJSON *next ; struct cJSON *prev ; struct cJSON *child ; ... } cJSON;
假如我们有以下cjson格式的数据
1 2 3 4 5 { "item1": "value", "item2": [1, 2, 3], "item3": 1 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 (gdb) p obj->child->string $1 = 0x555555559360 "item1" (gdb) p obj->child->next->string $2 = 0x5555555594c0 "item2" (gdb) p obj->child->next->next->string $3 = 0x555555559530 "item3" (gdb) p obj->child->next->next->next $4 = (struct cJSON *) 0x0(gdb) p obj->child->prev->string $5 = 0x555555559530 "item3" (gdb) p (double)cJSON_GetNumberValue(obj->child->next->child) $6 = 1(gdb) p (double)cJSON_GetNumberValue(obj->child->next->child->next) $7 = 2(gdb) p (double)cJSON_GetNumberValue(obj->child->next->child->next->next) $8 = 3(gdb) p (double)cJSON_GetNumberValue(obj->child->next->child->prev) $9 = 3(gdb) p obj->child->next->child->prev->next $10 = (struct cJSON *) 0x0(gdb) p obj->child->next->child->next->next->next $11 = (struct cJSON *) 0x0(gdb)
那么他在内存中就应该是这样的:
三. cJSON类型 cJSON共有以下11种类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 #define cJSON_Invalid (0) #define cJSON_False (1 << 0) #define cJSON_True (1 << 1) #define cJSON_NULL (1 << 2) #define cJSON_Number (1 << 3) #define cJSON_String (1 << 4) #define cJSON_Array (1 << 5) #define cJSON_Object (1 << 6) #define cJSON_Raw (1 << 7) #define cJSON_IsReference 256 #define cJSON_StringIsConst 512
我们常用的其实只有cJSON_Number、cJSON_String、cJSON_Array、cJSON_Object、cJSON_IsReference这几种类型。 每种类型都是由struct cJSON
结构体表示的,通过type
字段进行区分。
3.1 cJSON_Number类型 cJSON_Number类型很简单:
可以通过cJSON_CreateNumber()
创建一个类型为cJSON_Number的cJSON实例。
可以通过cJSON_SetNumberValue()
修改一个cJSON_Number类型的cJSON实例的值。
可以通过cJSON_GetNumberValue()
查看一个cJSON_Number类型的cJSON实例的值。
由于这些函数除了需要一个cJSON实例外不需要任何动态分配的空间,当没有把指针所有权交给其他cJSON节点时,需要手动调用cJSON_Delete()
来释放资源 。
3.2 cJSON_String类型 cJSON_String类型要麻烦一点,这是由于cJSON库函数会为字符串分配空间。
可以通过cJSON_CreateString()
创建一个类型为cJSON_String的cJSON实例,并为字符串申请足够的空间,用来拷贝。
可以通过cJSON_SetValuestring()
修改一个cJSON_String类型的cJSON实例的值。
可以通过cJSON_GetStringValue()
查看一个cJSON_String类型的cJSON实例的值。
这三个函数中,cJSON_SetValuestring()
需要重点关注: 我们不需要手动去释放原有cJSON实例中动态分配的字符串空间。如果空间足够存放新字符串,库函数不会重新分配内存。如果空间不够的话,库函数会重新分配内存,并自动释放旧的字符串空间。
1 2 3 4 5 6 7 cJSON *str = cJSON_CreateString("string1" ); cJSON_SetValuestring(str, "long string2" ); cJSON_SetValuestring(str, "string3" ); cJSON_Delete(str);
3.3 cJSON_Array类型 创建一个cJSON_Array类型很简单,仅仅创建一个cJSON实例,并把type字段的cJSON_Array位置1而已
1 cJSON *arr = cJSON_CreateArray();
与cJSON_String、cJSON_Number不同的是,cJSON_Array类型的节点通常可以有子树。 数组通常使用[]
来表示。例如一个不含元素的空数组就可以用[]
表示。 数组中的每个元素类型在逻辑上应当是同一种类,例如:
数字数组:[1, 2, 3, 4]
字符串数组:[“elem1”, “elem2”, “elem3”]
混合类型数组(为什么不用object类型呢?):[“elem1”, “elem2”, “elem3”, 4, 5, 6]
只需要明白数据中的每个元素都是数组子结点的兄弟节点。节点的类型并不关键。
可以使用cJSON_ArrayForEach()
来遍历数组中的每个元素。
1 2 3 4 5 cJSON *pm, *arr cJSON_ArrayForEach (pm. arr) { }
可以通过cJSON_GetArraySize()
函数来获取数组长度。
3.4 cJSON_Object类型 就像cJSON_Array类型一样,cJSON_Object的创建很简单,仅仅创建一个cJSON实例,并把type字段的cJSON_Object位置1而已。
1 cJSON *obj = cJSON_CreateObject();
如果说cJSON_Array就像C语言中的数组一样,那么cJSON_Object就像C语言中的结构体。他可以容纳各种类型的cJSON实例。 obj类型通常用{}
表示。例如一个空的obj就可以表示为{}
。
1 2 3 4 5 6 7 { "item1": "value1", "item2": 2, "item3": [4, 5, 6], "item4": { } }
可以看出,item1是一个字符串类型,item2是一个整数类型,item3是一个数组,item4是一个空的obj类型。 与之前不同的是,不同类型的cJSON对象通过一个字符串itemX
注册到了根节点中。这就是object和array两个类型的最大区别。如果你不需要字符串索引,只需要下标索引,那么就不必使用cJSON_Object。
3.5 cJSON_IsReference类型 cJSON_IsReference类型并不会深度拷贝字符串、数组、object对象。他仅仅拷贝指向动态内存的指针。灵活使用将会减少内存的使用,也更容易造成内存使用上的困惑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main (int argc, char **argv) { cJSON *number_reference = NULL ; cJSON *number_array; int numbers[] = {1 ,2 ,3 ,4 ,5 }; number_array = cJSON_CreateIntArray(numbers, 5 ); number_reference = cJSON_CreateArrayReference(number_array->child); assert(number_reference->child == number_array->child); cJSON_Delete(number_array); cJSON_Delete(number_reference); return 0 ; }
四. 转移所有权 每创建一个cJSON实例都会动态分配内存,为了确保不会有内存泄漏的问题,我们需要管理好创建的每一个cJSON实例。
当我们创建一个数字类型的cJSON对象时,我们通常这样做:
1 2 cJSON *num1 = cJSON_CreateNumber(1 ); cJSON_Delete(num1);
此时,这个对象的所有权归于num1指针,num1需要管理这个对象的释放,如果后续不需要了,就应当即使释放。
而当我们将这个数字类型的cJSON对象加入到一个数组类型的cJSON对象时,指针的所有权就发生了变化。
1 2 3 4 5 cJSON *arr = cJSON_CreateArray(); cJSON *num1 = cJSON_CreateNumber(1 ); cJSON_AddItemToArray(arr, num1); cJSON_Delete(arr);
此时数字类型的cJSON对象的内存已经被数组类型的cJSON对象接管,也就是说,num1已经不需要为数字类型的cJSON对象的生命周期负责了。 这就是本节标题——转移所有权。等arr进行释放的时候,会递归的释放每个一个cJSON对象。
在以上例子中,num1
这个中间变量其实并没有存在的意义。我们可以直接这样向arr中添加数字类型的cJSON实例:
1 2 3 cJSON *arr = cJSON_CreateArray(); cJSON_AddItemToArray(arr, cJSON_CreateNumber(1 )); cJSON_Delete(arr);
五. cJSON的内存管理Hook cJSON库提供了管理内存的Hook点,可以通过注册私有的管理内存函数来监控进程中是否有内存泄漏。
这里需要注意的是通过cJSON_Print()
会使得全局变量cjson_alloc递增1,在释放的时候需要使用cJSON_free()
来递减,否则会造成计数不准确。
这里举一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <cjson/cJSON.h> static int cjson_alloc = 0 ;static void *my_malloc (size_t size) { cjson_alloc++; return malloc (size); } static void my_free (void *pointer) { cjson_alloc--; free (pointer); } static cJSON_Hooks my_memory_hook = { my_malloc, my_free }; int main () { const char test[] = "{" \ "\"Image\":{" \ "\"Width\":800," \ "\"Height\":600," \ "\"Title\":\"Viewfrom15thFloor\"," \ "\"Thumbnail\":{" \ "\"Url\":\"http:/*www.example.com/image/481989943\"," \ "\"Height\":125," \ "\"Width\":\"100\"" \ "}," \ "\"IDs\":[116,943,234,38793]" \ "}" \ "}" ; cJSON_InitHooks(&my_memory_hook); cJSON *obj = cJSON_Parse(test); char *p = cJSON_Print(obj); printf ("%s\n" , p); cJSON_free(p); cJSON_Delete(obj); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
六. 一些容易造成内存异常的编码 6.1 未进行所有权转移 1 2 3 4 5 6 7 8 9 10 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *obj = cJSON_CreateObject(); cJSON *str1 = cJSON_CreateString("string1" ); cJSON_Delete(obj); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
这种错误很基础,但是容易被忽视,正确的做法是及时将所有权转移给obj
:
1 2 3 4 5 6 7 8 9 10 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *obj = cJSON_CreateObject(); cJSON *str1 = cJSON_CreateString("string1" ); cJSON_AddItemToObject(obj, "str1" , str1); cJSON_Delete(obj); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
更推荐的做法是:
1 2 3 4 5 6 7 8 9 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *obj = cJSON_CreateObject(); cJSON_AddStringToObject(obj, "str1" , "string1" ); cJSON_Delete(obj); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
直接不引入临时变量str1,创建之后立即进行所有权转移。
如果在接下来的代码中需要用到这个变量str1。那么请这样做:
1 2 3 4 5 6 7 8 9 10 11 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *str1; cJSON *obj = cJSON_CreateObject(); cJSON_AddItemToObject(obj, "str1" , str1 = cJSON_CreateString("string1" )); cJSON_Delete(obj); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
总之,需要正确处理每个对象的所有权,尽可能在创建对象后就进行所有权转移。
6.2 detach函数族 detach系列的函数仅仅是把cJSON对象从树中摘取下来。并没有进行释放。如果后续不再使用了,需要及时释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); int numbers[] = {1 ,2 ,3 ,4 }; cJSON *arr = cJSON_CreateIntArray(numbers, 4 ); cJSON_DetachItemFromArray(arr, 2 ); cJSON_Delete(arr); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
应该使用一个零时变量来保留detach函数的返回值,再决定是否需要释放这个被摘下来的节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); const char *strings[] = {"string1" , "string2" , "string3" , "string4" }; cJSON *arr = cJSON_CreateStringArray(strings, 4 ); cJSON *todel = cJSON_DetachItemFromArray(arr, 2 ); cJSON_Delete(todel); cJSON_Delete(arr); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
如果我们要做的就是删除数组中的一个节点(包括释放),可以直接使用cJSON_DeleteItemFromArray()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); const char *strings[] = {"string1" , "string2" , "string3" , "string4" }; cJSON *arr = cJSON_CreateStringArray(strings, 4 ); cJSON_DeleteItemFromArray(arr, 2 ); cJSON_Delete(arr); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
最后再看一下能否通过detach下来的节点访问他的兄弟节点呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); int numbers[] = {1 ,2 ,3 ,4 }; cJSON *arr = cJSON_CreateIntArray(numbers, 4 ); cJSON *detach = cJSON_DetachItemFromArray(arr, 2 ); assert(detach->next == NULL ); assert(detach->prev == NULL ); cJSON_Delete(detach); cJSON_Delete(arr); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
可以看出被摘下来的节点将是一个被兄弟们孤立的状态,当然他可以拥有自己孩子节点。
6.3 replace函数族 如果需要修改数组中的某个元素的内容,更建议使用replace函数来进行修改,而非直接操作元素的cJSON结构体。这样方便对整个cJSON库的内存管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *beginning = cJSON_CreateNull(); cJSON *middle = cJSON_CreateNull(); cJSON *end = cJSON_CreateNull(); cJSON *array = cJSON_CreateArray(); cJSON_AddItemToArray(array , beginning); cJSON_AddItemToArray(array , middle); cJSON_AddItemToArray(array , end); cJSON replacements[3 ]; memset (replacements, '\0' , sizeof (replacements)); cJSON_ReplaceItemViaPointer(array , beginning, &(replacements[0 ])); assert(replacements[0 ].prev == end); assert(replacements[0 ].next == middle); assert(middle->prev == &(replacements[0 ])); assert(array ->child == &(replacements[0 ])); cJSON_ReplaceItemViaPointer(array , middle, &(replacements[1 ])); cJSON_ReplaceItemViaPointer(array , end, &(replacements[2 ])); cJSON_Delete(beginning); cJSON_Delete(middle); cJSON_Delete(end); cJSON_free(array ); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
replace函数将会主动对废弃的节点进行cJSON_Delete()的动作,所以不必担心内存泄露。
6.4 cJSON_AddItemToObject将会对key值进行深拷贝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *object = cJSON_CreateObject(); cJSON *number = cJSON_CreateNumber(42 ); char *name = (char *)cJSON_strdup((const unsigned char *)"number" , &my_memory_hook); number->string = name; cJSON_AddItemToObject(object, number->string , number); printf ("name is %s\n" , name); cJSON_Delete(object); cJSON_free(name); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
6.5 cJSON_AddItemToObjectCS的key应当使用const char类型 在进行所有权转移的时候,我们通常使用cJSON_AddItemToObject()
函数的第二个参数来作为item的key。此时库函数会申请内存对key值进行深拷贝。 但如果我们不需要这种内存的申请时,可以使用cJSON_AddItemToObjectCS()
来转移所有权。
1 2 3 4 5 6 7 8 9 10 11 12 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *obj = cJSON_CreateObject(); const char key[] = "number12" ; cJSON_AddItemToObjectCS(obj, key, cJSON_CreateNumber(1 )); cJSON_Delete(obj); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
请注意,我们的key的类型为const char类型。这是因为在cJSON中,默认key值是不可以修改的。 但是如果我们去掉const声明。意味着我们可以通过修改key来间接修改cJSON对象的key值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *obj = cJSON_CreateObject(); char key[] = "number12" ; cJSON_AddItemToObjectCS(obj, key, cJSON_CreateNumber(1 )); print_cjson(obj); key[7 ] = '\0' ; print_cjson(obj); cJSON_Delete(obj); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
6.6 浅拷贝无法删除整棵树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *number_reference = NULL ; cJSON *number_array; int numbers[] = {1 ,2 ,3 ,4 ,5 }; number_array = cJSON_CreateIntArray(numbers, 5 ); number_reference = cJSON_CreateArrayReference(number_array->child); assert(number_reference->child == number_array->child); cJSON_Delete(number_reference); printf ("cjson_alloc is %d\n" , cjson_alloc); return 0 ; }
我们依旧可以通过number_array
来访问整棵树,所以如果这颗树不再需要了,需要对number_array进行delete操作。
6.7 通过cJSON_GetNumberValue进行打印 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main (int argc, char **argv) { cJSON_InitHooks(&my_memory_hook); cJSON *arr, *pm; int n; int numbers[] = {1 ,2 ,3 ,4 ,5 }; arr = cJSON_CreateIntArray(numbers, 5 ); cJSON_ArrayForEach(pm, arr) printf ("%d\n" , cJSON_GetNumberValue(pm)); cJSON_Delete(arr); return 0 ; }
在我的环境中,将会有如下打印:
1 2 3 4 5 6 user@debian:/tmp$ ./a.out 422585392 422585472 422585472 422585472 422585472
原因是cJSON_GetNumberValue
只会返回valuedouble字段,需要我们更具业务需要进行强转。