cJSON_Delete源码剖析

文章目录

  • 1. 释放cJSON结构体变量
    • 1.1 示例一
      • 1.1. 示例1JSON对象在cJSON中的内部框架图
  • 2. 调用cJSON_Delete, 程序宕掉, 问题会在哪?
    • 2.1 问题定位
    • 2.2 问题解决

1. 释放cJSON结构体变量

     见名知意, 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对象同时是:对象{}, 又同时是数组的.

1.1 示例一

     示例如下, 对于这样一个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
	}

     

1.1. 示例1JSON对象在cJSON中的内部框架图

     对于上面的这个JSON对象, 在cJSON内部的框架布局结构如下图1所示.
cJSON_Delete源码剖析_第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所示.
cJSON_Delete源码剖析_第2张图片
                                      图2 cJSON释放示例1中JSON对象的流程

     图2中的符号 × , 表示在该位置将调用free函数释放对应内存空间. 对于属于字符串类型的数据类型, 将多调用一次free函数, 即释放valuestring成员申请的内存空间.

	if (!(c->type&cJSON_IsReference) && c->valuestring) cJSON_free(c->valuestring);

     

2. 调用cJSON_Delete, 程序宕掉, 问题会在哪?

     最近的一个项目中, 我遇到了这个问题. 训练算法分析出来的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信号, 该信号的默认操作是终止程序运行.

2.1 问题定位

     经过分析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函数.
     

2.2 问题解决

     同时自己来管理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开源库.

你可能感兴趣的:(cJSON源码剖析)