C++ Protobuf 入门实战:基于通讯录项目吃透proto3语法与序列化

本文结合C++实战通讯录项目,从零带你吃透Protobuf。Protobuf是Google推出的跨语言序列化工具,广泛用于网络通信、数据存储,对比JSON/XML拥有更小体积、更快序列化速度,并且前后版本兼容性优秀。

文章从序列化基础概念讲起,一步步演示proto3协议文件编写、protoc编译生成C++代码;讲解标量类型、repeated数组、嵌套message、enum枚举语法;演示二进制文件读写,序列化与反序列化完整代码。同时整理大量开发踩坑点,包含字段编号规范、命名空间、版本校验等工程要点,适合C++后端入门学习Protobuf。


一、初识 ProtoBuf

1.1 序列化和反序列化

序列化:把对象转换为字节序列的过程,称为对象的序列化。

反序列化:把字节序列恢复为对象的过程,称为对象的反序列化。

什么场景需要序列化

  1. 存储数据:把内存里对象状态保存到文件 / 数据库。

  2. 网络传输:网络不能直接传输对象,传输前序列化,收到后反序列化还原对象。典型例子:Socket通信收发数据。

序列化实现方案

xml、json、protobuf

1.2 ProtoBuf 是什么

Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data -- think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

翻译:

Protocol Buffers(简称Protobuf)是Google推出的语言无关、平台无关、可扩展的结构化数据序列化方案。可用于数据通信协议、数据存储。

类似XML,但是体积更小、速度更快、代码更简单。

你只需要定义一次数据结构,protoc编译器会自动生成多语言代码,读写结构化数据;后续更新数据结构,不会破坏旧版本编译好的程序(兼容性强)。

Protobuf三大特点

  1. 语言无关、平台无关:支持C++、Java、Python等多语言,跨平台。

  2. 高效:相比XML,体积更小、序列化更快、使用简单。

  3. 扩展性、兼容性好:更新消息字段结构,旧程序不用修改代码仍可正常运行。

1.3 Protobuf工作原理流程图

完整流程文字描述:

  1. 编写.proto文件:定义结构化对象message以及里面的属性字段。

  2. 使用protoc编译器编译.proto,自动生成C++头文件.pb.h与源文件.pb.cc,里面封装好读写、序列化/反序列化接口。

  3. 业务代码#include引入生成的pb头文件,调用接口,完成字段读写、序列化、反序列化。

核心优势:不用手写协议解析代码,全部自动生成。


二、快速上手:通讯录项目1.0

项目目标:

  1. 对联系人信息做Protobuf序列化。

  2. 拿到序列化后的二进制,反序列化还原联系人信息并打印。

联系人包含:姓名、年龄。

步骤1:创建 .proto 文件

文件命名规范

• 全部小写,多个单词下划线分隔:snake_case.proto;示例:contacts.proto

• 代码缩进:2个空格

注释写法

cpp 复制代码
// 单行注释
/*
多行注释
*/

指定语法版本

proto3是最新语法,必须写在除去注释后的第一行,不写默认使用老旧proto2。

cpp 复制代码
syntax = "proto3";

package 命名空间(可选)

作用:命名空间,防止不同消息重名冲突。

cpp 复制代码
syntax = "proto3";
package contacts;

定义消息 message

message代表结构化对象。

命名规范:大驼峰(首字母大写)

cpp 复制代码
message PeopleInfo {

}

定义消息字段

字段格式:字段类型 字段名 = 唯一编号;

• 字段名:全小写,下划线分隔 snake_case

• 唯一编号:一旦上线,不能修改! 用来标识字段。

proto3 标量类型对照表(C++映射)

