一、nlohmann::json 常见用法
1. 引入和别名
#include <nlohmann/json.hpp>
using json = nlohmann::json;
以后就可以直接写 json 这个类型。
2. 字符串 -> JSON 对象(parse)
在服务器里最常用的就是这句:
cpp
json req = json::parse(line);
含义:
把一段 JSON 格式的字符串 line 解析成一个 json 类型的对象。
典型场景:客户端发来的请求:
cpp
{"cmd":"echo","msg":"hi"}
服务器:
cpp
try {
json req = json::parse(line);
} catch (const std::exception& e) {
// 说明 line 不是合法 JSON,直接按"坏请求"处理
}
3. 访问字段:operator[] 和 value()
假设有:
cpp
json req = json::parse(R"({"cmd":"echo","msg":"hi"})");
常用几种访问方式:
3.1 下标访问:operator[]
cpp
std::string cmd = req["cmd"]; // 如果没有 "cmd" 会抛异常
特点:
-
key 不存在、类型不匹配,可能抛异常
-
适合你"确定一定有"的字段
3.2 安全访问:value(key, default)
cpp
std::string cmd = req.value("cmd", "");
std::string msg = req.value("msg", "");
int age = req.value("age", 0); // 不存在就用 0
bool close = req.value("close", false);
规则:
-
"cmd"存在 → 转成你给的默认值类型(这里是std::string) -
"cmd"不存在 → 直接返回第二个参数(默认值)
4. 构造 JSON 响应
现在在 processLine 里大概是这样写的:
cpp
json resp;
resp["ok"] = true;
resp["data"] = msg;
也可以一口气用初始化列表:
cpp
json resp = {
{"ok", true},
{"data", msg}
};
数组也很简单:
cpp
json arr = json::array();
arr.push_back(1);
arr.push_back(2);
arr.push_back(3);
// 或者
json arr2 = {1, 2, 3};
5. JSON -> 字符串:dump()
cpp
std::string out = resp.dump(); // 一行压缩形式
std::string pretty = resp.dump(4); // 带缩进的格式化输出(4 空格缩进)
在网络里用的一般是第一种:
return resp.dump() + "\n";
这样客户端拿到的是一行 JSON,方便你用按行协议拆包。
6. 错误处理(parse 出错)
现在的模板:
cpp
std::string Server::processLine(const std::string& line){
json resp;
try {
json req = json::parse(line);
// 正常业务逻辑......
} catch (const std::exception& e) {
resp["ok"] = false;
resp["err"] = "bad json"; // 或 resp["err"] = e.what();
}
return resp.dump() + "\n";
}
只要 parse 失败(不是合法 JSON),就会进 catch,就可以统一返回一个错误 JSON。
7. 小结一句话
最常用:
-
json req = json::parse(line);解析请求 -
req.value("key", default)读字段 -
resp["key"] = value; resp.dump()构造响应
二、std::map::find 返回值说明
以项目常见的这种为例:
cpp
std::map<int, std::unique_ptr<Connection>> conns_;
1. find 返回什么?
auto it = conns_.find(fd);
find 返回的是一个"迭代器"(iterator),可以理解成:
- 指向
map里某一个元素的"指针样的东西"
map 的元素类型是:
std::pair<const Key, T>
在例子里:
cpp
Key = int
T = std::unique_ptr<Connection>
*it 的类型是:std::pair<const int, std::unique_ptr<Connection>>
2. 怎么判断有没有找到?
cpp
auto it = conns_.find(fd);
if (it == conns_.end()) {
// 没找到
} else {
// 找到了
}
end() 表示"序列末尾的下一个位置",也就是"找不到"的标准写法。
这是非常固定的模式:
cpp
auto it = m.find(key);
if (it != m.end()) {
// 用 it
}
3. 找到后怎么用?
访问 key 和 value:
cpp
int key_fd = it->first; // key
std::unique_ptr<Connection>& ptr = it->second; // value
Connection& conn = *ptr; // 引用
Connection* pconn = ptr.get(); // 原始指针
在项目中经常看到类似:
cpp
auto it = conns_.find(fd);
if (it == conns_.end()) return;
Connection& conn = *it->second;
理解为:
-
it是"指向某个键值对"的迭代器 -
it->second是那个unique_ptr<Connection> -
*it->second才是Connection&
4. 结合你的场景:保存一个指针出来
写过:
cpp
std::unique_ptr<Connection>* p_holder = nullptr;
{
std::lock_guard<std::mutex> lock(conns_mtx_);
auto it = conns_.find(fd);
if(it == conns_.end()) return;
p_holder = &it->second;
}
Connection& conn = *p_holder->get();
这里 step by step:
-
conns_.find(fd)→ 找到对应的迭代器it -
it->second是unique_ptr<Connection> -
&it->second就是"指向这个 unique_ptr 的指针" -
p_holder->get()→ 拿到Connection* -
*p_holder->get()→ 变成Connection&
这么写的目的是:
在解锁后还能继续访问 Connection 对象本身(但已经不动 map 的结构了)。
三、智能指针总结(unique_ptr / shared_ptr / weak_ptr)
1. 为什么要用智能指针
裸指针(T*)的问题:
-
忘记
delete→ 内存泄漏 -
new/delete分散在各处,不好维护 -
异常、提前 return 时很容易少写
delete -
多个地方持有同一指针时,很容易 double free
智能指针的核心思想:RAII(资源在对象生命周期结束时自动释放)。
2. std::unique_ptr:独占所有权
特点:
-
一个对象只能有一个
unique_ptr拥有它 -
不能拷贝,只能移动
-
适合"资源有唯一主人"的场景,比如一个连接
Connection只由Server管
常用写法:
cpp
#include <memory>
std::unique_ptr<Connection> conn = std::make_unique<Connection>();
// 访问
conn->fd = 10; // 像指针
(*conn).fd = 10; // 像引用
// 传递所有权(移动)
std::unique_ptr<Connection> other = std::move(conn);
// 此时 conn == nullptr,所有权给了 other
// 释放资源
conn.reset(); // 显式释放
// 或者 conn 在作用域结束自动释放
容器里用:
cpp
std::map<int, std::unique_ptr<Connection>> conns;
conns.emplace(fd, std::make_unique<Connection>());
3. std::shared_ptr:共享所有权
特点:
-
可以有多个
shared_ptr指向同一个对象 -
内部有引用计数,最后一个
shared_ptr析构时才真正 delete 对象 -
有
use_count()可以查看当前引用数
典型用法:
cpp
#include <memory>
auto p1 = std::make_shared<User>("Alice");
{
auto p2 = p1; // 引用计数 +1
} // p2 析构,计数 -1
// 最后一个 shared_ptr 消失时才 delete User
缺点:
-
有一点额外开销(引用计数管理)
-
两个对象互相持有 shared_ptr 时会产生"循环引用",导致内存泄漏(这时需要 weak_ptr)
一般用在:
-
同一个对象需要被多个模块共享管理
-
生命周期比较复杂,很难用一个"唯一主人"描述清楚的时候
4. std::weak_ptr:配合 shared_ptr 打破循环引用
特点:
-
不增加引用计数
-
只能观察 shared_ptr 管理的对象
-
需要用
lock()转成shared_ptr才能使用
例子:
cpp
std::shared_ptr<Node> a = std::make_shared<Node>();
std::shared_ptr<Node> b = std::make_shared<Node>();
a->next = b; // shared_ptr
b->prev = a; // 如果也是 shared_ptr,会循环引用
// 解决办法:prev 用 weak_ptr
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 不增加引用计数
};
5. 结合你现在的 NebulaChat
你现在的关键结构:
cpp
std::map<int, std::unique_ptr<Connection>> conns_;
理解为:
-
conns_是"连接管理器" -
每个
Connection只属于Server一个地方 -
当
map中这个元素被erase或者整个conns_析构时,对应连接自动 delete,不用你手动管理内存
结合 map.find:
cpp
auto it = conns_.find(fd);
if (it == conns_.end()) return;
Connection& conn = *it->second; // 从 unique_ptr 拿出真正的对象
这就是 map + unique_ptr + 智能指针三者一起工作的典型模式。