0 先验配置

查看自己的c++版本

# GCC
g++ -dM -E -x c++ /dev/null | grep __cplusplus

# Clang
clang++ -dM -E -x c++ /dev/null | grep __cplusplus

# MSVC (Windows)
cl /nologo /EP /dM /TP nul 2>nul | findstr __cplusplus

| 输出值       | 对应标准      |
| --------- | --------- |
| `199711L` | C++98/03  |
| `201103L` | **C++11** |
| `201402L` | **C++14** |
| `201703L` | **C++17** |
| `202002L` | C++20     |
| `202302L` | C++23     |

| 维度         | C++98                   | Modern C++ (11/14/17)                          |
| ---------- | ----------------------- | ---------------------------------------------- |
| **内存管理**   | 手动 `new/delete`,极易泄漏    | `std::unique_ptr`/`std::shared_ptr`,RAII 自动化   |
| **拷贝成本**   | 深拷贝是唯一选择( costly )      | **移动语义**(`std::move`),转移资源而非复制                 |
| **初始化**    | 赋值初始化、括号初始化混乱           | **统一初始化列表 `{}`**、`std::initializer_list`       |
| **构造函数**   | 重复代码只能抽函数               | **委托构造**(一个构造函数调用另一个)                          |
| **默认函数控制** | 编译器隐式生成,无法控制            | `= default`(显式默认)、`= delete`(显式禁用)             |
| **类型推导**   | 必须写冗长类型名                | `auto`、`decltype`、尾置返回类型                       |
| **并发**     | 依赖平台 API(pthread/Win32) | 标准库自带 `std::thread`、`std::mutex`、`std::future` |
| **Lambda** | 无,必须写仿函数类               | 匿名函数,算法配合利器                                    |
| **常量表达式**  | `#define` 或运行期计算        | `constexpr` 编译期计算                              |
| **范围 for** | 手写迭代器循环                 | `for (auto& x : container)`                    |

指定编译标准

g++ -std=c++11 your.cpp    # C++11

1 初始化列表

class Packet
{
private:
    /* data */
    std::vector<uint8_t> data_;
public:
    Packet(/* args */);
    ~Packet();

    Packet(size_t len) : data_(len) {}; // 直接构造vector
};

// 使用
int main(int argc, char const *argv[])
{
    Packet p(1024);  // 创建长度为 1024 字节的包,data_ 里有 1024 个 0
    
    return 0;
}

析构函数 ~Packet() 的作用:在对象被销毁前,做收尾清理。

对 Packet 来说,成员是 std::vector<uint8_t> data_

  • 你几乎不用在 ~Packet() 里写东西;

  • vector 自己会释放它在堆上申请的内存;

  • 这叫 RAII:资源随对象生命周期自动管理。

什么时候必须关心析构?

当类自己管理资源时,例如:

class Bad {

uint8_t* raw_; // 自己 new 出来的

public:

Bad(size_t n) : raw_(new uint8_t[n]) {}

~Bad() { delete[] raw_; } // 必须自己 delete,否则内存泄漏

};

Packet 用 vector,没有裸指针,所以析构通常交给编译器默认实现就够了。

1. Clang 是什么?

Clang 是一个 C / C++ / Objective-C 编译器前端,属于 LLVM 项目的一部分。

可以把它理解成和 GCC、MSVC 同类的工具:把源码编译成可执行文件或库。

特点

说明

错误信息

通常更清晰,会指出问题位置和原因

编译速度

往往比 GCC 更快(尤其大项目)

标准支持

对新标准(C++17/20/23)跟进较快

常见用途

macOS/iOS 默认工具链(Xcode)、Linux 开发、静态分析(clang-tidy)、IDE 补全/跳转的后端

各版本核心变化(简表)

标准

发布时间

代表性特性

定位

C++11

2011

auto、lambda、智能指针、move、范围 for、nullptrvector 初始化列表

现代 C++ 分水岭,变化最大

C++14

2014

泛型 lambda、make_unique、返回类型 auto、数字分隔符 '1'000

在 11 上小步完善

C++17

2017

std::optional/variant/string_view、结构化绑定、if constexpr、filesystem

实用特性多,工程上很成熟

C++20

2020

Concepts、Coroutines、Ranges、std::format、Modules(初步)

又一次大升级(你没问,但选型常提到)

常见表示成员变量的风格

写法

示例

常见来源

后缀下划线

data_

Google C++ 风格、很多开源 C++ 项目

前缀 m_

m_data

老 Windows/MFC、部分嵌入式代码

_ 前缀

_data

少数团队习惯

无后缀,用 this->

this->data = data

也能区分,但写起来啰嗦

Rule of Zero(现代 C++ 推荐)

一个成员都不用自己管,全用 vectorstringunique_ptr 等 RAII 类型:

class Packet {

std::vector<uint8_t> data_; // 就这一行管内存

// 不写析构、拷贝、移动 — 编译器生成的会正确委托给 vector

};

这叫 Rule of Zero:
编译器生成的拷贝会拷贝 vector(深拷贝);移动会移动 vector(快);析构会释放 vector 的内存。
你不需要手写那五个函数。

  • Packet(1024) → data_(1024) → 堆上约 1024 字节,元素初值为 0;

  • 拷贝 Packet → 两个 vector 各有一块堆内存;

  • 移动 Packet → 两个 vector 内部指针交换/转移,几乎 O(1)。

这也是现代 C++ 里用 vector 而不是 uint8_t* + new/delete 的原因:拷贝/移动/析构语义由标准库实现好了。

六、为什么要 private(封装)

class Packet {

private:

std::vector<uint8_t> data_; // 外部不能直接碰

public:

Packet(size_t len);

// 将来可以加: size(), data(), append(...) 等

};

1. 隐藏实现细节

外部只应知道「这是一个定长/可变的数据包」,不应依赖「底层是 vector 还是别的」。

以后若改成 std::span、内存池、共享缓冲区,只要改类内部,调用方代码不用改。

2. 防止误用

若 data_ 是 public:

Packet p(100);