|----------|-------------|------------------------------|
| proto类型 | C++类型 | 说明 |
| double | double | 双精度浮点 |
| float | float | 单精度浮点 |
| int32 | int32_t | 变长编码,负数效率低;负数多推荐sint32 |
| int64 | int64_t | 变长编码 |
| uint32 | uint32_t | 无符号32位变长 |
| uint64 | uint64_t | 无符号64位变长 |
| sint32 | int32_t | 有符号整型,负数编码效率更高 |
| sint64 | int64_t | 有符号64位,负数友好 |
| fixed32 | uint32_t | 定长4字节;数值经常大于2^28时,比uint32更快 |
| fixed64 | uint64_t | 定长8字节 |
| sfixed32 | int32_t | 定长4字节有符号 |
| sfixed64 | int64_t | 定长8字节有符号 |
| bool | bool | 布尔 |
| string | std::string | UTF-8 / ASCII字符串,长度上限2^32 |
| bytes | std::string | 原始任意二进制字节 |

变长编码:序列化后数字占用字节长度不是固定4/8字节,会根据数值大小动态变化。

字段编号规则

  1. 编号范围:1 ~ 536,870,911

  2. 19000 ~ 19999 保留,不能使用,编译报错。

  3. 编码开销:

1~15:占用1字节,高频字段优先用1~15

16~2047:占用2字节,留给后续扩展新增字段

✅ 最终完整 contacts.proto

cpp 复制代码
syntax = "proto3";
package contacts;

// 定义联系人消息
message PeopleInfo {
    string name = 1;
    int32 age = 2;
}

步骤2:编译.proto文件,生成C++代码

protoc编译命令通用格式

cpp 复制代码
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto

参数说明

protoc:编译器程序

--proto_path(简写 -I):proto文件搜索路径,可多个;import其他proto文件时使用

--cpp_out:指定C++生成文件的输出目录

path/to/file.proto:待编译的proto文件路径

本例编译命令(当前目录输出)

cpp 复制代码
protoc --cpp_out=. contacts.proto

执行成功,生成两个文件:

  1. contacts.pb.h:类声明头文件

  2. contacts.pb.cc:类实现源文件

自动生成C++代码说明

每一个message,都会生成一个对应的C++类

类里面自动生成:setter、getter、clear清理方法

头文件片段示例:

cpp 复制代码
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
 public:
  using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
  void CopyFrom(const PeopleInfo& from);
  using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
  void MergeFrom(const PeopleInfo& from) {
    PeopleInfo::MergeImpl(*this, from);
  }
  static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
    return "PeopleInfo";
  }

  // string name = 1;
  void clear_name();
  const std::string& name() const;
  template <typename ArgT0, typename ArgT1>
  void set_name(ArgT0&& arg0, ArgT1&& arg1);
  std::string* mutable_name();
  PROTOBUF_NODISCARD std::string* release_name();
  void set_allocated_name(std::string* name);

  // int32 age = 2;
  void clear_age();
  int32_t age() const;
  void set_age(int32_t value);
};

getter:字段同名,people.name()、people.age()

setter:set_ + 字段名 people.set_name("xxx")、people.set_age(20)

clear_xxx():清空字段内容

序列化/反序列化API(父类MessageLite提供)

cpp 复制代码
// 序列化
bool SerializeToOstream(std::ostream* output) const;  // 序列化写入文件流
bool SerializeToArray(void *data, int size) const;
bool SerializeToString(std::string* output) const;

// 反序列化
bool ParseFromIstream(std::istream* input);
bool ParseFromArray(const void* data, int size);
bool ParseFromString(const std::string& data);

注意:序列化API都是const成员函数,不会修改对象本身,只会把二进制数据写入传入的参数。

步骤3:C++代码,序列化与反序列化演示

新建main.cc

cpp 复制代码
#include <iostream>
#include "contacts.pb.h"
using namespace std;

