protobuf 使用详解

protobuf 使用详解

一、protobuf 简介

1、什么是 protobuf

Protocol Buffers,简称 protobuf,是 Google 提供的一种结构化数据序列化机制。
简单来说,protobuf 的作用就是:

序列化:把一个 C++ 对象转换成一段二进制数据

反序列化:把一段二进制数据重新恢复成 C++ 对象
protobuf 本身是语言无关、平台无关、可扩展的序列化数据格式,既可以用于网络通信协议,也可以用于本地数据存储。相比 XML、JSON 这类文本格式,protobuf 通常具有体积更小、解析速度更快、结构更明确的特点。

2、为什么需要 protobuf

在网络通信中,客户端和服务器之间传输的本质都是字节流

例如,一个登录请求可能包含:

cpp 复制代码
user_name = "zhangsan"
password = "123456"

但是在网络中传输时,它不可能直接以 C++ 对象的形式发送,而是要转换成一段字节数据。接收端收到这段字节后,还要知道:

  1. 这段数据表示什么消息

  2. 每个字段是什么意思

  3. 每个字段是什么类型

  4. 如何从字节流中还原出对象

这就需要一种统一的协议格式

常见的序列化方式有 XML、JSON、protobuf:

text 复制代码
XML:文本格式,可读性强,但是冗余较多,体积较大。
JSON:文本格式,使用简单,调试方便,常用于 Web 接口。
protobuf:二进制格式,体积小,解析快,适合高性能网络通信。

如果是公网 HTTP 接口,JSON 使用起来很方便;如果是服务器之间通信、RPC 调用、IM 即时通讯、游戏服务器、分布式服务内部通信等场景,protobuf 会更合适

3、protobuf 的基本工作流程

protobuf 的使用流程可以概括为:

text 复制代码
编写 .proto 文件
        ↓
使用 protoc 编译生成 .pb.h 和 .pb.cc
        ↓
C++ 程序引入生成的头文件
        ↓
调用 set_xxx() 设置字段
        ↓
调用 SerializeToString / SerializeToOstream 序列化
        ↓
调用 ParseFromString / ParseFromIstream 反序列化

对于 C++ 来说,protobuf 编译器会根据 .proto 文件生成两个文件:

text 复制代码
xxx.pb.h
xxx.pb.cc

其中:

text 复制代码
xxx.pb.h:声明消息类、成员函数、枚举、访问接口等。
xxx.pb.cc:实现具体的序列化、反序列化、字段访问等逻辑。

开发者只需要关心 .proto 中定义的数据结构,以及生成类的使用方式,不需要自己手动编写二进制编码和解码代码

二、protobuf 基本语法

1、syntax 语法版本

.proto 文件的第一行通常是:

protobuf 复制代码
syntax = "proto3";

这表示当前文件使用 proto3 语法

如果不写这一行,编译器可能会按照 proto2 的语法处理。proto2 和 proto3 在字段规则、默认值、required/optional 等方面存在差异,因此建议明确写出:

protobuf 复制代码
syntax = "proto3";

2、message 消息类型

message 类似于 C++ 中的 class 或 struct,用来定义一种数据结构

例如:

protobuf 复制代码
syntax = "proto3";

// 一个搜索请求类
message SearchRequest {
  string query = 1;           // 查询字符串
  int32 page_number = 2;      // 页数
  int32 result_per_page = 3;  // 每页结果数量
}

这个 SearchRequest 消息中有三个字段:

text 复制代码
query:查询字符串
page_number:页码
result_per_page:每页结果数量

生成 C++ 代码后,就会有一个对应的 C++ 类,可以通过成员函数访问字段

例如:

cpp 复制代码
SearchRequest request;
request.set_query("protobuf");
request.set_page_number(1);
request.set_result_per_page(10);

3、字段编号

protobuf 中每个字段后面都有一个编号:

protobuf 复制代码
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;

这里的 1、2、3 不是随便写的,它们是字段在二进制数据中的唯一标识

protobuf 序列化后不会保存字段名,例如不会保存 "query""page_number" 这些字符串,而是保存字段编号和字段值

这是protobuf和xml/json的一个很大区别,因为后面二者都是通过这样的key-value来保存的

