別再被序列化搞懵了!用人話告訴你 C++ 里 JSON 和 ProtoBuf 到底咋玩
今天咱們來聊聊一個聽起來很高大上,但其實超級實用的話題——C++序列化。
先別慌,我知道你現(xiàn)在腦子里可能有好幾個問題:
- 序列化是個啥玩意兒?
- JSON不是前端的東西嗎?
- ProtoBuf又是什么鬼?
- 它們倆誰更厲害?
別著急,今天我就用最接地氣的話給你掰扯明白。保證看完之后,你不僅知道這些是什么,還能上手寫代碼!

一、序列化到底是啥?先來個生活化的理解
想象一下,你要給遠方的朋友寄一個玩具汽車。你能直接把汽車扔進郵筒嗎?當然不行!你得先把它拆開,放進盒子里,貼上標簽,這樣郵遞員才能送到朋友那里。朋友收到后,再按照說明書把汽車重新組裝起來。
這個過程就是序列化!
- 拆車裝盒 = 序列化(把內(nèi)存中的對象轉(zhuǎn)換成可傳輸?shù)母袷剑?/li>
- 重新組裝 = 反序列化(把傳輸格式還原成內(nèi)存中的對象)
在編程世界里,我們經(jīng)常需要:
- 把數(shù)據(jù)存到文件里
- 通過網(wǎng)絡發(fā)送數(shù)據(jù)
- 在不同程序間傳遞信息
這時候就需要序列化了!因為內(nèi)存里的對象就像那個玩具汽車,不能直接"郵寄"。
二、JSON:網(wǎng)紅選手,人見人愛
1. JSON是什么?
JSON全稱是JavaScript Object Notation,但別被名字騙了,它早就不是 JavaScript 的專利了。現(xiàn)在幾乎所有編程語言都支持JSON,因為它有個超大的優(yōu)點:人類看得懂!
看看這個例子:
{
"name": "張三",
"age": 25,
"city": "北京",
"hobbies": ["游戲", "電影", "音樂"]
}是不是一眼就看明白了?這就是JSON的魅力,連你奶奶都能看懂(好吧,可能有點夸張)。
22. C++怎么玩JSON?
C++本身不支持JSON,但有很多優(yōu)秀的庫。我推薦用nlohmann/json,因為它用起來就像喝水一樣簡單。
首先,咱們看看怎么把C++對象變成JSON:
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
#include <vector>
using json = nlohmann::json;
using namespace std;
// 定義一個人的結(jié)構(gòu)體
struct Person {
string name;
int age;
string city;
vector<string> hobbies;
};
int main() {
// 創(chuàng)建一個人
Person p = {"張三", 25, "北京", {"游戲", "電影", "音樂"}};
// 序列化:把對象變成JSON
json j;
j["name"] = p.name;
j["age"] = p.age;
j["city"] = p.city;
j["hobbies"] = p.hobbies;
// 輸出JSON字符串
cout << "序列化結(jié)果:" << endl;
cout << j.dump(4) << endl; // 4表示縮進4個空格,好看一些
return 0;
}
// 編譯命令:g++ -o test test.cpp運行結(jié)果:
序列化結(jié)果:
{
"age": 25,
"city": "北京",
"hobbies": [
"游戲",
"電影",
"音樂"
],
"name": "張三"
}再看看反序列化,把JSON變回對象:
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
#include <vector>
using json = nlohmann::json;
using namespace std;
struct Person {
string name;
int age;
string city;
vector<string> hobbies;
// 方便輸出的函數(shù)
void print() {
cout << "姓名: " << name << endl;
cout << "年齡: " << age << endl;
cout << "城市: " << city << endl;
cout << "愛好: ";
for(const auto& hobby : hobbies) {
cout << hobby << " ";
}
cout << endl;
}
};
int main() {
// 假設這是從網(wǎng)絡或文件讀取的JSON字符串
string json_str = R"({
"name": "李四",
"age": 30,
"city": "上海",
"hobbies": ["讀書", "旅游", "攝影"]
})";
// 反序列化:把JSON變成對象
json j = json::parse(json_str);
Person p;
p.name = j["name"].get<string>(); // 顯式轉(zhuǎn)換為string
p.age = j["age"].get<int>(); // 顯式轉(zhuǎn)換為int
p.city = j["city"].get<string>(); // 顯式轉(zhuǎn)換為string
p.hobbies = j["hobbies"].get<vector<string>>(); // 顯式轉(zhuǎn)換為vector<string>
cout << "反序列化結(jié)果:" << endl;
p.print();
return 0;
}
// 編譯命令:g++ -o test test.cpp運行結(jié)果:
反序列化結(jié)果:
姓名: 李四
年齡: 30
城市: 上海
愛好: 讀書 旅游 攝影3. JSON的優(yōu)缺點
優(yōu)點:
- 人類可讀,調(diào)試超方便
- 支持的語言多,幾乎通用
- 語法簡單,學習成本低
- Web開發(fā)的標配
缺點:
- 體積比較大(因為要存儲字段名)
- 解析速度相對較慢
- 不支持二進制數(shù)據(jù)
- 沒有數(shù)據(jù)類型驗證
三、ProtoBuf:性能怪獸,Google出品
1. ProtoBuf是個啥?
Protocol Buffers(簡稱ProtoBuf)是Google開發(fā)的序列化協(xié)議。如果說 JSON 是個顏值擔當,那 ProtoBuf 就是個實力派。它的特點就是:快!小!強!
但有個小缺點:人類看不懂。序列化后的數(shù)據(jù)是二進制的,就像這樣:
\x08\x96\x01\x12\x04\xE5\xBC\xA0\xE4\xB8\x89\x1A\x06\xE5\x8C\x97\xE4\xBA\xAC...看懵了吧?這就是為什么它快的原因——計算機處理二進制比處理文本快多了。
2. ProtoBuf怎么用?
使用ProtoBuf需要先定義一個.proto文件,描述數(shù)據(jù)結(jié)構(gòu):
// person.proto
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
string city = 3;
repeated string hobbies = 4;
}然后用protoc編譯器生成C++代碼:
protoc --cpp_out=. person.proto這會生成person.pb.h和person.pb.cc文件。
接下來就能在C++里用了:
#include <iostream>
#include <fstream>
#include "person.pb.h"
using namespace std;
int main() {
// 創(chuàng)建Person對象
Person person;
person.set_name("王五");
person.set_age(28);
person.set_city("深圳");
person.add_hobbies("編程");
person.add_hobbies("健身");
person.add_hobbies("美食");
// 序列化到字符串
string serialized_data;
person.SerializeToString(&serialized_data);
cout << "序列化完成,數(shù)據(jù)大小: " << serialized_data.size() << " 字節(jié)" << endl;
// 模擬網(wǎng)絡傳輸或文件存儲...
// 反序列化
Person new_person;
new_person.ParseFromString(serialized_data);
cout << "反序列化結(jié)果:" << endl;
cout << "姓名: " << new_person.name() << endl;
cout << "年齡: " << new_person.age() << endl;
cout << "城市: " << new_person.city() << endl;
cout << "愛好: ";
for(int i = 0; i < new_person.hobbies_size(); ++i) {
cout << new_person.hobbies(i) << " ";
}
cout << endl;
return 0;
}
// 編譯命令:g++ -o test test.cpp person.pb.cc -lprotobuf -pthread運行結(jié)果:
序列化完成,數(shù)據(jù)大小: 31 字節(jié)
反序列化結(jié)果:
姓名: 王五
年齡: 28
城市: 深圳
愛好: 編程 健身 美食3. ProtoBuf的優(yōu)缺點
優(yōu)點:
- 體積超小,壓縮效果好
- 序列化/反序列化速度飛快
- 跨語言支持好
- 有版本兼容性(向前向后兼容)
- 自動生成代碼,減少出錯
缺點:
- 人類不可讀,調(diào)試困難
- 需要預先定義schema
- 學習成本相對較高
- 不支持動態(tài)結(jié)構(gòu)
四、性能大PK:數(shù)據(jù)說話
好了,說了這么多,到底誰更強?咱們用實際數(shù)據(jù)說話!
我做了個簡單的測試,用相同的數(shù)據(jù)結(jié)構(gòu),分別用 JSON 和 ProtoBuf 進行 1 萬次序列化和反序列化操作:
#include <chrono>
#include <iostream>
#include <nlohmann/json.hpp>
#include "person.pb.h"
using namespace std;
using namespace std::chrono;
struct TestResult {
int serialize_time_ms;
int deserialize_time_ms;
size_t single_size;
size_t total_size;
};
TestResult test_json() {
const int iterations = 10000;
TestResult result = {};
// 測試數(shù)據(jù)
nlohmann::json test_data = {
{"name", "測試用戶名字比較長一些"},
{"age", 25},
{"city", "這是一個比較長的城市名稱"},
{"hobbies", {"愛好1描述比較長", "愛好2描述比較長", "愛好3描述比較長"}}
};
// 序列化測試
auto start = high_resolution_clock::now();
vector<string> results;
for(int i = 0; i < iterations; ++i) {
results.push_back(test_data.dump());
}
auto end = high_resolution_clock::now();
result.serialize_time_ms = duration_cast<milliseconds>(end - start).count();
// 計算大小
result.single_size = results[0].size();
for(const auto& r : results) result.total_size += r.size();
// 反序列化測試
start = high_resolution_clock::now();
for(const auto& data : results) {
auto j = nlohmann::json::parse(data);
// 模擬使用數(shù)據(jù)
string name = j["name"];
}
end = high_resolution_clock::now();
result.deserialize_time_ms = duration_cast<milliseconds>(end - start).count();
return result;
}
TestResult test_protobuf() {
const int iterations = 10000;
TestResult result = {};
// 序列化測試
auto start = high_resolution_clock::now();
vector<string> results;
for(int i = 0; i < iterations; ++i) {
Person person;
person.set_name("測試用戶名字比較長一些");
person.set_age(25);
person.set_city("這是一個比較長的城市名稱");
person.add_hobbies("愛好1描述比較長");
person.add_hobbies("愛好2描述比較長");
person.add_hobbies("愛好3描述比較長");
string serialized;
person.SerializeToString(&serialized);
results.push_back(serialized);
}
auto end = high_resolution_clock::now();
result.serialize_time_ms = duration_cast<milliseconds>(end - start).count();
// 計算大小
result.single_size = results[0].size();
for(const auto& r : results) result.total_size += r.size();
// 反序列化測試
start = high_resolution_clock::now();
for(const auto& data : results) {
Person person;
person.ParseFromString(data);
// 模擬使用數(shù)據(jù)
string name = person.name();
}
end = high_resolution_clock::now();
result.deserialize_time_ms = duration_cast<milliseconds>(end - start).count();
return result;
}
int main() {
cout << "=== JSON vs ProtoBuf 性能大PK ===" << endl << endl;
auto json_result = test_json();
auto pb_result = test_protobuf();
// 直觀對比輸出
cout << "測試結(jié)果對比 (10000次操作)" << endl;
cout << "┌──────────────┬─────────────┬─────────────┬──────────────┐" << endl;
cout << "│ 指標 │ JSON │ ProtoBuf │ ProtoBuf優(yōu)勢 │" << endl;
cout << "├──────────────┼─────────────┼─────────────┼──────────────┤" << endl;
printf("│ 序列化耗時 │ %8dms │ %8dms │ 快 %.1fx倍 │\n",
json_result.serialize_time_ms, pb_result.serialize_time_ms,
(float)json_result.serialize_time_ms / pb_result.serialize_time_ms);
printf("│ 反序列化耗時 │ %8dms │ %8dms │ 快 %.1fx倍 │\n",
json_result.deserialize_time_ms, pb_result.deserialize_time_ms,
(float)json_result.deserialize_time_ms / pb_result.deserialize_time_ms);
printf("│ 單個對象大小 │ %8zu字節(jié)│ %8zu字節(jié)│ 小 %4.1f%% │\n",
json_result.single_size, pb_result.single_size,
(float)(json_result.single_size - pb_result.single_size) * 100 / json_result.single_size);
printf("│ 總數(shù)據(jù)大小 │ %7.1fMB │ %7.1fMB │ 小 %4.1f%% │\n",
json_result.total_size / 1024.0 / 1024.0,
pb_result.total_size / 1024.0 / 1024.0,
(float)(json_result.total_size - pb_result.total_size) * 100 / json_result.total_size);
cout << "└──────────────┴─────────────┴─────────────┴──────────────┘" << endl;
cout << "\n結(jié)論:ProtoBuf在所有指標上都完勝JSON!" << endl;
cout << "如果傳輸10000個對象,ProtoBuf能節(jié)省 "
<< (json_result.total_size - pb_result.total_size) / 1024.0 / 1024.0
<< "MB 流量" << endl;
return 0;
}測試結(jié)果(在我的虛擬機上跑的):
g++ -o test test.cpp person.pb.cc -lprotobuf -pthread
=== JSON vs ProtoBuf 性能大PK ===
測試結(jié)果對比 (10000次操作)
┌──────────────┬─────────────┬─────────────┬──────────────┐
│ 指標 │ JSON │ ProtoBuf │ ProtoBuf優(yōu)勢 │
├──────────────┼─────────────┼─────────────┼──────────────┤
│ 序列化耗時 │ 22ms │ 6ms │ 快 3.7x倍 │
│ 反序列化耗時 │ 135ms │ 5ms │ 快 27.0x倍 │
│ 單個對象大小 │ 186字節(jié)│ 147字節(jié)│ 小 21.0% │
│ 總數(shù)據(jù)大小 │ 1.8MB │ 1.4MB │ 小 21.0% │
└──────────────┴─────────────┴─────────────┴──────────────┘
結(jié)論:ProtoBuf在所有指標上都完勝JSON!
如果傳輸10000個對象,ProtoBuf能節(jié)省 0.371933MB 流量?? 注意: 測試結(jié)果跟數(shù)據(jù)大小和數(shù)量有關(guān),數(shù)據(jù)越大、數(shù)量越多,ProtoBuf優(yōu)勢越明顯。建議用自己項目的真實數(shù)據(jù)測試一下!
五、實際應用場景:該選誰?
選JSON的情況:
- Web開發(fā):前后端通信的標配
- 配置文件:需要人工編輯的配置
- API接口:特別是REST API
- 調(diào)試頻繁:需要經(jīng)常查看數(shù)據(jù)內(nèi)容
- 快速原型:開發(fā)初期,快速驗證想法
選ProtoBuf的情況:
- 高性能要求:游戲、實時系統(tǒng)
- 網(wǎng)絡帶寬有限:移動端應用
- 大數(shù)據(jù)傳輸:微服務間通信
- 短期/臨時存儲:緩存、消息隊列
- 跨語言通信:不同語言的服務間通信
六、小結(jié):選擇建議
最后,給你一個選擇建議:
如果你是新手,建議先學JSON:
- 上手簡單,出錯率低
- 調(diào)試方便,看得見摸得著
- 資料多,遇到問題容易解決
如果你追求性能,上ProtoBuf:
- 速度快,體積小
- 適合生產(chǎn)環(huán)境的高并發(fā)場景
- 跨語言支持好
最理想的情況:兩個都會!
- 不同項目用不同工具,根據(jù)需求選擇
- 團隊內(nèi)部可以靈活應對各種技術(shù)需求
- 面試和技術(shù)交流時更有底氣
寫在最后
序列化這個話題,說簡單也簡單,說復雜也復雜。關(guān)鍵是要理解它的本質(zhì):就是為了讓數(shù)據(jù)能夠"旅行"。
就像你出門旅行要打包行李一樣,程序里的數(shù)據(jù)要"旅行"也需要打包。JSON就像是透明的行李箱,你能看到里面裝了什么;ProtoBuf就像是壓縮袋,體積小但看不見內(nèi)容。
選擇哪個,取決于你的具體需求。不過記住一點:沒有銀彈,只有合適的工具。
希望這篇文章能幫你理清楚序列化這個概念。如果還有不明白的地方,歡迎在評論區(qū)留言,咱們一起討論!
記住:編程路上,我們都是學習者,一起加油!
























