C++ 学习笔记
一份完整的 C++ 学习笔记,从语法基础到工程实践。重点讲"为什么这么设计"(费曼式 📌 讲解),配 RyBOS 项目真实代码示例。
怎么用这份笔记
- 学习/复习 → 顺序看正文,重点看 📌 类比和"为什么"
- 查 API → 翻 附录 A 速查手册
- 检验 → 做 附录 B 自测题,盖住答案自答
- 落地 → 参考 RyBOS 项目真实代码,把知识串起来
所有代码示例均来自 RyBOS 模块库(cpp/ 和 qt/ 目录),可直接 #include 使用。
图表在 /Cpp学习笔记/diagrams/ 目录。图注名即原图文件名(如图注"图 1 · C++ 编译流程"对应 图1_C++编译流程.d2 / 图1_C++编译流程.d2.svg),改图请编辑同名 .d2 源文件后运行 scripts/build-d2.ps1 重新渲染。
1 · C++ 概述与编译流程
1.1 C++ 是什么
- C++ 由 Bjarne Stroustrup 于 1979 年在贝尔实验室创建,最初叫 "C with Classes"
- 设计哲学:零开销原则(zero-overhead)——你不为你不使用的东西付费
- 标准演进:C++98 → C++11(重大变革)→ C++14 → C++17 → C++20 → C++23
- 定位:系统级编程、高性能计算、嵌入式、游戏引擎、基础设施
TIP
📌 C++ vs 其他语言一句话:Python 让你快速写出能跑的代码,Java 让你写出不会崩的代码,C++ 让你写出跑得最快的代码——但"最快"的代价是你必须自己管理一切。
1.2 编译流程(四阶段)