这样做的好处是:

text 复制代码
减少传输数据大小;
提高解析效率;
方便后续协议升级

字段编号一旦使用,就不应该随意修改。因为旧程序和新程序都是依靠字段编号来识别数据的。如果把原来的字段编号改掉,就可能导致新旧版本之间无法正确解析

一般建议:

text 复制代码
1 ~ 15:适合留给高频字段,因为编码后占用空间更小。
16 之后:可以留给普通字段或后续扩展字段。

4、常见标量类型

protobuf 中常见的标量和对应到 C++ 中的关系大致如下:

text 复制代码
double  -> double
float   -> float
int32   -> int32_t // 变长类型,且编码负数时效率很低
int64   -> int64_t
sint32  -> int32_t // 编码负数效率高
sint64  -> int64_t
uint32  -> uint32_t
uint64  -> uint64_t
bool    -> bool
string  -> std::string
bytes   -> std::string

需要注意的是:

text 复制代码
string:要求是 UTF-8 或 ASCII 文本。
bytes:表示任意字节数据,可以存储二进制内容。

5、repeated 重复字段

如果一个字段可能出现多次,可以使用 repeated,对应到c++中就是数组

例如,一个人可能有多个电话号码:

protobuf 复制代码
message Person {
  string name = 1;
  repeated string phone = 2; // 定义了一个数组类型,vector<string>
}

生成 C++ 代码后,可以这样使用:

cpp 复制代码
Person person;
person.set_name("zhangsan");

person.add_phone("13800000000");
person.add_phone("13900000000");

for (int i = 0; i < person.phone_size(); ++i) {
    std::cout << person.phone(i) << std::endl;
}

常用接口包括:

cpp 复制代码
add_xxx();       // 添加一个 repeated 元素
xxx_size();      // 获取 repeated 元素个数
xxx(index);      // 获取指定下标元素
mutable_xxx();   // 获取可修改对象

6、enum 枚举类型

protobuf 支持枚举类型,例如:

protobuf 复制代码
message Person {
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  string name = 1;
  PhoneType type = 2;
}

proto3 中,枚举的第一个值必须是 0

这是因为 protobuf 中字段如果没有被设置,会使用默认值。对于枚举来说,默认值就是第一个枚举项,所以第一个枚举值必须从 0 开始

7、嵌套 message

一个 message 内部可以继续定义 message,也就是类中嵌套定义类

例如:

protobuf 复制代码
message Person {
  string name = 1;

  message PhoneNumber {
    string number = 1;
    string type = 2;
  }

  repeated PhoneNumber phones = 2;
}

这表示 PhoneNumberPerson 内部定义的消息类型。

C++ 使用时,嵌套类型的名字通常是:

cpp 复制代码
Person::PhoneNumber

例如:

cpp 复制代码
Person person;
Person::PhoneNumber* phone = person.add_phones();
phone->set_number("13800000000");

8、package 包名

.proto 文件中可以使用 package 防止命名冲突:

protobuf 复制代码
package tutorial;

对于 C++ 来说,package 会生成对应的命名空间

例如:

protobuf 复制代码
package tutorial;

message Person {
  string name = 1;
}

在 C++ 中使用时就是:

cpp 复制代码
tutorial::Person person;

9、import 引入其他 proto 文件

protobuf 支持引入其他 .proto 文件:

protobuf 复制代码
import "google/protobuf/timestamp.proto";

引入后,就可以使用其中定义好的消息类型:

protobuf 复制代码
google.protobuf.Timestamp last_updated = 5;

这在工程中很常见,例如时间戳、Any 类型、空消息等,都可以直接复用 protobuf 官方提供的 .proto 文件

10、option 选项

option 可以影响代码生成方式。

例如:

protobuf 复制代码
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";

这些选项主要影响对应语言生成代码的包名、类名、命名空间等

对于 C++ 来说,更常见的是:

protobuf 复制代码
option optimize_for = SPEED;

protobuf 支持三种常见优化模式:

text 复制代码
SPEED:默认选项,生成代码执行效率高,但是代码体积较大。
CODE_SIZE:生成代码体积更小,但是运行效率相对低。
LITE_RUNTIME:生成代码体积小,运行效率也较高,但是会牺牲反射功能,需要链接 libprotobuf-lite。

