见名知意, cJSON_Delete 函数用于释放一个cJSON结构体变量所申请的内存空间. 在阅读本章节内容之前, 我强烈推荐你先阅读 cJSON数据组装框架 章节, 这对你理解本节内容有很重要的帮助. 若你对JSON的某些语法或概念有些模糊了, 可以参考 JSON.org 。
先附上cJSON_Delele函数的内部代码实现:
void cJSON_Delete(cJSON *c)
{
cJSON *next;
while (c)
{
next=c->next;
if (!(c->type&cJSON_IsReference/*256*/) && c->child) cJSON_Delete(c->child);
if (!(c->type&cJSON_IsReference) && c->valuestring) cJSON_free(c->valuestring);
if (!(c->type&cJSON_StringIsConst/*512*/) && c->string) cJSON_free(c->string);
cJSON_free(c);
c=next;
}
}
此外, 在这里还需要你有掌握或了解过"递归"方面的知识. 因为cJSON_Delete函数内部采用了递归的方式, 不断递归释放cJSON对象或数组内部嵌套的其他对象或是数组, 直到最后的节点其成员child为空.
现在开始源码分析. while(c){} 循环能够保证即使是单个cJSON对象(即内部无嵌套其他类型数据)也能够成功被释放掉, 除非指针为空. cJSON *next(推荐定义的变量初始化, 改为cJSON *next = NULL; 尽管该代码中能够保证不会操作野指针.); 指针变量next用于指向cJSON中位于同级的cJSON对象、数组、或普通类型变量等. 第一次进来时候, next指针变量肯定是为空, 无论该cJSON对象内部是否嵌套其他数据类型. 因为cJSON的规则是:要么对象, 要么数组, 要么普通数据类型. 无法同时一个cJSON是这两种或多种数据类型的组合. 如下:
1) cJSON是一个对象: {}
2) cJSON是一个数组: []
3) cJSON是一个字符串: " "
4) cJSON是一个数组: 0~N
5) cJSON是一个空值: null(备: 小写), null是JSON中的一个特殊值, 可以对任何数据类型设置该值,包括数组、对象、数字和布尔类型. 若需了解更多, 可参考 处理JSON null 。
6) cJSON是一个布尔值: false, true(备: 小写)
但是不会出现一个JSON对象同时是:对象{}, 又同时是数组的.
示例如下, 对于这样一个JSON对象类型, 首先, 该JSON对象的各成员参数值如下:{next = 0x0, prev = 0x0, child = 0x609060, type = 6, valuestring = 0x0, valueint = 0, valuedouble = 0, string = 0x0}. 所以第一次时候, cJSON*next = c->next为空.
{
"name": "lixiaogang5",
"company": "HIKVISION",
"department": "R&D Center",
"jobs": "Backend Engineer",
"floor": 15
}
对于上面的这个JSON对象, 在cJSON内部的框架布局结构如下图1所示.
图1 cJSON对象内部框架布局结构图
JSON对象中的每个数据类型 key:value, 若type是字符串类型, 那么在创建该JSON数据类型时候, 除了为key(对应cJSON数据结构中的string成员)分配用户指定的字符串(比如上面示例中的name、company、jobs等)空间大小外, 还会为其对应的valuestring成员申请一片内存空间, 该空间大小为用户实际待存储的value字符串大小.
比如, 下面是向JSON Object中加入第一条数据类型 “name”: "lixiaogang5"时, 其value的内存空间申请过程. 如下:
static cJSON *cJSON_New_Item(void)
{
#if 0
//cJSON_malloc 就是malloc
static void *(*cJSON_malloc)(size_t sz) = malloc;
static void (*cJSON_free)(void *ptr) = free;
#endif
cJSON* node = (cJSON*)cJSON_malloc(sizeof(cJSON));
if (node) memset(node,0,sizeof(cJSON));
return node;
}
//cJSON_strdup就是strdup的一个实现.
static char* cJSON_strdup(const char* str)
{
size_t len;
char* copy;
len = strlen(str) + 1;
if (!(copy = (char*)cJSON_malloc(len))) return 0;
memcpy(copy,str,len);
return copy;
}
cJSON *cJSON_CreateString(const char *string)
{
cJSON *item=cJSON_New_Item();
if(item)
{
item->type=cJSON_String;
//申请用户待存储的字符串的长度内存空间大小, 并将其拷贝到申请的内存空间中.
item->valuestring=cJSON_strdup(string);
}
return item;
}
在示例中, JSON对象内部共有4个字符串的数据类型, 外加一个数值类型. 因此该对象的内存存储释放详细过程如下图2所示.
图2 cJSON释放示例1中JSON对象的流程
图2中的符号 × , 表示在该位置将调用free函数释放对应内存空间. 对于属于字符串类型的数据类型, 将多调用一次free函数, 即释放valuestring成员申请的内存空间.
if (!(c->type&cJSON_IsReference) && c->valuestring) cJSON_free(c->valuestring);
最近的一个项目中, 我遇到了这个问题. 训练算法分析出来的JSON报文中, 内部嵌套的某个JSON对象里, 其中某个为字符串类型的value, 我这边需要用自己模块内部的数据去替换其原有的value. 然后将这个JSON报文存储到文件(不存储数据库表是因为该JSON大小在20KB左右)之后便释放掉该JSON. 然后再cJSON_Delete位置处发生了段错误, 非法操作内存空间导致.
首先贴上算法吐出的JSON报文部分数据, 如下(备注: 以下的这个JSON报文是以透传的方式过来的, 即该JSON是作为网络交互JSON报文中的某个关键字key的value值, 即一个压缩且经转义后的字符串.):
{
"calibInfo": {
"VideoChannels": [{
....... //省略若干
"MediaInfo": {
"MediaType": 1,
/*
* FilePath的value是需要我这边解析替换掉, 然后再将
* 这条JSON报文存储.
*/
"FilePath": "",
"LocalPath": " ",
....... //省略若干
},
...... //省略若干
}],
}
}
我这边底层模块收到的JSON报文中, 上面这个算法是字符串, 因此需要调用cJSON库中的cJSON_Parse函数解析, 如下:
cJSON *pRootObj = cJSON_Parse(/*收到的训练算法压缩转业后的JSON字符串*/);
assert(pRootObj);
//继续其他义务处理.....
然后替换JSON中的对应key的value之后, 需要再次将其格式化为字符串. 存储好转以后的字符串之后, 需要释放解析JSON字符串cJSON_Parse函数的返回值. 即上面提到的 pRootObj指针对象; 然后就在这里使程序宕掉. gdb报错提示:Program received signal SIGABRT, Aborted. 非法操作内存, 导致底层内核触发abort函数, 从而发出SIGABRT信号, 该信号的默认操作是终止程序运行.
经过分析cJSON源码库及自己代码书写review, 导致程序宕掉的根因有两个:
1) 待替换的关键字key, 在训练算法透传过来的字符串中,其value为空, 因此不会为此(即valuestring成员)分配空间. 这部分可参考cJSON源码cJSON.c文件中的parse_string函数.
/*解析JSON是字符串的序列化数据.*/
static const char *parse_string(cJSON *item,const char *str)
{
const char *ptr=str+1;char *ptr2;char *out;int len=0;unsigned uc,uc2;
//不是字符串,不满足"key":value
if (*str!='\"') {ep=str;return 0;}
//计算key or value的长度.跳过第一个字符\", 忽略key的紧随后面的一个\"
while (*ptr!='\"' && *ptr && ++len) if (*ptr++ == '\\') ptr++; //跳过转义符(转义字符不计算在内) eg:"Key_\"00\":"
//申请key字符串的大概长度空间(+1 字符串结尾符, \0)
out=(char*)cJSON_malloc(len+1);
if (!out) return 0;
ptr=str+1;
ptr2=out;
while (*ptr!='\"' && *ptr)
{
if (*ptr!='\\') *ptr2++=*ptr++;
else
{
/*处理转义字符. by lixiaogang5.
* ---------------------------------------------------------------------------------
*| 转义字符 | 意义 | ASCII码值(十进制) |
*| \a |响铃(BEL) | 007 |
*| \b |退格(BS),将当前位置移到前一列 | 008 |
*| \f |换页(FF),将当期位置移到下页开头 | 012 |
*| \n |换行(LF),将当期位置移到下一行开头 | 010 |
*| \r |回车(CR),将当前位置移到本行开头 | 013 |
*| \t |水平制表(HT),跳到下一个TAB位置 | 009 |
*| \v |垂直制表(VT) | 011 |
*| \\ |代表一个反斜杠字符'\' | 092 |
*| \' |代表一个单引号(撇号)字符 | 039 |
*| \" |代表一个双引号字符 | 034 |
*| \? |代表一个问号 | 063 |
*| \0 |空字符(NUL) | 000 |
*| \ddd |1到3位八进制所代表的任意字符 | 三位八进制 |
*| \xhh |十六进制所代表的任意字符 | 十六进制 |
* ---------------------------------------------------------------------------------
*/
ptr++;
//具体转义字符
switch (*ptr)
{
case 'b': *ptr2++='\b'; break;
case 'f': *ptr2++='\f'; break;
case 'n': *ptr2++='\n'; break;
case 'r': *ptr2++='\r'; break;
case 't': *ptr2++='\t'; break;
case 'u': /* transcode utf16 to utf8.将utf16转换为utf8编码 */
// ptr+1, 去掉\转义符后面的第一个字符
uc=parse_hex4(ptr+1);ptr+=4; /* get the unicode char. */
if ((uc>=0xDC00 && uc<=0xDFFF) || uc==0) break; /* check for invalid. */
if (uc>=0xD800 && uc<=0xDBFF) /* UTF16 surrogate pairs. */
{
if (ptr[1]!='\\' || ptr[2]!='u') break; /* missing second-half of surrogate. */
uc2=parse_hex4(ptr+3);ptr+=6;
if (uc2<0xDC00 || uc2>0xDFFF) break; /* invalid second-half of surrogate. */
uc=0x10000 + (((uc&0x3FF)<<10) | (uc2&0x3FF));
}
len=4;if (uc<0x80) len=1;else if (uc<0x800) len=2;else if (uc<0x10000) len=3; ptr2+=len;
switch (len) {
case 4: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6;
case 3: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6;
case 2: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6;
case 1: *--ptr2 =(uc | firstByteMark[len]);
}
ptr2+=len;
break;
default: *ptr2++=*ptr; break;
}
ptr++;
}
}
//填充字符串结尾符 '\0'
*ptr2=0;
if (*ptr=='\"') ptr++;
item->valuestring=out;
item->type=cJSON_String;
return ptr;
}
2) 受C++的根深影响, 一瞬间习惯了对字符串的初始化赋值使用=号, 在C中, 字符串其实是char []的另外一种书写形式, 即char *str[] = “hello”, 等同于 char str[] = [‘h’,‘e’,‘l’,‘l’,‘o’,’\0’]. 因此, 对待替换的key关键字filepath对齐的value替换字符串时候, 需要使用strncpy函数.
同时自己来管理valuestring指针的内存申请. 在重新申请valuestring指针时候, 需要先对其判空处理.若不为空, 则释放掉指定的内存空间, 并置空. 然后realloc/calloc/malloc, 并strncpy. 如下:
//假如待替换的字符串是: char *str = "hello world.";
if(cJSON->valuestring) free(cJSON->valuestring); cJSON->valuestring = NULL;
cJSON->valuestring = (char*)calloc(1, strlen(str) + 1);
assert(cJSON->valuestring);
strncpy(cJSON->valuestring, str, strlen(str));
...... //其他义务处理.
cJSON_Delete(cJSON对象);
自己管理valuestring指针的内存空间, 在处理完其他义务之后, 使用cJSON_Delete函数时候, 在能够成功释放所有申请的内存空间的同时, 程序也完好的运行.
附: 若在使用源码库时候, 程序异常退出, 务必多review下自己的代码书写, 肯定是有某些地方的操作不正确, 或是方式不匹配导致, 包括但不限于cJSON开源库.