TIP
📌 打个比方:编译就像做菜。
- 预处理:把食材从冰箱拿出来、拆包装(展开
#include、替换 #define)
- 编译:把食材切成需要的形状、检查有没有坏的(语法分析、语义检查)
- 汇编:把切好的食材按食谱摆盘(生成机器码
.o 文件)
- 链接:把所有盘子的菜组合成一桌完整的宴席(合并多个
.o、解析外部库)
:::
| 阶段 |
做什么 |
典型错误 |
| 预处理 |
展开 #include、#define、#ifdef |
路径找不到 |
| 编译 |
语法分析 → 语义检查 → 中间代码 |
语法错误、类型不匹配 |
| 汇编 |
中间代码 → 机器码(.o) |
极少出错 |
| 链接 |
合并 .o,解析外部符号 |
undefined reference |
g++ -E main.cpp -o main.ii # 只预处理
g++ -S main.cpp -o main.s # 只编译,看汇编
g++ -c main.cpp -o main.o # 编译+汇编
g++ main.o other.o -o app # 链接
g++ main.cpp -o app # 一步到位
1.3 第一个 C++ 程序
#include <iostream>
int main()
{
std::cout << "Hello, C++!" << std::endl;
return 0;
}
:::tip
📌 #include 用 <> 还是 ""?<> 在系统目录搜索(标准库/第三方),"" 先在当前目录搜索(自己的头文件)。<> 告诉编译器"这是外来的","" 告诉编译器"先在我家找"。
1.4 C++ 与 C 的关系
| 特性 |
C |
C++ |
| 编程范式 |
面向过程 |
多范式 |
| 字符串 |
char[] |
std::string |
| 内存管理 |
malloc/free |
new/delete + 智能指针 |
| 错误处理 |
返回错误码 |
异常 try/catch |
| 泛型 |
宏或 void* |
模板 template |
C++ 调 C 库需要 extern "C":
extern "C" {
#include "some_c_library.h"
}
2 · 数据类型与变量
2.1 基本数据类型

| 类型 |
典型宽度 |
用途 |
char |
1 字节 |
字符 / 小整数 |
short |
2 字节 |
节省内存的小整数 |
int |
4 字节 |
通用整数 |
long long |
8 字节 |
64 位整数(C++11) |
float |
4 字节 |
单精度浮点(~7 位有效数字) |
double |
8 字节 |
双精度浮点(~15 位有效数字) |
bool |
1 字节 |
true / false |
TIP
📌 需要确切宽度时用 <cstdint>:
#include <cstdint>
int32_t a = 42; // 确切 32 位
uint64_t b = 100; // 确切 64 位无符号
uint8_t c = 0xFF; // 确切 8 位无符号
RyBOS 项目中大量使用 uint8_t、uint16_t、uint32_t,确保跨平台一致。
TIP
📌 浮点数精度问题:0.1 + 0.2 != 0.3,因为 0.1 在二进制中是无限循环小数。比较浮点数用误差范围:
bool almostEqual(double a, double b, double eps = 1e-9)
{
return std::abs(a - b) < eps;
}
2.2 变量初始化
int a; // 未初始化,值未定义!
int b = 10; // C 风格
int c(20); // 构造函数风格
int d{30}; // 列表初始化(C++11,推荐)
auto e = 40; // 自动类型推导
TIP
📌 为什么推荐 {} 列表初始化?
- 禁止窄化转换:
int x{3.14} 编译报错,而 int x = 3.14 默默截断
- 避免最令人头疼的解析:
int x{} 不会被误解析为函数声明
- 统一语法:基本类型、容器、对象都能用
{}
:::
2.3 const 与 constexpr
const int MAX = 100; // 运行时常量,只读
constexpr int BUF_SIZE = 256; // 编译期常量,可用于数组大小
RyBOS 的 KeyObfuscator 用 constexpr 定义编译期常量:
static constexpr uint8_t MASK_LAYER1 = 0xA7;
2.4 类型转换
// static_cast — 基本类型转换(最常用)
int i = static_cast<int>(3.14);
// dynamic_cast — 多态安全向下转换
Derived* d = dynamic_cast<Derived*>(base); // 失败返回 nullptr
// const_cast — 去掉 const
int* p = const_cast<int*>(constPtr);
// reinterpret_cast — 位模式重新解释(最危险)
long addr = reinterpret_cast<long>(ptr);
:::tip
📌 为什么用命名转换替代 C 风格 (int)d?命名转换让意图明确,编译器能帮你检查不安全的转换。RyBOS 中大量使用 static_cast:
return static_cast<uint16_t>(((value & 0xFF00) >> 8) | ((value & 0x00FF) << 8));
3 · 运算符与表达式
3.1 位运算详解
uint8_t a = 0b11001010; // C++14 二进制字面量
a & 0xFF // 按位与:掩码提取
a | 0x01 // 按位或:设置某位
a ^ 0x01 // 按位异或:翻转某位
a << 2 // 左移:乘 4
a >> 2 // 右移:除 4
TIP
📌 位运算经典用途:掩码提取 value & 0xFF、设置位 value | 0x01、清除位 value & ~0x01、翻转位 value ^ 0x01。
RyBOS 的 CommandProtocol 用位运算做字节序转换:
inline uint16_t swapEndian16(uint16_t value)
{
return static_cast<uint16_t>(((value & 0xFF00) >> 8) | ((value & 0x00FF) << 8));
}
3.2 短路求值
if (ptr != nullptr && ptr->size() > 0) // ptr 为空时不会访问 ptr->size()
&& 左边为 false 时右边不执行;|| 左边为 true 时右边不执行。这是安全特性,不只是优化。
4 · 控制流
4.1 if / else
if (score >= 90)
{
grade = 'A';
}
else if (score >= 80)
{
grade = 'B';
}
else
{
grade = 'C';
}
C++17 if with initializer:
if (auto it = map.find(key); it != map.end())
{
std::cout << "Found: " << it->second << std::endl;
}
// it 在这里不可用——限制作用域减少 bug
4.2 switch
switch (errorCode)
{
case 0:
std::cout << "OK";
break; // 不加 break 会穿透
default:
std::cout << "Unknown";
break;
}
4.3 循环
while (condition) { doSomething(); }
for (int i = 0; i < 10; ++i) { /* ... */ }
// 范围 for(C++11)
for (const auto& item : container) { /* 只读,零拷贝 */ }
for (auto& item : container) { /* 可修改 */ }
TIP
📌 范围 for 用 const auto& 还是 auto?只读用 const auto&(零拷贝),修改用 auto&,基本类型用 auto(拷贝比引用快)。
5 · 函数
5.1 参数传递

void byValue(int x); // 值传递:拷贝
void byRef(int& x); // 引用传递:可修改原值
void byConstRef(const int& x); // const 引用:只读不拷贝(黄金参数类型)
void byPtr(int* x); // 指针传递
TIP
📌 参数传递三条规则:
- 基本类型 → 值传递
- 只读大对象 →
const Type&(零拷贝 + 安全)
- 需要修改原值 →
Type&
RyBOS 中几乎所有只读参数都用 const &:
inline std::vector<std::string> split(const std::string& str, const std::string& delimiter)
5.2 默认参数与重载
void log(const std::string& msg, int level = 0, bool timestamp = true);
void print(int x);
void print(double x); // 重载:同名不同参数
TIP
📌 C++ 怎么实现函数重载?编译器做"名字修饰"(name mangling),把参数类型编码进函数名:print(int) → _Z5printi,print(double) → _Z5printd。C 没有这个机制,所以需要 extern "C" 关闭修饰。
5.3 内联函数
inline int square(int x) { return x * x; }
TIP
📌 inline 的真正含义:现代 C++ 中主要是允许函数定义出现在多个翻译单元而不冲突。这就是 Header-only 库的基础。RyBOS 的 StringUtils.h 中所有函数都是 inline。
5.4 std::optional(C++17)
std::optional<std::string> findUser(int id)
{
if (id == 1) return "Alice";
return std::nullopt;
}
RyBOS 的 FileSystem 用 std::optional 表示操作可能失败:
static std::optional<std::string> readText(const std::string& path);
static std::optional<FileInfo> getFileInfo(const std::string& path);
6 · 指针与引用
6.1 指针
int x = 42;
int* p = &x; // p 存储 x 的地址
std::cout << *p; // 解引用,输出 42
*p = 100; // 通过指针修改 x
TIP
📌 指针是什么?指针是存内存地址的变量。变量是房间,指针是写着房间号的纸条。*p 就是"按纸条找房间拿东西"。
6.2 空指针与动态内存
int* p = nullptr; // C++11 空指针
int* arr = new int[10];
delete[] arr; // new[] 配 delete[]
// 现代 C++:智能指针替代裸 new/delete
auto sp = std::make_shared<int>(42); // 自动释放
6.3 引用

| 特性 |
引用 |
指针 |
| 必须初始化 |
是 |
否 |
| 能否为空 |
否 |
是 |
| 能否重新绑定 |
否 |
是 |
| 安全性 |
高 |
低 |
经验:函数参数优先用引用(const Type&),需要"可能为空"时用指针。
6.4 移动语义(C++11)
std::string a = "hello";
std::string b = std::move(a); // 资源转移,不拷贝
TIP
📌 移动 vs 拷贝:拷贝是"复印一本书",移动是"把书递过去"。std::move 标记"我不要了,你可以偷它的资源"。
7 · 数组、向量与字符串
7.1 std::vector
std::vector<int> v;
v.push_back(1);
v.size();
v.reserve(100); // 预分配,避免多次扩容
std::vector<int> v1 = {1, 2, 3}; // 列表初始化
TIP
📌 vector 扩容:空间不够时分配更大的内存(通常 2 倍),移动旧元素,释放旧内存。倍数扩容保证 push_back 均摊 O(1)。reserve() 提前分配避免多次扩容。RyBOS 的 toHex 就用了 reserve:
result.reserve(len * (separator ? 3 : 2));
7.2 std::string 与 string_view
std::string s = "Hello";
s += " World";
s.find("World"); // 6
// C++17 string_view:零拷贝字符串视图
std::string_view sv = "Hello World";
7.3 关联容器
std::map<std::string, int> m; // 红黑树,O(log n),有序
std::unordered_map<int, std::string> um; // 哈希表,O(1),无序
// 结构化绑定(C++17)
for (const auto& [key, value] : m) { /* ... */ }
需要有序遍历用 map,只需快速查找用 unordered_map。RyBOS 的 ConfigManager 用 std::map 存储配置。
8 · 结构体与枚举
struct Point { double x, y, z; };
Point p{1.0, 2.0, 3.0};
enum class ErrorCode { OK = 0, NOT_FOUND = 1, INVALID = 40 };
ErrorCode code = ErrorCode::NOT_FOUND;
TIP
📌 struct vs class:唯一区别是默认访问权限(struct public,class private)。纯数据用 struct,有逻辑用 class。enum class 不会隐式转 int,比 enum 更安全。
RyBOS 的 DiskInfo 是纯数据结构:
struct DiskInfo
{
QString rootPath;
QString name;
qint64 bytesTotal = 0;
qint64 bytesFree = 0;
};
9 · 类与封装
9.1 类的定义
class Student
{
public:
Student(const std::string& name, int age)
: m_name(name), m_age(age) // 成员初始化列表
{
}
std::string getName() const { return m_name; }
void setAge(int age) { m_age = age; }
private:
std::string m_name; // m_ 前缀
int m_age;
};
9.2 访问控制
| 关键字 |
类内 |
派生类 |
外部 |
public |
是 |
是 |
是 |
protected |
是 |
是 |
否 |
private |
是 |
否 |
否 |
TIP
📌 封装的意义:把数据藏起来(private),只暴露接口(public)。内部实现可以自由修改,不影响使用者。RyBOS 的 ThreadPool 就是封装的典范——用户只需要 submit() 和 shutdown(),不需要知道内部有 m_workers、m_tasks、m_mutex:
class ThreadPool
{
public:
explicit ThreadPool(size_t threads = 0);
template<typename F, typename... Args>
auto submit(F&& f, Args&&... args) -> std::future<...>;
void shutdown();
private:
std::vector<std::thread> m_workers;
std::queue<std::function<void()>> m_tasks;
mutable std::mutex m_mutex;
std::condition_variable m_condition;
std::atomic<bool> m_stop;
};
9.3 const 成员函数与 mutable
class ConfigManager
{
mutable std::shared_mutex m_mutex; // const 函数也能锁
public:
std::string get(const std::string& key) const
{
std::shared_lock lock(m_mutex); // const 函数中锁 mutex
return m_config[key];
}
};
TIP
📌 mutable 的用途:const 函数需要修改某个不影响对象逻辑状态的成员(如缓存、互斥锁)。RyBOS 中大量使用 mutable std::mutex,让 const 接口也能做线程同步。
9.4 单例模式
static ConfigManager& instance()
{
static ConfigManager inst; // C++11 保证线程安全初始化
return inst;
}
TIP
📌 为什么用局部静态变量?C++11 保证局部静态变量的初始化是线程安全的。static ConfigManager inst; 在多线程下只会构造一次,不需要自己加锁。这是 Meyers' Singleton,最简洁安全的单例实现。
10 · 构造与析构
10.1 构造函数种类
class Widget
{
public:
Widget() : m_id(0), m_name("default") {} // 默认构造
Widget(int id, const std::string& name) // 参数构造
: m_id(id), m_name(name) {}
Widget(const Widget& other) // 拷贝构造
: m_id(other.m_id), m_name(other.m_name) {}
Widget(Widget&& other) noexcept // 移动构造(C++11)
: m_id(other.m_id), m_name(std::move(other.m_name)) {}
};
// 禁止拷贝
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;
// 使用编译器默认生成的
Widget() = default;
~Widget() = default;
TIP
📌 成员初始化列表 vs 函数体赋值?
- 初始化列表:
Widget() : m_name("default") {} — 直接构造,一次完成
- 函数体赋值:
Widget() { m_name = "default"; } — 先默认构造,再赋值,两次操作
对于类类型(如 std::string),初始化列表省掉一次默认构造的开销。始终优先用初始化列表。
10.2 析构与 RAII

class FileHandler
{
public:
FileHandler(const std::string& path) : m_file(std::fopen(path.c_str(), "r")) {}
~FileHandler() { if (m_file) std::fclose(m_file); } // 自动释放
private:
FILE* m_file;
};
TIP
📌 RAII(Resource Acquisition Is Initialization)是 C++ 最重要的设计理念:对象构造时获取资源,析构时释放资源。无论函数正常返回还是抛异常,局部对象的析构函数都会被调用,资源不会泄漏。
{
std::lock_guard<std::mutex> lock(m_mutex); // 构造时加锁
// ... 临界区代码
} // 离开作用域,析构时自动解锁
10.3 三/五/零法则
| 法则 |
含义 |
| 三法则 |
需要自定义析构、拷贝构造、拷贝赋值中任何一个,通常三个都需要 |
| 五法则 |
C++11 后,还需要移动构造和移动赋值 |
| 零法则 |
如果所有成员都有 RAII 语义,什么都不用写 |
RyBOS 的 ThreadPool 就 delete 了拷贝操作——线程池不应该被拷贝:
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
11 · 继承与多态
11.1 继承与虚函数
class Shape
{
public:
virtual double area() const { return 0; } // 虚函数
virtual ~Shape() = default; // 虚析构!
};
class Circle : public Shape
{
public:
Circle(double r) : m_radius(r) {}
double area() const override { return 3.14159 * m_radius * m_radius; }
private:
double m_radius;
};
11.2 多态

Shape* s = new Circle(5.0);
std::cout << s->area(); // 调用 Circle::area() → 78.54
delete s; // 虚析构确保调用 Circle 的析构函数
TIP
📌 多态为什么需要 virtual?没有 virtual,s->area() 会调用 Shape::area()(静态绑定)。有 virtual,会调用实际对象的 area()(动态绑定)。
虚函数表(vtable):编译器为每个有虚函数的类生成一张函数指针表。每个对象多存一个指向 vtable 的指针。这就是"零开销原则"——你不用多态就不付费。
虚析构为什么必须:delete s 时,如果析构不是 virtual,只调用 Shape 的析构,Circle 的析构不执行——资源泄漏。
11.3 override 与 final(C++11)
class Base
{
public:
virtual void foo() {}
virtual void bar() final {} // final:禁止子类再覆盖
};
class Derived : public Base
{
public:
void foo() override {} // override:写错会报错
// void bar() override; // 编译错误!bar 是 final
};
TIP
📌 override 为什么重要?如果你拼写错误(如 fo() 而不是 foo()),编译器以为你在定义新函数,不报错,bug 隐藏到运行时。加 override 后编译器会检查"父类是否有这个虚函数",拼写错误编译期就抓住。
12 · 模板编程
12.1 函数模板

template<typename T>
T maxValue(T a, T b)
{
return (a > b) ? a : b;
}
maxValue(3, 7); // T=int
maxValue(3.14, 2.71); // T=double
12.2 类模板
template<typename T>
class Stack
{
public:
void push(const T& value) { m_data.push_back(value); }
T pop() { T v = m_data.back(); m_data.pop_back(); return v; }
bool empty() const { return m_data.empty(); }
private:
std::vector<T> m_data;
};
Stack<int> intStack;
Stack<std::string> strStack;
12.3 模板实战:RyBOS 的 StringUtils::format
template<typename... Args>
inline std::string format(const std::string& fmt, Args... args)
{
std::ostringstream oss;
size_t argIndex = 0;
size_t pos = 0;
auto insertArg = [&](auto&& arg)
{
std::string placeholder = "{" + std::to_string(argIndex) + "}";
size_t placeholderPos = fmt.find(placeholder, pos);
if (placeholderPos != std::string::npos)
{
oss << fmt.substr(pos, placeholderPos - pos);
oss << arg;
pos = placeholderPos + placeholder.length();
argIndex++;
}
};
(insertArg(args), ...); // C++17 折叠表达式
oss << fmt.substr(pos);
return oss.str();
}
TIP
📌 模板是什么?模板是"代码生成器"——你写一份通用代码,编译器根据实际使用的类型生成具体版本。
模板 vs 继承多态:
- 模板:编译期多态(静态分发),零运行时开销,代码膨胀
- 虚函数:运行期多态(动态分发),有 vtable 开销,无代码膨胀
变参模板 + 折叠表达式(C++17):(insertArg(args), ...) 是折叠表达式,展开为 (insertArg(arg1), insertArg(arg2), ...)。这让你可以写接受任意数量参数的函数,类似 Python 的 *args。
13 · 智能指针与内存管理

13.1 为什么需要智能指针
裸 new/delete 的三大问题:
- 忘记
delete → 内存泄漏
delete 两次 → 双重释放,程序崩溃
- 异常抛出后跳过
delete → 泄漏
智能指针用 RAII 自动解决这三个问题。
13.2 std::unique_ptr — 独占所有权
#include <memory>
auto p = std::make_unique<int>(42);
// 不能拷贝,只能移动
auto p2 = std::move(p); // p 变 nullptr,p2 拥有对象
TIP
📌 unique_ptr 零开销:它本质上就是一个裸指针的薄包装,析构时自动 delete。没有引用计数、没有原子操作、没有控制块。默认首选 unique_ptr,只有需要共享所有权时才用 shared_ptr。
13.3 std::shared_ptr — 共享所有权
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // 引用计数 +1
p1.use_count(); // 2
// p1, p2 都销毁后,对象才被 delete
TIP
📌 shared_ptr 的开销:
- 控制块:额外分配一块内存存引用计数(
use_count + weak_count)
- 原子操作:引用计数的增减是原子操作(线程安全),有性能开销
- 两次内存分配:
std::make_shared 把对象和控制块合并为一次分配,更高效
循环引用陷阱:如果 A 持有 B 的 shared_ptr,B 也持有 A 的 shared_ptr,引用计数永远不归零,内存泄漏。解决方案:一方用 weak_ptr。
13.4 std::weak_ptr — 弱引用
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 不增加引用计数
if (auto locked = wp.lock()) // 提升为 shared_ptr
{
std::cout << *locked << std::endl; // 42
}
weak_ptr 不拥有对象,使用前需要 lock() 提升为 shared_ptr。如果对象已销毁,lock() 返回空。
14 · Lambda 与函数对象
14.1 Lambda 表达式(C++11)
auto add = [](int a, int b) { return a + b; };
add(3, 4); // 7
// 捕获外部变量
int x = 10;
auto addX = [x](int a) { return a + x; }; // 值捕获
auto addXRef = [&x](int a) { return a + x; }; // 引用捕获
auto captureAll = [=](int a) { return a + x; }; // 值捕获所有
auto captureAllRef = [&](int a) { return a + x; }; // 引用捕获所有
14.2 捕获方式
| 语法 |
含义 |
[] |
不捕获 |
[x] |
值捕获 x(拷贝一份) |
[&x] |
引用捕获 x |
[=] |
值捕获所有外部变量 |
[&] |
引用捕获所有外部变量 |
[=, &x] |
值捕获所有,但 x 引用捕获 |
[this] |
捕获 this 指针(成员函数中用) |
TIP
📌 Lambda 的本质:Lambda 是一个编译器自动生成的匿名类,重载了 operator()。[x] 捕获相当于这个类有一个 x 成员变量。
RyBOS 的 StringUtils::format 就用了 Lambda + 模板:
auto insertArg = [&](auto&& arg)
{
// 使用引用捕获 fmt, oss, pos, argIndex
};
14.3 std::function — 通用的可调用对象包装器
#include <functional>
std::function<int(int, int)> func;
func = [](int a, int b) { return a + b; }; // Lambda
func = std::bind(&MyClass::method, &obj, std::placeholders::_1, std::placeholders::_2); // 成员函数
int result = func(3, 4); // 7
RyBOS 的 ThreadPool 用 std::function<void()> 存储任意任务:
std::queue<std::function<void()>> m_tasks;
15 · 并发编程

15.1 std::thread
#include <thread>
void worker(int id)
{
std::cout << "Thread " << id << " running" << std::endl;
}
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); // 等待 t1 完成
t2.join(); // 等待 t2 完成
TIP
📌 join() vs detach()?
join():等待线程完成,主线程阻塞
detach():分离线程,让它在后台自行运行(守护线程)
detach() 的陷阱:分离后线程引用的局部变量可能在主线程中已销毁——悬空引用。优先用 join()。
15.2 std::mutex — 互斥锁
std::mutex m_mutex;
// 方式1:手动锁(不推荐,可能忘记 unlock)
m_mutex.lock();
sharedData.push_back(42);
m_mutex.unlock();
// 方式2:lock_guard(RAII,推荐)
{
std::lock_guard<std::mutex> lock(m_mutex); // 构造时加锁
sharedData.push_back(42);
} // 离开作用域自动解锁
// 方式3:unique_lock(更灵活,可配合 condition_variable)
std::unique_lock<std::mutex> lock(m_mutex);
lock.unlock(); // 可以手动解锁
lock.lock(); // 再锁上
15.3 std::condition_variable — 条件变量
std::mutex m_mutex;
std::condition_variable m_condition;
std::queue<Task> m_tasks;
bool m_stop = false;
// 生产者:提交任务
{
std::lock_guard<std::mutex> lock(m_mutex);
m_tasks.push(task);
}
m_condition.notify_one(); // 唤醒一个等待的线程
// 消费者:工作线程等待任务
{
std::unique_lock<std::mutex> lock(m_mutex);
m_condition.wait(lock, [&] { return !m_tasks.empty() || m_stop; });
// wait 会自动解锁 m_mutex,被唤醒后重新加锁
if (m_stop && m_tasks.empty()) return;
auto task = m_tasks.front();
m_tasks.pop();
}
TIP
📌 条件变量为什么需要 unique_lock 而不是 lock_guard?wait() 内部需要:1) 解锁 mutex(让其他线程能修改共享状态)2) 阻塞等待 3) 被唤醒后重新加锁。lock_guard 不支持手动解锁/加锁,unique_lock 可以。
wait 的第二个参数(谓词):wait(lock, pred) 等价于 while (!pred()) cv.wait(lock);——防止"虚假唤醒"(spurious wakeup),即没有 notify 却被唤醒的情况。
15.4 std::atomic — 原子操作
std::atomic<bool> m_stop{false};
std::atomic<int> counter{0};
// 无锁操作,线程安全
counter++; // 原子自增
counter.fetch_add(1); // 显式原子加
m_stop.store(true); // 原子写
m_stop.load(); // 原子读
TIP
📌 atomic vs mutex?atomic 适用于单个变量的简单操作(读、写、自增、比较交换),无锁,性能更高。mutex 适用于保护一段代码(多个变量的复合操作)。RyBOS 的 ThreadPool 用 std::atomic<bool> m_stop 做停止标志,不需要加锁。
15.5 std::future / std::async — 异步结果
#include <future>
// async 启动异步任务
std::future<int> result = std::async(std::launch::async, []
{
return 42;
});
// 做其他事情...
int value = result.get(); // 阻塞等待结果
RyBOS 的 ThreadPool::submit 返回 std::future,让调用者可以异步获取结果:
template<typename F, typename... Args>
auto submit(F&& f, Args&&... args) -> std::future<typename std::invoke_result<F, Args...>::type>;
16 · 文件与文件系统
16.1 std::filesystem(C++17)
#include <filesystem>
namespace fs = std::filesystem;
// 路径操作
fs::path p = "/data/config.ini";
p.parent_path(); // "/data"
p.filename(); // "config.ini"
p.extension(); // ".ini"
// 文件操作
fs::exists(p); // 是否存在
fs::file_size(p); // 文件大小
fs::create_directories(p); // 递归创建目录
fs::remove(p); // 删除文件
fs::rename(src, dst); // 重命名
// 遍历目录
for (const auto& entry : fs::directory_iterator("/data"))
{
std::cout << entry.path() << std::endl;
}
// 递归遍历
for (const auto& entry : fs::recursive_directory_iterator("/data"))
{
if (entry.is_regular_file())
{
std::cout << entry.path() << " : " << entry.file_size() << std::endl;
}
}
TIP
📌 std::filesystem 是 C++17 最重要的实用特性之一。以前跨平台文件操作要 #ifdef _WIN32 用 _wfopen / FindFirstFile,Linux 用 opendir / stat。现在一套 API 全平台通用。
RyBOS 的 FileSystem 模块就是基于 std::filesystem 封装的:
namespace fs = std::filesystem;
struct FileInfo
{
std::string path;
std::string name;
uint64_t size;
bool isDirectory;
fs::file_time_type lastWriteTime;
};
16.2 文件读写
// 文本读取
std::ifstream file("data.txt");
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
// 二进制读取
std::ifstream bin("data.bin", std::ios::binary);
std::vector<uint8_t> data((std::istreambuf_iterator<char>(bin)),
std::istreambuf_iterator<char>());
// 写入
std::ofstream out("output.txt");
out << "Hello" << std::endl;
17 · 异常处理
17.1 try / catch / throw
try
{
if (errorCondition)
{
throw std::runtime_error("Something went wrong");
}
}
catch (const std::runtime_error& e)
{
std::cerr << "Runtime error: " << e.what() << std::endl;
}
catch (const std::exception& e)
{
std::cerr << "Standard exception: " << e.what() << std::endl;
}
catch (...) // 捕获所有异常
{
std::cerr << "Unknown exception" << std::endl;
}
17.2 标准异常层次
std::exception
├── std::runtime_error // 运行时错误
│ ├── std::overflow_error
│ └── std::underflow_error
├── std::logic_error // 逻辑错误
│ ├── std::invalid_argument
│ ├── std::out_of_range
│ └── std::length_error
├── std::bad_alloc // new 失败
└── std::bad_cast // dynamic_cast 失败
TIP
📌 异常 vs 错误码?
- 异常:错误处理代码与正常逻辑分离,不可被忽略(不 catch 程序终止),但有一定开销
- 错误码:零开销,但容易被忽略(忘记检查返回值),污染正常逻辑
RyBOS 的选择:多数模块用 std::optional 或返回 bool + 输出参数,而不是异常。这是因为 RyBOS 面向嵌入式和系统编程场景,异常的开销(栈展开)不可接受。
C++ 异常的性能特点:没有异常抛出时几乎零开销(与 if 检查相当),抛出异常时开销很大(栈展开 + 堆分配异常对象)。所以"异常用于异常情况,不用于正常控制流"。
18 · CMake 构建系统