在服务器开发中,如果没有特殊限制,通常保持默认的 SPEED 即可

三、protobuf 编译和使用

1、安装 protobuf

Linux 下源码安装的一般流程如下:

bash 复制代码
tar zxf protobuf-cpp-3.19.6.tar.gz
cd protobuf-3.19.6

./configure
make
sudo make install
sudo ldconfig

安装完成后,查看版本:

bash 复制代码
protoc --version

如果能够正常输出版本号,说明 protoc 工具已经可以使用

2、编写 proto 文件

假设有一个文件:

text 复制代码
addressbook.proto

内容如下:

protobuf 复制代码
syntax = "proto3";

package tutorial;

import "google/protobuf/timestamp.proto";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

message AddressBook {
  repeated Person people = 1;
}

这个文件定义了一个电话簿结构:

text 复制代码
Person:表示一个联系人
PhoneNumber:表示联系人电话
PhoneType:表示电话类型
AddressBook:表示整个电话簿,里面可以保存多个联系人

3、生成 C++ 代码

使用 protoc 生成 C++ 文件:
-I表示proto文件所在地址,--cpp_out表示要输出的.h.cc文件的地址

bash 复制代码
protoc -I=./ --cpp_out=./ addressbook.proto

如果当前目录下有多个 .proto 文件,也可以:

bash 复制代码
protoc -I=./ --cpp_out=./ *.proto

执行后会生成:

text 复制代码
addressbook.pb.h
addressbook.pb.cc

4、编译 C++ 程序

假设有两个程序:

text 复制代码
add_person.cc:向电话簿中添加联系人
list_people.cc:读取电话簿并打印联系人信息

编译命令如下:

bash 复制代码
g++ -std=c++11 -o add_person add_person.cc addressbook.pb.cc -lprotobuf -lpthread
g++ -std=c++11 -o list_people list_people.cc addressbook.pb.cc -lprotobuf -lpthread

四、protobuf 在 C++ 中的常用接口

1、设置普通字段

对于如下字段:

protobuf 复制代码
string name = 1;
int32 id = 2;
string email = 3;

生成的c++文件中会为每个字段生成对应的set_xxx函数

cpp 复制代码
tutorial::Person person;

person.set_name("zhangsan");
person.set_id(1001);
person.set_email("zhangsan@example.com");

读取字段,生成的类中有一个和字段同名的函数用于访问:

cpp 复制代码
std::cout << person.name() << std::endl;
std::cout << person.id() << std::endl;
std::cout << person.email() << std::endl;

2、mutable_xxx 获取可修改对象

对于 string 类型,可以使用:

cpp 复制代码
getline(cin, *person->mutable_name());

mutable_name() 返回的是字段内部字符串的指针,可以直接修改内部内容。

例如:

cpp 复制代码
tutorial::Person person;
*person.mutable_name() = "lisi";

这和下面的写法效果类似:

cpp 复制代码
person.set_name("lisi");

3、添加 repeated 的字段

对于 repeated 字段:

protobuf 复制代码
repeated PhoneNumber phones = 4;

可以使用:

cpp 复制代码
tutorial::Person::PhoneNumber* phone_number = person.add_phones();
phone_number->set_number("13800000000");
phone_number->set_type(tutorial::Person::MOBILE);

add_phones() 会新建一个 PhoneNumber 对象,并返回它的指针

4、遍历 repeated 字段

读取 repeated 字段时,可以使用:

cpp 复制代码
for (int i = 0; i < person.phones_size(); ++i) {
    const tutorial::Person::PhoneNumber& phone_number = person.phones(i);
    std::cout << phone_number.number() << std::endl;
}

其中phones_size()表示电话数量,phones(i)表示获取第 i 个电话对象

5、判断字段是否存在

对于 message 类型字段,例如:

protobuf 复制代码
google.protobuf.Timestamp last_updated = 5;

可以使用:

cpp 复制代码
if (person.has_last_updated()) {
    std::cout << TimeUtil::ToString(person.last_updated()) << std::endl;
}