p.data_.clear(); // 外面随便清掉,类 invariant 被破坏

p.data_.resize(999999); // 绕过你的业务规则

private 后,只能通过你提供的接口操作,你可以在接口里做校验、日志、线程安全等。

3. 保持「不变量」(invariant)

例如约定:「Packet 长度一旦创建就不能为 0」——
在构造函数和 public 方法里保证;若 data_ 公开,外部无法保证。

4. 和拷贝/移动的关系

资源在 private 的 data_ 里;对外只暴露行为。
拷贝/移动由编译器 + vector 在幕后完成,调用方写:

Packet a(100);

Packet b = a; // 拷贝

Packet c = std::move(a); // 移动

不需要知道内部是深拷贝还是指针转移。

一、为什么 C 里 b = a 也是拷贝,C++ 却要单独强调?

C 里多数拷贝是「按位复制」(trivial)

int a = 10;

int b = a; // 4 字节从 a 的栈槽复制到 b 的栈槽,完事

int 没有「内部堆内存、文件句柄、锁」这类资源,编译器知道怎么复制,没有歧义。

C++ 里对象可以「拥有资源」,拷贝语义不再显然

Packet a(1024); // a 内部 vector 在堆上有 1024 字节

Packet b = a; // 应该复制 1024 字节?还是两个对象共用同一块?

若默认只做浅拷贝(只复制 vector 对象里的几个指针),两个 Packet 会指向同一块堆内存,析构时 double free。
所以 C++ 必须回答:谁负责拷贝?拷贝多深?

这就是 C++ 把「拷贝」单独拎出来说的原因——不是「有没有拷贝」,而是 对象类型的拷贝语义要由语言/程序员明确定义。

C++ 和其他语言的对比(直觉)

语言/模型

常见做法

C

基本类型按值;结构体按成员复制;指针只复制地址,资源你自己管

Java/C# 对象

变量多是「引用」,b = a 常是两个引用指向同一对象(不是深拷贝对象内容)

C++

值语义为主:b = a 默认是新对象拥有一份独立数据(对 vector 等是深拷贝)

C++ 强调拷贝,是因为它默认 值语义 + 可自定义,既要安全又要性能(于是又有移动、零拷贝式优化等)。

二、C++ 里和「拷贝/移动/销毁」相关的特殊成员函数

对一个类(如 Packet),编译器可能生成或使用这些函数:

函数

典型触发写法

作用

析构 ~Packet()

对象生命周期结束

释放资源

拷贝构造 Packet(const Packet& other)

Packet b = a; 或 Packet b(a);

用 other 构造新对象

拷贝赋值 operator=(const Packet& other)

b = a;ba 都已存在)

把 other 的内容赋给已存在的 b

移动构造 Packet(Packet&& other)

Packet b = std::move(a);

从 other 偷资源 构造 b

移动赋值 operator=(Packet&& other)

b = std::move(a);

把 other 的资源转移给 b

前 3 个是经典 Rule of Three;加上后 2 个是 Rule of Five。

  • 构造:新对象还没建完,在「出生」时用谁的数据。

  • 赋值:对象已经存在,可能要先释放 b 旧数据,再复制 a 的数据。

三、operator= 就是运算符重载吗?只对 Packet 有效吗?

是运算符重载,而且是 成员函数,第一个参数是隐式的 this

class Packet {

public:

Packet& operator=(const Packet& other) {

if (this != &other) {

data_ = other.data_; // vector 的 operator= 会做深拷贝

seq_ = other.seq_;

}

return *this; // 支持链式:p1 = p2 = p3

}

};

含义:

  • operator= 只在 左边是 Packet、右边也是 Packet(或能转成 const Packet&) 时,才会走这个函数。

  • int x = 10; 用的是 int 的内建赋值,和 Packet::operator= 无关。

  • 对 Packet 用 vector 时,你往往 不写 operator=,编译器生成的版本会逐个成员赋值;data_ = other.data_ 会调用 std::vector 自己的 operator=(深拷贝)。

所以:是重载,且针对「Packet 对 Packet 的赋值」;成员内部的 vectoruint32_t 各有自己的赋值规则。

1. 浅拷贝 vs 深拷贝(C++ 对象层)

浅拷贝(危险,对裸指针):

a.raw_ ──┐

├──→ [同一块堆内存]

b.raw_ ──┘

深拷贝(Packet + vector 的期望):

a.data_ ──→ [堆块 A,1024 字节]

b.data_ ──→ [堆块 B,1024 字节,内容相同]

 拷贝时 CPU 在干什么(和「上下文切换」的区别)

普通内存拷贝(例如 vector 深拷贝 1MB):

  1. CPU 从源地址 load 到寄存器/缓存

  2. 再 store 到目标地址

  3. 可能触发 cache line 填充、内存带宽占用

  4. 不会 因为「拷贝 1MB」就做一次 上下文切换

上下文切换(context switch) 是另一回事:

  • 操作系统把 CPU 从 线程 A 换到 线程 B

  • 要保存/恢复寄存器、切换页表、可能 cache 失效

  • 常见原因:时间片用完、阻塞 I/O、sleep、锁竞争

2 委托构造 delegating constructor

委托构造是 C++11 特性:一个构造函数在初始化列表里调用本类另一个构造函数,避免重复写 data_(len), seq_(seq)。

概念示例(讲解用,不是你文件里的现码):

class Packet {
public:
    // 主构造:真正干活的
    Packet(size_t len, uint32_t seq) : data_(len), seq_(seq) {}
    // 委托:seq 默认为 0,转交给上面那个
    Packet(size_t len) : Packet(len, 0) {}
    // 委托:默认长度 0
    Packet() : Packet(0, 0) {}
};
要点:

委托必须写在 成员初始化列表里:Packet(len) : Packet(len, 0) {}
被委托的构造负责初始化所有成员;委托方 不能再 初始化已委托的成员(C++ 规则)
用途:减少重复、统一 invariant(所有路径都经过同一个「主构造」)
你文件里的 Packet(/* args */) 若是想表达「默认包」,更现代的写法通常是委托到 Packet(0, 0) 或 = default 默认构造,而不是空函数体(空函数体对 vector、seq_ 的初始化语义和委托不同,这是后话)。

