ShineWinsu头像
关注
对于ProtoBuffer:嵌套 Message、repeated 限定符、C++ 常用函数、decode的核心解析封面图

对于ProtoBuffer:嵌套 Message、repeated 限定符、C++ 常用函数、decode的核心解析

开篇介绍:

hello 大家,本篇博客我们继续解析语法以及实战创建通讯录。

一、前置基础:Proto3 核心规则回顾

在学习嵌套 Message 和 repeated 限定符之前,我们需要先回顾 Proto3 的 3 个核心基础规则,这是后续所有内容的前提,也是 ProtoBuf 使用的 “通用准则”,贯穿整个开发过程。

1.1 Proto3 的基本文件结构

一个标准的 Proto3 文件,核心包含 3 个必选部分 + 可选的注释 / 导入 / 嵌套等内容,基本结构如下:

// 1. 声明使用Proto3语法(必选,Proto2需声明syntax="proto2",不声明默认Proto2)
syntax = "proto3";
// 2. 声明包名(必选,用于隔离命名空间,避免不同文件的Message重名)
package 包名;
// 3. 定义Message(必选,ProtoBuf的核心数据结构,类比C++的结构体/类)
message 消息名 {
    // 字段定义:字段类型 + 字段名 = 唯一标识数值(字段编号)
    标量类型/自定义类型 字段名 = 数字;
}

// 可选:注释(单行//,多行/* */)、嵌套Message、repeated字段、导入其他proto文件等

示例:一个简单的联系人基础信息 Proto3 定义

syntax = "proto3";
package connects; // 包名:connects(通讯录相关)

// 联系人基础信息Message
message PersonBase {
    string name = 1; // 姓名:字符串类型,字段编号1
    sint32 age = 2;  // 年龄:32位有符号整型,字段编号2
}

1.2 字段编号(唯一标识数值)的核心规则

字段编号是 ProtoBuf 中最核心、最基础的规则,也是后续嵌套 Message、repeated 字段的通用规则,必须牢牢记住:

ProtoBuf 的字段编号(= 后面的数字),仅需要在「同一个 Message 的大括号 {} 内部」唯一即可,不同 Message 之间的字段编号可以完全重复,无任何冲突。

关键说明

  1. 字段编号的作用:ProtoBuf 序列化时,并非存储字段名(如 name、age),而是存储字段编号(如 1、2),以此减少二进制数据体积,提升序列化效率;
  2. 编号范围:有效编号为 1~536870911,其中 19000~19999 为 ProtoBuf 保留编号,不可使用;
  3. 编号一旦确定,严禁修改:如果后续需要扩展字段,新增字段使用新的未用编号即可,修改已有字段的编号会导致旧的序列化数据无法反序列化,造成数据兼容问题;
  4. 可预留编号:为后续扩展预留部分编号(如 1~10 用做基础字段,11~20 预留),避免后续新增字段时编号混乱。

示例:不同 Message 的字段编号可重复

syntax = "proto3";
package connects;

// Message A:字段编号1为name
message A {
    string name = 1;
}
// Message B:字段编号1为age(与A重复,完全合法)
message B {
    sint32 age = 1;
}
// Message C:包含A和B,字段编号1为A,2为B(与A、B内部编号不冲突)
message C {
    A a = 1;
    B b = 2;
}

1.3 标量类型的定义与 Getter 函数规范

ProtoBuf 提供了一系列原生标量类型(如 string、int32、bool),对应 C++ 的原生类型(如 std::string、int32_t、bool),编译器会为每个标量字段自动生成Getter 函数(只读获取字段值),且 Getter 函数遵循统一的规范,这是后续调用生成代码的基础。

下表为实际开发中最常用的标量类型、Proto3 定义方式、C++ 对应的 Getter 函数语法、调用示例及返回值,建议熟记:

Proto3 字段类型字段定义示例C++ Getter 函数语法调用示例返回值说明
stringstring name=1const std::string& name() const;person.name();字符串 const 引用,避免值拷贝,提升效率,如 "张三"
sint32sint32 age=2int32_t age() const;person.age();32 位有符号整型值,如 25、-10
boolbool is_vip=3bool is_vip() const;person.is_vip();布尔值,true/false
uint64uint64 timestamp=4uint64_t timestamp() const;person.timestamp();64 位无符号整型值,如 1735689600000
doubledouble score=5double score() const;person.score();双精度浮点值,如 98.5
bytesbytes data=6const std::string& data() const;person.data();字节序列 const 引用,用于存储二进制数据
uint32uint32 id=7uint32_t id() const;person.id();32 位无符号整型值,如 1001
fixed64fixed64 hash=8uint64_t hash() const;person.hash();64 位无符号定长整型值,如 1234567890123456789

Getter 函数核心共性

  1. 函数名与字段名完全一致(如字段 name 对应函数 name (),字段 age 对应函数 age ());
  2. 函数末尾带const修饰,表明是纯读操作,调用后不会修改 Message 对象的任何字段;
  3. 标量类型(除 string、bytes)直接返回对应 C++ 原生类型值,string/bytes 返回const 引用(避免大字符串的拷贝开销,提升效率)。

二、嵌套 Message 全解析:定义、规则、两种方式对比

在实际业务中,“一个对象包含另一个对象” 是最常见的场景,比如:

  • 通讯录中的 “联系人” 包含 “电话号码”(电话号码有自己的属性,如号码、类型);
  • 订单中的 “订单信息” 包含 “商品信息”(商品信息有自己的属性,如商品 ID、名称、价格);
  • 用户中的 “用户信息” 包含 “地址信息”(地址信息有自己的属性,如省、市、详细地址)。

ProtoBuf 中的嵌套 Message就是为了适配这种场景而生,其本质类比 C++ 中的嵌套结构体 / 嵌套类,核心作用是将关联的复杂数据结构整合在一起,让.proto 文件的结构更清晰,数据关系更直观。

ProtoBuf 支持两种嵌套 Message 的定义方式,分别是「Message 内部直接嵌套定义子 Message」和「外部定义独立 Message + 内部声明使用」

2.1 嵌套 Message 的核心通用规则

无论使用哪种嵌套方式,都遵循以下 3 条通用规则,这是嵌套 Message 的基础,必须遵守:

  1. 子 Message 的字段编号仍需遵循 “同 Message 内部唯一” 的规则;
  2. 父 Message 与子 Message、不同子 Message 之间的字段编号可以重复,无任何冲突;
  3. 嵌套 Message 的使用本质是 “将子 Message 作为父 Message 的一个字段类型”,与使用原生标量类型(如 string、int32)的逻辑一致,仅需将 “字段类型” 替换为子 Message 的名称即可。

2.2 方式 1:Message 内部直接嵌套定义子 Message

2.2.1 语法规则

在父 Message 的大括号 {} 内部,直接定义需要嵌套的子 Message,子 Message 作为父 Message 的 “内部成员”,语法要点如下:

  1. 子 Message 的定义语法与普通 Message 完全一致,包含message 子消息名 { ... };
  2. 子 Message 本身不需要写字段编号(即不用加= 数字),因为子 Message 是 “数据结构定义”,而非父 Message 的一个 “字段”;
  3. 子 Message内部的普通字段(如 string、int32),必须按标准语法定义(字段类型 + 字段名 = 字段编号),且字段编号仅需在子 Message 内部唯一;
  4. 父 Message 若要使用内部定义的子 Message 作为字段,需将 “字段类型” 设为子 Message 的名称,按标准语法定义(子 Message 名 + 字段名 = 字段编号)。

2.2.2 完整代码示例

以 “联系人包含电话号码” 为例,父 Message 为 PersonInfo,内部嵌套定义子 Message Phone(电话号码):

syntax = "proto3";
package connects;

// 父Message:联系人信息
message PersonInfo {
    // 父Message的普通标量字段
    string name = 1;  // 姓名:字段编号1
    sint32 age = 2;   // 年龄:字段编号2

    // 内部直接嵌套定义子Message:电话号码(作为父Message的内部成员)
    // 注意:子Message Phone本身无需加字段编号,不是父Message的字段
    message Phone {
        // 子Message的普通字段:号码(字段编号1,与父Message的name=1重复,合法)
        string number = 1;
        // 子Message的普通字段:类型(如移动、联通,字段编号2,子Message内部唯一)
        string type = 2;
    }

    // 父Message使用内部嵌套的子Message作为字段
    // 字段类型:Phone(内部定义的子Message名),字段名:phone,字段编号:3
    Phone phone = 3;
}

2.2.3 编译后 C++ 类的特性

使用 ProtoBuf 编译器(protoc)编译上述.proto 文件后,会生成对应的 C++ 类,核心特性如下:

  1. 父 Message 生成独立的 C++ 类:connects::PersonInfo(包名::消息名);
  2. 内部嵌套的子 Message 生成组合命名的 C++ 类:connects::PersonInfo_Phone,命名规则为父消息名_子消息名(中间用下划线连接);
  3. 父 Message 类内部会自动生成子 Message 的类型别名:typedef PersonInfo_Phone Phone;,因此在使用时,可通过PersonInfo::Phone简写,无需写完整的PersonInfo_Phone,简化代码书写;
  4. 子 Message 类的访问权限:内部嵌套定义的子 Message,其生成的 C++ 类为全局可见,但从语义上,它是父 Message 的专属子结构,建议仅在父 Message 内部使用。

2.2.4 C++ 简单使用示例(基于内部嵌套定义)

#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    // 创建父Message对象
    connects::PersonInfo person;
    person.set_name("张三");
    person.set_age(25);

    // 操作内部嵌套的子Message字段:两种方式均可(别名/完整类名)
    // 方式1:使用父类内部的别名 PersonInfo::Phone
    connects::PersonInfo::Phone* phone = person.mutable_phone();
    phone->set_number("13800138000");
    phone->set_type("移动");

    // 方式2:使用完整类名 PersonInfo_Phone(不推荐,代码冗余)
    // connects::PersonInfo_Phone* phone = person.mutable_phone();

    // 打印数据
    cout << "姓名:" << person.name() << endl;
    cout << "年龄:" << person.age() << endl;
    cout << "电话号码:" << person.phone().number() << endl;
    cout << "电话类型:" << person.phone().type() << endl;

    return 0;
}

2.3 方式 2:外部定义独立 Message + 内部声明使用(推荐)

2.3.1 语法规则

先在所有 Message 的外部(全局作用域),定义一个独立的 Message(类比 C++ 的全局结构体),然后在需要使用该 Message 的父 Message 大括号 {} 内部,将其作为普通字段类型声明使用,语法要点如下:

  1. 独立 Message 的定义语法与普通 Message 完全一致,无任何特殊要求;
  2. 父 Message 使用该独立 Message 时,完全遵循 Proto3 的标准字段定义语法:独立Message名 + 字段名 + = + 字段编号,与使用原生标量类型(如 string、int32)的格式完全一致;
  3. 独立 Message 的字段编号仅需在其自身内部唯一,与父 Message、其他 Message 的字段编号可重复;
  4. 多个父 Message 可以复用同一个独立 Message,无需重复定义。

2.3.2 完整代码示例

同样以 “联系人包含电话号码” 为例,先全局外部定义独立的 Phone Message,再在 PersonInfo 中声明使用:

syntax = "proto3";
package connects;

// 步骤1:全局外部定义独立的Message:电话号码(可被所有Message复用)
message Phone {
    string number = 1;  // 号码:字段编号1
    string type = 2;    // 类型:字段编号2
}

// 步骤2:父Message:联系人信息,声明使用外部独立的Phone
message PersonInfo {
    string name = 1;  // 姓名:字段编号1(与Phone的number=1重复,合法)
    sint32 age = 2;   // 年龄:字段编号2(与Phone的type=2重复,合法)
    Phone phone = 3;  // 使用外部独立Message作为字段:类型=Phone,字段名=phone,编号=3
}

// 步骤3:其他Message可直接复用Phone(无需重复定义)
message CompanyInfo {
    string company_name = 1;
    Phone office_phone = 2; // 复用外部的Phone,实现公司办公电话的定义
}

2.3.3 编译后 C++ 类的特性

编译上述.proto 文件后,生成的 C++ 类与内部嵌套定义有明显区别,核心特性如下:

  1. 每个独立的 Message 都会生成独立的 C++ 类,类名与 Message 名完全一致,无组合命名:connects::Phone、connects::PersonInfo、connects::CompanyInfo;
  2. 所有生成的 C++ 类均为全局可见,可直接通过 “包名::类名” 访问,无需通过父类别名;
  3. 复用独立 Message 的字段,直接调用对应类的操作函数即可,逻辑与标量字段一致。

2.3.4 C++ 简单使用示例(基于外部定义 + 内部使用)

#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    // 1. 操作联系人信息,使用外部的Phone
    connects::PersonInfo person;
    person.set_name("张三");
    person.set_age(25);
    person.mutable_phone()->set_number("13800138000");
    person.mutable_phone()->set_type("移动");

    // 2. 操作公司信息,复用外部的Phone(无需重复定义)
    connects::CompanyInfo company;
    company.set_company_name("XX科技有限公司");
    company.mutable_office_phone()->set_number("010-12345678");
    company.mutable_office_phone()->set_type("固定电话");

    // 打印数据
    cout << "=== 联系人信息 ===" << endl;
    cout << "姓名:" << person.name() << endl;
    cout << "电话:" << person.phone().number() << "(" << person.phone().type() << ")" << endl;

    cout << "=== 公司信息 ===" << endl;
    cout << "公司名:" << company.company_name() << endl;
    cout << "办公电话:" << company.office_phone().number() << "(" << company.office_phone().type() << ")" << endl;

    return 0;
}

2.4 两种嵌套方式的对比与实际开发推荐

2.4.1 核心对比表

为了更直观地看出两种方式的区别,我们制作了核心对比表,从定义位置、复用性、可读性、维护性、编译后类名等维度进行对比:

对比维度方式 1:内部直接嵌套定义方式 2:外部定义 + 内部声明使用(推荐)
定义位置父 Message 内部,作为内部成员全局外部,独立于所有 Message
复用性仅能被父 Message 使用,无法被其他 Message 复用可被所有 Message 复用,无需重复定义
可读性子 Message 与父 Message 紧耦合,阅读时需在父 Message 内部查找子 Message 定义先定义后使用,符合常规编码习惯,阅读更顺畅
维护性修改子 Message 时,需在父 Message 内部操作,若多个地方需要类似结构,需重复定义并多次修改子 Message 仅需定义一次,修改一次即可作用于所有使用的地方,维护成本低
编译后类名组合命名(父_子),需通过父类别名简写独立命名(与 Message 名一致),直接访问
适用场景子 Message 仅为父 Message 专属,无复用需求子 Message 为通用结构(如 Phone、Address、Goods),有复用需求

2.4.2 实际开发推荐用法

优先使用方式 2:外部定义独立 Message + 内部声明使用,原因如下:

  1. 符合常规编码习惯:先定义后使用,是所有编程语言的通用编码逻辑,团队协作时,其他开发者能快速找到 Message 的定义位置,降低阅读成本;
  2. 提升代码复用性:通用的子 Message(如 Phone、Address、Goods)仅需定义一次,即可被所有需要的父 Message 复用,避免重复定义相同的结构,减少代码冗余;
  3. 降低维护成本:当通用子 Message 的结构需要修改(如给 Phone 新增int32 area_code = 3字段)时,仅需修改一次独立的 Message 定义,所有使用该 Message 的父 Message 都会自动生效,无需逐个修改,避免漏改、错改;
  4. 便于扩展:后续若新增一个需要使用该子 Message 的父 Message,直接声明使用即可,无需重新定义,提升开发效率。

方式 1 的适用场景:仅当子 Message 是父 Message 的专属结构,且确定永远不会被其他 Message 复用(如父 Message 的一个临时子结构,仅用于内部数据封装)时,可使用方式 1,让子 Message 与父 Message 的耦合度更高,结构更紧凑。

2.5 嵌套 Message 的进阶使用:多层嵌套

ProtoBuf 支持多层嵌套(即子 Message 内部再嵌套定义孙 Message),无论是方式 1 还是方式 2,都可以实现,且遵循所有通用规则。以下为方式 2 的多层嵌套示例(推荐方式),适配 “通讯录→联系人→电话号码→号码归属地” 的多层数据结构:

syntax = "proto3";
package connects;

// 第一层:独立Message:号码归属地
message Area {
    string province = 1; // 省
    string city = 2;     // 市
}

// 第二层:独立Message:电话号码(嵌套使用Area)
message Phone {
    string number = 1;  // 号码
    string type = 2;    // 类型
    Area area = 3;      // 归属地:嵌套使用Area
}

// 第三层:独立Message:联系人(嵌套使用Phone)
message PersonInfo {
    string name = 1;    // 姓名
    sint32 age = 2;     // 年龄
    Phone phone = 3;    // 电话号码:嵌套使用Phone
}

// 第四层:独立Message:通讯录(嵌套使用PersonInfo)
message Connects {
    string name = 1;        // 通讯录名称
    repeated PersonInfo contacts = 2; // 联系人列表:后续会讲的repeated字段
}

