小白的自学项目分析记录-------超全面的cJSON源代码分析(上)

写在前面
编译器:visual studio 2017

  • 问题一:
    cJSON.c文件中出现strcpy()函数和strcpy_s()函数、sprintf()函数和sprintf_s()函数的替换
    1
    解决办法:
    在cJSON.c的头文件添加:#define _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

具体参考:
C++中strcpy()函数和strcpy_s()函数的使用及注意事项

  • 问题二:
    在下载的cJSON源代码中的test.c文件里,fopen_s参数检测不够,fopen显示不安全
    2
    解决办法:
f=fopen(filename,"rb");fseek(f,0,SEEK_END);len=ftell(f);fseek(f,0,SEEK_SET);//原test.c
//增加检测参数
f=fopen_s(&f,filename,"rb");fseek(f,0,SEEK_END);len=ftell(f);fseek(f,0,SEEK_SET);//替换后
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

cJSON.h

#ifndef cJSON__h//防止cJSON.h被重复引用
#define cJSON__h
...//自己的代码
#endif

#ifndef/#define/#endif的格式:

#ifndef cJSON_H:   "if not define cJSON.h"  如果不存在cJSON.h
#define cJSON_H:   引入cJSON.h
最后一句:#endif     否则不需要引入
//综合使用C和C++
#ifdef __cplusplus //c++编译环境中才会定义__cplusplus,如果是c++程序,肯定会有__cplusplus
extern "C" {  //告诉编译器下面的函数是c语言函数(因为c++和c语言对函数的编译转换不一样,主要是c++中存在重载)
#endif
...//自己的代码
#ifdef __cplusplus
}
#endif

关于malloc/free:
malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free


**

下面是关于cjson.h和cjson.c的说明和分析

**

首先在头文件中定义出了cJSON和cJSON_Hooks的结构体,cJSON提供了不同类型的指针,cJSON_Hooks提供了分配和释放空间的函数指针,并给出了char类型的静态全局变量指针ep,随后定义了ep报错的指针函数,返回ep所指向字符串的地址,使用const修饰返回值,说明ep所指向的字符串是常量。(以下按照顺序解释函数)

cJSON_strcasecmp()忽略大小写比较字符串

传入两个字符型指针变量,参数使用const进行修饰,说明内部不会修改这两个值。若s1指向为空,s1 == NULL的情况下,如果s2也是NULL就相等返回0,不然就是s1 < s2返回1;在判断s1不为空时,若s2指向为空,那么s1>s2返回1。接着对s1、s2遍历,不区分大小写,即都以小写形式进行比较,循环比较每个字符,如果s1为空,说明s1==s2,遍历结束。不相等则跳出循环,将不相同的那个字符的小写形式进行相减,可以得到两个串的大小。

这里有两点:
第一,tolower函数,功能:将大写强制转化为小写,头文件:#include<ctype.h>;
第二,(const unsigned char *)强制转换字符串。

接着定义了两个函数指针,对静态全局函数指针变量进行赋值,使cJSON_malloc=malloc,使cJSON_free=free,分别用于开辟空间和释放内存。

cJSON_strdup()常用的一种字符串拷贝库函数,一般和free()函数成对出现

这里有两点:
第一,strlen()函数,用于计算字符串的长度,直到空字符结束,但是不包括控字符,故这个考虑了+1;
第二,memcpy()函数,用于拷贝任何类型的对象,表示从str拷贝len长度的字节到copy中去。有几个注意点,首先要判断指针不为空,其次按照定义,被拷贝的参数在第二个,待拷贝的参数在第一个,最后由于该函数拷贝是一个一个字节拷贝,故需要将void* 强转为char*,

cJSON_InitHooks()初始化钩子,内存申请和释放接口

向cJSON提供malloc、realloc和free功能,根据cJSON_Hooks结构体,重置cJSON_malloc和JSON_free。注意:这里malloc的函数原型为
void *malloc,函数指针为 *(*malloc),根据传入参数中是否携带指定的申请和释放接口,进行选择使用哪一个内存接口