3  = default / = delete

= default:我要编译器生成默认实现
class Packet {

public:

Packet() = default;

~Packet() = default;

Packet(const Packet&) = default; // 默认拷贝构造

Packet& operator=(const Packet&) = default; // 默认拷贝赋值

Packet(Packet&&) = default; // 默认移动构造

Packet& operator=(Packet&&) = default; // 默认移动赋值

};

用途:

  • 语义就是「成员该怎么拷贝/移动/析构,全交给成员(如 vector)」
  • 在类内 声明 时写 = default,有时比完全省略更清晰(尤其混合自定义构造时)
  • Rule of Zero 的显式写法:五个特殊函数都 default,你只写业务构造函数

= delete:禁止某种操作

class Packet {

public:

Packet(const Packet&) = delete; // 禁止拷贝构造

Packet& operator=(const Packet&) = delete; // 禁止拷贝赋值

};

效果:

Packet a(1024);

Packet b = a; // 编译错误:拷贝构造已 delete

b = a; // 编译错误:拷贝赋值已 delete

用途:单例、独占资源、unique_ptr 成员、「只能移动不能拷贝」的类型。

unique_ptr:不可拷贝、只可移动(Rule of Five 的典型)

std::unique_ptr<T> 独占 堆上对象:拷贝被 delete,移动 allowed。

#include <memory>

std::unique_ptr<int> p1 = std::make_unique<int>(42);

// std::unique_ptr<int> p2 = p1; // 错误:拷贝构造 = delete

std::unique_ptr<int> p2 = std::move(p1); // OK:移动

// 此时 p1 == nullptr,p2 拥有那块 int

若类里有一个 unique_ptr 成员:

class Connection {

std::unique_ptr<Socket> sock_;

public:

// 编译器生成的拷贝构造会尝试拷贝 sock_ → unique_ptr 拷贝被 delete → 类整体不可拷贝

Connection(Connection&&) = default; // 移动 OK

Connection& operator=(Connection&&) = default;

Connection(const Connection&) = delete;

Connection& operator=(const Connection&) = delete;

};

这就是 Rule of Five 在实践中的样子:

成员类型 拷贝 移动 类通常策略

vector

深拷贝

转移

Rule of Zero,= default 即可

unique_ptr

禁止

转移

类也 不可拷贝、可移动,常显式 delete 拷贝

对比你的 Packet

std::vector<uint8_t> data_; // 可拷贝、可移动 → Packet 默认可拷贝可移动

// 若改成:

// std::unique_ptr<uint8_t[]> data_; // Packet 会天然「不可拷贝」

Rule of Three / Five / Zero 是什么?

都是关于:类管理资源时,要不要自己写「析构 / 拷贝 / 移动」 的经验法则。

Rule of Three(三条规则,C++98 时代)

如果你必须自定义下面任意一个:

  1. 析构函数 ~T()
  2. 拷贝构造函数 T(const T&)
  3. 拷贝赋值 operator=(const T&)

那通常 三个都要自己写(或明确 = default / = delete)。

原因:默认拷贝可能是浅拷贝,和自定义析构(如 delete[])配在一起会 double free。

Rule of Five(五条规则,C++11 起)

在 Three 基础上,加上:

  1. 移动构造函数 T(T&&)
  2. 移动赋值 operator=(T&&)

否则移动可能仍走拷贝,性能差;或语义不对。

Rule of Zero(零规则,现代 C++ 推荐)

一个资源都不用手管,成员全是 vectorstringunique_ptr 等 RAII 类型:

  • 五个函数都不手写;
  • 编译器生成的默认版本会逐个成员正确析构/拷贝/移动。

你的 Packet 若只有 vector + uint32_t → 适合 Rule of Zero。
若成员有 unique_ptr → 类往往不可拷贝、只可移动(见 unique.cpp)。

Rule of Three: 自己管资源 → 常要写 析构 + 拷贝×2

Rule of Five: 再加 移动×2

Rule of Zero: 成员都是 RAII → 五个都不写

4 public/private/protected/explicit

1. public / private / protected

关键字 谁能访问

private

仅本类(+ 友元)

protected

本类 + 派生类

public

任何人

封装要回答的问题不是「能不能访问」,而是:

  • 不变量(invariant) 谁保证?例如:Packet 长度、checksum 与 payload 一致。
  • 实现细节 能否以后换?例如:vector 换成内存池,调用方不应感知。

3. explicit——防止「不小心构造出来」

explicit Packet(size_t len); // 禁止 Packet p = 1024;

Packet p(1024); // OK

网关里常见:Packet(size_t len)Port(int) 若允许隐式转换,容易在重载、容器插入时 静默出错。
学习动作:对你课表里的每个单参数构造,问一句:「隐式转换会不会在生产代码里埋雷?」

封装层次(从外到内):

public 接口  ←── 外部用户唯一可见的"契约"
    │
protected  ←── 子类可扩展,外部不可见
    │
private  ←── 内部实现细节,连子类都不信任
    │
friend  ←── "白名单":特定类/函数可穿透 private(不是 public!)

1. 友元(friend)是什么?

友元是 C++ 里一种 「打破封装,但只允许特定对象访问」 的机制。

默认:private 只有 本类自己的成员函数 能访问。

加上友元后:你点名的一个函数或类 也可以访问 private / protected 成员。

class Packet {

private:

uint32_t magic_;

uint32_t length_;

std::vector<uint8_t> payload_;

friend class ProtocolParser; // 只有 ProtocolParser 能碰 private

};

class ProtocolParser {

public:

Packet parse(const uint8_t* buf, size_t n) {

Packet p;

p.magic_ = read_u32(buf); // OK:友元允许

p.length_ = read_u32(buf + 4);

// ...

return p;

}

};

void handler() {

Packet p;

// p.magic_ = 1; // 错误:Handler 不是友元,不能访问

}

可以把它理解成:

对比 含义

public

谁都能访问

private + getter/setter