需要注意:proto3 中普通标量字段默认没有 has_xxx(),因为标量字段如果没有设置,会直接使用默认值

例如:

text 复制代码
string 默认值是空字符串;
bool 默认值是 false;
数值类型默认值是 0;
repeated 默认是空列表;
enum 默认是第一个枚举值

6、序列化和反序列化

把对象写入文件:

cpp 复制代码
fstream output("addressbook.data", ios::out | ios::trunc | ios::binary);

if (!address_book.SerializeToOstream(&output)) {
    cerr << "Failed to write address book." << endl;
}

这里使用的是SerializeToOstream(),它会把 protobuf 对象序列化成二进制数据,然后写入输出流

从文件读取对象:

cpp 复制代码
fstream input("addressbook.data", ios::in | ios::binary);

if (!address_book.ParseFromIstream(&input)) {
    cerr << "Failed to parse address book." << endl;
}

这里使用的是ParseFromIstream(),它会从输入流中读取二进制数据,并解析成 protobuf 对象

7、序列化到字符串

网络通信中更常用的是序列化到字符串:

cpp 复制代码
std::string data;
address_book.SerializeToString(&data);

反序列化:

cpp 复制代码
tutorial::AddressBook address_book;
address_book.ParseFromString(data);

这里的 std::string 不一定是文本字符串,它可以存储任意二进制数据

五、protobuf 编码原理

1、protobuf 为什么比 JSON 更小

JSON 是文本格式,字段名会出现在数据中

例如:

json 复制代码
{
  "name": "zhangsan",
  "id": 1001,
  "email": "zhangsan@example.com"
}

这里的 "name""id""email" 都会被传输

protobuf 不会直接传字段名,而是传字段编号

例如:

protobuf 复制代码
string name = 1;
int32 id = 2;
string email = 3;

序列化后,protobuf 只需要记录字段编号 + 字段类型 + 字段值,这就是 protobuf 数据更紧凑的重要原因

2、wire type

protobuf 序列化后的数据不是简单地把字段值拼接起来,它还会保存字段的 wire type

常见 wire type 包括:

text 复制代码
0:Varint,用于 int32、int64、uint32、uint64、sint32、sint64、bool、enum
1:64-bit,用于 fixed64、sfixed64、double
2:Length-delimited,用于 string、bytes、嵌套 message、packed repeated
5:32-bit,用于 fixed32、sfixed32、float

字段的 key 由字段编号和 wire type 组成:

text 复制代码
field_num << 3 | wire_type

例如int32 age = 1;,如果 age 的值为 5,字段编号是 1,wire type 是 0,那么 key 就是1 << 3 | 0 = 8

所以序列化时会先写入字段 key,再写入字段值

这也是为什么接收端必须有 .proto 文件,因为 protobuf 二进制本身不是完全自描述的。它不保存字段名,接收端需要根据 .proto 中的字段编号和类型来解释数据

3、Varints 变长编码

普通 C++ 中,一个 int32_t 固定占 4 个字节

例如:

cpp 复制代码
int32_t a = 1;
int32_t b = 100000;

虽然 1 很小,但它依然占 4 个字节

Varints 的思想是:

text 复制代码
数值小,就少占字节;
数值大,才多占字节。

Varints 使用每个字节的最高位作为标志位:

text 复制代码
最高位为 1:后面还有字节;
最高位为 0:当前字节是最后一个字节;
剩下 7 位:存储有效数据。

所以一个较小的整数可能只需要 1 个字节,而不是固定 4 个字节,这就是 protobuf 对小整数非常友好的原因

4、为什么负数不建议直接用 int32

Varints 对正数很友好,但是对负数不友好

原因是负数在计算机中使用补码表示,高位通常都是 1。Varints 的本质是尽量去掉高位多余的 0,但是负数高位是 1,就无法很好地压缩

因此,如果使用 int32 表示负数,protobuf 内部可能会把它当成一个很大的无符号数来处理,导致编码后占用较多字节

5、ZigZag 编码

为了解决负数编码效率低的问题,protobuf 提供了:

protobuf 复制代码
sint32
sint64

它们会使用 ZigZag 编码

ZigZag 的核心思想是:

text 复制代码
把有符号整数映射成无符号整数
让绝对值小的负数也能变成较小的正数
再使用 Varints 编码

例如:

text 复制代码
0   -> 0
-1  -> 1
1   -> 2
-2  -> 3
2   -> 4

这样 -1-2 这类小负数也可以被编码成很小的整数,再配合 Varints 就能减少空间占用

所以:

text 复制代码
如果字段不会出现负数,可以使用 int32、uint32
如果字段可能出现负数,并且数值通常不大,建议使用 sint32、sint64
如果数值通常很大,接近 32 位或 64 位上限,可以考虑 fixed32、fixed64

6、Length-delimited 编码

对于下面这些类型:

text 复制代码
string
bytes
嵌套 message
packed repeated

protobuf 会使用 Length-delimited 编码

它的结构大致是:

text 复制代码
字段 key + 数据长度 + 数据内容

例如:

protobuf 复制代码
string name = 1;

如果 name 是 "abc",那么 protobuf 需要知道这个字符串有多长,所以会先写入长度,再写入具体内容

六、使用示例:电话簿小程序

下面通过一个电话簿程序完整演示 protobuf 在 C++ 中的使用

这个程序包含三个部分:

text 复制代码
addressbook.proto:定义电话簿协议结构
add_person.cc:添加联系人并保存到文件
list_people.cc:读取文件并打印联系人

1、定义 addressbook.proto

protobuf 复制代码
syntax = "proto3";

package tutorial;

import "google/protobuf/timestamp.proto";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

message AddressBook {
  repeated Person people = 1;
}

这里定义了两个主要 message:

text 复制代码
Person:单个联系人
AddressBook:电话簿,内部保存多个 Person

Person 中包含:

text 复制代码
name:姓名
id:联系人 ID
email:邮箱
phones:多个电话号码
last_updated:最后更新时间

2、生成 C++ 文件

执行:

bash 复制代码
protoc -I=./ --cpp_out=./ addressbook.proto

生成:

text 复制代码
addressbook.pb.h
addressbook.pb.cc

然后 C++ 代码中引入:

cpp 复制代码
#include "addressbook.pb.h"

3、添加联系人程序 add_person.cc

核心逻辑如下:

cpp 复制代码
void PromptForAddress(tutorial::Person* person) {
  cout << "Enter person ID number: ";
  int id;
  cin >> id;
  person->set_id(id);
  cin.ignore(256, '\n');

  cout << "Enter name: ";
  getline(cin, *person->mutable_name());

  cout << "Enter email address (blank for none): ";
  string email;
  getline(cin, email);
  if (!email.empty()) {
    person->set_email(email);
  }

  while (true) {
    cout << "Enter a phone number (or leave blank to finish): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }

    tutorial::Person::PhoneNumber* phone_number = person->add_phones();
    phone_number->set_number(number);

    cout << "Is this a mobile, home, or work phone? ";
    string type;
    getline(cin, type);

    if (type == "mobile") {
      phone_number->set_type(tutorial::Person::MOBILE);
    } else if (type == "home") {
      phone_number->set_type(tutorial::Person::HOME);
    } else if (type == "work") {
      phone_number->set_type(tutorial::Person::WORK);
    } else {
      cout << "Unknown phone type. Using default." << endl;
    }
  }

  *person->mutable_last_updated() = TimeUtil::SecondsToTimestamp(time(NULL));
}

主函数逻辑如下:

cpp 复制代码
int main(int argc, char* argv[]) {
  GOOGLE_PROTOBUF_VERIFY_VERSION;

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

  tutorial::AddressBook address_book;

  {
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": File not found. Creating a new file." << endl;
    } else if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }

  PromptForAddress(address_book.add_people());

  {
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) {
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }

  google::protobuf::ShutdownProtobufLibrary();

  return 0;
}

流程是:

text 复制代码
1、打开电话簿文件
2、如果文件存在,就先反序列化读取已有联系人
3、调用 add_people() 新增一个 Person
4、填写联系人信息
5、重新序列化写回文件

其中address_book.ParseFromIstream(&input)表示从文件反序列化,address_book.SerializeToOstream(&output)表示序列化写入文件

4、查看联系人程序 list_people.cc