int main()
{
    string people_str;
    {
        // proto里package为contacts,生成命名空间contacts
        contacts::PeopleInfo people;
        people.set_age(20);
        people.set_name("张珊");

        // 序列化,二进制存入string
        if (!people.SerializeToString(&people_str)) {
            cout << "序列化联系人失败." << endl;
        }
        cout << "序列化后的 people_str: " << people_str << endl;
    }

    {
        contacts::PeopleInfo people;
        // 反序列化,从二进制字符串还原对象
        if (!people.ParseFromString(people_str)) {
            cout << "反序列化联系人失败." << endl;
        }
        cout << "Parse age: " << people.age() << endl;
        cout << "Parse name: " << people.name() << endl;
    }
    return 0;
}

编译命令(g++)

cpp 复制代码
g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf

参数说明:

-std=c++11:必须开启C++11语法

-lprotobuf:链接protobuf库,缺少会报链接错误

运行程序

cpp 复制代码
./TestProtoBuf

输出示例:

cpp 复制代码
序列化后的 people_str: 张珊
Parse age: 20
Parse name: 张珊

重点:Protobuf序列化输出二进制字节序列,不是可读文本。

使用std::string存放二进制,终端打印会出现乱码、换行,属于正常现象。

二进制数据相对JSON/XML,破解成本更高,安全性相对更好。

整体流程总结

  1. 编写.proto文件,定义message消息和字段,指定proto3语法、package命名空间。

  2. protoc编译器编译proto文件,自动生成 .pb.h 和 .pb.cc。

  3. C++业务代码引入pb.h,调用set/get操作对象,调用SerializeToString序列化、ParseFromString反序列化。

  4. 编译项目时,带上-lprotobuf链接protobuf库,运行测试。

重点考点&坑点总结

  1. proto3第一行必须写syntax = "proto3";,否则是proto2语法。

  2. 字段编号一旦线上使用,永久不能修改,只能新增字段。

  3. 19000~19999编号预留,禁止使用。

  4. 序列化结果是二进制,不是明文文本,不能直接当成字符串阅读。

  5. 编译C++代码必须加 -lprotobuf,否则链接报错。

  6. package会生成C++命名空间,使用消息类时要写 命名空间::消息名。

  7. message命名:大驼峰;字段名:小写+下划线。


三、字段修饰规则

  1. singular(默认):字段可以出现0次或者1次,最多1次。proto3里所有字段默认都是singular。

  2. repeated:字段可以出现任意次数(0次、1次、多次),保留元素顺序。等价于数组。

cpp 复制代码
syntax = "proto3";
package contacts;
message PeopleInfo {
    string name = 1;
    int32 age = 2;
    repeated string phone_numbers = 3;
}

repeated 用来保存多个同类型数据,一个联系人存多个手机号就用这个。


四、消息类型定义与使用

4.1 嵌套消息定义(写在同一个message内部)

cpp 复制代码
syntax = "proto3";
package contacts;
message PeopleInfo {
    string name = 1;
    int32 age = 2;
    // 嵌套子消息Phone
    message Phone {
        string number = 1;
    }
    repeated Phone phone = 3;
}

4.2 非嵌套消息(同级定义,两个独立message)

cpp 复制代码
syntax = "proto3";
package contacts;
message Phone {
    string number = 1;
}
message PeopleInfo {
    string name = 1;
    int32 age = 2;
    repeated Phone phone = 3;
}

4.3 导入外部proto文件(import)

phone.proto

cpp 复制代码
syntax = "proto3";
package phone;
message Phone {
    string number = 1;
}

contacts.proto

cpp 复制代码
syntax = "proto3";
package contacts;
import "phone.proto";
message PeopleInfo {
    string name = 1;
    int32 age = 2;
    // 导入其他包消息,必须 包名.消息名
    repeated phone.Phone phone = 3;
}

备注:proto3和proto2的消息可以互相导入使用。

4.4 通讯录2.0完整proto定义

cpp 复制代码
syntax = "proto3";
package contacts;
// 联系人
message PeopleInfo {
    string name = 1;      //姓名
    int32 age = 2;        //年龄
    message Phone {
        string number = 1; //电话号码
    }
    repeated Phone phone = 3;
}
//通讯录,可以存放多个联系人
message Contacts {
    repeated PeopleInfo contacts = 1;
}