谁都能通过你提供的接口访问

friend

只有你信任的那一个类/函数 能直接访问内部字段

为什么要用?
在网关里,解析器需要一次性写入很多 header 字段;若每个字段都写 setter,代码又长又慢。友元表示:解析是 Packet 的内部事务,只信任 Parser。

代价: Packet 和 ProtocolParser 绑紧了——Parser 知道 Packet 内部长什么样,改字段名两边都要改。

友元也可以是 函数:

class Packet {

friend Packet parsePacket(const uint8_t* buf, size_t n);

};

 数据流:网络字节流 → Parser → Packet → Handler

你还不熟悉这条链,用 收一个 TCP 包 的例子从头到尾走一遍。

现实中发生了什么?

  1. 网卡收到电信号,内核 TCP 栈拼成一段 字节(byte stream)。

  2. 你的网关程序 read() / recv() 读到内存里,例如:

    [0xDE 0xAD 0x00 0x00 0x00 0x10 ... payload ...]

    \____ magic ____/ \_ len _/ \___ 16 字节数据 ___/

  3. 这段 raw bytes 还不是 C++ 对象,只是 uint8_t buf[256]

各角色干什么?

┌─────────────────┐

│ 网络 / 内核 │ 字节流:0xDE 0xAD 0x00 0x10 ...

└────────┬────────┘

│ recv() 读到用户态 buffer

┌─────────────────┐

│ ProtocolParser │ 按协议格式「解读」字节:magic 对不对?length 多少?

│ (解析器) │ 校验 checksum,切出 payload

└────────┬────────┘

│ 填好字段

┌─────────────────┐

│ Packet │ 有类型的 C++ 对象:magic_, length_, payload_

│ (数据包对象) │ 带 invariant:length == payload.size()

└────────┬────────┘

│ 交给业务

┌─────────────────┐

│ 业务 Handler │ 根据 seq/cmd 做路由、鉴权、转发

│ (业务处理) │ 只关心「这是什么命令、内容是什么」

└─────────────────┘

为什么要分 Parser 和 Handler? --- 解耦合

  • Parser:懂 二进制格式(大端/小端、字段偏移、版本号)。
  • Handler:懂 业务逻辑(登录、心跳、转发到哪个服务)。

分开后:协议改版只动 Parser;业务改版只动 Handler。

Getter 和 Setter 是什么?

对 private 成员,对外提供 读 / 写 的小函数,而不是直接暴露字段。

Getter(读)

class Packet {

private:

uint32_t magic_;

std::vector<uint8_t> payload_;

public:

uint32_t magic() const { return magic_; } // 读 magic

const std::vector<uint8_t>& payload() const { return payload_; } // 读 payload(不拷贝)

size_t payloadSize() const { return payload_.size(); }

};

  • 名字常见:magic()getMagic()(团队风格不同)
  • 后面加 const 表示:这个函数 不会修改对象(只读)

业务 Handler 用法:

void onPacket(const Packet& p) {

if (p.magic() != 0xDEAD) return;

auto& data = p.payload(); // 只读访问

}

Setter(写)

class Packet {

public:

void setMagic(uint32_t m) { magic_ = m; }

void setPayload(std::vector<uint8_t> pl) {

payload_ = std::move(pl);

length_ = static_cast<uint32_t>(payload_.size()); // 保持 invariant

}

};

  • Setter 可以带校验:长度超限、magic 非法就拒绝
  • 这就是封装的好处:改内部状态必须经过你的规则

Getter / Setter / 友元 怎么选?

方式 谁用 特点

public 字段

所有人直接改

简单,易出错,不推荐生产

getter + setter

外部按需读写

接口清晰,可在 setter 里校验

只有 getter,无 setter

Handler 只读

解析后不允许随便改 header

友元 Parser

只有 Parser 能写 private

解析路径快、集中,Handler 仍用 getter 读

5 移动语义

#include <memory>
#include <cstring>
#include <algorithm>

class Packet {
    std::unique_ptr<uint8_t[]> data_;
    size_t len_;
    uint32_t seqNum_;      // 序列号
    uint16_t flags_;       // 标志位

public:
    // 1. 默认构造
    Packet() : data_(nullptr), len_(0), seqNum_(0), flags_(0) {}

    // 2. 普通构造
    explicit Packet(size_t len, uint32_t seq = 0) 
        : data_(std::make_unique<uint8_t[]>(len)), 
          len_(len), 
          seqNum_(seq), 
          flags_(0) {
        std::memset(data_.get(), 0, len);
    }

    // 3. 拷贝构造(深拷贝)— 左值用
    Packet(const Packet& other)
        : data_(std::make_unique<uint8_t[]>(other.len_)),
          len_(other.len_),
          seqNum_(other.seqNum_),
          flags_(other.flags_) {
        std::memcpy(data_.get(), other.data_.get(), len_);
    }

    // 4. 拷贝赋值
    Packet& operator=(const Packet& other) {
        if (this != &other) {
            // 拷贝并交换惯用法(Copy-and-Swap)
            Packet tmp(other);      // 拷贝
            swap(tmp);              // 无异常交换
        }
        return *this;
    }

    // 5. 移动构造(重点!)— 右值用
    Packet(Packet&& other) noexcept          // ✅ noexcept 必须!
        : data_(std::move(other.data_)),     // ✅ 偷走指针
          len_(other.len_),                  // ✅ 复制标量
          seqNum_(other.seqNum_),
          flags_(other.flags_) {
        // ✅ 关键:置空源对象!
        other.len_ = 0;
        other.seqNum_ = 0;
        other.flags_ = 0;
        // other.data_ 已经被 unique_ptr 的移动构造置空了,不需要手动处理
    }

    // 6. 移动赋值
    Packet& operator=(Packet&& other) noexcept {
        if (this != &other) {
            data_ = std::move(other.data_);  // 释放原有资源,偷走新的
            len_ = other.len_;
            seqNum_ = other.seqNum_;
            flags_ = other.flags_;
            
            other.len_ = 0;                  // 置空源对象
            other.seqNum_ = 0;
            other.flags_ = 0;
        }
        return *this;
    }