cJSON_New_Item()内部构造函数,清空内存,申请一个cJSON结构体大小的内存,初始化为0

memset()函数:作用:在一块内存中填充某个给定值,这是对较大的结构或数组清零的一种最快的方法

cJSON_Delete()用以删除一整个json结构,会将所有的节点全部释放内存

这里先删除儿子节点,然后判断类型,并和256,512比较,具体原因未知,最后删除自身节点

parse_number()解析输入文本以生成一个数字,并将结果填充到item中

根据不同的类型(比如 负数,0,1-9,小数,指数)记录并输出为数字,保存结果。其中指数的科学计数法那里要说明一点,1.23E+10 = 1.23*10^10

紧接着定义了pow2gt()函数,用于返回比x大的最小2的N次幂,在实现上就是把x占用到的最高位为1的位 到 第0位,都置位为1,再+1,然后定义一个printbuffer类型的结构体,主要是用来将json数据打印到缓冲区时,进行提供缓存空间的信息。

ensure()协助 printbuffer 分配内存的一个函数

确保p所指向的缓冲区中,能够提供needed大小的缓冲给打印功能使用。总体的过程是: 首先通过传入参数p的合法性检测,为保证内存充足重新定义needed所需内存,判断内存是否够用,若够用直接返回可用的内存位置;若不够用则重新定义内存大小newsize和申请对应newsize的内存空间newbuffer,对新申请的newbuffer再次进行申请判断,失败则全部置空并返回空指针,成功就将原有数据拷贝到新空间,释放掉原来buffer的内存空间;最后更新新的缓存信息,返回缓存中可用的内存位置。(这里再次用到了memcpy)

update()静态作用域函数,意为更新,传入参数为缓存结构

返回当前缓存区已使用的内存偏移量,返回的是int类型的地址

print_number()静态作用域函数,将item中的数字打印成字符串

首先定义了char类型和double类型的指针,通过判断double类型的指针指向的值判断。
若值为0: 使用两个字节,根据p是否为空,决定使用从哪里分配的缓存,p为空,由结构体申请,p不为空,由ensure函数申请, 然后将字符串“0”拷贝到缓存中去,(其中strcpy函数要求目标数组和原数组容量相同)。
若值为整数(除0外): 其中fabs(((double)item->valueint)-d)<=DBL_EPSILON表示标示差小于最小误差值,即可以理解为整数,(fabs用于float、double取绝对值,abs用于int整型,DBL_EPSILON和 FLT_EPSILON:双精度和单精度的最小误差)在整数范围内,由于2^64+1可以放在21位的数内,所以这里给的是21个字节,然后将字符串通过sprintf函数拷贝到item中去。
若值为小数: 同样的,给出了64个字节的空间,关于小数的位数进行了细化分类,最后都通过sprintf函数拷贝到item中去。

parse_hex4()静态作用域函数,将十六进制的字符串转换为数字表示

将字符串的字符逐个取出进行分析,然后计算到整数中,由于是16位,需要左移3次4位,最后完成4个字节的整数解析

parse_string()将输入的文本解析为非转义的c的字符串,然后填充到item中,应保证str是已经去除开头空字符的串

在此前定义了一个静态字符数组firstByteMark[7],对应于0,0,192,224,240,248,252,是用来做UTF格式转换的,返回值为解析出一个字符串之后的首地址。

首先定义了三个指针,两个无符号数,声明并初始化了长度,通过是否以双引号开头判断传入的str 是否属于字符串;通过ptr遍历,跳过转义引号,计算长度,接着通过字符串长度申请内存并检查合理性;然后将遍历下标赋给指针ptr,将字符串长度的空间赋给指针ptr2,开始遍历字符串是否为转义字符,若不带 ’ \ ’ 符号,两指针同时递增,若带 ’ \ ’ 符号,开始判断不同类型的转义符,其中判断 ’ u ’ (也就是编码方式),将UTF16转化成UTF8,最后由于申请内存空间为len+1,需要在结尾填充上 ’ \0 ’ ,然后将ptr结尾的引号跳过,将传入对象赋值,并返回跳过后所处的位置,