4.5 protoc编译命令 & 生成C++代码

编译指令:

cpp 复制代码
protoc --cpp_out=. contacts.proto

执行后生成两个文件:contacts.pb.h、contacts.pb.cc

生成类对应关系

  1. 嵌套消息PeopleInfo.Phone → C++类:PeopleInfo_Phone

  2. PeopleInfo → C++类:PeopleInfo

  3. Contacts → C++类:Contacts

Protobuf自动生成的成员函数规则

普通字段:set_xxx()赋值;xxx()读取值;clear_xxx()清空

repeated数组字段:

add_xxx():新增一个元素,返回对象指针

xxx_size():获取数组元素数量

xxx(index):读取下标index的元素(const只读)

mutable_xxx(index):获取可修改的元素指针

4.6 通讯录2.0 write.cc 写入代码

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/**
 * 新增联系人
 */
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{
    cout << "------------新增联系人------------" << endl;
    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people_info_ptr->set_name(name);

    cout << "请输入联系人年龄: ";
    int age;
    cin >> age;
    people_info_ptr->set_age(age);
    cin.ignore(256, '\n');

    for(int i = 1;; i++)
    {
        cout << "请输入联系人电话" << i << "(只输入回车完成电话新增): ";
        string number;
        getline(cin, number);
        if (number.empty())
        {
            break;
        }
        PeopleInfo_Phone* phone = people_info_ptr->add_phone();
        phone->set_number(number);
    }
    cout << "------------添加联系人成功------------" << endl;
}

int main(int argc, char *argv[])
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;
    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
        return -1;
    }
    Contacts contacts;
    // 先读取已存在的contacts
    fstream input(argv[1], ios::in | ios::binary);
    if (!input)
    {
        cout << argv[1] << ": File not found. Creating a new file." << endl;
    }
    else if (!contacts.ParseFromIstream(&input))
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }
    // 新增一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 向磁盘文件写入新的contacts
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output))
    {
        cerr << "Failed to write contacts." << endl;
        input.close();
        output.close();
        return -1;
    }
    input.close();
    output.close();

    // 程序结束,释放protobuf全局资源
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

write.cc Makefile

cpp 复制代码
all:write read
write:write.cc contacts.pb.cc
    g++ -o $@ $^ -std=c++11 -lprotobuf
read:read.cc contacts.pb.cc
    g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
    rm -f write read

运行write

cpp 复制代码
make
./write contacts.bin

程序逻辑:

  1. 打开contacts.bin,文件不存在则新建Contacts对象;存在就二进制反序列化读取原有通讯录

  2. 调用add_contacts()新增联系人,录入姓名、年龄、多个电话

  3. 序列化,覆盖写入到contacts.bin二进制文件

  4. ShutdownProtobufLibrary()释放protobuf全局资源

4.7 通讯录2.0 read.cc 读取打印代码

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/**
 * 打印联系人列表
 */
void PrintfContacts(const Contacts& contacts)
{
    for (int i = 0; i < contacts.contacts_size(); ++i)
    {
        const PeopleInfo& people = contacts.contacts(i);
        cout << "------------联系人" << i+1 << "------------" << endl;
        cout << "姓名: " << people.name() << endl;
        cout << "年龄: " << people.age() << endl;
        int j = 1;
        for (const PeopleInfo_Phone& phone : people.phone())
        {
            cout << "电话" << j++ << ": " << phone.number() << endl;
        }
    }
}

int main(int argc, char* argv[])
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;
    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
        return -1;
    }
    // 以二进制方式读取contacts
    Contacts contacts;
    fstream input(argv[1], ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input))
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }
    // 打印contacts
    PrintfContacts(contacts);

    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

运行读取程序:

cpp 复制代码
./read contacts.bin
  1. 命令行直接解码protobuf二进制文件(不用写C++代码)