    // 辅助:交换(用于拷贝赋值)
    void swap(Packet& other) noexcept {
        using std::swap;
        swap(data_, other.data_);
        swap(len_, other.len_);
        swap(seqNum_, other.seqNum_);
        swap(flags_, other.flags_);
    }

    // 7. 析构 — unique_ptr 自动处理,无需手写

    // 访问器
    size_t size() const { return len_; }
    uint8_t* data() { return data_.get(); }
    const uint8_t* data() const { return data_.get(); }
    bool empty() const { return len_ == 0; }
};

五、std::move 是什么?

不是「搬内存」,而是 static_cast<T&&>:把表达式变成「可以移动」的右值引用。

Packet a(1024);

Packet b = std::move(a); // 告诉编译器:可以动 a 了,用移动构造

注意:

  • std::move 本身不移动任何东西,只改 值类别;
  • 真正移动发生在 移动构造/移动赋值 被选中时;
  • 对 std::vector 这类已有移动语义的类型,通常 Rule of Zero 就够(编译器生成的移动会移动 data_)。

一、右值引用 && 到底是什么?

1. 先从「表达式」分两类:左值 vs 右值

C++ 里,不是「变量在左、常量在右」,而是看 能不能取地址、有没有名字、是不是临时量。

类型 直觉 例子

左值 (lvalue)

有名字、能反复用、&x 合法

Packet a;a*ptr

右值 (rvalue)

临时、快消失、一般不能 &

42Packet(1024)std::move(a) 的结果

Packet a(1024);

Packet b = a; // a 是左值 → 拷贝

Packet c = Packet(4096); // Packet(4096) 是临时右值 → 可移动

Packet d = std::move(a); // move 后表达式是右值 → 移动

右值 ≈ 「这个对象我马上就要扔,你可以把它的资源偷走」。


2. 左值引用 & vs 右值引用 &&

void foo(Packet& p); // 只接受左值:必须是有名字的变量

void bar(Packet&& p); // 只接受右值:临时对象、move 后的结果

引用 绑定对象 典型用途

T&

左值

读/改已有对象,不拷贝

const T&

左值 或 右值

万能 const 引用(右值也能绑,但只能读)

T&&

仅 右值

移动构造、移动赋值、「我要偷资源」

Packet a(1024);

foo(a); // OK

foo(Packet(1024)); // 错误:临时量是右值,不能绑到 Packet&

bar(Packet(1024)); // OK:右值绑到 Packet&&

bar(std::move(a)); // OK:显式把 a 当右值

// bar(a); // 错误:a 是左值,不能绑到 Packet&&


3. && 在声明里的两种含义(容易混)

含义 A:右值引用类型(参数、变量)

void consume(Packet&& p); // p 是右值引用

Packet&& ref = Packet(1024); // 少见;ref 实际是左值(有名字了!)

含义 B:移动构造函数里的「引用折叠」

Packet(Packet&& other) noexcept // 移动构造:other 是右值引用

: data_(std::move(other.data_)) {}

这里 Packet&& 表示:这个构造函数专门接管「别人不要了」的那个 Packet。


4. 为什么需要 &&?—— 区分拷贝和移动

// 拷贝:other 还要活着,必须深拷贝

Packet(const Packet& other) : data_(other.data_) {}

// 移动:other 快死了,可以偷 data_ 内部的堆指针

Packet(Packet&& other) noexcept : data_(std::move(other.data_)) {}

若没有 && overload,右值也会走 const Packet& 拷贝构造 → 大报文仍然深拷贝。


5. std::move 做了什么?(不 move,只 cast)

std::move(a) // 等价于 static_cast<Packet&&>(a)

  • 不移动任何东西,只把 a 从「左值表达式」变成「右值表达式」;
  • 真正移动发生在:移动构造 / 移动赋值 被调用时。

Packet b = std::move(a); // 1. move → 右值 2. 移动构造偷资源 3. a 进入 moved-from


6. moved-from 状态

移动之后,源对象 仍析构,必须处于 合法但未指定 状态:

Packet a(1'000'000);

Packet b = std::move(a);

// b:拥有原来那 1MB

// a:data_ 通常是空 vector,但标准不保证;安全做法:只析构/赋值为空,不再当「正常包」用

1. 「裸缓冲区」是什么意思?

不用 vector/string 帮你管内存,自己持有一块堆内存的 原始指针 + 长度:

class PacketRaw {

uint8_t* data_; // 裸指针:指向堆上 byte 数组

size_t size_;

size_t capacity_;

public:

PacketRaw(size_t n) : data_(new uint8_t[n]), size_(n), capacity_(n) {}

~PacketRaw() { delete[] data_; } // 你必须自己释放

// 若不写移动,默认拷贝只复制指针 → 浅拷贝 → double free

// 所以必须手写移动构造/移动赋值/析构(Rule of Five)

};

对比 vector<uint8_t> data_ 裸缓冲区 uint8_t* data_

谁分配/释放

vector

你自己 new[] / delete[]

默认拷贝

深拷贝(安全)

浅拷贝(危险)

Rule

Rule of Zero 够用

Rule of Five,必须手写移动

「裸」= 没有智能包装,直接操作指针。


2. 缓冲区「在哪里」?

PacketRaw 对象(通常在栈上)

┌──────────────────┐

│ data_ ──────────┼──→ 堆 (heap): [ byte0 | byte1 | ... ] ← 真正的大报文在这里

│ size_ │

│ capacity_ │

└──────────────────┘

  • 对象本身:栈或别的对象成员里;
  • 大报文字节:data_ 指向的 堆 上;
  • 移动:把 data_ 指针「转给」新对象,源对象 data_ = nullptr(典型写法)。

RVO / NRVO 是什么?怎么知道有没有发生?

1. 名字

缩写 全称 场景

RVO

Return Value Optimization

return 临时对象

NRVO

Named RVO

return 有名字的局部变量

// RVO:返回无名临时量

Packet makeA() {

return Packet(1024); // 直接在调用方栈槽构造,无拷贝无移动

}

