GitHub : miloyip/json-tutorial(轻量级JSON)
一、项目介绍
本工程是一个轻量版JSON,来自GitHub,由腾讯 T4 专家、互动娱乐事业群魔方工作室群游戏客户端技术总监叶劲峰(Milo Yip)开发,把项目地址赋于此:miloyip/json-tutorial。
项目共分为8部分,用C语言实现了一个轻量级的JSON,用来入门学习一些编程的基础知识非常好,几乎不需要任何其他知识,懂C语言即可入手。
关于作者:
叶劲峰(Milo Yip)现任腾讯 T4 专家、互动娱乐事业群魔方工作室群游戏客户端技术总监。他获得香港大学认知科学学士(BCogSc)、香港中文大学系统工程及工程管理哲学硕士(MPhil)。他是《游戏引擎架构》译者、《C++ Primer 中文版(第五版)》审校。他曾参与《天涯明月刀》、《斗战神》、《爱丽丝:疯狂回归》、《美食从天降》、《王子传奇》等游戏项目,以及多个游戏引擎及中间件的研发。他是开源项目 RapidJSON 的作者,开发 nativejson-benchmark 比较 41 个开源原生 JSON 库的标准符合程度及性能。他在 1990 年学习 C 语言,1995 年开始使用 C++ 于各种项目。
二、知识点总结
1. 项目中的命名格式:
1.1 xxx.h文件中的#ifndef (tutorial01)
C 语言有头文件的概念,需要使用 #include
去引入头文件中的类型声明和函数声明。但由于头文件也可以 #include
其他头文件,为避免重复声明,通常会利用宏加入 include 防范(include guard):
#ifndef LEPTJSON_H__
#define LEPTJSON_H__
/* ... */
#endif /* LEPTJSON_H__ */
宏的名字必须是唯一的,通常习惯以 _H__
作为后缀。由于 leptjson 只有一个头文件,可以简单命名为 LEPTJSON_H__
。如果项目有多个文件或目录结构,可以用 项目名称_目录_文件名称_H__
这种命名方式。
1.2 变量命名格式(tutorial01)
通常枚举值用全大写(如 LEPT_NULL
),而类型及函数则用小写(如 lept_type
)
2. 善用枚举(tutorial01)
本项目中的错误码,均是通过枚举来定义的,既可清楚得表明意义,代码又简洁优雅。如本项目中的错误码所示:
enum {
LEPT_PARSE_OK = 0,
LEPT_PARSE_EXPECT_VALUE,
LEPT_PARSE_INVALID_VALUE,
LEPT_PARSE_ROOT_NOT_SINGULAR
};
3. 宏定义函数
3.1 do-while的使用(tutorial01)
有些同学可能不了解 EXPECT_EQ_BASE
宏的编写技巧,简单说明一下。反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用 do { /*...*/ } while(0)
包裹成单个语句,否则会有如下的问题:
#define M() a(); b()
if (cond)
M();
else
c();
/* 预处理后 */
if (cond)
a(); b(); /* b(); 在 if 之外 */
else /* <- else 缺乏对应 if */
c();
只用 { }
也不行:
#define M() { a(); b(); }
/* 预处理后 */
if (cond)
{ a(); b(); }; /* 最后的分号代表 if 语句结束 */
else /* else 缺乏对应 if */
c();
用 do while 就行了:
#define M() do { a(); b(); } while(0)
/* 预处理后 */
if (cond)
do { a(); b(); } while(0);
else
c();
3.2 哪些情况下必须用宏定义函数?(tutorial01)
如测试框架使用了 __LINE__
这个编译器提供的宏,代表编译时该行的行号。如果用函数或内联函数,每次的行号便都会相同。
//一段在全局位置的代码:
static int main_ret = 0;
static int test_count = 0;
static int test_pass = 0;
#define EXPECT_EQ_BASE(equality, expect, actual, format) \
do {\
test_count++;\
if (equality)\
test_pass++;\
else {\
fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n", __FILE__, __LINE__, expect, actual);\
main_ret = 1;\
}\
} while(0)
4. 优雅的代码
4.1 形参太多时用结构体指针传递(tutorial 01)
函数间通常会传递多个形参,当形参较多时既影响程序美观,又影响程序速度。因此通常把多个参数存到一个结构体对象里,在函数间只传递这个对象的地址即可。
typedef struct {
/*********
Something
...
...
*********/
} info;
info a;
void function(info* a);
4.2 errno的使用(tutorial02)
errno在初试时常被程序员置为0,一旦程序检查出错误如越界等,就会改变errno的值。然而,errno的值在被修改后不会自动改回0,所以如果下次判断到errno不为0,不一定是程序又出错了,而可能是上一次错误后errno没被改回0。
所以,在判错时,通常用errno加上错误检查,如本项目中用errno检查越界,不能简单地if(errno == ERANGE),而是if(errno == ERANGE && (n == HUGE_VAL || v->n == -HUGE_VAL))。
4.3 union的使用(tutorial03)
本项目中,所有数据(不管是字符串,还是数字、布尔值、字符等)均用一个结构体lept_value
来表示,但如何把如此多的类型融于一个结构体?可以用union这个类型。
如下所示,结构体lept_value
可表示数字或字符串,如果是数字,则存储在double n
中,如果是字符串,则存储在char* s
中,并用size_t len
来表示字符串的长度。
typedef struct {
char* s;
size_t len;
double n;
lept_type type; //类型标识,用来表示某个对象中存的是数字还是字符串
}lept_value;
~~~
但由于本项目中一个lept_value对象只会存储一个值,要么数字、要么字符串,所以上述结构体显然浪费了空间。可用union修改如下:
typedef struct {
union {
struct { char* s; size_t len; }s; /* string */
double n; /* number */
}u;
lept_type type;
}lept_value;
5. 数据结构
5.1 手写动态数组
本项目实现了堆栈的动态压入及弹出操作(以字节为操作单位)。每次可要求压入任意大小的数据,它会返回数据起始的指针。以下代码的意思,是把结构体lept_context
中的const char* json
作为一个字符串,即原始数据;并把char* stack
作为一个动态增长的栈。然后把字符串const char* json
压入到栈char* stack
中(每当栈空间不够时,以1.5倍扩容)。用size
记录当前这个栈的总容量(不一定全用完,可能有空余),而top记录当前已被使用的栈容量(即栈顶),所以始终有top <= size
。
#ifndef LEPT_PARSE_STACK_INIT_SIZE
#define LEPT_PARSE_STACK_INIT_SIZE 256
#endif
typedef struct {
const char* json;
char* stack;
size_t size, top;
}lept_context;
static void* lept_context_push(lept_context* c, size_t size) {
void* ret;
assert(size > 0);
if (c->top + size >= c->size) {
if (c->size == 0)
c->size = LEPT_PARSE_STACK_INIT_SIZE;
while (c->top + size >= c->size)
c->size += c->size >> 1; /* c->size * 1.5 */
c->stack = (char*)realloc(c->stack, c->size);
}
ret = c->stack + c->top;
c->top += size;
return ret;
}
static void* lept_context_pop(lept_context* c, size_t size) {
assert(c->top >= size);
return c->stack + (c->top -= size);
}