上述示例中,每层 Message 都是独立定义的,可单独复用,比如 Area 可被 Phone、Address 等其他 Message 复用,Phone 可被 PersonInfo、CompanyInfo 等复用,完美适配多层复杂数据结构,且结构清晰、易于维护。

三、repeated 限定符深度解析:背景、本质、语法与核心特性

在实际业务中,除了 “对象包含对象”,还有一个常见需求是 **“一个字段对应多个值”**,比如:

  • 一个联系人可以有多个电话号码(移动、联通、固定电话);
  • 一个通讯录可以有多个联系人;
  • 一个用户可以有多个邮箱地址;
  • 一个订单可以有多个商品。

但 ProtoBuf3 中,所有字段的默认规则是单值的,即一个字段只能存储 0 个或 1 个值,无法直接存储多个值。为了解决这个问题,ProtoBuf 提供了repeated 限定符,用于将字段标记为可重复字段,支持存储任意多个值,这也是处理 “多值集合” 场景的核心方案。

3.1 字段的两个核心限定符:singular(默认单值)与 repeated(可重复多值)

ProtoBuf3 中,所有字段都有一个限定符,用于指定字段的 “值数量规则”,核心有两个限定符:singular和repeated,其中 singular 是默认值,无需手动声明。

3.1.1 单值限定符:singular(proto3 默认,无需手动声明)

核心规则

指定字段为单值字段,该字段在 Message 中可以被设置0 次(不设置)或 1 次,最多只能有 1 个值,无法设置多个值,若尝试设置多个值,后设置的值会覆盖前一个值。

语法

无需手动声明singular关键字,直接按标准语法定义字段即可,以下两种写法等价,推荐省略 singular(简化代码):

// 推荐写法:默认singular,省略关键字
string name = 1;
sint32 age = 2;

// 等价写法:手动声明singular(无必要,不推荐)
singular string name = 1;
singular sint32 age = 2;
使用场景

适用于一个字段对应唯一一个值的场景,比如联系人的姓名、年龄、身份证号,商品的 ID、名称,订单的编号等,这些字段在业务中天然只有一个值,使用默认的 singular 即可。

C++ 示例:单值字段多次设置会被覆盖
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    connects::PersonInfo person;
    // 多次设置单值字段name,后设置的值覆盖前一个
    person.set_name("张三");
    person.set_name("李四");
    person.set_name("王五");

    // 多次设置单值字段age,后设置的值覆盖前一个
    person.set_age(20);
    person.set_age(25);

    // 最终仅保留最后一次设置的值
    cout << "姓名:" << person.name() << endl; // 输出:王五
    cout << "年龄:" << person.age() << endl;  // 输出:25

    return 0;
}

3.1.2 可重复多值限定符:repeated(需手动声明)

核心规则

指定字段为可重复字段,该字段在 Message 中可以被设置0 次、1 次或任意多次,支持存储多个值,且重复设置的值会保留顺序(即先添加的值在前,后添加的值在后,不会被覆盖)。

语法

必须在字段类型前手动声明 repeated 关键字,其余语法规则与单值字段完全一致(字段类型 + 字段名 = 字段编号),无任何额外要求:

// 正确写法:repeated + 字段类型 + 字段名 = 字段编号
repeated Phone phone = 3;        // 自定义Message类型的可重复字段
repeated string email = 4;       // 原生标量类型的可重复字段
repeated uint32 id_list = 5;     // 原生标量类型的可重复字段
使用场景

适用于一个字段对应多个值的场景,比如联系人的多个电话号码、多个邮箱,通讯录的多个联系人,订单的多个商品,用户的多个地址等,这些字段在业务中需要存储多个值,必须使用 repeated 限定符。

核心优势
  1. 保留顺序:添加的值按顺序存储,遍历的时候按添加顺序读取,适配需要 “有序集合” 的场景;
  2. 动态扩展:支持 0 个值(未设置)、1 个值或任意多个值,无需提前指定长度,适配数据量不固定的业务场景;
  3. 类型兼容:可修饰所有 ProtoBuf 类型,包括原生标量类型(string、int32 等)和自定义 Message 类型(Phone、PersonInfo 等),使用方式统一。

3.2 repeated 限定符的核心本质:等价于 C++ 结构体中的动态数组成员

这是理解 repeated 字段的最核心、最关键的知识点,也是后续学习 repeated 字段所有操作函数的基础 ——ProtoBuf 中被 repeated 修饰的字段,其本质完全等同于 C++ 中结构体的动态数组成员 **,二者的底层设计逻辑、实际使用目的、存储数据的形式完全一致 **。

3.2.1 核心类比:ProtoBuf repeated 字段 ↔ C++ 结构体动态数组成员

我们以 “通讯录包含多个联系人” 为例,将 ProtoBuf 的 repeated 字段定义与 C++ 的结构体动态数组成员定义做逐行等价类比,让你直观理解其本质:

步骤 1:定义 “数组元素类型”(二者一致)

无论是 ProtoBuf 的 repeated 字段,还是 C++ 的数组成员,都需要先定义数组的元素类型,这是定义数组的前提:

// ProtoBuf:定义数组元素类型——PersonInfo(Message)
message PersonInfo {
    string name = 1;
    sint32 age = 2;
    repeated Phone phone = 3;
}

// C++:定义数组元素类型——PersonInfo(结构体)
struct PersonInfo {
    std::string name;
    int32_t age;
    std::vector<Phone> phone; // Phone为另一个结构体
};
步骤 2:定义 “包含数组的外层容器”(二者一致)

ProtoBuf 中的 Message 和 C++ 中的结构体,都是 “封装数组的外层容器”,用于将数组字段和其他相关字段整合在一起:

// ProtoBuf:外层容器——Connects(Message),包含repeated字段(动态数组)
message Connects {
    string book_name = 1; // 通讯录名称(单值字段)
    repeated PersonInfo contacts = 2; // 联系人列表(repeated字段,本质是PersonInfo类型的动态数组)
}

// C++:外层容器——Connects(结构体),包含动态数组成员
struct Connects {
    std::string book_name; // 通讯录名称(普通成员)
    std::vector<PersonInfo> contacts; // 联系人列表(动态数组成员,与ProtoBuf的repeated字段完全等价)
};

3.2.2 为什么是 “动态数组” 而非 “静态数组”?

C++ 中的数组分为静态数组(如PersonInfo arr[10])和动态数组(如std::vector<PersonInfo>),而 ProtoBuf 的 repeated 字段对应的是动态数组,原因如下:

  1. 业务需求适配:实际开发中,“多值字段” 的元素数量是动态变化的(比如一个联系人可能有 1 个电话,也可能有 3 个电话;一个通讯录可能有 5 个联系人,也可能有 100 个联系人),静态数组需要提前指定固定长度,无法适配这种动态变化的场景;
  2. ProtoBuf 底层实现:repeated 字段在 C++ 中是通过 ProtoBuf 专属的RepeatedPtrField模板类实现的,其本质是一个动态的对象指针数组,会自动管理内存的分配与释放,支持动态添加、删除元素,无需开发者手动调用new/delete,与 C++ 的std::vector功能一致,但对 ProtoBuf 的 Message 对象做了专门优化。

3.2.3 RepeatedPtrField:ProtoBuf 专属的动态对象指针数组(底层实现)

当我们在.proto 文件中定义了一个 repeated 字段后,ProtoBuf 编译器会在生成的 C++ 代码中,将该字段声明为RepeatedPtrField <元素类型> 类型,比如:

// .proto文件中的repeated字段
repeated PersonInfo contacts = 2;

对应的 C++ 生成代码(简化版):

namespace connects {
    class Connects : public ::PROTOBUF_NAMESPACE_ID::Message {
    private:
        // repeated字段的底层实现:RepeatedPtrField<PersonInfo>
        ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<::connects::PersonInfo> contacts_;
    };
}
RepeatedPtrField 的核心特性
  1. 自动内存管理:添加元素时,自动创建对象并分配内存;删除元素时,自动销毁对象并释放内存;清空数组时,自动销毁所有元素并释放全部内存,无需开发者手动管理;
  2. 对象指针存储:内部存储的是元素对象的指针,而非对象本身,这样在添加、删除元素时,仅需操作指针,无需拷贝整个对象,提升效率(尤其对于大的 Message 对象);
  3. 支持动态增删:提供了Add()、Remove()、Clear()、Size()等方法,支持动态添加、删除、清空元素,获取元素个数;
  4. 兼容 STL 迭代器:支持范围 for 循环、迭代器遍历,与 C++ 的 STL 容器(如std::vector)的遍历方式一致,降低使用成本。

总结:你可以将RepeatedPtrField<元素类型>理解为 “ProtoBuf 定制版的std::vector<元素类型*>”,它在std::vector的基础上,针对 ProtoBuf 的 Message 对象做了内存管理、效率优化等适配,让开发者可以更安全、更高效地操作多值字段。

3.3 repeated 限定符的语法规则与注意事项

repeated 限定符的语法看似简单,但实际使用中仍有一些需要注意的细节,接下来我们将梳理 repeated 的完整语法规则和核心注意事项,避免踩坑。

3.3.1 完整语法规则

  1. 位置要求:repeated 关键字必须写在字段类型的前面,顺序不能颠倒,否则编译器会报错;
    • 正确:repeated Phone phone = 3;
    • 错误:Phone repeated phone = 3; / Phone phone repeated = 3;
  2. 类型兼容:repeated 可以修饰所有 ProtoBuf 支持的类型,包括:
    • 原生标量类型:string、sint32、bool、uint64、double 等;
    • 自定义 Message 类型:Phone、PersonInfo、Area 等(嵌套或外部定义的均可);
    • 后续会学到的枚举类型(enum)、Map 类型等;
  3. 字段编号规则:repeated 字段的字段编号仍需遵循 “同 Message 内部唯一” 的规则,与单值字段、其他 repeated 字段之间不能重复,不同 Message 之间的字段编号可重复;
  4. 命名规范:repeated 字段的字段名建议使用复数形式,直观表明这是一个 “多值集合”,比如:
    • 多个电话号码:repeated Phone phones = 3;(而非 phone)
    • 多个联系人:repeated PersonInfo contacts = 2;(而非 contact)
    • 多个邮箱:repeated string emails = 4;(而非 email)
  5. 与嵌套 Message 结合:repeated 可以修饰嵌套的 Message 类型,无论是方式 1 还是方式 2 的嵌套 Message,使用方式完全一致,无任何特殊要求。

3.3.2 核心注意事项

  1. repeated 仅改变存储形式,不影响其他规则:给字段添加 repeated 修饰后,仅将字段的存储形式从 “单值” 改为 “多值数组”,ProtoBuf 的其他所有规则(如字段编号、包名、命名规范、编译规则)均保持不变;
  2. repeated 字段的 “0 个值” 与 “未设置” 等价:如果一个 repeated 字段没有添加任何值,那么它的状态为 “空”,与单值字段的 “未设置” 状态一致,序列化时不会被写入二进制数据,减少存储体积;
  3. 元素的独立性:repeated 数组中的每一个元素,都是对应元素类型的独立、完整的对象,彼此之间相互独立,修改其中一个元素的字段值,不会影响其他元素;
  4. 索引从 0 开始:遍历 repeated 数组时,元素的索引从0开始计数,与 C++、Java 等编程语言的数组索引规则一致,且索引不能超过 “元素个数 - 1”,否则会触发越界错误;
  5. 无需手动初始化:repeated 字段的底层实现 RepeatedPtrField 会在 Message 对象创建时自动初始化为空数组,无需开发者手动调用初始化函数,直接调用添加、遍历等函数即可。

3.3.3 不同类型的 repeated 字段示例(全覆盖)

为了让你全面掌握 repeated 的使用,以下给出原生标量类型、外部定义的 Message 类型、内部嵌套的 Message 类型三种常见场景的 repeated 字段完整示例,覆盖实际开发中的 99% 使用场景:

syntax = "proto3";
package connects;

// 场景1:外部定义的Message类型(可复用)
message Phone {
    string number = 1;
    string type = 2;
}

// 场景2:父Message内部嵌套定义的Message类型(专属)
message PersonInfo {
    string name = 1;
    sint32 age = 2;

    // 内部嵌套定义:紧急联系人(专属)
    message EmergencyContact {
        string name = 1;
        string phone = 2;
    }

    // repeated修饰:外部定义的Phone类型(复数命名phones)
    repeated Phone phones = 3;
    // repeated修饰:内部嵌套的EmergencyContact类型(复数命名emergency_contacts)
    repeated EmergencyContact emergency_contacts = 4;
    // repeated修饰:原生标量类型string(复数命名emails)
    repeated string emails = 5;
    // repeated修饰:原生标量类型uint32(复数命名ids)
    repeated uint32 ids = 6;
}

// 场景3:repeated修饰自定义Message类型(PersonInfo)
message Connects {
    string book_name = 1; // 通讯录名称(单值)
    repeated PersonInfo contacts = 2; // 联系人列表(repeated,复数命名contacts)
}

3.4 repeated 字段的核心特性总结

  1. 多值存储:支持 0 次、1 次或任意多次赋值,适配 “一个字段对应多个值” 的业务场景;
  2. 顺序保留:添加的值按顺序存储,遍历顺序与添加顺序一致,支持有序集合;
  3. 动态扩展:底层基于 RepeatedPtrField 实现动态数组,无需提前指定长度,支持动态增删;
  4. 类型兼容:可修饰所有 ProtoBuf 类型(原生标量、自定义 Message、枚举等),使用方式统一;
  5. 自动内存管理:无需手动 new/delete,RepeatedPtrField 自动管理元素的内存分配与释放;
  6. 空值优化:无元素时不序列化,减少二进制存储体积;
  7. 元素独立:数组内元素彼此独立,修改互不影响;
  8. 语法简单:仅需在字段类型前添加 repeated 关键字,其余规则与单值字段一致。

四、ProtoBuf 编译核心知识:编译命令、生成文件、C++ 类基础特性

在学习编译后 C++ 常用函数之前,我们需要先掌握 ProtoBuf 的编译核心知识,包括 “如何将.proto 文件编译为 C++ 代码”、“编译后生成哪些文件”、“生成的 C++ 类有哪些基础特性”,这是调用后续所有操作函数的前提,也是将 ProtoBuf 落地到 C++ 开发中的关键步骤。

4.1 ProtoBuf 编译器:protoc

ProtoBuf 的编译器是protoc,它是一个跨平台的命令行工具,能够将.proto 定义文件编译为 C++、Java、Python、Go 等多种编程语言的代码,其核心作用是 “将.proto 中的 Message 定义、字段定义、repeated 限定符等,转换为对应语言的类、函数、数据结构”。

4.1.1 protoc 的安装(Linux Ubuntu 示例)

如果你的系统中尚未安装 protoc 和 Protobuf 开发库,可以通过以下命令快速安装(基于 Ubuntu 18.04/20.04/22.04):

# 更新软件源
sudo apt update
# 安装protoc编译器和Protobuf C++开发库
sudo apt install -y protobuf-compiler libprotobuf-dev
# 验证安装成功(查看版本,需3.x及以上)
protoc --version
# 输出示例:libprotoc 3.6.1(版本号可不同,3.x即可)

4.1.2 核心编译命令

protoc 的核心编译命令格式为:

protoc -I=PROTO源文件目录 --cpp_out=C++代码输出目录 PROTO源文件路径
命令参数详解
  1. -I=PROTO 源文件目录:指定.proto 源文件的所在目录(简称 “源目录”),如果.proto 文件在当前目录,可简写为-I=.(.表示当前目录);
  2. --cpp_out=C++ 代码输出目录:指定编译后生成的 C++ 代码的输出目录,该目录会自动创建(如果不存在);
  3. PROTO 源文件路径:指定需要编译的.proto 文件的具体路径(可指定多个,用空格分隔)。
简化编译命令(最常用)

如果.proto 文件在当前目录,且希望编译后的 C++ 代码也生成在当前目录,可使用简化命令:

protoc -I=. --cpp_out=. xxx.proto

其中xxx.proto是你的.proto 文件名(如connects.proto)。

多文件编译示例

如果有多个.proto 文件(如connects.proto、order.proto、user.proto)都在当前目录,需要一次性编译为 C++ 代码,可使用:

protoc -I=. --cpp_out=. connects.proto order.proto user.proto

4.2 编译后生成的两个核心文件

将.proto 文件编译为 C++ 代码后,会在指定的输出目录中生成两个核心文件:xxx.pb.h和xxx.pb.cc(其中xxx为.proto 文件的名称,如connects.pb.h、connects.pb.cc),这两个文件包含了所有操作 Message 对象的类和函数,是 C++ 开发中唯一需要引入和编译的文件。

4.2.1 xxx.pb.h:头文件(声明所有类和函数)

该文件是 C++ 的头文件,核心包含:

  1. 所有 Message 对应的 C++ 类的声明(包括嵌套 Message 的组合类、独立 Message 的类);
  2. 所有字段的操作函数声明(如 Getter、Setter、clear、mutable、repeated 字段的 add、size 等);
  3. 序列化 / 反序列化函数的声明(如 SerializeToString、ParseFromString、SerializeToOstream、ParseFromIstream 等);
  4. 其他辅助函数的声明(如 CopyFrom、Clear、IsInitialized 等);
  5. 必要的头文件引入、命名空间定义、宏定义等。

