这里写自定义目录标题
JSON格式介绍
JSON(JavaScript Object Notation)
,是一种序列化的格式,最大的优点在于可读性极强,以及可直接嵌入到js代码中,所以广泛运用于web数据的收发。
JSON格式有以下基本类型:
- null类型:值为null,表示为空
- bool类型:值为true和false
- number类型:值为int、double(即整数或小数
- string类型:形如 “abc”
以及以下复合类型:
- list类型(也称array类型)
- dict类型(也称object类型)
解析json字符串
整套解析流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WwkHLTjs-1660653072769)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/231baf4270a64bf6aa38ca5961507611~tplv-k3u1fbpfcp-watermark.image?)]
创建JObject类
我们需要把json的类型对应到计算机语言的类型。
由于json的数据在我们看来都是字符串,那么有如下对应关系:
- "null"对应我们构造的null类型
- “true","false"对应内部的bool类型即可
- number类型数据对应int、double类型
- string类型数据对应string即可
- list类型对应C++中的vector
- dict类型对应C++中的map或unordered_map
我们在计算机语言中,需要构造一个对象类型,用于将以上类型全部涵盖。
在C++中通过std::variant来进行,还需要一个枚举tag来表示当前对象内存储的数据类型。
enum TYPE
{
T_NULL,
T_BOOL,
T_INT,
T_DOUBLE,
T_STR,
T_LIST,
T_DICT
};
using null_t = string;
using int_t = int32_t;
using bool_t = bool;
using double_t = double;
using str_t = string;
using list_t = vector<JObject>;
using dict_t = map<string, JObject>;
class JObject
{
public:
using value_t = variant<bool_t, int_t, double_t, str_t, list_t, dict_t>;
...
private:
TYPE m_type;
value_t m_value;
};
创建Parser类
我们有了JObject,可以把所有的JSON数据接收起来,现在要做的就是扫描JSON字符串,对其中的数据进行读取处理,然后转化为JObject。
关键代码如下:
JObject Parser::parse()
{
char token = get_next_token();
if (token == 'n')
{
return parse_null();
}
if (token == 't' || token == 'f')
{
return parse_bool();
}
if (token == '-' || std::isdigit(token))
{
return parse_number();
}
if (token == '\"')
{
return parse_string();
}
if (token == '[')
{
return parse_list();
}
if (token == '{')
{
return parse_dict();
}
throw std::logic_error("unexpected character in parse json");
}
以上就是整个字符串的解析过程,每次通过get_next_token这个方法得到整个字符串的下一个token,根据token决定解析对应的数据类型。
get_next_token方法
跳过空白符号,以及跳过注释(标准的JSON格式不支持注释,我这里硬加的,为了vscode的JSON格式配置文件解析
char Parser::get_next_token()
{
while (std::isspace(m_str[m_idx])) m_idx++;
if (m_idx >= m_str.size())
throw std::logic_error("unexpected character in parse json");
//如果是注释,记得跳过
skip_comment();
return m_str[m_idx];
}
parse_null和parse_bool
由于这两个很简单,就放在一起了。
- parse_null
JObject Parser::parse_null()
{
if (m_str.compare(m_idx, 4, "null") == 0)
{
m_idx += 4;
return {};
}
throw std::logic_error("parse null error");
}
- parse_bool
bool Parser::parse_bool()
{
if (m_str.compare(m_idx, 4, "true") == 0)
{
m_idx += 4;
return "true";
}
if (m_str.compare(m_idx, 5, "false") == 0)
{
m_idx += 5;
return "false";
}
throw std::logic_error("parse bool error");
}
parse_number
JObject Parser::parse_number()
{
auto pos = m_idx;
//integer part
if (m_str[m_idx] == '-')
{
m_idx++;
}
if (isdigit(m_str[m_idx]))
while (isdigit(m_str[m_idx]))
m_idx++;
else
{
throw std::logic_error("invalid character in number");
}
if (m_str[m_idx] != '.')
{
return (int) strtol(m_str.c_str() + pos, nullptr, 10);
}
//decimal part
if (m_str[m_idx] == '.')
{
m_idx++;
if (!std::isdigit(m_str[m_idx]))
{
throw std::logic_error("at least one digit required in parse float part!");
}
while (std::isdigit(m_str[m_idx]))
m_idx++;
}
return strtof64(m_str.c_str() + pos, nullptr);
}
parse_list
JObject Parser::parse_list()
{
JObject arr((list_t()));//得到list类型的JObject
m_idx++;
char ch = get_next_token();
if (ch == ']')
{
m_idx++;
return arr;
}
while (true)
{
arr.push_back(parse());
ch = get_next_token();
if (ch == ']')
{
m_idx++;
break;
}
if (ch != ',') //如果不是逗号
{
throw std::logic_error("expected ',' in parse list");
}
//跳过逗号
m_idx++;
}
return arr;
}
parse_dict
JObject Parser::parse_dict()
{
JObject dict((dict_t()));//得到dict类型的JObject
m_idx++;
char ch = get_next_token();
if (ch == '}')
{
m_idx++;
return dict;
}
while (true)
{
//解析key
string key = std::move(parse().Value<string>());
ch = get_next_token();
if (ch != ':')
{
throw std::logic_error("expected ':' in parse dict");
}
m_idx++;
//解析value
dict[key] = parse();
ch = get_next_token();
if (ch == '}')
{
m_idx++;
break; //解析完毕
}
if (ch != ',')//没有结束,此时必须为逗号
{
throw std::logic_error("expected ',' in parse dict");
}
//跳过逗号
m_idx++;
}
return dict;
}
完善JObject类
很明显,我们需要为JObject类提供一个方法,此方法可以让调用者直接访问到std::variant里面对应的数据,并且我们也需要提供一个方法能让JObject快速初始化为对应的类型。
初始化接口
添加好下面这些方法后,外界可通过调用方法把JObject的内部状态改变。
void Null()
{
m_type = T_NULL;
m_value = "null";
}
void Int(int_t value)
{
m_value = value;
m_type = T_INT;
}
void Bool(bool_t value)
{
m_value = value;
m_type = T_BOOL;
}
void Double(double_t value)
{
m_type = T_DOUBLE;
m_value = value;
}
void Str(string_view value)
{
m_value = string(value);
m_type = T_STR;
}
void List(list_t value)
{
m_value = std::move(value);
m_type = T_LIST;
}
void Dict(dict_t value)
{
m_value = std::move(value);
m_type = T_DICT;
}
Value方法:
#define THROW_GET_ERROR(erron) throw std::logic_error("type error in get "#erron" value!")
template<class V>
V &Value()
{
//添加安全检查
if constexpr(IS_TYPE(V, str_t))
{
if (m_type != T_STR)
THROW_GET_ERROR(string);
} else if constexpr(IS_TYPE(V, bool_t))
{
if (m_type != T_BOOL)
THROW_GET_ERROR(BOOL);
} else if constexpr(IS_TYPE(V, int_t))
{
if (m_type != T_INT)
THROW_GET_ERROR(INT);
} else if constexpr(IS_TYPE(V, double_t))
{
if (m_type != T_DOUBLE)
THROW_GET_ERROR(DOUBLE);
} else if constexpr(IS_TYPE(V, list_t))
{
if (m_type != T_LIST)
THROW_GET_ERROR(LIST);
} else if constexpr(IS_TYPE(V, dict_t))
{
if (m_type != T_DICT)
THROW_GET_ERROR(DICT);
}
void *v = value();
if (v == nullptr)
throw std::logic_error("unknown type in JObject::Value()");
return *((V *) v); //强转即可
}
重载方法让对象更好用
当JObject为dict类型时,我们可以直接用下标运算符进行key-value的赋值(得益于隐式转化和运算符重载
JObject &operator[](string const &key)
{
if (m_type == T_DICT)
{
auto &dict = Value<dict_t>();
return dict[key];
}
throw std::logic_error("not dict type! JObject::opertor[]()");
}
同样如果为list对象,我们也准备了push_back等方法
void push_back(JObject item)
{
if (m_type == T_LIST)
{
auto &list = Value<list_t>();
list.push_back(std::move(item));
return;
}
throw std::logic_error("not a list type! JObjcct::push_back()");
}
当然,为了让类更好用,你也可以重载很多其他方法,但是注意别忘了类型的安全检查!
完善Parser类
我们之前是完成了整个字符串到JObject的解析过程,但是每次都需要创建一个Parser类,然后调用方法,这样的过程未免有些繁琐,我们可以对外提供FromString的静态方法,然后充分利用一个对象便可完成整个解析过程。
如下:
JObject Parser::FromString(string_view content)
{
static Parser instance;
instance.init(content);
return instance.parse();
}
完成序列化和反序列化过程
我们前面所做的工作其实已经把这个过程相当于完成了,当然还差一个把JObject变为JSON字符串的方法,现在添加上就是,也不是很难,按照相似的逻辑反推一遍就行。如下给JObject添加一个to_string方法:
string JObject::to_string()
{
void *value = this->value();
std::ostringstream os;
switch (m_type)
{
case T_NULL:
os << "null";
break;
case T_BOOL:
if (GET_VALUE(bool))
os << "true";
else os << "false";
break;
case T_INT:
os << GET_VALUE(int);
break;
case T_DOUBLE:
os << GET_VALUE(double);
break;
case T_STR:
os << '\"' << GET_VALUE(string) << '\"';
break;
case T_LIST:
{
list_t &list = GET_VALUE(list_t);
os << '[';
for (auto i = 0; i < list.size(); i++)
{
if (i != list.size() - 1)
{
os << ((list[i]).to_string());
os << ',';
} else os << ((list[i]).to_string());
}
os << ']';
break;
}
case T_DICT:
{
dict_t &dict = GET_VALUE(dict_t);
os << '{';
for (auto it = dict.begin(); it != dict.end(); ++it)
{
if (it != dict.begin()) //为了保证最后的json格式正确
os << ',';
os << '\"' << it->first << "\":" << it->second.to_string();
}
os << '}';
break;
}
default:
return "";
}
return os.str();
}
有关JObject的方法也都补充的差不多了,那么我们现在要考虑的是如何通过JObject这个中间对象将我们自定义的任何一个类给序列化和反序列化?
如图:
所有的序列化和反序列化的过程都依托于JObject进行。而Parser这个类在中间作为一个方便使用的对外接口。
序列化接口设计
在Parser类中添加一个ToJSON的静态方法,用来对任意类型进行序列化,这个方法使用模板。
代码如下:
template<class T>
static string ToJSON(T const &src)
{
//如果是基本类型
if constexpr(IS_TYPE(T, int_t))
{
JObject object(src);
return object.to_string();
} else if constexpr(IS_TYPE(T, bool_t))
{
JObject object(src);
return object.to_string();
} else if constexpr(IS_TYPE(T, double_t))
{
JObject object(src);
return object.to_string();
} else if constexpr(IS_TYPE(T, str_t))
{
JObject object(src);
return object.to_string();
}
//如果是自定义类型调用方法完成dict的赋值,然后to_string即可
json::JObject obj((json::dict_t()));
src.FUNC_TO_NAME(obj);
return obj.to_string();
}
如上代码如果是基本类型则直接初始化JObject调用to_string方法,如果是自定义类型,则需要在该类型中嵌入一个方法,这个方法名字我们用FUNC_TO_NAME这个宏代替。
也就是说,如果是自定义的类型,那么肯定就是对应JSON数据的dict类型,所以只需要你对该类型定义对应的方法,该方法需要将值传递给JObject,为了简化这个过程我们用宏来替代。
宏定义如下:
#define FUNC_TO_NAME _to_json
#define FUNC_FROM_NAME _from_json
#define START_TO_JSON void FUNC_TO_NAME(json::JObject & obj) const{
#define to(key) obj[key]
//push一个自定义类型的成员
#define to_struct(key, struct_member) json::JObject tmp((json::dict_t())); struct_member.FUNC_TO_NAME(tmp); obj[key] = tmp
#define END_TO_JSON }
#define START_FROM_JSON void FUNC_FROM_NAME(json::JObject& obj) {
#define from(key, type) obj[key].Value<type>()
#define from_struct(key, struct_member) struct_member.FUNC_FROM_NAME(obj[key])
#define END_FROM_JSON }
基本使用如下:
struct Base
{
int pp;
string qq;
START_FROM_JSON //生成反序列化相关的方法
pp = from("pp", int);
qq = from("qq", string);
END_FROM_JSON
START_TO_JSON //生成序列化相关的代码
to("pp") = pp;
to("qq") = qq;
END_TO_JSON
};
struct Mytest
{
int id;
std::string name;
Base q;
START_TO_JSON //序列化相关代码
to_struct("base", q);
to("id") = id;
to("name") = name;
END_TO_JSON
START_FROM_JSON //反序列化相关代码
id = from("id", int);
name = from("name", string);
from_struct("base", q);
END_FROM_JSON
};
实际上上面代码等效于下面的代码,以Base类为例:
struct Base
{
int pp;
string qq;
void _from_json(json::JObject& obj){ //反序列化
pp = obj.Value<int>();
qq = obj.Value<string>();
}
void _to_json(json::JObject& obj){//序列化代码
obj["pp"] = pp;
obj["qq"] = qq;
}
};
欢迎使用Markdown编辑器
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法1 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G
合理的创建标题,有助于目录的生成
直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC
语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片
.
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
项目 | Value |
---|---|
电脑 | $1600 |
手机 | $12 |
导管 | $1 |
设定内容居中、居左、居右
使用:---------:
居中
使用:----------
居左
使用----------:
居右
第一列 | 第二列 | 第三列 |
---|---|---|
第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE | ASCII | HTML |
---|---|---|
Single backticks | 'Isn't this fun?' | ‘Isn’t this fun?’ |
Quotes | "Isn't this fun?" | “Isn’t this fun?” |
Dashes | -- is en-dash, --- is em-dash | – is en-dash, — is em-dash |
创建一个自定义列表
-
Markdown
- Text-to- HTML conversion tool Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n−1)!∀n∈N 是通过欧拉积分
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=∫0∞tz−1e−tdt.
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML 图表
可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:
这将产生一个流程图。:
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart的流程图:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
注脚的解释 ↩︎