cpp 复制代码
protoc --decode=contacts.Contacts contacts.proto < contacts.bin

作用:读取二进制bin文件,根据proto定义解析,输出文本格式内容。

中文会以八进制UTF-8编码展示。

核心知识点汇总

  1. repeated:代表动态数组,add_xxx()添加元素,_size()获取元素个数。

  2. 消息可以嵌套定义,嵌套子消息编译后类名是父类_子类。

  3. import可以引入外部proto,跨文件使用消息类型,跨包引用必须写包名.消息。

  4. ParseFromIstream:二进制流反序列化(读);SerializeToOstream:序列化写入二进制流(写)。

  5. GOOGLE_PROTOBUF_VERIFY_VERSION:校验protobuf头文件和库版本是否匹配,防止版本冲突。

  6. ShutdownProtobufLibrary():程序结束释放protobuf全局资源,小项目可省略,大型工程建议加上。

  7. protoc自带--decode,可以直接解析pb二进制文件,快速调试验证。

重要:文件读写一定要用 ios::binary,windows下不加binary会发生换行符转换,导致protobuf解析失败;Linux下影响小,但规范写法必须带上。


五、protobuf enum枚举类型

5.1 定义规则

语法支持我们定义枚举类型并使用。在.proto文件中枚举类型的书写规范为:

• 枚举类型名称:使用驼峰命名法,首字母大写。例如:MyEnum

• 常量值名称:全大写字母,多个字母之间用_连接。例如:ENUM_CONST = 0;

示例:定义PhoneType枚举

cpp 复制代码
enum PhoneType {
  MP = 0;  // 移动电话
  TEL = 1; // 固定电话
}

枚举定义3条核心规则

  1. 0值常量必须存在,且要作为第一个元素。为了与proto2的语义兼容:第一个元素作为默认值,且值为0。

  2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。

  3. 枚举的常量值在32位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。

5.2 定义时注意

将两个具有相同枚举值名称的枚举类型放在单个.proto文件下测试时,编译后会报错:某某某常量已经被定义!

规则总结

• 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。

• 单个.proto文件下,最外层枚举类型和嵌套枚举类型,不算同级。

• 多个.proto文件下,若一个文件引入了其他文件,且每个文件都未声明package,每个proto文件中的枚举类型都在最外层,算同级。

• 多个.proto文件下,若一个文件引入了其他文件,且每个文件都声明了package,不算同级。

示例1:同级枚举,常量重名,编译报错

cpp 复制代码
// 情况1:同级枚举类型包含相同枚举值名称
enum PhoneType {
  MP = 0;  // 移动电话
  TEL = 1; // 固定电话
}

enum PhoneTypeCopy {
  MP = 0;  // 移动电话  // 编译后报错: MP 已经定义
}

示例2:嵌套在message内,不同级,允许同名

cpp 复制代码
// 情况2:不同级枚举类型包含相同枚举值名称
enum PhoneTypeCopy {
  MP = 0;  // 移动电话  // 用法正确
}

message Phone {
  string number = 1; // 电话号码
  enum PhoneType {
    MP = 0;  // 移动电话
    TEL = 1; // 固定电话
  }
}

示例3:多文件无package,引入后算同级,重名报错

cpp 复制代码
// phone1.proto
enum PhoneType {
  MP = 0;  // 移动电话
  TEL = 1; // 固定电话
}

// phone2.proto
import "phone1.proto"
enum PhoneTypeCopy {
  MP = 0;  // 移动电话  // 编译后报错: MP 已经定义
}

示例4:多文件带package,隔离命名空间,同名没问题

cpp 复制代码
// phone1.proto
package phone1;
enum PhoneType {
  MP = 0;  // 移动电话 // 用法正确
  TEL = 1; // 固定电话
}

// phone2.proto
package phone2;
enum PhoneTypeCopy {
  MP = 0;  // 移动电话
}

5.3 升级通讯录至2.1版本