print_string_ptr()将提供的cstring呈现为可打印的转义版本,str为传入字符串,p为缓存指针,返回解析出的字串地址

测试str中是否携带着空格(ASCII码32),引号,以及转义字符反斜杠,结果用flag进行标识。若没有携带上述字符,用p指针分配内存(注意这里多分配了+3的内存,是因为要加两个引号和\0),然后将out分配给ptr2,将传入字符串拷贝到ptr2中,在字符串前后加上引号,并在字符末尾加\0,存储到out所指向的内存中,并将地址进行返回;如果传入的是空字符串,那么就只填上一个双引号间填充空的打印到内存或者缓存。最后就是转义字符的处理,先找 “\ 这俩符号及转义符号、Unicode编码方式,分配检查内存,同样最后补上双引号另一边和补0。

print_string()使用一个对象调用上面的 print_string_ptr()函数

作用是 二次封装,将item中的valuestring打印到分配的内存中或者是缓存p中。局部作用域,返回输出值,这里调用的目的是直接面向cJson的对象item。

按照代码行,接着声明了一些函数原型解析一个值,表明了这些原函数与打印该函数成对存在。

skip()用于跳转空白和换行/回车

作用是 跳过空字符或者一个控制字符,即在ascii码中小于等于32的字符

cJSON_ParseWithOpts()解析一个对象-创建一个新的根节点,然后进行填充(cJSON类型的函数)

首先新建了一个根节点,初始化错误标识(全局变量ep)并检测内存申请状态,然后在对传入的值去除开头的不可见字符后,调用parse_value,并检查解析是否解析成功,不成功就删除新节点。接着检测是否要求以null结尾的,如果不为null就释放内存,并将ep指向出错的位置;最后将当前的结束位置进行赋值回传;返回新建的节点。函数中参数中提供了require_null_terminated是为了确保json字符串必须以’\0’字符作为结尾。若参数提供了return_parse_end,将返回json字符串解析完成后剩余的部分。

接着做了三个二次封装,分别是cJSON_Parse、cJSON_Print和cJSON_PrintUnformatted函数,对应于调用缺省的选项进行解析、打印cJSON到文本中调用 print_value()和打印无格式的cJSON到文本中调用 print_value()(该函数在下面介绍)

cJSON_PrintBuffered()打印cJSON到缓存中 调用print_value

item为待解析打印的json数据,prebuffer为预分配到缓存的大小,fmt控制是否需要json格式。通过上面定义的printbuffer结构体定义一个对象,赋值并调用打印值。

parse_value() 解析值, &&&&&&&&&&&&&&&&& 解析器的核心 &&&&&&&&&&&&&&&&&&

遇到什么格式就进行什么格式的解析,从这里进入的解析一般还会递归回来调用这里的功能。根据传入的值,判断三种特定的数据类型并赋值item->type,然后根据传入是引号,负号、中括号、大括号分别解析字符串、数字、数组和对象。如果以上都不是,置ep指针到出错的位置。

print_value()打印一个值到文本方式中

item为待打印的对象,depth 当前对象到根节点的深度 fmt 是否打印json格式, p为缓存入口。通过定义结构体指针p判断cJSON_NULL、cJSON_False和cJSON_True,若使用缓存模式进行打印,根据cJson.h头文件自定义的变量类型和1111 1111与运算,用ensure()函数分配内存,用拷贝字符串函数strcpy;若不使用缓存方式打印,则直接调用cJSON_strdup()函数。而关于cJSON_Number、cJSON_String、cJSON_Array和cJSON_Object,两种情况都是直接调用对应的打印函数。返回将item中数据组织成一个串的起始地址,也会被递归的调用。

parse_array()解析数组,根据输入的文本,建立一个数组