使用方式:在 C++ 代码中,通过#include "xxx.pb.h"引入该头文件,即可使用所有生成的类和函数。

4.2.2 xxx.pb.cc:源文件(实现所有函数)

该文件是 C++ 的源文件,核心包含:

  1. 所有 Message 对应的 C++ 类的构造函数、析构函数实现;
  2. 所有字段操作函数的具体实现(如 set_name、name、add_phone、phone_size 等);
  3. 序列化 / 反序列化函数的具体实现(ProtoBuf 的核心序列化逻辑都在这里);
  4. 内存管理、对象拷贝、字段检查等辅助函数的具体实现。

使用方式:该文件不需要手动修改,也不需要在 C++ 代码中引入,只需在编译 C++ 程序时,将该文件与你的业务代码一起编译即可(如g++ main.cpp connects.pb.cc -o main)。

4.2.3 核心注意事项

  1. 不要手动修改生成的文件:xxx.pb.h和xxx.pb.cc是 protoc 自动生成的文件,手动修改会导致编译错误、序列化 / 反序列化异常,若需要修改数据结构,应修改.proto 文件后重新编译;
  2. 生成的文件与.proto 文件一一对应:每个.proto 文件编译后会生成独立的xxx.pb.h和xxx.pb.cc,多个.proto 文件编译后会生成多组对应的文件,彼此独立;
  3. 生成的文件跨平台:protoc 生成的 C++ 代码是跨平台的,可在 Linux、Windows、Mac 等系统上编译运行,无需修改;
  4. 依赖 Protobuf 开发库:编译生成的 C++ 代码时,需要链接 Protobuf 的 C++ 开发库(libprotobuf),否则会出现链接错误。

4.3 编译后 C++ 类的基础特性

无论.proto 文件中定义的是普通 Message、嵌套 Message,还是包含 repeated 字段的 Message,编译后生成的 C++ 类都遵循以下基础特性,这些特性是所有操作函数的通用前提,适用于所有生成的类:

4.3.1 继承自 ProtoBuf 的 Message 基类

所有生成的 C++ 类都会公有继承自 ProtoBuf 的核心基类::PROTOBUF_NAMESPACE_ID::Message(PROTOBUF_NAMESPACE_ID 是 ProtoBuf 的命名空间宏,对应实际的google::protobuf),该基类提供了所有 Message 对象的通用核心功能,包括:

  1. 序列化 / 反序列化功能(如 SerializeToString、ParseFromString);
  2. 对象拷贝功能(如 CopyFrom、MergeFrom);
  3. 字段清空功能(如 Clear);
  4. 字段初始化检查功能(如 IsInitialized);
  5. 内存管理功能(如析构函数、Swap)。

优势:所有生成的类都拥有统一的基础接口,开发者无需记忆不同类的通用函数,做到 “一次学习,所有类通用”。

4.3.2 被 final 修饰,不可被继承

生成的 C++ 类都会被final 关键字修饰(C++11 及以上特性),比如:

class PersonInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
    // 类的内容
};

final 关键字的作用:禁止该类被其他类继承,原因如下:

  1. 保证 ProtoBuf 的核心逻辑不被破坏:ProtoBuf 的 Message 基类的核心功能(如序列化、反序列化)是高度封装的,子类继承并修改后,可能会导致序列化 / 反序列化异常;
  2. 提升编译效率:编译器对 final 修饰的类会做更多的优化,提升代码的运行效率;
  3. 简化设计:ProtoBuf 的设计初衷是 “数据结构定义→自动生成代码→调用函数”,无需开发者进行类的继承扩展,所有需求都可通过修改.proto 文件实现。

4.3.3 自动生成默认构造函数和析构函数

protoc 会为每个生成的 C++ 类自动生成无参默认构造函数和析构函数,且析构函数为虚函数(因为继承自 Message 基类),比如:

namespace connects {
    class PersonInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
    public:
        // 无参默认构造函数
        PersonInfo();
        // 虚析构函数(继承自Message基类,保证多态析构)
        virtual ~PersonInfo();
    };
}

使用优势:开发者可以直接通过类名 对象名;创建 Message 对象,无需手动定义构造函数,析构函数会自动释放对象的所有资源(包括 repeated 字段的 RepeatedPtrField、字符串等),无需手动管理内存。

4.3.4 自动生成 CopyFrom 函数(深拷贝)

所有生成的 C++ 类都会自动生成CopyFrom 成员函数,用于实现同类型 Message 对象的深拷贝,将源对象的所有字段值(包括单值字段、repeated 字段)完全拷贝到当前对象,覆盖当前对象的原有值。

CopyFrom 函数的语法
// 类内声明
void CopyFrom(const 类名& from);

// 示例:PersonInfo类的CopyFrom函数
void CopyFrom(const PersonInfo& from);
核心特性
  1. 深拷贝:不仅拷贝单值字段,还会拷贝 repeated 字段的所有元素,源对象和目标对象的字段值完全独立,修改其中一个对象的字段值,不会影响另一个;
  2. 覆盖原有值:拷贝前会先清空当前对象的所有字段值,再将源对象的字段值拷贝过来,最终当前对象与源对象完全一致;
  3. 参数为 const 引用:参数类型为const 类名&,既避免了修改源对象,又减少了对象拷贝的开销(尤其是大对象);
  4. 通用继承:除了类专属的 CopyFrom 函数,还会继承 Message 基类的 CopyFrom 函数,支持基类指针的多态拷贝。
C++ 使用示例
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    // 创建源对象并设置值
    connects::PersonInfo src_person;
    src_person.set_name("张三");
    src_person.set_age(25);
    src_person.add_phones()->set_number("13800138000");
    src_person.add_phones()->set_number("13900139000");

    // 创建目标对象,初始为空
    connects::PersonInfo dst_person;
    cout << "拷贝前目标对象姓名:" << dst_person.name() << endl; // 空
    cout << "拷贝前目标对象电话数:" << dst_person.phones_size() << endl; // 0

    // 深拷贝:将src_person拷贝到dst_person
    dst_person.CopyFrom(src_person);

    // 拷贝后目标对象与源对象完全一致
    cout << "拷贝后目标对象姓名:" << dst_person.name() << endl; // 张三
    cout << "拷贝后目标对象电话数:" << dst_person.phones_size() << endl; // 2
    cout << "拷贝后目标对象第一个电话:" << dst_person.phones(0).number() << endl; // 13800138000

    // 修改源对象,目标对象不受影响(深拷贝特性)
    src_person.set_name("李四");
    src_person.mutable_phones(0)->set_number("18800188000");
    cout << "修改源对象后,目标对象姓名:" << dst_person.name() << endl; // 仍为张三
    cout << "修改源对象后,目标对象第一个电话:" << dst_person.phones(0).number() << endl; // 仍为13800138000

    return 0;
}

4.3.5 自动生成 Clear 函数(清空所有字段)

所有生成的 C++ 类都会自动生成Clear 成员函数,用于清空对象的所有字段值,将对象恢复到 “刚创建时的初始状态”,包括:

  1. 单值字段:重置为 “未设置” 状态(如 string 为空,int32 为 0,bool 为 false);
  2. repeated 字段:清空所有元素,释放内存,元素个数置 0;
  3. 嵌套 Message 字段:调用嵌套对象的 Clear 函数,清空其所有字段。
Clear 函数的语法
// 类内声明
void Clear();

// 示例:PersonInfo类的Clear函数
void Clear() override; // override表示重写Message基类的Clear函数
C++ 使用示例
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    connects::PersonInfo person;
    person.set_name("张三");
    person.set_age(25);
    person.add_phones()->set_number("13800138000");

    cout << "Clear前:" << endl;
    cout << "姓名:" << person.name() << endl; // 张三
    cout << "年龄:" << person.age() << endl;  // 25
    cout << "电话数:" << person.phones_size() << endl; // 1

    // 清空所有字段
    person.Clear();

    cout << "Clear后:" << endl;
    cout << "姓名:\"" << person.name() << "\"" << endl; // 空字符串
    cout << "年龄:" << person.age() << endl;            // 0
    cout << "电话数:" << person.phones_size() << endl;   // 0

    return 0;
}

4.3.6 支持序列化 / 反序列化的核心函数

所有生成的 C++ 类都会继承 Message 基类的核心序列化 / 反序列化函数,这是 ProtoBuf 的核心功能,也是我们使用 ProtoBuf 的最终目的。常用的序列化 / 反序列化函数包括:

  1. SerializeToString:将 Message 对象序列化为std::string(二进制格式);
  2. ParseFromString:从std::string(二进制格式)反序列化为 Message 对象;
  3. SerializeToOstream:将 Message 对象序列化为输出流(如文件流、标准输出流),适用于文件存储、网络传输;
  4. ParseFromIstream:从输入流(如文件流、标准输入流)反序列化为 Message 对象,适用于文件读取、网络接收。

这些函数的详细用法、参数、返回值,我们会在实战案例中结合完整代码详细讲解,这里先做简单介绍,让你有一个整体认知。

五、编译后 C++ 常用函数全解析:单值字段 + repeated 字段

掌握 ProtoBuf 的核心,最终落脚到调用编译后生成的 C++ 函数操作 Message 对象。本文的这一部分是核心中的核心,也是实际开发中使用频率最高的内容。

前置说明:

  1. 所有示例均基于以下.proto 文件(简易通讯录,覆盖单值字段、repeated 字段、嵌套 Message),确保示例的一致性和实用性;
    syntax = "proto3";
    package connects;
    
    // 外部定义的Phone Message
    message Phone {
        string number = 1; // 单值字段:号码
        string type = 2;   // 单值字段:类型
    }
    
    // 包含单值字段和repeated字段的PersonInfo Message
    message PersonInfo {
        string name = 1;        // 单值字段:姓名
        sint32 age = 2;         // 单值字段:年龄
        repeated Phone phones = 3; // repeated字段:电话号码列表
    }
    
    // 包含repeated字段的Connects Message
    message Connects {
        string book_name = 1;   // 单值字段:通讯录名称
        repeated PersonInfo contacts = 2; // repeated字段:联系人列表
    }
    
  2. 所有生成的 C++ 类均在connects命名空间下,使用时需加connects::前缀;
  3. 函数名中的字段名均为.proto 文件中定义的字段名(如 name、age、phones、contacts),遵循 “字段名与函数名联动” 的规则,方便记忆;
  4. 所有函数均为成员函数,通过 Message 对象调用(如person.set_name("张三")、person.phones_size())。

5.1 模块 1:单值字段通用操作函数

ProtoBuf 中,所有单值字段(无论原生标量类型还是自定义 Message 类型)都会生成一套通用的操作函数,共 4 个核心函数,分别实现 “清空字段”、“只读获取字段值(Getter)”、“设置字段值(Setter)”、“获取可修改字段指针(mutable)” 的功能,覆盖单值字段的所有操作场景。

这 4 个函数的命名规则、语法、使用逻辑完全一致,仅需将 “字段名” 替换为实际的字段名即可,做到 “一次学习,所有单值字段通用”。

5.1.1 函数 1:clear_字段名 () —— 清空单值字段

核心功能

将指定的单值字段重置为初始状态(未设置状态),释放字段占用的内存(如 string 的内存),序列化时该字段不会被写入二进制数据。

  • 对于 string/bytes 类型:重置为空字符串"";
  • 对于数值类型(sint32、uint64、double 等):重置为 0;
  • 对于 bool 类型:重置为 false;
  • 对于自定义 Message 类型:调用该 Message 对象的 Clear () 函数,清空其所有字段。
语法(函数签名)
// 通用语法
void clear_字段名();

// 示例:清空name字段、age字段、book_name字段
void clear_name();    // PersonInfo类:清空姓名
void clear_age();     // PersonInfo类:清空年龄
void clear_book_name(); // Connects类:清空通讯录名称
void clear_number();  // Phone类:清空号码
参数

无参数。

返回值

void(无返回值),直接修改当前 Message 对象的字段状态。

实现本质

直接修改 Message 对象内部的字段存储变量,将其置为初始值,并释放非基础类型(如 string)的内存。

使用场景
  1. 当需要取消某个字段的设置时(如用户误输入,需要清空该字段);
  2. 序列化前清理无用字段,减少二进制数据体积;
  3. 重复使用 Message 对象时,清空旧的字段值,避免影响新值。
C++ 使用示例
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    connects::PersonInfo person;
    // 设置字段值
    person.set_name("张三");
    person.set_age(25);
    person.mutable_phones(0)->set_number("13800138000");

    cout << "清空前:" << endl;
    cout << "姓名:" << person.name() << endl; // 张三
    cout << "年龄:" << person.age() << endl;  // 25

    // 清空指定字段
    person.clear_name();
    cout << "清空name后:" << endl;
    cout << "姓名:\"" << person.name() << "\"" << endl; // 空字符串
    cout << "年龄:" << person.age() << endl;            // 25(未被清空)

    // 清空所有字段(使用Clear()函数,前面已讲)
    person.Clear();

    return 0;
}

5.1.2 函数 2:字段名 () const —— 只读获取字段值(Getter)

核心功能

只读获取指定单值字段的当前值,函数末尾的const修饰表明这是纯读操作,调用后不会修改 Message 对象的任何字段,是获取字段值的最常用函数。

该函数的返回值类型与.proto 中的字段类型一一对应,具体对应关系见本文1.3 节 标量类型的定义与 Getter 函数规范。

语法(函数签名)
// 通用语法
返回值类型 字段名() const;

// 示例:不同类型字段的Getter函数
const std::string& name() const;    // PersonInfo类:获取姓名(string→const string&)
int32_t age() const;               // PersonInfo类:获取年龄(sint32→int32_t)
const std::string& book_name() const; // Connects类:获取通讯录名称(string→const string&)
const std::string& number() const;  // Phone类:获取号码(string→const string&)
double score() const;               // 若有double score=4→double score() const;
bool is_vip() const;                // 若有bool is_vip=5→bool is_vip() const;
参数

无参数。

返回值

与.proto 字段类型对应的 C++ 类型,核心规则:

  1. 原生标量类型除 string/bytes 外,直接返回对应 C++ 原生类型值(如 sint32→int32_t,uint64→uint64_t,bool→bool);
  2. string/bytes 类型返回const std::string&(常量引用),避免大字符串的拷贝开销,提升效率;
  3. 自定义 Message 类型返回const 类名 &(常量引用),如const Phone& phone() const;。
实现本质

直接返回 Message 对象内部的字段存储变量的值或引用,无额外计算,效率极高。

使用场景

所有需要读取字段值的场景,如打印字段值、判断字段值、将字段值赋值给其他变量等,是实际开发中使用频率最高的函数之一。

核心注意事项

返回的是const 引用 / 值,不可直接修改,若需要修改字段值,需使用set_字段名()或mutable_字段名()函数。

C++ 使用示例
#include "connects.pb.h"
#include <iostream>
#include <string>
using namespace std;

int main() {
    connects::PersonInfo person;
    person.set_name("张三");
    person.set_age(25);

    // 1. 直接打印字段值(最常用)
    cout << "姓名:" << person.name() << endl;
    cout << "年龄:" << person.age() << endl;

    // 2. 将字段值赋值给其他变量
    string name = person.name(); // const string& 赋值给 string,自动拷贝
    int32_t age = person.age();  // int32_t 直接赋值
    cout << "赋值后的姓名:" << name << endl;
    cout << "赋值后的年龄:" << age << endl;

    // 3. 判断字段值
    if (person.age() >= 18) {
        cout << "该联系人是成年人" << endl;
    } else {
        cout << "该联系人是未成年人" << endl;
    }

    // 错误示例:不可直接修改const引用的值(编译报错)
    // person.name() = "李四"; // 编译错误:assignment of read-only reference
    // person.age() = 30;      // 编译错误:lvalue required as left operand of assignment

    return 0;
}

5.1.3 函数 3:set_字段名 () —— 设置字段值(Setter)

核心功能

为指定的单值字段设置新值,覆盖字段的原有值(如果有),是设置单值字段值的最常用函数。

该函数支持多种参数类型(如 const 引用、右值、原生类型),适配不同的使用场景,且编译器会自动生成对应的重载函数,无需开发者手动处理。

语法(函数签名)
// 通用语法(编译器自动生成重载,支持多种参数类型)
void set_字段名(参数类型 值);

// 示例:不同类型字段的Setter函数
// string类型:支持const string&、const char*、string&&
void set_name(const std::string& value);
void set_name(const char* value);
void set_name(std::string&& value);

// sint32类型:支持int32_t
void set_age(int32_t value);

// uint64类型:支持uint64_t
void set_id(uint64_t value);

// double类型:支持double
void set_score(double value);

// bool类型:支持bool
void set_is_vip(bool value);

// 自定义Message类型:无set函数,需使用mutable_字段名()
// (原因:自定义Message类型是对象,需要先获取指针再设置其内部字段)
参数