更新contacts.proto(通讯录2.1),新增枚举字段并使用

cpp 复制代码
syntax = "proto3";
package contacts;

// 联系人
message PeopleInfo {
  string name = 1;        // 姓名
  int32 age = 2;         // 年龄

  message Phone {
    string number = 1;   // 电话号码
    enum PhoneType {
      MP = 0; // 移动电话
      TEL = 1;// 固定电话
    }
    PhoneType type = 2;  // 类型
  }
  repeated Phone phone = 3; // 电话
}

// 通讯录
message Contacts {
  repeated PeopleInfo contacts = 1;
}

编译命令

cpp 复制代码
protoc --cpp_out=. contacts.proto

生成 contacts.pb.h 关键代码片段

cpp 复制代码
// 新生成的 PeopleInfo_Phone_PhoneType 枚举类
enum PeopleInfo_Phone_PhoneType : int {
  PeopleInfo_Phone_PhoneType_MP = 0,
  PeopleInfo_Phone_PhoneType_TEL = 1,

  PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MIN_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::min(),
  PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MAX_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::max()
};

// 更新的 PeopleInfo_Phone 类
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message {
 public:
  typedef PeopleInfo_Phone_PhoneType PhoneType;
  static inline bool PhoneType_IsValid(int value) {
    return PeopleInfo_Phone_PhoneType_IsValid(value);
  }
  template<typename T>
  static inline const std::string& PhoneType_Name(T enum_t_value) {...}
  static inline bool PhoneType_Parse(
    ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, PhoneType* value) {...}

  // ::contacts::PeopleInfo.Phone.type = 2;
  void clear_type();
  ::contacts::PeopleInfo_Phone_PhoneType type() const;
  void set_type(::contacts::PeopleInfo_Phone_PhoneType value);
};

知识点:

  1. 在proto文件中定义的枚举类型,编译生成的C++代码中会有对应枚举类;提供校验枚举值_IsValid、获取枚举名字_Name方法。

  2. 使用枚举的消息字段,自动生成setter/getter/clear_相关接口。


5.4 write.cc (通讯录2.1,写入二进制文件)

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/**
 * 新增联系人
 */
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{
    cout << "------------新增联系人------------" << endl;

    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people_info_ptr->set_name(name);

    cout << "请输入联系人年龄: ";
    int age;
    cin >> age;
    people_info_ptr->set_age(age);
    cin.ignore(256, '\n');

    for(int i = 1; ; i++)
    {
        cout << "请输入联系人电话" << i << " (只输入回车完成电话新增): ";
        string number;
        getline(cin, number);
        if (number.empty())
        {
            break;
        }
        PeopleInfo_Phone* phone = people_info_ptr->add_phone();
        phone->set_number(number);

        cout << "选择此电话类型 (1、移动电话  2、固定电话) : ";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch (type) {
            case 1:
                phone->set_type(PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
                break;
            case 2:
                phone->set_type(PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
                break;
            default:
                cout << "非法选择,使用默认值! " << endl;
                break;
        }
    }
    cout << "------------添加联系人成功------------" << endl;
}

int main(int argc, char *argv[])
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
        return -1;
    }

    Contacts contacts;
    // 先读取已存在的 contacts
    fstream input(argv[1], ios::in | ios::binary);
    if (!input)
    {
        cout << argv[1] << ": File not found. Creating a new file." << endl;
    }
    else if (!contacts.ParseFromIstream(&input))
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }
    input.close();

    // 新增一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 向磁盘文件写入新的 contacts
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output))
    {
        cerr << "Failed to write contacts." << endl;
        input.close();
        output.close();
        return -1;
    }
    output.close();

    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

5.5 read.cc(通讯录2.1,读取并打印二进制文件)

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

/**
 * 打印联系人列表
 */