首先建立一个孩子节点,通过数组特征 ‘ [ ’ 判断是否数组,验证是数组的value,对类型进行赋值,对value进行skip去除不可见字符,并根据 ‘ ] ’ 判断是否为空数组。接着将孩子节点赋给传入的孩子节点,调用cJSON_New_Item()清空内存,申请一个cJSON结构体大小的内存,检查内存,跳过空白,用parse_value()函数获取值并检验合法性。如果存在 " , " 那么说明后续还要继续进行解析,也就是兄弟节点,即数组有多个元素,那么进行循环创建节点,链接到链表中,解析值,内存合法性检查。最后检查是否存在数组结束的右括号 ’ ] ’ ,然后返回结束位置,或者置位错误指向出错位置,否则返回0。

print_array()将对象数组打印成文本

item为待打印的对象,depth 当前对象到根节点的深度 fmt 是否打印json格式, p为缓存入口。这里注意定义的entries是因为malloc的函数原型是指针函数,所以是指向指针的指针。代码解释:先获得数组的孩子节点(第一个元素),循环计算数组的条目数量,如果数目为0,通过p指针申请内存,只打印一个” [ ] “。然后同理,通过指针p分别进入缓存模式和不适用缓存的分支,
以缓存方式打印时,先将 ‘ [ ’ 写进缓存中,从第一个child开始进行遍历,在循环中,调用print_value()打印值并更新,遍历时输出字符串采用"aa", “bb”(中间有空格)。遍历完所有的孩子节点后补上右括号,并将out指向这次填充的最开始处。
不使用缓存打印时,根据元素个数申请二维字符指针,分配一个数组来保存每个数组的值,并检查内存申请,初始化指针为NULL,和使用缓存方式一样,循环遍历数组所有元素,在循环中,使用中间变量ret进行遍历并将结果存入二维指针中,然后判断解析值是否出错,以fail为标记,并计算长度。遍历结束,如果没有解析错误,尝试分配一个输出的数组,如果失败了则打印失败;如果解析有错误,将之前申请的所有内存释放。如果以上最终没有错误情况,将所有的字符串全都复制到新开辟的大的串中,准备输出,先补上左括号 ‘ [ ’ ,然后依次补上 ’ , ’ 和空格 ‘ ’,最后补上右括号 ‘ ] ’ 。

parse_object()解析对象,根据文本输入,创建一个json对象.

首先建立一个孩子节点,通过对象特征 ‘ { ’ 判断是否对象,验证是对象的value,对类型进行赋值,对value进行skip去除不可见字符,并根据 ‘ } ’ 判断是否为空数组。接着将孩子节点赋给传入的孩子节点,调用cJSON_New_Item()清空内存并检查内存合法性,申请一个cJSON结构体大小的内存,检查内存,跳过间距,用parse_string()函数获取字符串值并检验合法性,因为跳过whitespace等控制字符后,会直接遇到string类型的数据;(以下加粗部分为区分parse_array数组)使用child->valuestring获得待解析的字符串,然后将child->valuestring的值给child->string并初始化,紧接着是遇到 ’ :’ 的处理,若没有冒号,检查对应是否有值成对,否则报错;若有冒号,将 ’ :’ 后的值通过parse_value()函数获取出来赋值给child,并且指针移到获取该值后字串的第一个可见字符处,这里和解析数组一样,相比多出处理冒号一步,需要指针后移一位。 如果存在 " , " 那么说明后续还要继续进行解析,并进行循环中创建对象,链接到链表中,解析值,内存合法性检查,这里同样多出赋值child->string并初始化child->valuestring,和 跳过空白判断冒号并检查合法性的步骤。最后检查是否出现结束的右大括号 ‘ } ’ ,返回最后一个值结束的位置,或者返回0并置位ep。

print_object()将一个对象,打印到文本中