与.proto 字段类型对应的 C++ 类型,核心规则:

  1. 原生标量类型:参数为对应 C++ 原生类型(如 sint32→int32_t,uint64→uint64_t);
  2. string/bytes 类型:支持三种参数类型(最灵活):
    • const std::string& value:常量引用,适用于已有 string 对象的场景,避免拷贝;
    • const char* value:字符串常量指针,适用于直接传字符串字面量的场景(如set_name("张三"));
    • std::string&& value:右值引用,适用于临时 string 对象的场景,支持移动语义,提升效率;
  3. 自定义 Message 类型:无 set_字段名 () 函数,因为自定义 Message 类型是一个对象,包含多个自身的字段,无法通过一个 set 函数设置所有值,需使用mutable_字段名()获取对象指针后,再设置其内部字段。
返回值

void(无返回值),直接修改当前 Message 对象的字段值。

实现本质

将传入的参数值赋值给 Message 对象内部的字段存储变量,对于 string 类型,会自动管理内存(释放原有内存,分配新内存)。

使用场景

所有需要设置单值字段值的场景,如初始化字段值、修改字段值、从用户输入 / 数据库读取后设置字段值等,是实际开发中使用频率最高的函数之一。

C++ 使用示例(覆盖所有参数类型)
#include "connects.pb.h"
#include <iostream>
#include <string>
using namespace std;

int main() {
    connects::PersonInfo person;

    // 1. string类型字段的三种设置方式(推荐使用const char*,最简洁)
    // 方式1:const char*(直接传字符串字面量,最常用)
    person.set_name("张三");
    // 方式2:const std::string&(传已有string对象)
    string name = "李四";
    person.set_name(name);
    // 方式3:std::string&&(传临时string对象,移动语义)
    person.set_name(string("王五"));

    // 2. 数值类型字段的设置(直接传对应原生类型值)
    person.set_age(25); // sint32→int32_t
    // person.set_id(1001); // 若有uint64 id=4→person.set_id(1001ULL);(ULL表示uint64_t)
    // person.set_score(98.5); // 若有double score=5→person.set_score(98.5);

    // 3. bool类型字段的设置
    // person.set_is_vip(true); // 若有bool is_vip=6→person.set_is_vip(true);

    // 打印最终值
    cout << "姓名:" << person.name() << endl; // 王五
    cout << "年龄:" << person.age() << endl;  // 25

    return 0;
}

5.1.4 函数 4:mutable_字段名 () —— 获取可修改字段指针

核心功能

返回指定单值字段的可修改指针,通过该指针可以直接修改字段值,或对于自定义 Message 类型字段,通过该指针设置其内部的字段值(这是自定义 Message 类型字段的唯一修改方式)。

语法(函数签名)
// 通用语法
字段类型* mutable_字段名();

// 示例:不同类型字段的mutable函数
std::string* mutable_name();    // PersonInfo类:name字段→string*
Phone* mutable_phone();         // 若有Phone phone=4→Phone*(自定义Message类型)
std::string* mutable_number();  // Phone类:number字段→string*
参数

无参数。

返回值

字段类型对应的可修改指针(非 const):

  1. 原生标量类型(仅 string/bytes):返回std::string*(因为数值类型 /bool 类型直接用 set_字段名 () 更简洁,无需指针);
  2. 自定义 Message 类型:返回对应 Message 类的指针(如Phone*、PersonInfo*),这是修改自定义 Message 类型字段的唯一方式。
实现本质
  1. 对于未设置的字段:先自动初始化该字段(如创建空 string 对象、创建空的 Message 对象),再返回其指针;
  2. 对于已设置的字段:直接返回其指针。

核心优势:无需手动检查字段是否初始化,直接调用即可,避免空指针异常。

使用场景
  1. 修改 string/bytes 类型字段的部分值(如在原有字符串后追加内容),而不是整体覆盖;
  2. 操作自定义 Message 类型字段(这是唯一方式),通过返回的指针设置其内部的字段值;
  3. 需要多次修改同一个字段时,获取一次指针后多次使用,避免多次调用 set_字段名 () 的开销。
核心注意事项
  1. 返回的是可修改指针,可以直接修改字段值,修改后会直接反映到 Message 对象中;
  2. 无需手动释放指针:指针指向的是 Message 对象内部的字段存储变量,由 Message 对象统一管理内存,当 Message 对象被析构时,指针指向的内存会自动释放,无需开发者手动调用delete;
  3. 数值类型 /bool 类型不推荐使用该函数,直接使用set_字段名()更简洁、更高效。
C++ 使用示例
#include "connects.pb.h"
#include <iostream>
#include <string>
using namespace std;

int main() {
    connects::PersonInfo person;
    person.set_name("张三");

    // 场景1:修改string类型字段的部分值(追加内容)
    std::string* name_ptr = person.mutable_name();
    name_ptr->append("(先生)"); // 在原有姓名后追加内容
    cout << "修改后的姓名:" << person.name() << endl; // 张三(先生)

    // 场景2:操作自定义Message类型字段(唯一方式)
    // 假设PersonInfo有一个单值的Phone字段:Phone main_phone = 4;
    // 1. 获取Phone对象的可修改指针(自动初始化,无需手动创建)
    // connects::Phone* main_phone_ptr = person.mutable_main_phone();
    // 2. 通过指针设置Phone对象的内部字段
    // main_phone_ptr->set_number("13800138000");
    // main_phone_ptr->set_type("移动");
    // 3. 读取Phone对象的字段值
    // cout << "主电话:" << person.main_phone().number() << endl;

    // 场景3:多次修改同一个字段,减少开销
    std::string* desc_ptr = person.mutable_name(); // 假设有string desc=5;
    desc_ptr->clear();
    desc_ptr->append("姓名:");
    desc_ptr->append("张三");
    desc_ptr->append(",年龄:25");

    return 0;
}

5.1.5 单值字段 4 个核心函数的使用总结表

函数名核心功能主要使用场景核心特点
clear_字段名 ()清空字段,重置为初始状态取消字段设置、清理无用字段、重复使用对象无参、无返回值、释放内存
字段名 () const只读获取字段值(Getter)打印、赋值、判断字段值、所有读操作const、返回值 /const 引用、不可修改
set_字段名 ()设置字段值(Setter)初始化、修改字段值、所有写操作(原生类型)多参数重载、无返回值、覆盖原有值
mutable_字段名 ()获取可修改字段指针修改 string 部分值、操作自定义 Message 类型字段无参、返回可修改指针、自动初始化

5.2 模块 2:repeated 字段专属操作函数)

repeated 字段的操作函数是 ProtoBuf 的核心重点,也是实际开发中处理 “多值集合” 的关键。所有 repeated 字段都会生成一套专属的操作函数,共 6 个核心函数,分别实现 “获取元素个数”、“清空整个数组”、“按索引只读获取元素”、“按索引获取可修改元素指针”、“新增元素并返回指针”、“获取整个数组引用(遍历)” 的功能,覆盖 repeated 字段的所有操作场景(增、删、改、查、遍历、清空)。

这 6 个函数的命名规则、语法、使用逻辑完全一致,仅需将 “字段名” 替换为实际的 repeated 字段名即可,且遵循 “数组操作逻辑”(与 C++ 的 std::vector 操作逻辑一致),做到 “一次学习,所有 repeated 字段通用”。

前置核心记忆点:repeated 字段的本质是动态数组,因此其操作函数的命名和功能完全贴合 “数组操作”,比如:

  • 获取元素个数:字段名_size()(类比vector::size());
  • 清空数组:clear_字段名()(类比vector::clear());
  • 按索引获取元素:字段名(index)(类比vector::operator[]);
  • 新增元素:add_字段名()(类比vector::push_back());

所有函数均基于.proto中定义的复数命名 repeated 字段(phones/contacts)讲解,完全贴合实际开发的命名规范。

5.2.1 函数 1:字段名_size() const —— 获取数组元素个数
核心功能

只读获取当前 repeated 字段(动态数组)中的元素总个数,是遍历、判断数组是否为空的基础函数,类比 C++vector::size()。

语法(函数签名)
// 通用语法
int 字段名_size() const;

// 示例:基于通讯录proto的实际函数
int phones_size() const;    // PersonInfo类:获取电话号码个数
int contacts_size() const;  // Connects类:获取联系人个数
参数

无参数,函数末尾const修饰为纯读操作,不修改对象。

返回值

int:数组中当前的元素个数,无元素时返回 0,有元素时返回实际数量(如 3 个电话返回 3)。

实现本质

调用底层RepeatedPtrField的size()方法,直接返回动态数组的当前元素数量,无额外计算,效率 O (1)。

使用场景
  1. 遍历 repeated 数组:作为 for 循环的边界条件(for(int i=0; i<xxx_size(); i++));
  2. 判断数组是否为空:if(xxx_size() == 0),避免遍历空数组;
  3. 统计元素数量:如统计通讯录的联系人总数、单个联系人的电话总数。
C++ 使用示例
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    connects::PersonInfo person;
    // 初始状态:无元素,返回0
    cout << "初始电话数:" << person.phones_size() << endl; // 输出:0

    // 新增3个电话号码
    person.add_phones()->set_number("13800138000");
    person.add_phones()->set_number("13900139000");
    person.add_phones()->set_number("18800188000");
    cout << "新增后电话数:" << person.phones_size() << endl; // 输出:3

    // 判断数组是否为空
    if (person.phones_size() > 0) {
        cout << "该联系人有电话号码" << endl;
    } else {
        cout << "该联系人无电话号码" << endl;
    }

    return 0;
}
5.2.2 函数 2:clear_字段名() —— 清空整个 repeated 数组
核心功能

清空repeated 字段中的所有元素,销毁每个元素对象并释放内存,将数组恢复为空状态,元素个数置 0,类比 C++vector::clear()。

语法(函数签名)
// 通用语法
void clear_字段名();

// 示例:基于通讯录proto的实际函数
void clear_phones();    // PersonInfo类:清空所有电话号码
void clear_contacts();  // Connects类:清空所有联系人
参数

无参数。

返回值

void(无返回值),直接修改当前 Message 对象的 repeated 字段状态。

实现本质

调用底层RepeatedPtrField的Clear()方法,逐个销毁数组中的所有元素对象,释放其占用的内存,最终将数组大小置 0。

使用场景
  1. 批量删除所有元素:如清空联系人的所有电话、清空整个通讯录;
  2. 重复使用 Message 对象:清空旧的元素数据,避免影响新的业务逻辑;
  3. 处理无效数据:当 repeated 字段的所有元素均为无效数据时,直接清空。
C++ 使用示例
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    connects::PersonInfo person;
    // 新增2个电话号码
    person.add_phones()->set_number("13800138000");
    person.add_phones()->set_number("13900139000");
    cout << "清空前电话数:" << person.phones_size() << endl; // 输出:2

    // 清空所有电话号码
    person.clear_phones();
    cout << "清空后电话数:" << person.phones_size() << endl; // 输出:0
    cout << "清空后是否为空:" << (person.phones_size() == 0 ? "是" : "否") << endl; // 输出:是

    return 0;
}
5.2.3 函数 3:字段名(int index) const —— 按索引只读获取元素
核心功能

按索引只读获取repeated 数组中指定位置的元素,函数末尾const修饰为纯读操作,不可修改元素的字段值,类比 C++vector::at(index)(带越界检查)。

语法(函数签名)
// 通用语法
const 元素类型& 字段名(int index) const;

// 示例:基于通讯录proto的实际函数
const ::connects::Phone& phones(int index) const;    // PersonInfo类:获取指定索引的电话
const ::connects::PersonInfo& contacts(int index) const; // Connects类:获取指定索引的联系人
参数

int index:数组的索引值,从 0 开始计数,必须满足0 ≤ index < 字段名_size(),否则会触发越界错误(程序崩溃)。

返回值

const 元素类型 &:指定索引位置元素的常量引用,元素类型为 repeated 字段的原始类型(如Phone/PersonInfo)。

实现本质

调用底层RepeatedPtrField的索引访问方法,返回指定位置元素对象的常量引用,带越界检查,索引非法时直接终止程序。

使用场景

所有需要按索引读取元素的场景,如获取第一个电话号码、获取最后一个联系人、指定位置的元素数据查询。

核心注意事项
  1. 索引从 0 开始,与 C++/Java 等编程语言的数组索引规则一致;
  2. 必须做越界判断(index < 字段名_size()),否则会导致程序崩溃;
  3. 返回的是常量引用,仅能读取元素的字段值,不可直接修改,修改需使用mutable_字段名(int index)。
C++ 使用示例
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    connects::PersonInfo person;
    // 新增2个电话号码
    person.add_phones()->set_number("13800138000");
    person.add_phones()->set_number("13900139000");
    int phone_count = person.phones_size();

    // 按索引读取第一个电话(index=0)
    if (phone_count > 0) {
        const connects::Phone& first_phone = person.phones(0);
        cout << "第一个电话号码:" << first_phone.number() << endl; // 输出:13800138000
    }

    // 按索引读取最后一个电话(index=phone_count-1)
    if (phone_count > 0) {
        const connects::Phone& last_phone = person.phones(phone_count - 1);
        cout << "最后一个电话号码:" << last_phone.number() << endl; // 输出:13900139000
    }

    // 错误示例:索引越界(程序崩溃)
    // person.phones(10); // 10 >= 2,触发越界错误

    return 0;
}
5.2.4 函数 4:mutable_字段名(int index) —— 按索引获取可修改元素指针
核心功能

按索引获取repeated 数组中指定位置元素的可修改指针,通过该指针可以直接修改元素的所有字段值,是按索引修改 repeated 元素的唯一方式。

语法(函数签名)
// 通用语法
元素类型* mutable_字段名(int index);

// 示例:基于通讯录proto的实际函数
::connects::Phone* mutable_phones(int index);    // PersonInfo类:获取指定索引电话的可修改指针
::connects::PersonInfo* mutable_contacts(int index); // Connects类:获取指定索引联系人的可修改指针
参数

int index:数组的索引值,从 0 开始计数,必须满足0 ≤ index < 字段名_size(),否则触发越界错误。

返回值

元素类型*:指定索引位置元素的可修改指针,元素类型为 repeated 字段的原始类型(如Phone*/PersonInfo*)。

实现本质

调用底层RepeatedPtrField的索引访问方法,返回指定位置元素对象的可修改指针,带越界检查,索引非法时终止程序。

使用场景

所有需要按索引修改元素的场景,如修改联系人的第一个电话号码、修改通讯录中指定联系人的姓名 / 年龄、更新元素的某个字段值。

核心注意事项
  1. 索引需做越界判断,避免程序崩溃;
  2. 返回的是可修改指针,修改指针指向的字段值会直接反映到原 Message 对象中;
  3. 无需手动释放指针:指针由底层RepeatedPtrField管理,Message 对象析构时自动释放,无需调用delete。
C++ 使用示例
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    connects::PersonInfo person;
    // 新增1个电话号码(初始值)
    person.add_phones()->set_number("13800138000");
    cout << "修改前电话号码:" << person.phones(0).number() << endl; // 输出:13800138000

    // 按索引获取可修改指针,修改电话号码
    if (person.phones_size() > 0) {
        connects::Phone* phone_ptr = person.mutable_phones(0);
        phone_ptr->set_number("18800188000"); // 修改号码
        phone_ptr->set_type("移动"); // 新增并设置电话类型
    }

    cout << "修改后电话号码:" << person.phones(0).number() << endl; // 输出:18800188000
    cout << "修改后电话类型:" << person.phones(0).type() << endl;   // 输出:移动

    // 修改通讯录中指定联系人的信息
    connects::Connects book;
    book.add_contacts()->set_name("张三");
    book.add_contacts()->set_age(25);
    if (book.contacts_size() > 1) {
        connects::PersonInfo* p_ptr = book.mutable_contacts(1);
        p_ptr->set_name("李四");
        p_ptr->set_age(30);
    }

    return 0;
}
5.2.5 函数 5:add_字段名() —— 新增元素并返回可修改指针(最高频)
核心功能

在 repeated 数组的末尾新增一个空的元素对象,并返回该元素的可修改指针,通过指针可直接为新元素的所有字段赋值,是向 repeated 字段添加元素的最核心、最高频函数,类比 C++vector::push_back(),

oi,这个函数我们要格外注意了,我们要知道,这个函数返回的就是向repeated的字段数组新增加的一个空对象的指针,就是说proto直接就帮我们创建好了一个新的字段对象,然后将其指针返回,我们可以调用该对象的设置函数去给这个对象的字段进行赋值,也就是set_xxx

语法(函数签名)
// 通用语法
元素类型* add_字段名();

// 示例:基于通讯录proto的实际函数
::connects::Phone* add_phones();    // PersonInfo类:新增电话并返回指针
::connects::PersonInfo* add_contacts(); // Connects类:新增联系人并返回指针
参数

无参数。

返回值

元素类型*:新创建元素对象的可修改指针,元素类型为 repeated 字段的原始类型(如Phone*/PersonInfo*)。

实现本质

调用底层RepeatedPtrField的Add()方法,自动创建空的元素对象(分配内存),将对象添加到数组末尾,返回其可修改指针,全程无需开发者手动调用new。

使用场景