void PrintfContacts(const Contacts& contacts) {
    for (int i = 0; i < contacts.contacts_size(); ++i) {
        const PeopleInfo& people = contacts.contacts(i);
        cout << "----------联系人" << i+1 << "----------" << endl;
        cout << "姓名: " << people.name() << endl;
        cout << "年龄: " << people.age() << endl;
        int j = 1;
        for (const PeopleInfo_Phone& phone : people.phone()) {
            cout << "电话" << j++ << ": " << phone.number();
            cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
        }
    }
}

int main(int argc, char* argv[]) {
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    if (argc != 2) {
        cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
        return -1;
    }

    Contacts contacts;
    // 以二进制方式读取 contacts
    fstream input(argv[1], ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }
    // 打印 contacts
    PrintfContacts(contacts);

    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

读写验证演示

bash 复制代码
# 写入
./write contacts.bin
------------新增联系人------------
请输入联系人姓名: 李四
请输入联系人年龄: 25
请输入联系人电话1(只输入回车完成电话新增):12333
选择此电话类型 (1、移动电话  2、固定电话) : 2
请输入联系人电话2(只输入回车完成电话新增):

------------添加联系人成功------------

# 读取打印
./read contacts.bin
----------联系人1----------
姓名: 张三
年龄: 20
电话1: 13111111111 (MP)  // 这里打印出 MP 是因为未设置该字段,protobuf自动使用枚举第一个元素作为默认值
电话2: 15111111111 (MP)
----------联系人2----------
姓名: 李四
年龄: 25
电话1: 12333 (TEL)

核心知识点汇总

  1. protobuf 枚举 enum

  2. proto3枚举第一个必须是0,作为默认值;推荐非负数。

  3. 枚举可嵌套在message内部,嵌套枚举和外层枚举不属于同一命名域,常量名允许重复。

  4. 无package时,多个import的proto文件顶层枚举属于同一命名空间,常量不能重名;package可以隔离命名空间,解决重名冲突。

  5. protoc编译后,自动生成枚举C++类型,提供:_IsValid校验枚举值、_Name(枚举值)获取枚举名字符串。

  6. 枚举字段自动生成set_type()、type()、clear_type()接口。

  7. proto3枚举字段不赋值,默认值就是枚举第一项(值=0的常量)。

  8. 业务代码要点

  9. ParseFromIstream:从文件流读取二进制protobuf数据,反序列化。

  10. SerializeToOstream:将protobuf对象序列化写入文件流。

  11. repeated数组调用add_xxx()新增元素,返回新增对象指针,用于填充嵌套message。

  12. 枚举类型::枚举常量 给枚举字段赋值;对象.枚举类型_Name(值) 把枚举数字转字符串名称打印。

  13. GOOGLE_PROTOBUF_VERIFY_VERSION:校验protobuf库版本,防止头文件和链接库版本不一致。

  14. ShutdownProtobufLibrary():程序退出,释放protobuf全局资源。

  15. 输入流坑点:cin >> age读取数字后会遗留换行符,要用cin.ignore()吃掉换行,防止后面getline读到空字符串。

相关推荐
qq_349447951 小时前
阿里云centos7.9系统安装jenkins
服务器·阿里云·jenkins
2401_868534781 小时前
网络环境规划核心考点全梳理
网络·设计模式
不怕犯错,就怕不做1 小时前
RK linux在buildroot中如何添加linux命令
linux·驱动开发·嵌入式硬件
M158227690551 小时前
ModbusTCP 转 Profinet 网关:上位机控制 PN 伺服方案
服务器·网络·tcp/ip
小羊在睡觉1 小时前
HLS视频
linux·后端·golang
宵时待雨1 小时前
linux笔记归纳18:传输层协议TCP
linux·网络·笔记·网络协议·tcp/ip
王志来137944730082 小时前
安防监控工控工业服务器配套
运维·服务器·python
吴声子夜歌2 小时前
Shell编程——条件测试与比较
linux·运维·shell
音草2 小时前
Linux 基础指令入门:常用命令、Shell 原理与权限管理
linux