此文为学习知乎上“Milo的编程”专栏所载的从零开始的JSON教程所作的学习笔记,同时也是学习简书的Markdown语法的时间,因为一直想找一个能做学习笔记的网站,并且由于笔记的很多内容涉及代码块,支持Markdown语法的简书似乎是个不错的选择。同时希望自己能够坚持,持续学习。(由于是学习笔记,大量的内容来自于专栏,专栏作者为Milo Yip,特此说明)
一、启程
1. JSON是什么?
JSON是一种用于数据交换的文本格式,具有类似功能的语言有XML、YAML等等,但JSON的语法最简单。一个简单的例子的JSON文本的例子:
{
"title": "Design Patterns",
"subtitle": "Elements of Reusable Object-Oriented Software",
"author": [
"Erich Gamma",
"Richard Helm",
"Ralph Johnson",
"John Vlissides"
],
"year": 2009,
"weight": 1.8,
"hardcover": true,
"publisher": {
"Company": "Pearson Education",
"Country": "India"
},
"website": null
}
由此可以看见,一个JSON对象的表示是用{...}
,JSON的数据类型有六种数据类型:
Type | Format |
---|---|
null | null |
booleab |
true or false
|
number | 浮点数 |
string | "Design Patterns" |
array | [...] |
objecte | {} |
可以发现一个object的数据类型可以是另一个object,这说明JSON是一种树状的结构。一个JSON库应当提供的功能包括:
- parse:将JSON文本(即JSON字符串)解析为对象,肯定是一个树状的对象
- stringify:将一个对象文本化,即把一个对象表示为一串JSON文本
- access:提供方法使得程序中可以访问已经parse过的对象的数据结构
(事实上,对于access不是特别能理解,希望在感悟下)
2. 编译环境配置
pass
此处的内容之后再补充,对于编译环境的搭建CMake、Git的使用还是要抽时间本地使用下
3.头文件与API设计
- 头文件#include 防范:为了防止重复引用头文件而采取的一种方法,一般为每个头文件中预定义一个唯一的宏,然后判断是否这个宏已经定义过了。
#ifndef LEPTJSON_H__
#define LEPTJSON_H__
/* 此处写入头文件内容,如各种声明 */
#endif /*LEPTJSON_H__*/
一般以_H__
作为后缀,并且要保证唯一。
- 六种数据类型:将
true
和false
算作两种类型,那么共有其中类型,定义一个enum类型。为了方便使用,用关键字typedef
在处理下:
typedef enum {
LEPT_NULL,
LEPT_FALSE,
LEPT_TRUE,
LEPT_NUMBER,
LEPT_STRING,
LEPT_ARRAY,
LEPT_OBJECT
} lept_type;
一些作者提到的注意点:C语言没有明明空间的概念,因此需要保证标识符的唯一性,因此一般用项目名称的简写做前缀,枚举值用大写而函数和类型定义用小写
- JSON是一个树状的数据结构,每个结点用一个结构表示,当前第一节只考虑三种类型
null
,false
,true
,因此就当前来讲,这个结构只需要存储一个数据类型即可,以下是定义:
typedef struct {
lept_type type;
} lept_value;
- API定义:
typedef enum {
LEPT_PARSE_OK ,
LEPT_PARSE_EXPECT_VALUE,
LEPT_PARSE_INVALID_VALUE,
LEPT_PARSE_ROOT_NOT_SINGULAR
} lept_parse_result;
/*API-1,解析一个json,即parse,结果填入一个lept_value。注意const的用法是保证json不会被改动*/
lept_parse_result lept_parse(lept_value &v, const char* json);
/*API-2,获取结果,即access,注意const 以及指针的使用(防止对象拷贝)*/
lept_type lept_get_tyepe(const lept_value* v);
- 此单元json只包含
null
,false
,true
三种类型,语法描述为:
json_text = ws value ws
ws = *(%0x20, %x0x09, %0x0A, %x0D) ------* 表示零或多个
value = 'null' / 'false' / 'true' -----/ 表示其中一个
前面已经提到了lept_parse
的返回类型,下面表示本单元语法下分别代表的含义:
typedef enum {
LEPT_PARSE_OK , //解析成功
LEPT_PARSE_EXPECT_VALUE, //JSON串只有空白
LEPT_PARSE_INVALID_VALUE,
LEPT_PARSE_ROOT_NOT_SINGULAR //一个值、空白后还有其他值
} lept_parse_result;
- TTD(测试驱动开发)
是一种开发方法,主要是:- 加入一个测试
- 运行所有测试,新的测试会失败
- 编写实现代码
- 运行所有测试,如有失败返回3
- 重构代码,并测试,如有失败返回3
- 返回1
TTD是先写测试在写开发,优点是实现刚好满足测试的代码,且容易把控质量。本项目提供了一个测试框架。以下做代码学习。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "leptjson.h"
static int main_ret = 0;
static int test_count = 0;
static int test_pass = 0;
//注意宏分多行写的技巧
//注意fprintf和printf区别:注意format为什么可以这么写:类似于char * = “%d”“%d”这样写没毛病,注意stderr
//printf是向stdout,fprintf是向FILE*输出,stderr和stdout等都是该类型,
//stderr时另一个输
//出流
#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)
#define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d")
static void test_parse_null() {
lept_value v;
v.type = LEPT_TRUE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}
/* ... */
static void test_parse() {
test_parse_null();
/* ... */
}
int main() {
test_parse();
printf("%d/%d (%3.2f%%) passed\n", test_pass, test_count, test_pass * 100.0 / test_count);
return main_ret;
}
4. 课后习题
其他的就不关注 了,有一点有意思的要注意下,当有一个函数申明为static后意味这个函数只在当前编译单元使用