所有向 repeated 字段添加元素的场景,如为联系人新增电话号码、为通讯录新增联系人、向订单新增商品,是实际开发中使用频率最高的 repeated 字段函数。

核心优势
  1. 一步到位:新增元素 + 返回可修改指针,直接赋值,无需额外的索引操作;
  2. 自动内存管理:无需手动创建元素对象,底层自动分配内存,避免内存泄漏;
  3. 保留顺序:新元素始终添加到数组末尾,严格保留添加顺序。
C++ 使用示例(两种写法,推荐链式调用)
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    connects::PersonInfo person;
    // 写法1:链式调用(推荐,代码简洁,一行完成新增+赋值)
    person.add_phones()->set_number("13800138000");
    person.add_phones()->set_number("13900139000")->set_type("联通"); // 多字段链式赋值

    // 写法2:先获取指针,再赋值(适合多字段赋值,逻辑清晰)
    connects::Phone* phone_ptr = person.add_phones();
    phone_ptr->set_number("18800188000");
    phone_ptr->set_type("移动");

    // 打印所有电话,验证添加顺序
    cout << "共" << person.phones_size() << "个电话号码:" << endl;
    for (int i=0; i<person.phones_size(); i++) {
        cout << "电话" << i+1 << ":" << person.phones(i).number() 
             << "(" << person.phones(i).type() << ")" << endl;
    }

    return 0;
}

运行结果:

共3个电话号码:
电话1:13800138000()
电话2:13900139000(联通)
电话3:18800188000(移动)
5.2.6 函数 6:字段名() const —— 获取整个数组常量引用(遍历)
核心功能

获取整个 repeated 字段(动态数组)的常量引用,返回底层RepeatedPtrField容器的引用,支持范围 for 循环和迭代器遍历,是批量遍历 repeated 元素的最优方式。

语法(函数签名)
// 通用语法
const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<元素类型>& 字段名() const;

// 示例:基于通讯录proto的实际函数
const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::connects::Phone >& phones() const;
const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::connects::PersonInfo >& contacts() const;
参数

无参数,函数末尾const修饰为纯读操作。

返回值

const RepeatedPtrField <元素类型>&:底层动态数组容器的常量引用,RepeatedPtrField是 ProtoBuf 专属的动态对象指针数组,兼容 C++ 迭代器和范围 for 循环。

实现本质

直接返回 Message 对象中 repeated 字段的底层RepeatedPtrField容器引用,无额外计算,效率 O (1)。

使用场景

批量遍历 repeated 数组的所有元素,如打印联系人的所有电话号码、遍历通讯录的所有联系人并输出信息、批量判断元素的字段值。

核心优势
  1. 遍历简洁:支持 C++11 及以上的范围 for 循环,代码量少,可读性高;
  2. 兼容迭代器:支持普通迭代器和 const 迭代器,适配各种遍历场景;
  3. 效率高:直接操作底层容器,无元素拷贝,遍历效率与vector一致。
C++ 使用示例(推荐范围 for 循环)
#include "connects.pb.h"
#include <iostream>
using namespace std;

int main() {
    connects::PersonInfo person;
    // 新增3个电话号码
    person.add_phones()->set_number("13800138000")->set_type("移动");
    person.add_phones()->set_number("010-12345678")->set_type("固定电话");
    person.add_phones()->set_number("18800188000")->set_type("电信");

    // 方式1:范围for循环(推荐,代码最简洁)
    cout << "=== 范围for循环遍历电话 ===" << endl;
    int i = 1;
    for (const auto& phone : person.phones()) {
        cout << "电话" << i++ << ":" << phone.number() << "(" << phone.type() << ")" << endl;
    }

    // 方式2:迭代器遍历(兼容C++11之前的版本)
    cout << "\n=== 迭代器遍历电话 ===" << endl;
    i = 1;
    for (RepeatedPtrField<connects::Phone>::const_iterator it = person.phones().begin();
         it != person.phones().end(); ++it) {
        cout << "电话" << i++ << ":" << it->number() << "(" << it->type() << ")" << endl;
    }

    return 0;
}

运行结果:

=== 范围for循环遍历电话 ===
电话1:13800138000(移动)
电话2:010-12345678(固定电话)
电话3:18800188000(电信)

=== 迭代器遍历电话 ===
电话1:13800138000(移动)
电话2:010-12345678(固定电话)
电话3:18800188000(电信)

5.2.7 repeated 字段 6 个核心函数使用总结表

函数名核心功能类比 C++vector使用场景核心特点
字段名_size () const获取数组元素个数vector::size()遍历边界、统计数量、判断空const、返回 int、O (1) 效率
clear_字段名 ()清空所有元素并释放内存vector::clear()批量删除、重复使用对象无参、无返回、销毁所有元素
字段名 (int index) const按索引只读获取元素vector::at(index)按索引读取元素值const、返回常量引用、带越界检查
mutable_字段名 (int index)按索引获取可修改元素指针无直接类比按索引修改元素字段返回可修改指针、带越界检查
add_字段名 ()末尾新增元素并返回指针vector::push_back()新增元素(最高频)无参、返回指针、自动创对象
字段名 () const获取整个数组常量引用vector::cbegin()批量遍历所有元素const、支持范围 for / 迭代器

5.3 单值 + repeated 字段函数通用调用规则

  1. 所有函数均为 Message 类的成员函数:通过 Message 对象直接调用,如person.set_name("张三")、book.add_contacts();
  2. 字段名严格一致:函数名中的字段名与.proto 中定义的字段名完全一致(大小写、拼写),如.proto 中phones对应函数phones_size()/add_phones();
  3. const 函数不可修改对象:末尾带const的函数为纯读操作,不可通过该函数修改对象的任何字段;
  4. 指针无需手动管理:所有mutable_xxx()/add_xxx()返回的指针,由 ProtoBuf 底层自动管理,无需手动new/delete,避免内存泄漏;
  5. 越界检查必做:所有按索引操作的函数(xxx(index)/mutable_xxx(index)),必须判断index < xxx_size(),否则程序崩溃。

六、实战案例:基于 Proto3 实现简易通讯录系统

结合前文讲解的嵌套 Message、repeated 限定符、编译后 C++ 函数,实现一个可交互、支持数据持久化的简易通讯录系统,覆盖用户输入、数据封装、序列化(字符串 + 文件)、反序列化、数据打印全流程,

6.1 需求分析

  1. 支持新增联系人:输入姓名、年龄、多个电话号码(回车结束电话输入);
  2. 支持数据序列化:将通讯录数据序列化为二进制字符串和本地二进制文件(持久化);
  3. 支持数据反序列化:从二进制字符串和本地文件中恢复通讯录数据;
  4. 支持数据打印:遍历并打印反序列化后的所有联系人信息;
  5. 输入处理:解决 C++cin/getline混合输入的缓冲区残留问题。

6.2 项目结构

项目仅需 3 个文件,结构极简,适合快速上手:

contact_system/
├── connects.proto  # Proto3数据结构定义
├── main.cpp        # C++业务逻辑实现(核心)
└── CMakeLists.txt  # 编译脚本(或直接使用g++命令)

6.3 完整 Proto3 定义(connects.proto)

使用外部定义独立 Message的推荐方式,包含Phone(电话)、PersonInfo(联系人)、Connects(通讯录)三层结构,核心使用repeated实现多电话、多联系人的存储:

syntax = "proto3";
package connects;

// 电话信息:独立Message,可复用
message Phone {
    string number = 1; // 电话号码
    string type = 2;   // 电话类型(移动/联通/电信/固定电话,可选)
}

// 联系人信息:包含单值字段+repeated Phone字段
message PersonInfo {
    string name = 1;        // 姓名(单值)
    sint32 age = 2;         // 年龄(单值)
    repeated Phone phones = 3; // 多个电话号码(repeated)
}

// 通讯录信息:包含repeated PersonInfo字段
message Connects {
    repeated PersonInfo contacts = 1; // 多个联系人(repeated)
}

6.4 完整 C++ 业务代码(main.cpp)

逐行添加注释,解析核心函数的调用和业务逻辑,解决输入缓冲区、序列化 / 反序列化异常等问题:

#include "connects.pb.h"
#include <iostream>
#include <string>
#include <fstream>
#include <google/protobuf/repeated_field.h>
using namespace std;
using namespace connects;

// 函数声明:新增联系人(封装用户输入逻辑)
void AddPersonInfo(PersonInfo* person_info_ptr);
// 函数声明:打印通讯录所有联系人信息
void PrintContacts(const Connects& con);