打印电话簿的核心逻辑如下:

cpp 复制代码
void ListPeople(const tutorial::AddressBook& address_book) {
  for (int i = 0; i < address_book.people_size(); i++) {
    const tutorial::Person& person = address_book.people(i);

    cout << "Person ID: " << person.id() << endl;
    cout << "  Name: " << person.name() << endl;

    if (person.email() != "") {
      cout << "  E-mail address: " << person.email() << endl;
    }

    for (int j = 0; j < person.phones_size(); j++) {
      const tutorial::Person::PhoneNumber& phone_number = person.phones(j);

      switch (phone_number.type()) {
        case tutorial::Person::MOBILE:
          cout << "  Mobile phone #: ";
          break;
        case tutorial::Person::HOME:
          cout << "  Home phone #: ";
          break;
        case tutorial::Person::WORK:
          cout << "  Work phone #: ";
          break;
        default:
          cout << "  Unknown phone #: ";
          break;
      }

      cout << phone_number.number() << endl;
    }

    if (person.has_last_updated()) {
      cout << "  Updated: " << TimeUtil::ToString(person.last_updated()) << endl;
    }
  }
}

主函数逻辑如下:

cpp 复制代码
int main(int argc, char* argv[]) {
  GOOGLE_PROTOBUF_VERIFY_VERSION;

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

  tutorial::AddressBook address_book;

  {
    fstream input(argv[1], ios::in | ios::binary);
    if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }

  ListPeople(address_book);

  google::protobuf::ShutdownProtobufLibrary();

  return 0;
}

流程是:

text 复制代码
1、打开电话簿文件
2、使用 ParseFromIstream 反序列化
3、遍历 AddressBook 中的 people
4、打印每个联系人的信息

5、编译运行

生成 C++ 文件:

bash 复制代码
protoc -I=./ --cpp_out=./ addressbook.proto

编译添加联系人程序:

bash 复制代码
g++ -std=c++11 -o add_person add_person.cc addressbook.pb.cc -lprotobuf -lpthread

编译查看联系人程序:

bash 复制代码
g++ -std=c++11 -o list_people list_people.cc addressbook.pb.cc -lprotobuf -lpthread

添加联系人:

bash 复制代码
./add_person addressbook.data

示例输入:

text 复制代码
Enter person ID number: 1
Enter name: zhangsan
Enter email address (blank for none): zhangsan@example.com
Enter a phone number (or leave blank to finish): 13800000000
Is this a mobile, home, or work phone? mobile
Enter a phone number (or leave blank to finish):

查看联系人:

bash 复制代码
./list_people addressbook.data

可能输出:

text 复制代码
Person ID: 1
  Name: zhangsan
  E-mail address: zhangsan@example.com
  Mobile phone #: 13800000000
  Updated: 2026-05-02T08:00:00Z
相关推荐
资深流水灯工程师1 小时前
UART 通讯DMA+IDLE模式笔记
笔记·单片机·嵌入式硬件
Soley1 小时前
用 Boost.Log 封装一个更顺手的 C++17 日志库:GoodLog
c++
HAPPY酷1 小时前
从Public到Private:UE5 C++类创建路径差异全解析
java·c++·ue5
无敌昊哥战神1 小时前
【LeetCode 37】解数独 (Sudoku Solver) —— 回溯法详解 (Python/C/C++)
c语言·c++·python·算法·leetcode
AI进化营-智能译站2 小时前
ROS2 C++开发系列08-传感器数据缓存与指令解析方式之数组、向量与字符串实战
开发语言·c++·缓存·ai
hello_读书就是赚钱2 小时前
提示词工程学习笔记
笔记·学习
AI进化营-智能译站2 小时前
ROS2 C++开发系列14-Lambda表达式处理传感器数据流|文件IO保存机器人实验日志
开发语言·c++·ai·机器人
二哈赛车手2 小时前
新人笔记---多策略搭建策略执行链实现RAG检索后过滤
java·笔记·spring·设计模式·ai·策略模式
AI进化营-智能译站2 小时前
ROS2 C++开发系列15-模板实现通用算法|宏定义ROS2调试开关|一次编码适配多平台
java·c++·算法·ai