// NRVO:返回同名局部对象

Packet makeB() {

Packet p(1024);

// ... 可能修改 p ...

return p; // 常见优化:直接在 caller 的 p 位置构造

}

优化含义:省略拷贝/移动,在 调用方返回目的地 直接构造对象。


2. 和移动的关系

Packet make() {

Packet p(1'000'000);

return p;

}

Packet q = make();

情况 发生什么

有 NRVO

p 直接在 q 的内存构造 → 无深拷贝、无移动

无 NRVO(C++17 起仍常见兜底)

return p → 移动 p 到 q → 仍无 1MB memcpy

很老编译器 + 无移动

可能 深拷贝 1MB(现在少见)

移动是 NRVO 失败时的优秀后备;RVO/NRVO 成功时更好。


3. 你怎么「知道」是 RVO/NRVO 还是移动?

方法 1:看汇编 / 编译器报告(最可靠)

g++ -std=c++17 -O2 -fno-elide-constructors move_demo.cpp -S -o move_demo.s

# -fno-elide-constructors 故意关掉 RVO,对比有无移动调用

g++ -std=c++17 -O2 move_demo.cpp -S -o move_demo_opt.s

# 默认 -O2 常能看 NRVO:没有 memcpy 1MB,没有调用移动构造

在 -O2 且 未 使用 -fno-elide-constructors 时,简单 return local 常 看不到 移动构造函数调用。

方法 2:加日志(学习用)

Packet(const Packet&) { std::cout << "copy\n"; }

Packet(Packet&&) { std::cout << "move\n"; }

Packet make() {

Packet p(1024);

return p;

}

// Debug -O0:可能打印 move 或 copy

// Release -O2:可能什么都不打印(NRVO)

Debug / -O0 常禁用 elision → 看到 move 不等于 Release 也会 move。
Release -O2 什么都不打印 → 很可能是 RVO/NRVO。

方法 3:C++17 标准保证(记这个就够)

对 纯右值 prvalue 返回:

return Packet(1024); // C++17 起:即使没 RVO,也禁止多余拷贝(保证一次构造)

对 局部左值 return p;:NRVO 允许但非必须;失败则 必须移动(有移动构造时),不能拷贝 unless 真的需要。


4. 一张决策图

return 大 Packet

编译器 -O2?

┌────┴────┐

是 否 (-O0 / -fno-elide-constructors)

│ │

▼ ▼

NRVO/RVO? 常看到 move 或 copy(便于观察)

┌─┴─┐

成功 失败

│ │

▼ ▼

零额外 移动构造(vector 偷指针,仍无 1MB 拷贝)

构造

一、RAII 有哪些?哪些明显不是?

RAII 是什么(一句话)

Resource Acquisition Is Initialization:在对象构造时取得资源,在析构时自动释放;资源生命周期 = 对象生命周期。

{

Packet p(1024); // 构造:vector 在堆上拿内存

} // 析构:vector 自动 release


常见 是 RAII 的类型(Modern C++ 里应优先用)

类型 管理的资源 析构时

std::vector<T>

动态数组(堆)

释放元素 + 内存

std::string

字符缓冲区

释放

std::unique_ptr<T>

独占堆对象

delete

std::shared_ptr<T>

共享堆对象

引用计数为 0 时释放

std::fstream / std::ifstream

文件句柄

关闭文件

std::lock_guard / std::unique_lock

互斥锁

解锁