int main(int argc, char* argv[]) {
    // 验证ProtoBuf库版本,避免版本兼容问题
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    // 命令行参数检查:需传入持久化文件路径(如./main contacts.bin)
    if (argc != 2) {
        cerr << "Usage: " << argv[0] << " [PERSIST_FILE_PATH]" << endl;
        return -1;
    }
    const string persist_file = argv[1];

    // 1. 初始化通讯录对象,新增联系人
    Connects con;
    // 新增空联系人,返回可修改指针(核心repeated函数:add_contacts())
    PersonInfo* person_ptr = con.add_contacts();
    // 传入指针,封装用户输入
    AddPersonInfo(person_ptr);

    // 2. 序列化1:序列化为二进制字符串(用于网络传输/内存交互)
    string bin_str;
    if (!con.SerializeToString(&bin_str)) {
        cerr << "Error: 序列化到二进制字符串失败!" << endl;
        return -1;
    }
    cout << "\n✅ 序列化到二进制字符串成功,字符串长度:" << bin_str.size() << " 字节" << endl;

    // 3. 反序列化1:从二进制字符串恢复数据
    Connects con_from_str;
    if (!con_from_str.ParseFromString(bin_str)) {
        cerr << "Error: 从二进制字符串反序列化失败!" << endl;
        return -1;
    }
    cout << "\n=== 从二进制字符串反序列化的通讯录数据 ===" << endl;
    PrintContacts(con_from_str);

    // 4. 序列化2:序列化为本地二进制文件(持久化存储,核心)
    fstream wf(persist_file, ios::binary | ios::out | ios::trunc);
    if (!wf.is_open()) {
        cerr << "Error: 打开文件" << persist_file << "失败!" << endl;
        return -1;
    }
    if (!con.SerializeToOstream(&wf)) {
        cerr << "Error: 序列化到文件" << persist_file << "失败!" << endl;
        wf.close();
        return -1;
    }
    wf.close();
    cout << "\n✅ 序列化到本地文件" << persist_file << "成功!" << endl;

    // 5. 反序列化2:从本地二进制文件恢复数据(持久化读取)
    fstream rf(persist_file, ios::binary | ios::in);
    if (!rf.is_open()) {
        cerr << "Error: 打开文件" << persist_file << "失败!" << endl;
        return -1;
    }
    Connects con_from_file;
    if (!con_from_file.ParseFromIstream(&rf)) {
        cerr << "Error: 从文件" << persist_file << "反序列化失败!" << endl;
        rf.close();
        return -1;
    }
    rf.close();
    cout << "\n=== 从本地文件" << persist_file << "反序列化的通讯录数据 ===" << endl;
    PrintContacts(con_from_file);

    // 销毁ProtoBuf全局对象,释放内存
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

/**
 * @brief 新增联系人:处理用户输入,封装到PersonInfo对象
 * @param person_info_ptr PersonInfo对象的可修改指针(由add_contacts()返回)
 */
void AddPersonInfo(PersonInfo* person_info_ptr) {
    cout << "============ 开始新增联系人 ============" << endl;

    // 输入姓名:使用getline接收带空格的姓名(如"张三 先生")
    cout << "请输入联系人姓名:";
    string name;
    getline(cin, name);
    person_info_ptr->set_name(name); // 单值字段函数:set_name()

    // 输入年龄:cin接收数值,需处理缓冲区残留的换行符
    cout << "请输入联系人年龄:";
    int age;
    while (!(cin >> age) || age < 0 || age > 150) {
        cin.clear(); // 清除输入错误标志
        cin.ignore(1024, '\n'); // 忽略缓冲区错误输入
        cout << "年龄输入无效,请输入0-150的整数:";
    }
    person_info_ptr->set_age(age); // 单值字段函数:set_age()
    cin.ignore(1024, '\n'); // 忽略cin后缓冲区残留的换行符,避免影响后续getline

    // 输入多个电话号码:死循环,回车结束输入
    cout << "请输入电话号码(直接回车结束输入):" << endl;
    int phone_idx = 1;
    while (true) {
        cout << "电话" << phone_idx << ":";
        string phone_num;
        getline(cin, phone_num);
        // 回车结束:phone_num为空
        if (phone_num.empty()) {
            break;
        }
        // 新增电话,链式赋值(核心repeated函数:add_phones())
        Phone* phone_ptr = person_info_ptr->add_phones();
        phone_ptr->set_number(phone_num);
        // 可选:输入电话类型
        cout << "请输入该电话类型(移动/联通/电信/固定电话,默认空):";
        string phone_type;
        getline(cin, phone_type);
        if (!phone_type.empty()) {
            phone_ptr->set_type(phone_type);
        }
        phone_idx++;
    }

    cout << "============ 联系人新增成功 ============" << endl;
}

/**
 * @brief 打印通讯录所有联系人信息
 * @param con 通讯录Connects对象的常量引用(纯读,不修改)
 */
void PrintContacts(const Connects& con) {
    // 获取联系人个数(repeated函数:contacts_size())
    int person_count = con.contacts_size();
    if (person_count == 0) {
        cout << "通讯录为空!" << endl;
        return;
    }

    // 遍历所有联系人:范围for循环(repeated函数:contacts())
    for (int i = 0; i < person_count; i++) {
        const PersonInfo& person = con.contacts(i); // 按索引只读获取联系人
        cout << "-------- 联系人" << i+1 << " --------" << endl;
        cout << "姓名:" << person.name() << endl; // 单值字段函数:name()
        cout << "年龄:" << person.age() << endl;  // 单值字段函数:age()

        // 遍历该联系人的所有电话
        int phone_count = person.phones_size();
        if (phone_count == 0) {
            cout << "电话号码:无" << endl;
            continue;
        }
        cout << "电话号码:" << endl;
        for (int j = 0; j < phone_count; j++) {
            const Phone& phone = person.phones(j); // 按索引只读获取电话
            cout << "  " << j+1 << ". " << phone.number();
            if (!phone.type().empty()) {
                cout << "(" << phone.type() << ")";
            }
            cout << endl;
        }
    }
}

6.5 编译与运行

6.5.1 步骤 1:编译.proto 文件生成 C++ 代码

在项目根目录执行 protoc 编译命令,生成connects.pb.h和connects.pb.cc:

# 核心编译命令:当前目录编译,生成C++代码到当前目录
protoc -I=. --cpp_out=. connects.proto

执行成功后,项目根目录会新增两个文件:connects.pb.h(头文件)、connects.pb.cc(源文件)。

6.5.2 步骤 2:编译 C++ 业务代码

直接使用 g++ 命令(推荐,极简)
# 编译main.cpp + connects.pb.cc,链接ProtoBuf库,生成可执行文件main
g++ -std=c++11 main.cpp connects.pb.cc -o main `pkg-config --cflags --libs protobuf`

参数说明:

  • -std=c++11:支持 C++11 的范围 for 循环等特性;
  • pkg-config --cflags --libs protobuf:自动获取 ProtoBuf 的头文件路径和链接库,避免手动指定。

6.5.3 步骤 3:运行可执行文件

# 直接运行:传入持久化文件路径(如contacts.bin)
./main contacts.bin

6.5.4 典型运行结果

============ 开始新增联系人 ============
请输入联系人姓名:张三
请输入联系人年龄:25
请输入电话号码(直接回车结束输入):
电话1:13800138000
请输入该电话类型(移动/联通/电信/固定电话,默认空):移动
电话2:010-12345678
请输入该电话类型(移动/联通/电信/固定电话,默认空):固定电话
电话3:
============ 联系人新增成功 ============

✅ 序列化到二进制字符串成功,字符串长度:45 字节

=== 从二进制字符串反序列化的通讯录数据 ===
-------- 联系人1 --------
姓名:张三
年龄:25
电话号码:
  1. 13800138000(移动)
  2. 010-12345678(固定电话)

✅ 序列化到本地文件contacts.bin成功!

=== 从本地文件contacts.bin反序列化的通讯录数据 ===
-------- 联系人1 --------
姓名:张三
年龄:25
电话号码:
  1. 13800138000(移动)
  2. 010-12345678(固定电话)

运行成功后,项目根目录会生成二进制持久化文件contacts.bin,即使删除可执行文件,该文件仍保留通讯录数据,重新运行程序时可直接从该文件反序列化恢复数据。

完整示例代码

connects.proto:

syntax="proto3";
package connects;

//ok,那么在本文件中,我们来进一步学习proto的语法,那么其实也就是对上次所学习的扩展啦
//那么我们首先需要知道的就是,我们是可以是在一个message结构体中定义新的message结构体的
//其实这也就是嵌套定义,就像我们C++所使用的嵌套类,嵌套结构体等等
//但是需要注意,里面的message不需要我们加上唯一标识数值哦
//但是里面的message的里面的字段,依旧是需要加上唯一标识数值,但是是可以和外面的字段字段唯一标识数值一样
//因为 ProtoBuf 的字段编号(唯一标识)只需要在「同一个 Message 内部唯一」,不同 Message 之间的字段编号可以重复。
//这一点我们需要知道,因为我们以后也有可能会用到这个
//eg:
/*
 message Example
 {
    sint32 age=1;
    message Example1
    {
        string name=1;//注意这里是可以和外面的字段唯一标识数值一样的哦,因为是不同的message结构体,所以是可以相同的
        //因为 ProtoBuf 的字段编号(唯一标识)只需要在「同一个 Message 内部唯一」,不同 Message 之间的字段编号可以重复。
    }
 }
 */
//当然了,也支持不在message结构体内定义message结构体就可以使用别的message结构体
//其实也就在外面创建message结构体,然后在要使用的那个message结构体中,声明要被使用的message结构体即可
//依旧是遵循 字段类型 字段名 = 唯一标识数值
//只不过定义message结构体的话,字段类型就是换为要被使用的message结构体的名字
//eg:
/*
message Example1
{
    string name=1;//注意这里是可以和别的message的字段唯一标识数值一样的哦,因为是不同的message结构体,所以是可以相同的
    //因为 ProtoBuf 的字段编号(唯一标识)只需要在「同一个 Message 内部唯一」,不同 Message 之间的字段编号可以重复。
}
message Example
{
    sint32 age=1;
    Example1 ex=2;
}
*/
//这两种嵌套使用方式,我一般比较推荐第二种啦,更加符合我们平时定义字段的习惯
//像第一种方式的话,就没有在嵌套message结构体加唯一标识数值,总感觉是怪怪的哈哈哈


// 下面是ProtoBuf嵌套Message的详细解析
// 嵌套Message:简单说就是一个Message里用到另一个Message,类比C++的嵌套类/嵌套结构体
// 作用:把关联的复杂数据结构整合在一起,让proto文件的结构更清晰,
// 适配「一个对象包含另一个对象」的业务场景(比如联系人包含多个电话对象)
// 【最核心前置规则】:所有字段的唯一标识数值(=后面的数字),仅需要在「它自己所属的同一个Message大括号内部」唯一即可!
// 【通俗解释】:不同的Message之间,字段的数字标识可以重复,完全不冲突;只有在同一个{}里的字段,数字不能重复

/************************ 方式1:Message内部直接嵌套定义子Message ************************/
// 通俗特点:在父Message的{}里面,直接定义需要用到的子Message,子Message是父Message的“内部成员”
// 核心规则1:内部嵌套的子Message本身(只是结构体定义),不需要写唯一标识数值(不用加 = 数字),因为它不是一个“字段”,只是一个数据结构定义
// 核心规则2:子Message内部的普通字段(string/int32等),必须写唯一标识数值,且这个数字仅需在「该子Message自己的{}内部」唯一
// 核心规则3:父Message的普通字段,唯一标识数值仅需在「父Message自己的{}内部」唯一,和子Message的字段数字可重复,无任何冲突
/*
// 父Message:Example(相当于外层大结构体)
message Example {
    // 父Message的普通字段:年龄
    // 唯一标识=1:仅在Example这个{}内部唯一即可,这是父Message的第一个字段
    sint32 age = 1;

    // 内部嵌套定义子Message Example1:是Example的内部结构体,仅能在Example内部使用
    // 注意:这个子Message Example1本身,不用写 = 数字,因为它不是字段,只是结构体定义
    message Example1 {
        // 子Message的普通字段:姓名
        // 唯一标识=2:仅在Example1这个{}内部唯一即可,和父Message的age=1重复也没关系,完全不冲突
        string name = 2; 
        // 重点提醒:如果在Example1里再加一个字段,就不能写=2了(同一个{}里数字不能重复),比如不能再写int32 id=2
    }
}
*/

/************************ 方式2:外部定义Message,内部声明使用 【推荐使用】************************/
// 通俗特点:先在所有Message外面,单独定义一个独立的Message(相当于全局结构体),然后在需要用的父Message{}里,直接把它当作普通字段来用
// 核心规则1:使用时完遵循ProtoBuf基础写法——「字段类型 + 字段名 = 唯一标识数值」,和定义string/int32字段的格式一模一样
// 核心规则2:这里的“字全段类型”,直接换成「外部定义好的独立Message名称」就行,其他要求完全不变
// 核心规则3:唯一标识数值的约束,还是遵循最核心规则——仅在「使用它的父Message{}内部」唯一即可
// 推荐原因:① 符合平时写代码的习惯,先定义再使用,读起来更顺;② 这个独立的Message,其他任意Message都能直接用,不用重复定义;③ 后续修改这个Message,所有用到它的地方都能生效,维护更方便
// 简单说:这种方式“好读、好复用、好维护”,实际开发中优先用这种!

// 步骤1:全局外部定义独立的Message(相当于全局结构体),和定义普通Message的写法完全一样
/*
// 独立Message:Example1,所有Message都能直接使用它
message Example1 {
    // 普通字段:姓名
    // 唯一标识=1:仅在Example1这个{}内部唯一即可,这是Example1的第一个字段
    string name = 1;
    // 假如再加一个字段,比如手机号,就可以写 string phone = 2; (同一个{}里数字依次排就行)
}
*/

// 步骤2:在父Message的{}里,声明使用上面定义好的独立Message
/*
// 父Message:Example2
message Example2 {
    // 父Message的普通字段:年龄
    // 唯一标识=1:仅在Example2这个{}内部唯一即可,和外部Example1里的name=1重复,完全不冲突!
    sint32 age = 1;

    // 声明使用外部独立的Example1:把Example1当作“字段类型”,和写string/int32的格式完全一样
    // 字段类型:Example1(外部定义的Message名称)
    // 字段名称:ex(自定义,随便起,符合命名规范就行)
    // 唯一标识=2:仅在Example2这个{}内部唯一即可(因为age已经用了1,所以这里用2)
    Example1 ex = 2;

    // 假如再加一个字段,比如地址,就可以写 string address = 3; (按顺序排,同一个{}里不重复就行)
}
*/
// 【PersonInfo关联Phone的两种核心写法解析:独立写法 & 内部嵌套写法】
// 独立写法:Phone单独全局定义,是独立的Message,仅被PersonInfo引用,二者无嵌套关系
//  编译后C++类名:connects::Phone(无PersonInfo_Phone类,类名与Message名一致)
//  C++访问方式:直接用connects::Phone
//  C++ add_phone()返回值:connects::Phone*
// 内部嵌套写法:Phone定义在PersonInfo内部,是PersonInfo的专属子Message,二者为嵌套关系
//  编译后C++类名:connects::PersonInfo_Phone(按主消息名_子消息名规则生成)
//  C++访问方式:PersonInfo内自动生成别名,可简写为connects::PersonInfo::Phone(等价完整类名)
//  C++ add_phone()返回值:connects::PersonInfo::Phone*(或connects::PersonInfo_Phone*)


//那么接下来我们再思考一个问题,我们定义的字段,有时候是肯定要支持多个数据的,比如一个人的电话号码就不可能只有一个
//那么这就需要我们知道了,在protobuffer中,默认是给所有字段都支持一个数据的,不支持同一个字段有多个值
//但是,肯定也能由我们手动指定可重复,而这就需要我们去在字段类型前面加上repeated限定符
//加上这个之后,同一个字段,我们就可以令其有多个值(其实本质是转换为数组了,这个我下面会详细说)
//那么默认是单个值的,就是在字段类型前面加上singular限定符了
//•singular:消息中可以包含该字段零次或一次(不超过一次)。 proto3 语法中,字段默认使用该规则。
//•repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组。

// ---------------------- ProtoBuf 字段单值/多值限定符:singular / repeated ----------------------
// 业务场景:实际开发中部分字段需要存储多个值(比如一个人会有多个电话号码、多个地址)
// 核心前提:ProtoBuf中所有字段,默认只支持「单个值」,无法直接存储多个同字段数据
// 解决办法:通过添加「限定符」手动指定字段是「单值」还是「可重复多值」,限定符写在「字段类型」前面
// 两个核心限定符:singular(单值,默认)、repeated(可重复多值,手动指定),以下详细讲解

// 1. 单值限定符:singular(proto3 语法默认规则,无需手动写这个关键字!)
// 作用:指定字段为「单值字段」,该字段在消息中可写0次(不写)或1次,最多只能有1个值,绝对不能写多个
// 通俗理解:就是常规的单个字段,比如一个人的姓名、年龄,一般只有一个,用默认规则即可
// 示例:string name = 1;  等价于  singular string name = 1; (手动写singular也可以,但是没必要)

// 2. 可重复多值限定符:repeated(需手动写在字段类型前,核心重点!)
// 作用:指定字段为「可重复字段」,该字段在消息中可写0次、1次或任意多次,支持存储多个值
// 关键特性:重复写入的值会「保留顺序」,不会乱序
// 本质理解:加上repeated后,该字段会被转换成「数组/列表」类型,专门用来存多个同类型数据
// 核心用法:直接把 repeated 写在「字段类型」前面,其余语法(类型+字段名=唯一标识)完全不变
// 适用场景:需要存储多个同类型数据的字段,比如电话号码、邮箱、地址等

// ---------------------- 经典示例:联系人信息(姓名/年龄单值 + 多个电话号码多值)----------------------
// 结合之前的嵌套Message,实现「一个联系人对应多个电话号码」的实际业务需求
// 重点:repeated 修饰嵌套Message类型字段,用法和修饰原生类型(string/int32)完全一致

// 嵌套定义:电话号码子消息(单个电话的信息)
/*
message Phone 
{
    string number = 1; // 单个电话号码(单值,默认singular)
}
*/

// 联系人主消息
/*
message PeopleInfo 
{
    // 单值字段:姓名、年龄(默认singular,无需手动加关键字,最多各存1个值)
    string name = 1; // 姓名:单值,只能写1个
    int32 age = 2;   // 年龄:单值,只能写1个

    // 可重复多值字段:电话号码列表(手动加repeated,支持存多个Phone类型数据)
    // 写法:repeated + 字段类型(Phone) + 字段名(phones) + = + 唯一标识
    // 理解:phones 本质是一个Phone类型的数组/列表,可添加多个电话号码,且保留添加顺序
    repeated Phone phones = 3; // 电话号码:多值,可写0个、1个或多个(完美适配实际业务)
}
*/

// ---------------------- 核心要点提炼----------------------
// 1. proto3 默认单值:所有字段不写限定符,就是singular单值,最多1个值,不能多写;
// 2. 多值手动加repeated:需要存多个值时,直接在字段类型前加repeated,其余语法不变;
// 3. repeated 本质是数组:保留值的顺序,支持0/1/任意多次赋值,原生类型/自定义Message类型都能修饰;

message Phone
{
    string number = 1;
    //ProtoBuf 的字段编号(唯一标识)只需要在「同一个 Message 内部唯一」,不同 Message 之间的字段编号可以重复。
}

message PersonInfo
{
    string name = 1;
    sint32 age = 2;
    repeated Phone phone = 3;
}

message Connects
{
    repeated PersonInfo contacts = 1;//联系人信息,通讯录中记录的肯定是联系人的信息啊哇
}

//ok,那么接下来,我就来解析一下,repeated的定义为数组,究竟是什么意思???
//那么其实也是很好理解的,大家可以类比为我们在结构体中定义一个 数组成员变量 !!!
//那么在proto中的repeated字段中,就是相当于将该字段定义为一个数组,一个存储着该类型字段的数组
//比如
/*
message Connects
{
    repeated PersonInfo = 1;//联系人信息,通讯录中记录的肯定是联系人的信息啊哇
}
*/
//其实就是相当于:
/*
struct Connects
{
    PersonInfo arr[];
}
*/
//大家可以这么理解,那么知道了这一点之后,我们后续理解repeated相关的函数就会非常好理解了

// 给字段添加repeated限定符后,说它被「定义为数组」,具体到底是什么意思?
// 这个知识点是后续学习所有repeated相关操作函数的核心基础,只有彻底理解其本质,后续学习增、删、改、查等函数时
// 才能做到一看就懂、一通百通,所以一定要认真理解这个核心类比逻辑,把底层本质吃透!
// 首先给出最核心、最直白的理解结论:proto中被repeated修饰的字段,其本质完全等同于我们在C++编程语言中
// 定义结构体时,在结构体内部声明的「数组成员变量」,二者的底层设计逻辑、实际使用目的、存储数据的形式完全一致
// 我们可以直接用C++结构体数组成员的思路,来理解proto中repeated字段的所有逻辑,零门槛无难度!
// 再进一步通俗解释:当我们在proto的message中,给某个字段加上repeated限定符后,就相当于明确告诉ProtoBuf编译器
// 这个字段不再是存储单个数据的普通字段,而是要被处理成一个「专用数组」,这个数组的唯一作用,就是专门存储
// 该字段类型对应的所有数据,数组中的每一个元素,都是该类型的一个独立、完整的数据单元,和C++数组中存储
// 同类型结构体对象的逻辑完全一样!
// 这里要先明确一个重要前提:和C++中定义数组成员必须先确定数组元素的类型一样,在proto中使用repeated定义数组类型字段
// 也必须先提前定义好「数组要存储的元素类型」,这个元素类型可以是ProtoBuf的原生基础类型(如string/int32/sint32等)
// 也可以是我们自己自定义的任意message类型(比如联系人信息、电话号码信息等),只有先定义好元素类型,后续才能
// 用repeated基于该类型定义对应的数组字段,这是语法规范,也是实际使用的必要前提,缺一不可!
// 接下来我们结合实际的业务场景示例,用proto代码+对应的C++结构体代码做直接对比,让大家能最直观、最清晰地
// 理解二者的对应关系,示例选用最贴近实际开发的「通讯录」场景——通讯录的核心需求就是存储多个联系人信息,
// 正好需要用到数组类型,完美适配repeated的使用场景,能更好地贴合实际开发,理解起来也更有代入感!

// 第一步:提前定义好repeated数组要存储的元素类型——联系人信息message
// 这个PersonInfo就等同于C++中我们为数组提前定义的结构体类型,是数组中每个元素的具体数据结构
// 里面定义了联系人的核心基础信息,每个字段都是普通的单值字段(默认singular),符合常规数据定义逻辑
// message PersonInfo {
//     string name = 1;    // 联系人姓名:单值字段,每个联系人仅有一个姓名
//     int32 age = 2;      // 联系人年龄:单值字段,每个联系人仅有一个年龄
//     string phone = 3;   // 联系人电话号码:单值字段,单个联系人的一个电话号码(如需多个可结合嵌套+repeated)
//     string address = 4; // 联系人居住地址:单值字段,每个联系人的核心居住地址
// }

// 第二步:在proto的message中,使用repeated定义数组类型字段,实现存储多个联系人信息的需求
// 这个Connects就等同于C++中的外层结构体,专门用来封装通讯录相关的所有数据,其中核心的contacts字段
// 就是用repeated定义的数组字段,专门存储多个PersonInfo类型的联系人信息,完全匹配通讯录的业务需求
// 重点强调:repeated字段的完整语法必须是「repeated + 数组元素类型 + 自定义字段名 + = + 唯一标识数值」
// 五个部分缺一不可,尤其是不能省略自定义字段名,这是proto的语法硬性要求,省略会直接导致编译报错!
// message Connects {
//     // repeated + PersonInfo(元素类型) + contacts(自定义字段名) + = + 1(唯一标识)
//     // 本质理解:这个contacts字段,就是一个专门用于存储PersonInfo类型数据的数组,数组的长度不固定
//     // 支持动态添加、删除元素,和C++中动态数组/结构体数组的逻辑完全一致,能完美适配“通讯录中存储任意多个联系人”的需求
//     // 字段名contacts是自定义的,可根据实际业务需求修改(如person_list、contact_list等),只要符合命名规范即可
//     // 唯一标识数值1,依旧遵循之前的核心规则:仅在当前Connects这个message的{}内部唯一即可,和其他message的字段标识无冲突
//     repeated PersonInfo contacts = 1; // 通讯录核心字段:存储多个联系人信息,对应C++结构体中的数组成员变量
// }

// 对应C++结构体的直观类比代码(完全等价,一看就懂)
// 这部分代码把proto中的定义直接转换成了C++中大家最熟悉的结构体写法,二者的每一个部分都是一一对应的
// 能让大家从最熟悉的C++语法角度,彻底理解proto中repeated字段的底层本质,消除理解障碍!
/*
// 第一步:先定义数组的元素类型——联系人结构体,完全对应proto中的PersonInfo message
// 结构体中的每个成员变量,都和proto中PersonInfo的字段一一对应,存储单个联系人的完整信息
// struct PersonInfo {
//     string name;    // 对应proto PersonInfo.name,联系人姓名
//     int32_t age;    // 对应proto PersonInfo.age,联系人年龄
//     string phone;   // 对应proto PersonInfo.phone,联系人电话号码
//     string address; // 对应proto PersonInfo.address,联系人居住地址
// };

// 第二步:定义外层通讯录结构体,完全对应proto中的Connects message
// 结构体中定义数组成员变量,专门存储多个PersonInfo类型的联系人信息,对应proto中repeated修饰的contacts字段
// struct Connects {
//     // arr数组:专门存储PersonInfo类型数据,完全对应proto中Connects.contacts字段
//     // 这里用动态数组形式更贴合ProtoBuf的实际实现,和proto中repeated支持动态增删的特性完全一致
//     // 其本质就是一个存储自定义结构体类型的数组,和proto中repeated字段的本质毫无区别!
//     PersonInfo arr[];
// };
*/

// 现在我们做一次「proto与C++的逐行核心对应拆解」,把二者的对应关系讲透、讲细,彻底夯实理解基础:
// 1. proto中的message PersonInfo  <====>  C++中的struct PersonInfo:二者都是「数组元素的基础类型定义」,
//    用于规定数组中每个元素的具体数据结构,是定义数组的前提和基础;
// 2. proto中的message Connects  <====>  C++中的struct Connects:二者都是「封装数组的外层容器」,
//    用于把数组字段和其他可能的相关字段封装在一起,形成一个完整的业务数据结构;
// 3. proto中的repeated PersonInfo contacts = 1  <====>  C++中的PersonInfo arr[]:二者都是「结构体/消息中的数组成员」,
//    核心作用就是存储多个同类型的独立数据单元,是实现“多值存储”的核心载体,也是repeated字段的本质所在;
// 4. proto中repeated后的PersonInfo  <====>  C++中数组前的PersonInfo:二者都是「数组的元素类型」,
//    规定了数组中只能存储该类型的数据,保证数组内数据类型的一致性;

// 补充几个关于repeated数组本质的关键注意点,进一步完善理解,避免后续踩坑:
// 1. repeated定义的数组是「动态数组」:和C++中的静态数组不同,ProtoBuf中的repeated数组支持动态增、删、改元素,
//    无需提前指定固定长度,能根据实际业务需求灵活存储任意多个数据(0个、1个、数十个、数百个均可),
//    这也是ProtoBuf适配实际开发中“数据量不固定”需求的重要特性;
// 2. 数组内元素是「独立的完整数据」:repeated数组中的每一个元素,都是对应元素类型的一个独立、完整的对象,
//    比如contacts数组中的每个元素,都是一个完整的PersonInfo对象,包含name、age、phone、address等所有字段,
//    彼此之间相互独立,修改其中一个元素的字段值,不会影响其他元素;
// 3. 语法规则不冲突:给字段加repeated修饰后,仅改变字段的存储形式(从单值变为数组),其他所有proto基础语法规则
//    均保持不变,尤其是字段的唯一标识数值规则,依旧遵循「所属message内部唯一」的核心规则,不受repeated修饰的影响;

// 最后再强调一次:掌握「repeated字段 = C++结构体数组成员」这个核心类比关系的重要性!
// 因为ProtoBuf编译器根据.proto文件生成的C++/Java/Python等各语言的代码中,对repeated字段的所有操作函数
// 都是基于「数组操作逻辑」设计的,比如C++中会生成add_contacts()(向数组中添加一个新的PersonInfo元素)、
// contacts_size()(获取数组的当前元素个数,即通讯录中联系人的数量)、mutable_contacts(int index)(获取数组中
// 指定索引位置的元素,用于修改该位置的联系人信息)、clear_contacts()(清空数组中所有元素,即清空通讯录)等函数,
// 这些函数的命名、功能、使用逻辑,完全和我们平时在C++中操作结构体数组的思路一致,只要现在吃透了repeated的数组本质,

//下面我们来学习一下关于我们上面所学的一些函数使用方法
//还是那句话,要牢记就是数组,就是类!!!

// ---------------------- ProtoBuf编译后C++代码(contacts.pb.h) 实用常用函数全解析 ----------------------
// 核心前置知识:
// 1. protoc根据proto自动生成对应C++类,嵌套Message生成「外部类_内部类」命名(PeopleInfo.Phone→PeopleInfo_Phone);
// 2. 所有类继承ProtoBuf的Message基类,自带通用拷贝/合并能力,final修饰不可继承,保证代码稳定性;
// 3. repeated数组字段,C++底层通过RepeatedPtrField实现——ProtoBuf专属动态对象指针数组,自动管理内存,无需手动new/delete;
// 4. 仅解析实际开发中80%场景会用到的高频函数,剔除release_/set_allocated_等极少使用的方法;

// ================================= 模块1:嵌套Message生成类 - PeopleInfo_Phone 【单值字段常用函数】=================================
// 生成逻辑:proto中PeopleInfo内嵌套定义Phone消息,protoc自动生成该类,对应单个电话号码的操作,所有函数为string number=1字段的配套操作
//这个我们就要格外注意了,我们上面说到,可以嵌套定义message结构体,也就是一个结构体中可以有其他的结构体
//那么当我们想去访问被嵌套的结构体时,我们要怎么访问呢?如何去调用它的函数呢???
//其实此时该被嵌套的结构体的类名字就是 外层结构体名字_内层结构体名字 这个形式,从外到内,使用_隔开
//那么这是我们使用一个结构体内嵌套定义另一个结构体时,我们要这样子的
//而要是是在外面定义,然后结构体内部自己使用外面定义的结构体的话,就不需要这样子
//那就是和直接访问一样,不需要所谓的 外层结构体名字_内层结构体名字 

class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
    // 通用拷贝方法【常用】:实现同类型对象深拷贝,覆盖当前对象所有字段值
    // 语法:void CopyFrom(const 类名& from);
    // 参数:const PeopleInfo_Phone& from - 源对象(const+引用,避免修改源对象、减少拷贝开销)
    // 返回值:void - 无返回值,直接深拷贝源对象所有字段到当前对象
    // 实现本质:逐字段拷贝源对象内容,包括空字段状态,完全覆盖当前对象原有值
    // 用法:PeopleInfo_Phone p1, p2; p2.set_number("13800138000"); p1.CopyFrom(p2); // p1的number与p2完全一致
    using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
    void CopyFrom(const PeopleInfo_Phone& from);

    // ---------------------- 单值字段string number=1 专属核心常用函数 ----------------------
    // 1. 字段清空【高频】:将number字段重置为空状态,释放内存
    // 语法:void clear_字段名();
    // 参数:无
    // 返回值:void - 无返回值,重置字段为“未设置”,释放字符串占用内存
    // 实现本质:销毁当前字段的字符串对象,标记字段为空,后续调用number()返回空字符串
    // 用法:PeopleInfo_Phone p; p.set_number("13800138000"); p.clear_number(); // p.number() 返回""
    void clear_number();

    // 2. 常量获取字段值【高频】:只读获取number值,不可修改
    // 语法:const 字段类型& 字段名() const;
    // 参数:无(末尾const,保证调用后对象字段不被修改,纯读操作)
    // 返回值:const std::string& - 字符串常量引用,避免值拷贝,提升效率
    // 实现本质:返回对象内number字段的原生字符串引用,未设置则返回空字符串常量
    // 用法:PeopleInfo_Phone p; p.set_number("13800138000"); std::string num = p.number(); // num = "13800138000"
    const std::string& number() const;

    // 3. 设置字段值【高频】:为number赋值,支持多种传参形式(字符串、引用、右值等)
    // 语法:void set_字段名(字段类型值/引用/右值);
    // 参数:const std::string& / std::string&& / const char* - 待设置的电话号码字符串
    // 返回值:void - 无返回值,直接为字段赋值,覆盖原有值
    // 实现本质:将传入值赋值给对象内的number字段,自动管理字符串内存,无需手动分配
    // 用法:PeopleInfo_Phone p; p.set_number("13800138000"); // 直接传字符串常量;p.set_number(std::string("13900139000")); // 传字符串对象
    template <typename ArgT0 = const std::string&, typename... ArgT>
    void set_number(ArgT0&& arg0, ArgT... args);
    //这是什么?这不就是万能模版吗!!!

    // 4. 可变获取字段指针【高频】:返回number字段的可修改指针,直接修改字段值
    // 语法:字段类型* mutable_字段名();
    // 参数:无
    // 返回值:std::string* - 字符串可修改指针,若字段为空则自动初始化空字符串
    // 实现本质:检查字段是否初始化,未初始化则创建空字符串,返回其可修改指针
    // 用法:PeopleInfo_Phone p; p.mutable_number()->assign("13800138000"); // 直接通过指针修改值
    // 这个函数就是返回该字段的指针啦,就相当于我们平时定义类时,提供返回成员变量指针的API,用于外界通过传址引用修改值
};