item为待打印的对象,depth 当前对象到根节点的深度,这里depth可以计算应该缩进的字符数, fmt 是否打印json格式, p为缓存入口。相比打印数组多一个指向指针的指针分配内存空间,多一个指针分配2个指针数组存储对象里的键和值,长度由5扩展为7。同样的,先获取根节点的孩子节点,计算包含的节点个数,通过数量判断,如果数量为空,根据p指针分配内存空间,如果是个空的json对象,那么就打印一个空的花括号对,根据是否有格式选择转义字符转行,根据根节点深度选择空格,并最后以补0结尾。接着通过指针p分别进入缓存模式和不适用缓存的分支,
如果是要求打印到缓存中,先将左括号和换行符根据要求输出到缓存中,遍历孩子节点,记录深度,开始处理每个孩子节点,fmt格式输出,先打印应该输入的缩进,接着调用print_string_ptr()打印字符串到缓存中,并更新offset的值,随后处理冒号,并调整缩进格式,记录长度,处理完后将值解析出来放到p指向的缓存中,再次更新offset,最后计算该节点的最终长度,确保内存容量,检查是否还有后续节点,然后换行后进行下一个节点的遍历。遍历结束后,将缩进和 ’ } '补上。
如果不使用缓存的情况下,先给对象和名称分配二维字符指针出来分配空间,检查合法性并初始化为0,赋值孩子节点,记录长度和深度,开始遍历孩子节点,通过print_string_ptr和print_value循环递归将所有的值都挂载在二维数组上,并同时以键对值赋给双指针str和ret,记录长度。随后申请一个总的输出字串数组,检查内存,如果分配失败,那么将所有的已分配的内存均进行释放,分配完毕后开始编写输出,先输出 ’ { ’ 和换行符,然后根据需要制备缩进符号,转行符和冒号,将打印到各个子内存中的数据都拷贝一份到要返回的大内存中,并将原有的子内存进行释放,再将对象和名称本身内存释放。根据fmt格式补上缩进符,最后将右边 ’ } '补上。

cJSON_GetArraySize()、cJSON_GetArrayItem()、cJSON_GetObjectItem()

分别对应功能:获取数组的元素个数、获取数组中第item个元素、获取第object个对象中名称为string的元素。代码实现:都是通过遍历数组或对象的子节点。

suffix_object():用于数组列表处理,将item链接到prev之后

create_reference():用于处理引用,创建一个参照对象,复制一个item并初始化,新item的type为cJSON_IsReference

cJSON_AddItemToArray():将item添加到数组中

cJSON_AddItemToObject():将名字为string的item添加到object中

cJSON_AddItemToObjectCS():将名字为字符串常量的item添加到object中,并设置type为cJSON_StringIsConst

cJSON_AddItemReferenceToArray():将item复制一份出来,添加到数组中

cJSON_AddItemReferenceToObject():将名字为string的item复制一份出来,添加到object中

cJSON_DetachItemFromArray():将array中的第which个item从array中摘取下来,作为返回值

cJSON_DeleteItemFromArray():从array中删除第which个元素

cJSON_DetachItemFromObject():从object中摘除名字为string的item,返回这个item

cJSON_DeleteItemFromObject():将指定名字string的item从object中删除

cJSON_InsertItemInArray():插入newitem到第which个位置,如果不存在这个位置,则添加

cJSON_ReplaceItemInArray():将数组项替换为newitem

cJSON_ReplaceItemInObject():将对象项替换为newitem

创建基本类型的对象:NULL、True、False、Bool、Number、String、Array、Object:
cJSON_CreateNull()、cJSON_CreateTrue()、cJSON_CreateFalse()、cJSON_CreateBool()、cJSON_CreateNumber()、cJSON_CreateString()、cJSON_CreateArray()、cJSON_CreateObject()

创建不同类型的数组:int、float、double、string
cJSON_CreateIntArray()、cJSON_CreateFloatArray()、cJSON_CreateDoubleArray()、cJSON_CreateStringArray()

cJSON_Duplicate():复制item并返回

recurse决定是否递归的将item的子节点也都复制一份,首先创建一个新的item,拷贝所有类型的变量到newitem中,然后走到子节点上,然后遍历整个链表进行递归的复制,循环中复制每个新孩子节点,为空则删除这个对象,链接nptr到链表中,继续遍历。

cJSON_Minify():一个mini版本的json数据遍历功能

cJson.c的最后一个函数,首先声明一个字符型指针,然后判断是否为空字符,缩进符,回车符,转行符,双斜杠注释,多行注释,带双引号的字符串,和其他特征的数据,若为空则不判断直接终止。

ps:

源代码注释版在(下)中

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