std::FILE* 的包装(或用 fstream

C 文件

关闭

std::thread(join 在析构)

线程

join/detach 策略

socket 的 RAII 包装类(自定义)

fd

close()

std::optionalstd::variant

不直接管 OS 资源,但管对象状态

按规则析构内部

你的 Packet 里:

std::vector<uint8_t> data_; // RAII ✓

std::unique_ptr<int> ptr1_; // RAII ✓

→ 符合 Rule of Zero:不用手写析构/移动/拷贝。


明显 不是 RAII 的(或本身不管释放)

东西 为什么不是 RAII

裸指针 T*

只存地址,析构时 不会 delete

malloc 返回的 void*

必须 free,和对象生命周期无关

整数 fd int sock_fd_

只是一个数,析构时 不会 close

C 数组 uint8_t buf[1024](栈上)

随栈帧消失,但不叫「取得/释放资源」那种 RAII

new 出来的指针存成员但不 delete

反模式

引用 T&

不拥有资源

std::span / 指针+长度视图

不拥有 内存,只「借用」

迭代器

不拥有容器

普通 int / bool / enum

无资源

反例(典型非 RAII,网关里要避免):

class BadPacket {

uint8_t* data_; // 裸指针:谁 delete?→ 必须手写 Rule of Five

int fd_; // socket fd:谁 close?→ 必须手写析构

};


灰色地带(要会辨认)

类型 说明

std::shared_ptr

是 RAII,但 共享所有权;网络连接通常用 unique_ptr 更合适

std::weak_ptr

不延长生命周期,辅助 RAII,不单独管资源

内存池里的一块

池本身可能是 RAII;uint8_t* 从池取出后,归还责任在你,不算完整 RAII

std::move_only_function 等

管 callable 资源,算 RAII

记法:问自己——这个对象析构时,资源会不会自动、确定地释放? 会 → RAII;不会 → 你还得写析构或别用。

6 C++20新重载

Modern 运算符重载要点清单

运算符 Modern 写法 注意

==

friend bool operator==(const T&, const T&) = default;

C++20 可与 <=> 联动

<=>

friend auto operator<=>(const T&, const T&) = default;

成员逐个可比较时最省事

<<

非成员 friend,返回 ostream&

调试用,别泄露敏感 payload

拷贝

= delete(网络类)

替代 Java「禁止克隆」的显式表达

移动

= default; + noexcept

容器友好,标准库会优化

不要 学老式写法:

bool operator==(const Packet& other) const { ... } // 可以,但 C++20 更常 symmetry 用 friend + = default

不要 对 moved-from 对象做业务假设;比较仅用于调试或未移动的对象。

6 网关协议栈继承图

[ 字节流 / Socket ]

TransportHandler → TcpHandler / UdpHandler

Packet 对象

ApplicationHandler → HttpHandler / WebSocketHandler

业务路由 / 鉴权

GatewayCore::dispatch() ← final,不允许被继承篡改

7 虚函数与接口设计

2. 抽象基类 vs 接口类

类型 典型内容 网关例子

抽象基类 ABC

纯虚 + 部分已实现(process() 模板方法)

ProtocolHandler 带 validate() 默认实现

接口类 Interface

仅 虚析构 + 纯虚函数,无数据成员/无实现

IProtocolHandler 只有 handle() / name()

// 接口类(Modern 网关常见)

class IProtocolHandler {

public:

virtual ~IProtocolHandler() = default;

virtual bool handle(Packet& pkt) = 0;

virtual std::string_view name() const noexcept = 0;

IProtocolHandler(const IProtocolHandler&) = delete;

IProtocolHandler& operator=(const IProtocolHandler&) = delete;

protected:

IProtocolHandler() = default; // 禁止外部直接构造接口

};

接口类要点:多态边界清晰;实现类 TcpHandler 承担全部状态(socket、缓冲区等)。
override — 强制编译器检查
class TcpHandler : public IProtocolHandler {
public:
    bool handle(Packet& pkt) override;           // ✓ 确实重写基类虚函数
    std::string_view name() const noexcept override;
    // bool handle(Packet pkt) override;         // ✗ 签名错了,编译失败(没有 override 可能静默隐藏)
};
没有 override	有 override
写错参数/const 可能变成 新函数,多态不生效
签名不匹配 → 编译错误
虚析构 — 为什么必须 virtual?
规则:只要类会通过 基类指针/引用 删除或销毁派生对象,基类析构必须是 虚函数。

IProtocolHandler* p = new TcpHandler(...);
delete p;  // 若 ~IProtocolHandler 非 virtual → 只调基类析构 → 派生类资源泄漏
unique_ptr<Base> 同理:

std::unique_ptr<IProtocolHandler> p = std::make_unique<TcpHandler>(...);
// p 析构时调用 delete,内部类型是 TcpHandler,但 delete 表达式是 Base*
// → 需要 virtual ~Base() 才能 ~TcpHandler() → ~TcpHandler 成员
C++ 标准对 unique_ptr<Derived> 转 unique_ptr<Base> 有特殊 deleter,但 接口设计仍应虚析构;手写 delete base_ptr 和旧代码路径都会踩坑。

7 静态多态 CRTP

二、虚函数多态 vs CRTP 静态多态
虚函数(动态多态)	CRTP(静态多态)
绑定时机
运行期 查 vtable
编译期 知道真实类型 Derived
调用
handler->handle(pkt) → 间接跳转
handler.handle(pkt) → 常可 内联 到 TcpHandler::handle
开销
vptr + 虚表查找 + 难内联
无 vtable;可能 零抽象开销
类型
运行时多种 Handler 混在一个 vector<unique_ptr<Base>>
每种 Handler 独立类型;混用要 std::variant / 模板 / 宏展开
典型场景
插件、运行时注册、类型运行时才知道
网关热路径、模板已知、追求极致吞吐
网关直觉:

控制面 / 插件化 → 虚函数 + 接口类。
数据面 / 每包都 dispatch → CRTP 或 if (proto==TCP) + 特化,避免每包 vtable。
三、CRTP 长什么样?
核心模式
template <typename Derived>
class ProtocolHandlerCrtp {
public:
    bool process(Packet& pkt) {
        // 编译期已知 Derived → 可内联 Derived::handle
        return static_cast<Derived*>(this)->handle(pkt);
    }
    std::string_view name() const {
        return static_cast<const Derived*>(this)->name();
    }
};
class TcpHandlerCrtp : public ProtocolHandlerCrtp<TcpHandlerCrtp> {
public:
    bool handle(Packet& pkt) { /* TCP 逻辑 */ return true; }
    std::string_view name() const { return "TCP"; }
};
为什么是 TcpHandler : public Base<TcpHandler>?

基类模板参数是 派生类自己 → 基类方法里能 static_cast<Derived*>(this) 调派生实现;
名字 Curiously Recurring:派生类声明时基类已经「知道」派生类型(编译期)。
static_cast 在这里:
不是绕过类型系统,而是告诉编译器:「这个 Base<TcpHandler>* 实际就是 TcpHandler」。
若 Derived 写错(class Bad : public Base<Other>),属于 未定义行为。

四、虚函数版网关处理器(完整示例)
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
struct Packet {
    std::size_t len{0};
};
class IProtocolHandler {
public:
    virtual ~IProtocolHandler() = default;
    virtual bool handle(Packet& pkt) = 0;
    virtual std::string_view name() const = 0;
};
class TcpHandlerV : public IProtocolHandler {
public:
    bool handle(Packet& pkt) override {
        pkt.len += 4;  // 模拟解析头
        return true;
    }
    std::string_view name() const override { return "TCP"; }
};
class UdpHandlerV : public IProtocolHandler {
public:
    bool handle(Packet& pkt) override {
        pkt.len += 2;
        return true;
    }
    std::string_view name() const override { return "UDP"; }
};
class GatewayVirtual {
public:
    void registerHandler(std::unique_ptr<IProtocolHandler> h) {
        handlers_.push_back(std::move(h));
    }
    // 热路径:对每个 handler 虚调用
    __attribute__((noinline))
    std::size_t dispatchAll(Packet& pkt) {
        std::size_t sum = 0;
        for (auto& h : handlers_)
            sum += h->handle(pkt) ? pkt.len : 0;
        return sum;
    }
private:
    std::vector<std::unique_ptr<IProtocolHandler>> handlers_;
};
特点:运行时往 vector 里塞不同 Handler;h->handle(pkt) 一定走虚函数(除非编译器去虚化并证明类型唯一,很难跨 vector)。

五、CRTP 版网关处理器(完整示例)
#include <iostream>
#include <string>
#include <string_view>
struct Packet {
    std::size_t len{0};
};
template <typename Derived>
class ProtocolHandlerCrtp {
public:
    __attribute__((noinline))
    bool process(Packet& pkt) {
        return static_cast<Derived*>(this)->handle(pkt);
    }
    std::string_view name() const {
        return static_cast<const Derived*>(this)->name();
    }
};
class TcpHandlerC : public ProtocolHandlerCrtp<TcpHandlerC> {
public:
    bool handle(Packet& pkt) {
        pkt.len += 4;
        return true;
    }
    std::string_view name() const { return "TCP"; }
};
class UdpHandlerC : public ProtocolHandlerCrtp<UdpHandlerC> {
public:
    bool handle(Packet& pkt) {
        pkt.len += 2;
        return true;
    }
    std::string_view name() const { return "UDP"; }
};
// 编译期固定两种 Handler(静态多态常见写法)
class GatewayCrtp {
public:
    __attribute__((noinline))
    std::size_t dispatchAll(Packet& pkt) {
        std::size_t sum = 0;
        sum += tcp_.process(pkt) ? pkt.len : 0;
        sum += udp_.process(pkt) ? pkt.len : 0;
        return sum;
    }
private:
    TcpHandlerC tcp_;
    UdpHandlerC udp_;
};
// 若要在编译期遍历多种类型,可用 std::tuple + std::apply(C++17)
特点:tcp_.process(pkt) 编译期类型是 TcpHandlerC → handle 可内联进 dispatchAll 循环体。

六、汇编层面对比(你要看什么)
用 -O2 -std=c++17 编译,对比 dispatchAll 循环(可用 Compiler Explorer 或本地 g++ -S)。

虚函数版(典型)
; 伪汇编示意
mov     rax, [handler]      ; 取对象地址
mov     rax, [rax]          ; 取 vptr
call    [rax + OFFSET]      ; 间接调用 handle — 无法内联跨编译单元
标志:

两次内存 load(对象 → vtable)
call *reg 间接跳转
handle 体 不在 dispatchAll 里展开
CRTP 版(典型,-O2)
; 伪汇编示意 — handle 逻辑可能直接出现在循环里
add     DWORD PTR [pkt+len], 4    ; TcpHandler::handle 内联
add     DWORD PTR [pkt+len], 2    ; UdpHandler::handle 内联
标志:

无 vptr、无间接 call
热路径变成 直线代码 + 寄存器,对 CPU 分支预测、I-cache 更友好
量化直觉(数量级,非绝对)
场景	相对成本
虚调用 + 难内联
每次 dispatch 多 数 ns~数十 ns + 阻止周围优化
CRTP 全内联
接近手写 if (tcp) ... else if (udp) ...
百万 PPS 网关
每包省一次间接调用 → CPU 占用明显下降
注意:若虚函数版只有 一种 派生类且整个 dispatchAll 在同一 TU 且 LTO,编译器偶尔能 去虚化;但 vector<unique_ptr<IHandler>> 多态混用通常 去不掉。

七、如何自己测性能差异(概念 + 命令)
两段 相同逻辑 的 micro-benchmark(学习用):

// 公共:循环 N 次 dispatch
constexpr int N = 10'000'000;
Packet pkt{0};
// Virtual
GatewayVirtual gw;
gw.registerHandler(std::make_unique<TcpHandlerV>());
gw.registerHandler(std::make_unique<UdpHandlerV>());
for (int i = 0; i < N; ++i) gw.dispatchAll(pkt);
// CRTP
GatewayCrtp gw2;
for (int i = 0; i < N; ++i) gw2.dispatchAll(pkt);
g++ -std=c++17 -O2 -DNDEBUG bench_virtual.cpp -o bench_v
g++ -std=c++17 -O2 -DNDEBUG bench_crtp.cpp -o bench_c
time ./bench_v
time ./bench_c
# 或 perf stat -r 5 ./bench_v
预期(O2、小 handle、多态 vector):CRTP 常快 1.2x~3x 甚至更多;
若 handle 很重(解析 HTTP),差距会 缩小(瓶颈在业务而非 vcall)。

工具	看什么
time
总墙钟时间
perf stat
branch-misses、instructions(CRTP 往往指令更少)
g++ -S
有没有 call *
八、CRTP 在网关/交换机里的典型用法
1. 静态 Handler 链(数据面)
template <typename... Handlers>
class Pipeline {
    std::tuple<Handlers...> chain_;
public:
    bool run(Packet& p) {
        bool ok = true;
        std::apply([&](auto&... h) {
            ((ok = ok && h.process(p)), ...);  // C++17 fold
        }, chain_);
        return ok;
    }
};
using GatewayPipe = Pipeline<TcpHandlerC, HttpParserC, AuthC>;
编译期展开 → 无 vtable,类似手写流水线。

2. 混入(Mixin)能力
template <typename Derived>
class StatsMixin {
public:
    void bump() { static_cast<Derived*>(this)->stats_.packets++; }
};
class TcpHandlerC : public ProtocolHandlerCrtp<TcpHandlerC>,
                    public StatsMixin<TcpHandlerC> { ... };
3. final + CRTP 防继承链被改
class TcpHandlerC final : public ProtocolHandlerCrtp<TcpHandlerC> { ... };
九、何时用虚函数、何时用 CRTP(决策表)
需求	选
运行时加载 .so 插件
虚函数
Handler 类型编译期固定、追求 PPS
CRTP
多种 Handler 存在同一容器且运行时增减
虚函数(或 std::variant + visit 静态分发)
交换机每包固定 2~3 级处理
CRTP / 手写 / variant
需要虚析构 + unique_ptr<Base>
虚函数接口
接口类 + 零开销
CRTP 或 concept + 模板
std::variant 折中:

using HandlerVariant = std::variant<TcpHandlerC, UdpHandlerC>;
void dispatch(HandlerVariant& h, Packet& p) {
    std::visit([&](auto& concrete) { concrete.process(p); }, h);
}
运行时选分支,但 visit 常编译成跳转表,仍无 vptr,比虚函数更接近 CRTP。

更多推荐