// ================================= 模块2:主消息类 - PeopleInfo 【单值+repeated字段常用函数】=================================
// 生成逻辑:对应proto中的PeopleInfo消息,包含单值字段(name/age)和repeated Phone字段,重点解析repeated phone=3的高频操作
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
    using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
    void CopyFrom(const PeopleInfo& from); // 通用拷贝,用法同PeopleInfo_Phone,不再赘述

    // 类型别名【实用】:将嵌套类PeopleInfo_Phone简写为Phone,简化代码书写,开发中优先用该别名
    // 用法:PeopleInfo::Phone phone; // 等价于 PeopleInfo_Phone phone; 代码更简洁
    typedef PeopleInfo_Phone Phone;

    // ---------------------- repeated .PeopleInfo.Phone phone=3 专属核心常用函数【开发中最高频】 ----------------------
    // 底层实现:该字段在C++中是RepeatedPtrField<PeopleInfo_Phone>类型——ProtoBuf动态对象指针数组,自动管理内存,支持动态增删
    // 核心规律:repeated字段的操作函数均以「字段名」为核心,遵循「_size/clear_/add_/字段名(索引)/mutable_字段名(索引)」命名规则
    // 这个是很重要的,但是也很好理解,我们依旧是要牢牢掌握一点,本质就是数组!!!

    // 1. 获取数组元素个数【高频】:返回当前电话号码的数量
    // 其实就是相当于调用vector xxx.size()获取数组长度罢了
    // 语法:int 字段名_size() const;
    // 参数:无(const,纯读操作,不修改对象)
    // 返回值:int - 数组中元素的个数,无电话则返回0
    // 实现本质:调用RepeatedPtrField的size()方法,返回底层动态数组的元素数量
    // 用法:PeopleInfo p; int phoneCount = p.phone_size(); // 初始为0,添加后返回实际数量
    int phone_size() const;
    // 那么这个函数其实就是相当于返回我们所定义的repeated字段,有几个值!!!

    // 2. 清空整个数组【高频】:删除所有电话号码,重置为空数组
    // 其实就是相当于调用vector xxx.clear()清空数组罢了
    // 语法:void clear_字段名();
    // 参数:无
    // 返回值:void - 无返回值,删除底层数组所有对象,释放内存,元素个数置0
    // 实现本质:调用RepeatedPtrField的Clear()方法,逐个销毁数组内的Phone对象,清空数组
    // 用法:PeopleInfo p; p.add_phone()->set_number("13800138000"); p.clear_phone(); // p.phone_size() 变为0
    void clear_phone();

    // 3. 按索引获取不可修改元素【高频】:只读获取指定位置的电话号码,不可修改
    // 其实就是相当于调用vector xxx[]获取数组指定下标的元素罢了,依旧是从0开始计数哦
    // 语法:const 元素类型& 字段名(int index) const;
    // 参数:int index - 数组索引,从0开始,必须小于phone_size(),否则越界报错
    // 返回值:const PeopleInfo_Phone& - Phone对象常量引用,仅可读,不可修改字段值
    // 实现本质:通过RepeatedPtrField按索引访问,返回对应位置对象的常量引用
    // 用法:PeopleInfo p; p.add_phone()->set_number("13800138000"); std::string num = p.phone(0).number(); // 获取第0个电话
    const ::PeopleInfo_Phone& phone(int index) const;

    // 4. 按索引获取可修改元素指针【高频】:返回指定位置Phone对象的可修改指针,直接修改该电话
    // 这个函数就是返回该字段的指针啦,就相当于我们平时定义类时,提供返回成员变量指针的API,用于外界通过传址引用修改值
    // 语法:元素类型* mutable_字段名(int index);
    // 参数:int index - 数组索引,从0开始,必须小于phone_size(),否则越界报错
    // 返回值:PeopleInfo_Phone* - Phone对象可修改指针,可直接调用set_number()等方法
    // 实现本质:通过RepeatedPtrField按索引访问,返回对应位置对象的可修改指针
    // 用法:PeopleInfo p; p.add_phone()->set_number("13800138000"); p.mutable_phone(0)->set_number("13900139000"); // 修改第0个电话
    ::PeopleInfo_Phone* mutable_phone(int index);

    // 5. 新增数组元素【最高频】:向数组末尾添加一个空的Phone对象,返回其可修改指针
    // 这个函数非常的重要!!!就类似调用vector xxx.push_back()向数组添加新元素
    // 但是本函数不同的是,当我们调用了该函数之后,proto会直接就组末尾创建空的对象
    // 然后把这个新的对象的指针返回,我们要获取到该返回值,然后再调用该指针所指向的对象的set等等函数啦
    // 这就相当于是直接修改新添加的元素!!!
    // 语法:元素类型* add_字段名();
    // 参数:无
    // 返回值:PeopleInfo_Phone* - 新创建的Phone对象可修改指针,直接调用set_*方法赋值即可
    // 实现本质:RepeatedPtrField自动在数组末尾创建空的Phone对象,管理其内存,返回可修改指针(无需手动new)
    // 用法:PeopleInfo p; p.add_phone()->set_number("13800138000"); p.add_phone()->set_number("13900139000"); // 添加2个电话
    ::PeopleInfo_Phone* add_phone();

    // 6. 获取整个不可修改数组【常用】:返回整个repeated字段的常量引用,用于遍历所有元素
    // 语法:const RepeatedPtrField<元素类型>& 字段名() const;//和上面的是重载关系
    // 参数:无(const,纯读操作)
    // 返回值:const RepeatedPtrField<PeopleInfo_Phone>& - 底层动态数组常量引用,支持范围for遍历
    // 实现本质:直接返回底层的RepeatedPtrField容器,用于批量遍历、读取所有元素
    // 用法:PeopleInfo p; /* 添加电话 */ for (const auto& phone : p.phone()) { std::cout << phone.number() << std::endl; }
    const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo_Phone >& phone() const;
};

// ================================= 模块3:通讯录主类 - Contacts 【repeated PeopleInfo字段常用函数】=================================
// 生成逻辑:对应proto中的Contacts消息,核心为repeated PeopleInfo contacts=1字段,操作函数与PeopleInfo的repeated Phone字段完全一致
// 核心规律:所有repeated字段的操作函数命名、用法、参数、返回值均遵循统一规则,掌握一种即可触类旁通
class Contacts final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
    using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
    void CopyFrom(const Contacts& from); // 通用拷贝,用法同前,不再赘述

    // ---------------------- repeated .PeopleInfo contacts=1 专属核心常用函数【与repeated Phone完全一致】 ----------------------
    // 底层实现:RepeatedPtrField<PeopleInfo>动态对象指针数组,自动管理联系人对象内存

    // 1. 获取联系人个数【高频】:返回通讯录中联系人的数量
    // 语法:int 字段名_size() const;
    // 用法:Contacts c; int peopleCount = c.contacts_size(); // 初始0,添加后返回实际数量
    int contacts_size() const;

    // 2. 清空通讯录【高频】:删除所有联系人,释放内存
    // 语法:void clear_字段名();
    // 用法:Contacts c; /* 添加联系人 */ c.clear_contacts(); // c.contacts_size() 变为0
    void clear_contacts();

    // 3. 按索引获取不可修改联系人【高频】:只读获取指定位置的联系人信息
    // 语法:const 元素类型& 字段名(int index) const;
    // 用法:Contacts c; /* 添加联系人 */ std::string name = c.contacts(0).name(); // 获取第0个联系人姓名
    const ::PeopleInfo& contacts(int index) const;

    // 4. 按索引获取可修改联系人指针【高频】:修改指定位置的联系人信息
    // 语法:元素类型* mutable_字段名(int index);
    // 用法:Contacts c; /* 添加联系人 */ c.mutable_contacts(0)->set_age(25); // 修改第0个联系人年龄
    ::PeopleInfo* mutable_contacts(int index);

    // 5. 新增联系人【最高频】:向通讯录末尾添加空的PeopleInfo对象,返回可修改指针
    // 语法:元素类型* add_字段名();
    // 实现本质:自动创建空PeopleInfo对象,无需手动new,内存由ProtoBuf管理
    // 用法:Contacts c; c.add_contacts()->set_name("张三"); // 新增联系人并设置姓名;c.add_contacts()->set_age(25);
    ::PeopleInfo* add_contacts();

    // 6. 获取整个不可修改通讯录【常用】:遍历所有联系人
    // 语法:const RepeatedPtrField<元素类型>& 字段名() const;
    // 用法:Contacts c; /* 添加联系人 */ for (const auto& people : c.contacts()) { std::cout << people.name() << std::endl; }
    const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo >& contacts() const;
};

connects.cpp:

#include "connects.pb.h"
#include <iostream>
#include <string>
#include <fstream>
using namespace std;

