一. cJSON简介

cJSON格式是一种用于表示和交换数据的轻量级数据格式,常用于网络通信和数据存储。以下是一些常见的cJSON使用场景:

  1. 网络通信:cJSON常用于客户端和服务器之间的数据传输。客户端可以将数据转换为cJSON格式,并通过网络发送给服务器,服务器收到数据后可以使用cJSON解析库解析数据。同样地,服务器也可以将数据转换为cJSON格式返回给客户端。

  2. 数据存储:cJSON可以用于将数据以JSON格式存储在文件或数据库中。由于cJSON的简单性和易用性,它是一种常见的选择来存储和读取结构化数据。

  3. API接口:很多Web服务的API接口返回的数据格式是JSON。使用cJSON可以方便地解析和处理这些API返回的数据。

  4. 配置文件:cJSON可以用于存储应用程序的配置信息。将配置信息以JSON格式存储在文件中,可以方便地读取和修改配置。

总的来说,cJSON格式的使用场景非常广泛,适用于各种需要表示和交换数据的情况。它的简单性、轻量性和易用性使得它成为处理JSON数据的常用工具之一。

二. cJSON结构体

若想在使用cJSON库的时候不犯一些内存方面的错误,理解cJSON结构体是很关键的。

从数据结构的角度来看,struct cJSON是一颗孩子兄弟树

  • child字段指向孩子节点
  • nextprev字段是这颗树的同一深度下的兄弟节点的双向链表实现。
1
2
3
4
5
6
7
8
9
10
11
/* The cJSON structure: */
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;

...
} 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_tree

三. cJSON类型

cJSON共有以下11种类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* cJSON Types: */
#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) /* raw json */

#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");
/* string1会被释放 */
cJSON_SetValuestring(str, "long string2");
/* long string2不会被释放、string3将使用这块内存 */
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);
/* 可以看到number_reference和number_array的孩子都指向同一块内存 */
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();
/* 忘记把str1注册到obj中导致的内存泄漏 */
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);

/* detach节点后并没有进行释放,导致内存泄露*/
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);

/* detach+delete */
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);

/* detach节点后并没有进行释放,导致内存泄露*/
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]));

/* 错误,Replace函数已经对被替换的废弃节点进行了释放,不可以再次delete */
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);
/* 错误!
cJSON_AddItemToObject会对第二个参数进行深拷贝,
并且如果发现obj的string字段非空,会进行free动作,
此时再访问name字段会访问无效指针 */
printf("name is %s\n", name);

cJSON_Delete(object);
/* 错误,name已经被释放过了 */
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,会间接将number12修改为number1 */
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);
/* 错误,删除浅拷贝仅仅删除指定的cJSON节点,并不会递归删除子树,这样会造成内存泄露 */
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字段,需要我们更具业务需要进行强转。