18.1 CMake 基础
cmake_minimum_required(VERSION 3.10)
project(RyBOS VERSION 1.0.0 LANGUAGES CXX)
# C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# MSVC UTF-8 支持
if(MSVC)
add_compile_options(/utf-8)
endif()
# StringUtils/CMakeLists.txt
add_library(StringUtils INTERFACE)
# INTERFACE 库没有源文件,只传播 include 路径
target_include_directories(StringUtils INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
TIP
📌 INTERFACE 关键字:普通的 add_library 会生成 .lib/.a 文件。INTERFACE 库不生成任何二进制——它只是一个"属性传播器",把 include 路径、编译选项传递给依赖它的目标。
为什么 Header-only 用 INTERFACE?因为 Header-only 库没有需要编译的 .cpp,不需要生成二进制文件。但使用者需要知道头文件在哪——target_include_directories(... INTERFACE ...) 就把这个信息传递给了使用者。
RyBOS 大量使用这个模式:
add_library(StringUtils INTERFACE)
add_library(TaskScheduler INTERFACE)
add_library(CommandProtocol INTERFACE)
18.3 有源文件的库
# WebSocket/CMakeLists.txt
add_library(WebSocket STATIC
src/WebSocket.cpp
)
target_include_directories(WebSocket PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
)
# 平台条件链接
if(WIN32)
target_link_libraries(WebSocket PUBLIC ws2_32)
else()
target_link_libraries(WebSocket PUBLIC pthread)
endif()
18.4 关键概念:PUBLIC / PRIVATE / INTERFACE
| 关键字 |
自己用 |
传播给依赖者 |
PRIVATE |
是 |
否 |
PUBLIC |
是 |
是 |
INTERFACE |
否 |
是 |
TIP
📌 打个比方:
- PRIVATE:你自己用的工具,不借给别人
- PUBLIC:你自己用,也借给别人
- INTERFACE:你不自己用,但借给别人(Header-only 库专用)
例如 WebSocket 模块需要 ws2_32(Windows Socket 库),用 PUBLIC 意味着依赖 WebSocket 的模块也能用 socket 函数。如果只是 WebSocket 内部用,就该用 PRIVATE。
传统 C++ 库:头文件(.h 声明)+ 源文件(.cpp 实现)→ 编译成 .lib/.a → 链接。
Header-only:所有代码都在 .h 中,使用者只需 #include,不需要编译和链接。
19.2 实现要点
// StringUtils.h
#pragma once // 或传统 include guard
#include <string>
#include <vector>
namespace RyB
{
// 所有函数必须 inline(允许多个翻译单元包含同一定义)
inline std::vector<std::string> split(const std::string& str, const std::string& delimiter)
{
// 实现...
}
// 模板函数天然 inline,不需要加 inline 关键字
template<typename Container>
inline std::string join(const Container& container, const std::string& delimiter)
{
// 实现...
}
}
TIP
📌 Header-only 的优缺点:
| 优点 |
缺点 |
使用者只需 #include,零配置 |
编译时间增加(每次 include 都重新编译实现) |
无链接问题(没有 undefined reference) |
不能隐藏实现(商业库可能需要保护源码) |
| 模板天然适合 Header-only |
修改头文件会触发所有依赖重新编译 |
| 跨平台零烦恼 |
— |
RyBOS 的选择:绝大多数模块都是 Header-only,因为 RyBOS 是内部库,不需要保护源码,且追求零配置易用性。
20 · 跨平台开发
20.1 条件编译
// 纯 C++ 模块用 _WIN32
#ifdef _WIN32
#include <windows.h>
#include <winsock2.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#endif
// Qt 模块用 Q_OS_WIN
#ifdef Q_OS_WIN
// Windows 特定代码
#else
// Linux/macOS 特定代码
#endif
20.2 CMake 中的平台判断
if(WIN32)
target_link_libraries(MyMod PUBLIC ws2_32) # Windows Socket
elseif(UNIX)
target_link_libraries(MyMod PUBLIC pthread) # Linux pthread
endif()
if(MSVC)
add_compile_options(/utf-8) # MSVC UTF-8 源码
endif()
20.3 字符编码跨平台
// Windows 文件路径可能包含非 ASCII 字符
#ifdef _WIN32
// Windows 用 UTF-16 宽字符
FILE* fp = _wfopen(L"data/文件.txt", L"r");
#else
// Linux 用 UTF-8
FILE* fp = fopen("data/文件.txt", "r");
#endif
:::tip
📌 跨平台的核心原则:把平台差异隔离在最小范围内。RyBOS 的做法:
- 用
#ifdef 把平台差异包在函数内部
- 对外提供统一的 API(调用者不需要知道平台差异)
- 用
std::filesystem 替代平台特定的文件 API
- 用
<cstdint> 的固定宽度类型替代 int/long
:::
21 · 编码规范(RyBOS 风格)
21.1 命名约定
| 类别 |
规则 |
示例 |
| 命名空间 |
RyB |
namespace RyB { } |
| 类名 |
Qt 模块加 Qt 前缀 |
QtHttpServer · ThreadPool |
| 成员变量 |
m_ 前缀 |
m_port · m_lastError |
| 静态成员 |
s_ 前缀 |
s_instance |
| 局部指针 |
p 前缀 |
pSocket · pSrc |
| 成员指针 |
m_p 前缀 |
m_pSocket · m_pServer |
21.2 大括号风格(Allman)
// Allman 风格:左大括号独占一行
void func()
{
if (condition)
{
doSomething();
}
}
// K&R 风格(RyBOS 不用)
void func() {
if (condition) {
doSomething();
}
}
21.3 Doxygen 注释
/**
* @file StringUtils.h
* @brief 字符串工具库(Header-only)
* @author RyB
* @version 1.0.0
*
* 【模块概述】
* 提供字符串分割、拼接、替换、编码转换等功能。
*
* 【使用示例】
* @code
* auto parts = RyB::StringUtils::split("a,b,c", ",");
* @endcode
*/
/**
* @brief 分割字符串
* @param str 原始字符串
* @param delimiter 分隔符
* @return 分割后的字符串向量
*/
inline std::vector<std::string> split(const std::string& str, const std::string& delimiter);
21.4 模块目录结构
ModuleName/
├── CMakeLists.txt # 构建配置
├── src/
│ └── ModuleName.h # Header-only 实现
├── example/
│ └── main.cpp # 使用示例
├── test/ # 单元测试(可选)
├── 3rd/ # 第三方依赖(可选)
└── README.md # 模块说明
附录 A · 标准库速查
字符串
| 功能 |
代码 |
| 声明 |
std::string s = "hello"; |
| 长度 |
s.length() / s.size() |
| 拼接 |
s += " world" / s + " world" |
| 子串 |
s.substr(0, 3) |
| 查找 |
s.find("ell") → 1 / std::string::npos |
| 替换 |
s.replace(0, 2, "HI") |
| 转 C 字符串 |
s.c_str() |
| 数字转字符串 |
std::to_string(42) |
| 字符串转数字 |
std::stoi("42") / std::stod("3.14") |
| 流式拼接 |
std::ostringstream oss; oss << x; |
容器
| 容器 |
特点 |
头文件 |
std::vector |
动态数组,尾部 O(1) |
<vector> |
std::list |
双向链表,任意位置 O(1) |
<list> |
std::deque |
双端队列 |
<deque> |
std::map |
有序键值对(红黑树) |
<map> |
std::unordered_map |
哈希键值对 |
<unordered_map> |
std::set |
有序集合 |
<set> |
std::unordered_set |
哈希集合 |
<unordered_set> |
std::queue |
FIFO 队列 |
<queue> |
std::stack |
LIFO 栈 |
<stack> |
std::array |
固定大小数组 |
<array> |
智能指针
| 指针 |
语义 |
创建方式 |
unique_ptr |
独占 |
std::make_unique<T>(args...) |
shared_ptr |
共享 |
std::make_shared<T>(args...) |
weak_ptr |
弱引用 |
shared_ptr 赋值给 weak_ptr |
并发
| 功能 |
代码 |
| 创建线程 |
std::thread t(func, args...); |
| 等待线程 |
t.join(); |
| 互斥锁 |
std::mutex m; std::lock_guard<std::mutex> lock(m); |
| 条件变量 |
std::condition_variable cv; cv.wait(lock, pred); |
| 原子操作 |
std::atomic<int> a{0}; a++; |
| 异步任务 |
std::future<int> f = std::async(std::launch::async, func); |
文件系统
| 功能 |
代码 |
| 路径 |
fs::path p = "/data/file.txt"; |
| 存在 |
fs::exists(p) |
| 大小 |
fs::file_size(p) |
| 创建目录 |
fs::create_directories(p) |
| 遍历 |
for (auto& e : fs::directory_iterator(dir)) |
附录 B · 自测题与闪卡
基础题
Q1:int x{3.14} 和 int x = 3.14 的区别是什么?
{} 列表初始化禁止窄化转换,int x{3.14} 编译报错。int x = 3.14 默默截断为 3。
Q2:const int& x 作为函数参数有什么好处?
零拷贝(引用不复制对象)+ 只读(const 保证不修改)= 安全且高效。
Q3:为什么 delete 基类指针时需要虚析构函数?
没有虚析构,delete 只调用基类析构,子类析构不执行 → 资源泄漏。
Q4:std::move 做了什么?
什么都不做,只是把左值标记为右值(static_cast<T&&>),让编译器选择移动构造/赋值。
Q5:RAII 是什么?举一个例子。
Resource Acquisition Is Initialization。对象构造获取资源,析构释放资源。std::lock_guard 构造时加锁,析构时解锁。
进阶题
Q6:std::vector 的 reserve() 和 resize() 区别?
reserve 只改变 capacity(预分配内存),不改变 size。resize 改变 size(实际创建/销毁元素)。
Q7:shared_ptr 循环引用怎么解决?
一方使用 weak_ptr。weak_ptr 不增加引用计数,打破循环。
Q8:模板和虚函数实现多态的区别?
模板是编译期多态(静态分发,零开销,代码膨胀)。虚函数是运行期多态(动态分发,vtable 开销,无代码膨胀)。
Q9:condition_variable::wait 为什么需要 unique_lock?
wait 内部需要先解锁 mutex(让其他线程修改共享状态),被唤醒后再加锁。unique_lock 支持 unlock()/lock(),lock_guard 不支持。
Q10:Header-only 库中函数为什么必须 inline?
头文件被多个 .cpp 包含时,函数定义会出现多次。inline 告诉链接器允许多个定义,取其中一个即可,避免 undefined reference 或重复定义错误。