/*
message Phone
{
    string number = 1;
    //ProtoBuf 的字段编号(唯一标识)只需要在「同一个 Message 内部唯一」,不同 Message 之间的字段编号可以重复。
}

message PersonInfo
{
    string name = 1;
    sint32 age = 2;
    repeated Phone phone = 3;
}

message Connects
{
    repeated PersonInfo contacts = 1;//联系人信息,通讯录中记录的肯定是联系人的信息啊哇
}
*/

void AddPersonInfo(connects::PersonInfo* person_info_ptr)
{
    cout << "-------------新增联系人-------------" << endl;

    cout << "请输入联系人姓名: ";
    string name;
    getline(cin,name);
    person_info_ptr->set_name(name);
    cin.ignore(256,'\n');//忽略输入缓冲区的前256个字符或者忽略第一次碰到的'\n'前的字符
    // cin.ignore(256, '\n') 解析:C++输入流缓冲区清理函数(需包含<iostream>)
    // 功能:清除标准输入缓冲区中字符,解决缓冲区残留字符导致后续cin/getline输入异常的问题
    // 参数1:256 → 本次最多忽略/清除256个字符,足够覆盖常规输入残留
    // 参数2:'\n' → 终止符,遇到该字符立即停止操作,且会将'\n'本身也清除
    // 执行逻辑:从缓冲区头部开始清字符,遇'\n'或清满256个字符,满足其一即停止
    // 核心适用场景:cin >> 读取数据后(会残留换行符),接getline/cin.get()前必加,避免后续读空内容
    // 这么一来下次读取输入的话,就不会被前面的输入影响到,而是从上一次回车之后的输入开始进行

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

    //接下来我们就要让用户输入电话号码了,而由于我们不知道用户有多少个号码
    //所以我们只能死循环进行让用户输入电话号码的操作,不然就嘎嘣脆了哈哈哈
    for(int i = 1; ; i++)//这也是死循环哦
    {
        cout << "请输入联系人电话" << i << "(只输入回车完成电话新增): ";
        string phone_number;
        getline(cin,phone_number);

        //如果新一次读取的phone_number为空,那么就说明用户没有新电话号码要输入了,那么我们就可以直接终止循环了
        if(phone_number.empty())
        {
            break;
        }

        //将用户输入的电话号码存进PersonInfo中的Phone结构体字段中的num
        person_info_ptr->add_phone()->set_number(phone_number);//真复杂,我们不妨拆分一下

        //其实就是像下面这样子
        // connects::Phone* phone = person_info_ptr->add_phone();
        // phone->set_number(phone_number);

    }
    cout << "-----------添加联系人成功-----------" << endl;
}

/**
* 打印联系人列表
*/
void PrintfContacts(const connects::Connects& con1,int size) 
{
    for (int i = 0; i < size; ++i) 
    {
        //就像遍历数组一样,也是使用下标哦
        //只不过是将下标传入函数作为参数
        const connects::PersonInfo& people = con1.contacts(i);
        cout << "------------联系人" << i+1 << "------------" << endl;
        cout << "姓名:" << people.name() << endl;
        cout << "年龄:" << people.age() << endl;
        int j = 1;

        //就像遍历数组一样,也是使用下标哦
        //只不过是将下标传入函数作为参数
        //Phone也是可重复字段,所以我们也需要进行遍历哦
        for (const connects::Phone& phone : people.phone())//范围for循环,字段名()函数是直接返回原始数组的哦,支持范围for 
        {
            cout << "电话" << j++ << ": " << phone.number() << endl;
        }
    }
}
int main(int argc,char* argv[])
{
    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
        return -1;
    }

    connects::Connects con;
    
    //向connects中添加新的联系人信息

    connects::PersonInfo* person_info_ptr= con.add_contacts();
    //此时就相当于新建了一个新的PersonInfo对象,我们可以直接调用该对象的函数进行赋值等等操作

    //传入函数,进行添加新联系人信息
    AddPersonInfo(person_info_ptr);

    //接下来依旧是进行序列化和反序列化的老套操作
    //向我们创建的数据结构添加了数据之后,我们就可以进行尝试序列化和反序列化了

    //序列化
    //注意:虽然是可重复字段,但是序列化和反序列化是不需要循环遍历的哦,直接序列化反序列化就行
    //proto自己会进行处理,也就是当我们输入数据、获取数据时,才需要循环遍历的哦
    string out;
    if(!con.SerializeToString(&out))//传入string的地址哦
    {
        cout<<"string序列化失败"<<endl;
        return -1;
    }
    cout<<"string序列化成功,序列化后的数据为:"<<out<<endl;


    //反序列化
    //对于可重复字段的反序列化,不需要使用遍历
    connects::Connects con1;
    if(!con1.ParseFromString(out))//从字符串中
    {
        cout<<"string反序列化失败"<<endl;
        return -1;
    }

    //那么我们要想将所有联系人的信息都打印出来的话,就得使用循环去遍历P~~~数组
    //至于我们怎么知道数组有多少个元素呢???不是有提供字段名_size函数吗哈哈哈
    int size=con1.contacts_size();
    PrintfContacts(con1,size);

    //我们再来使用文件的方式进行序列化和反序列化试试
    
    //序列化
    fstream wf("connects.bin",std::ios::binary|std::ios::out);
    if(!con.SerializeToOstream(&wf)) //传入输出流的地址
    {
        cout<<"文件序列化失败"<<endl;
        return -1;
    }
    wf.close();
    cout<<"文件序列化成功"<<endl;

    //反序列化 
    fstream rf("connects.bin",std::ios::binary|std::ios::in);
    connects::Connects con2;
    if(!con2.ParseFromIstream(&rf)) //传入输入流的地址 
    {
        cout<<"文件反序列化失败"<<endl;
        return -1;
    }
    rf.close();

    //那么我们要想将所有联系人的信息都打印出来的话,就得使用循环去遍历P~~~数组
    //至于我们怎么知道数组有多少个元素呢???不是有提供字段名_size函数吗哈哈哈
    int size1=con1.contacts_size();
    PrintfContacts(con1,size1);

    // for (int i = 0; i < size1; ++i) 
    // {
    //     //就像遍历数组一样,也是使用下标哦
    //     //只不过是将下标传入函数作为参数
    //     const connects::PersonInfo& people = con1.contacts(i);
    //     cout << "------------联系人" << i+1 << "------------" << endl;
    //     cout << "姓名:" << people.name() << endl;
    //     cout << "年龄:" << people.age() << endl;
    //     int j = 1;

    //     //就像遍历数组一样,也是使用下标哦
    //     //只不过是将下标传入函数作为参数
    //     //Phone也是可重复字段,所以我们也需要进行遍历哦
    //     for (const connects::Phone& phone : people.phone())//范围for循环,字段名()函数是直接返回原始数组的哦,支持范围for 
    //     {
    //         cout << "电话" << j++ << ": " << phone.number() << endl;
    //     }
    // }

    // for(int i=0;i<size1;++i)
    // {
    //     //就像遍历数组一样,也是使用下标哦
    //     //只不过是将下标传入函数作为参数
    //     cout<<"string反序列化成功,name="<<con1.contacts(i).name()<<",age="<<con1.contacts(i).age()<<endl;
    // }
    return 0;

}

七、二进制数据验证:protoc --decode 命令的使用

ProtoBuf 提供了官方的二进制数据验证工具——protoc --decode命令,可直接将二进制序列化数据(字符串 / 文件)解析为人类可读的文本格式,无需编写代码,是验证序列化数据正确性的最便捷、最可靠方式,适合开发调试和问题排查。

7.1 命令核心作用

从标准输入中读取指定 ProtoBuf 类型的二进制数据,将其解析为结构化的文本格式并输出到标准输出,支持验证本地二进制文件、网络传输的二进制字符串等所有 ProtoBuf 序列化数据。

7.2 完整命令语法

protoc --decode=包名.消息类型  Proto文件路径  < 二进制数据文件路径

关键参数解析

  1. --decode=包名.消息类型:指定要解析的 ProtoBuf 数据类型,包名 + 消息类型必须与.proto 中定义的完全一致(如connects.Connects);
  2. Proto文件路径:指定数据结构对应的.proto 文件路径(如./connects.proto),编译器需要通过该文件解析字段含义;
  3. < 二进制数据文件路径:Linux 重定向符,将本地二进制文件的内容作为标准输入传递给 protoc 命令。

7.3 实际使用示例(验证通讯录二进制文件)

基于前文实战案例生成的contacts.bin二进制文件,使用protoc --decode命令解析为文本格式:

7.3.1 执行验证命令

protoc --decode=connects.Connects ./connects.proto < ./contacts.bin

7.3.2 解析结果(文本格式)

contacts {
  name: "张三"
  age: 25
  phones {
    number: "13800138000"
    type: "移动"
  }
  phones {
    number: "010-12345678"
    type: "固定电话"
  }
}

7.3.3 特殊字符解析说明

如果联系人姓名包含中文,解析结果中可能会出现八进制转义字符(如\345\274\240\344\270\211),这是因为 protoc 将 UTF-8 编码的中文转换为了八进制表示,属于正常现象,不影响数据正确性:

contacts {
  name: "\345\274\240\344\270\211" # 对应中文“张三”
  age: 25
  phones {
    number: "13800138000"
  }
}

7.4 命令使用场景

  1. 开发调试:快速验证序列化数据是否正确,无需编写反序列化代码;
  2. 问题排查:当反序列化失败时,通过该命令判断二进制数据是否损坏或格式错误;
  3. 数据校验:验证生产环境的 ProtoBuf 二进制数据是否符合预期的字段结构;
  4. 跨语言验证:验证其他语言(Java/Python/Go)序列化的二进制数据是否与 C++ 定义的.proto 兼容。

八、核心知识点总结

8.1 核心知识点总结

8.1.1 嵌套 Message

  1. 两种定义方式:内部直接嵌套(专属结构,无复用)、外部定义 + 内部使用(推荐,可复用、易维护);
  2. 核心规则:子 Message 字段编号仅需自身内部唯一,与父 Message 字段编号可重复;
  3. 编译后 C++ 类:内部嵌套生成父_子组合类(如PersonInfo_Phone),外部定义生成独立类(如Phone)。

8.1.2 repeated 限定符

  1. 核心本质:等价于 C++ 结构体中的动态数组成员,底层由RepeatedPtrField实现;
  2. 核心特性:支持任意次赋值、保留添加顺序、自动内存管理、无元素时不序列化;
  3. 语法:仅需在字段类型前加repeated,其余规则与单值字段一致,字段名建议复数命名。

8.1.3 编译后 C++ 函数

  1. 单值字段:4 个核心函数(clear_xxx()/xxx() const/set_xxx()/mutable_xxx()),覆盖清、读、写、修改全操作;
  2. repeated 字段:6 个核心函数(xxx_size()/clear_xxx()/xxx(index)/mutable_xxx(index)/add_xxx()/xxx()),覆盖增、删、改、查、遍历、统计全操作;
  3. 通用规则:函数名与字段名一致、const 函数纯读、指针无需手动管理、按索引操作必做越界判断。

8.1.4 序列化 / 反序列化

  1. 四种核心函数:SerializeToString()/ParseFromString()(字符串)、SerializeToOstream()/ParseFromIstream()(文件 / 流);
  2. 核心特点:序列化后为二进制数据(体积小、效率高),支持跨语言、跨平台传输;
  3. 注意事项:repeated 字段序列化 / 反序列化无需遍历,ProtoBuf 底层自动处理。

8.2 开发最佳实践

8.2.1 .proto 文件编写规范

  1. 包名规范:使用项目 / 模块名作为包名(如connects/order/user),避免命名冲突;
  2. 字段编号规范:① 1~10 用于基础字段,11~20 预留扩展;② 编号一旦确定严禁修改,避免兼容问题;③ 不使用 19000~19999 保留编号;
  3. 命名规范:① Message 名使用大驼峰(如PersonInfo);② 字段名使用小驼峰(如phoneNumber)或下划线(如phone_number,推荐);③ repeated 字段名复数命名(如phones/contacts);
  4. 注释规范:为每个 Message、字段添加注释,说明业务含义、使用场景,提升可维护性;
  5. 结构规范:通用结构(如Phone/Address)统一外部定义,避免重复定义。

8.2.2 编译与运行规范

  1. 编译命令:使用protoc -I=源目录 --cpp_out=输出目录 文件名.proto,确保.proto 文件路径正确;
  2. C++ 编译:① 支持 C++11 及以上;② 链接 ProtoBuf 库(pkg-config --cflags --libs protobuf);③ 将生成的xxx.pb.cc与业务代码一起编译;
  3. 版本规范:开发、测试、生产环境使用同一版本的 ProtoBuf 库(protoc+libprotobuf),避免版本兼容问题。

8.2.3 C++ 代码编写规范

  1. 命名空间:使用.proto 定义的包名作为 C++ 命名空间(如using namespace connects;),避免类名冲突;
  2. 输入处理:混合使用cin/getline时,必须调用cin.ignore()清理缓冲区残留的换行符;
  3. 错误处理:对序列化 / 反序列化、文件操作、索引访问的返回值 / 状态做严格的错误判断,避免程序崩溃;
  4. 内存管理:① 不手动new/deleteProtoBuf 生成的对象 / 指针;② 程序结束时调用google::protobuf::ShutdownProtobufLibrary()释放全局内存;
  5. const 使用:纯读操作的函数 / 参数使用const修饰(如void Print(const Connects& con)),提升代码健壮性和效率。

8.2.4 性能优化

  1. 减少拷贝:优先使用引用 / 指针传递 Message 对象,避免大对象的值拷贝;
  2. 批量操作:向 repeated 字段添加大量元素时,直接使用add_xxx()链式赋值,避免多次函数调用;
  3. 空字段优化:不设置无效的字段值,ProtoBuf 会自动忽略空字段,减少序列化后的二进制体积;
  4. 复用 Message 对象:重复使用同一个 Message 对象时,调用Clear()清空旧数据,避免频繁创建 / 销毁对象的开销。

8.2.5 兼容性设计

  1. 字段扩展:新增字段使用预留的字段编号,不修改已有字段的编号和类型;
  2. 字段可选:非必填字段不强制设置值,ProtoBuf 会自动处理空字段,保证旧版本程序能正常解析新版本数据;
  3. 类型兼容:尽量使用通用标量类型(如sint32/uint64),避免使用小众类型,提升跨语言兼容性;
  4. 测试兼容:新增字段后,必须测试旧版本程序解析新版本数据、新版本程序解析旧版本数据,确保双向兼容。

至此,我们从 Proto3 的核心基础规则出发,完成了嵌套 Message、repeated 限定符的深度解析,吃透了编译后 C++ 的高频操作函数,并最终落地到简易通讯录系统的完整实战,实现了从语法定义到代码开发、从数据封装到持久化存储、从功能实现到二进制验证的全流程学习,构建了一套完整的 ProtoBuf C++ 开发知识体系。

本次学习的核心,并非单纯记忆语法和函数,而是理解 ProtoBuf 的设计本质 —— 以二进制序列化为核心,通过简洁的语法定义数据结构,借助编译器自动生成跨语言代码,实现高效的数据传输与存储;而嵌套 Message 的复用设计、repeated 字段的动态数组本质,更是 ProtoBuf 适配实际业务复杂数据结构的关键,吃透这些底层逻辑,才能做到 “一通百通”,无论面对何种业务场景的数结构定义,都能快速上手。

从实战中我们也能发现,ProtoBuf 的开发并非孤立的语法使用,而是与 C++ 工程化开发深度结合的:从 protoc 编译命令的使用、生成文件的编译链接,到输入缓冲区的处理、文件流的操作、错误边界的判断,再到代码规范与性能优化,每一个环节都是实际项目中必须关注的点。而 protoc --decode 这个官方验证工具,更是开发调试的 “利器”,能帮助我们快速验证序列化数据的正确性,大幅提升问题排查效率。

ProtoBuf 作为跨语言、跨平台的高性能序列化框架,是后端开发、中间件开发、分布式系统通信中的核心工具之一,本次通讯录实战所掌握的知识,是 ProtoBuf 开发的基础也是核心 —— 嵌套 Message、repeated 字段、序列化 / 反序列化、编译后函数调用,这四大核心内容覆盖了实际开发中 90% 以上的使用场景。

后续大家可以基于本次实战进行扩展:为通讯录添加联系人修改 / 删除功能、实现多联系人批量添加、结合网络库完成通讯录数据的网络传输、对接数据库实现持久化数据的增删改查,或是尝试用 Java/Python 解析本次 C++ 序列化的二进制文件,体验 ProtoBuf 跨语言的核心优势。

技术的掌握源于反复的实践,建议大家多动手修改.proto 文件、调试 C++ 代码,在实践中巩固语法规则和函数使用,理解开发规范与兼容性设计的重要性。相信通过本次学习,大家已经具备了将 ProtoBuf 应用到实际项目中的能力,后续只需结合业务需求灵活变通,就能发挥其高效、简洁、跨平台的核心价值。

最后,感谢大家的跟随学习,希望这篇内容能成为大家 ProtoBuf C++ 开发路上的实用指南,也祝愿大家在后续的开发中能学以致用,打造更高效、更健壮的分布式系统与数据交互模块。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2503_92929084/article/details/157149016

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--