CRTP(奇异递归模板模式)
需要预先学习: 模版基础 继承 多态 CRTP(奇异递归模板模式) CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)是一种继承写法:派生类把自己作为模版参数传给基类。 template <typename Derived> class Base { // 基类通过模版参数 Derived "知道"派生类是谁 }; class Widget : public Base<Widget> // Widget 把自己传给 Base { }; 这样基类在编译期就知道"谁继承了我",可以直接使用派生类的类型——由此得到一类不依赖 virtual 的多态能力,称为静态多态(static polymorphism,编译期多态)。 最小示例 #include <iostream> template <typename Derived> class Base { public: void inter...
虚拟地址
需要预先了解: 内存基础 虚拟地址 内存基础 里说过:内存中每个字节都有唯一的编号,CPU 通过编号定位并读写数据。不过有一个重要的事实没有展开——你在 C++ 代码里拿到的所有地址,都不是物理内存的真实编号,而是虚拟地址(virtual address)。 物理内存长什么样、数据实际放在哪,进程既不知道也不需要知道。进程眼里只有一套自己的虚拟地址空间,而把虚拟地址翻译成物理地址、决定数据实际存放位置的工作,由硬件和操作系统在幕后完成。 这篇文章先确认"你看到的全是虚拟地址"这件事,再看虚拟地址和物理地址的映射是怎么实现的,最后聊聊这套机制换来了什么。 你看到的地址都是虚拟地址 先看一段最普通的代码: #include <cstdio> int g_value = 42; // 全局变量,位于数据区 int main() { int local = 10; // 局部变量,位于栈 int* p = &g_value; // 指针,存储的是 g_value 的虚拟地址...
静态库与动态库的基础知识
需要预先学习: 内存基础 虚拟地址 extern、static 与 inline 关键字总结 extern “C” 库 假设有一些许多项目都要用到的函数,比如在某一个项目里面写了个 calculate.cpp。此时如果另一个项目也想用这几个函数,最直接的做法是把源码复制过去。但是这种做法会导致以下几个问题: 如果有很多项目都想用到这份代码,需要多次重复复制一样的源码 如果发现其中有一个 bug,进行修复时需要在每个项目里都改一遍 每个项目都要单独编译这份代码,会造成编译时间延长,也会导致生成的二进制文件体积膨胀 有可能会出现不同项目中的 ``calculate.cpp` 版本不一致,导致行为不一致 库(library)就是为解决这类问题出现的。它是一批已经编译好的目标代码,连同符号表、重定位信息、初始化信息等元数据打包在一起。库可以被链接器合并进可执行文件,也可以在程序运行时被加载器映射进进程的地址空间。 从使用者的角度看,库提供了两样东西: 一组可以调用的符号(函数名、变量名),以及它们对应的机器码和数据 一份 ABI(Applica...
静态库与动态库实操
需要预先学习: 静态库与动态库的基础知识 准备工作 这篇只用手写命令,不涉及 CMake、Meson、Bazel 这类构建系统。目标是把"每一步发生了什么"看清楚,而不是记住某个构建工具的配置写法。 示例工程 准备一个最小工程,目录结构如下: project/ ├── include/ │ └── add.h ├── src/ │ ├── add.cpp │ └── main.cpp └── build/ include/add.h: #pragma once int add(int a, int b); int sub(int a, int b); src/add.cpp: #include "add.h" int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } src/main.cpp: #include <cstdio...
多线程编程基础
基础概念 多线程编程之前,先搞清楚几个基本概念:并发、并行、进程、线程、协程。它们经常被混用,但含义差别很大,而且这些差异最终都落在硬件层面。 并发与并行 并发(concurrency)指多个任务在同一时间段内交替推进——宏观上看它们"同时"在进行,微观上可能只有一个执行单元在轮流处理。并行(parallelism)指多个任务在同一时刻真正同时执行,这要求有多个执行单元(多个 CPU 核心)。 两者的关系可以用一个经典表述概括:并发是程序结构层面的性质,并行是执行层面的性质。一个程序可以设计成并发的(多个任务交替推进),但只有在多核机器上运行时才可能并行。 单核并发(时间片轮流): CPU 核心: [任务A][任务B][任务A][任务B][任务A][任务B] ... └── 每个时刻只有 1 个任务在跑 ──┘ 双核并行: 核心 1: [任务A][任务A][任务A][任务A] ... 核心 2: [任务B][任务B][任务B][任务B] ... └── 同一时刻有 2 个任务在跑 ──┘ 注意:并发程序...
线程传参
需要预先学习: 多线程编程基础 传参的本质:构造时拷贝 std::thread 的构造函数把实参按值拷贝(或移动)到线程内部,然后在线程入口函数中使用这些拷贝。这一点决定了传参的大部分规则: #include <iostream> #include <thread> void print(int n) { std::cout << n << std::endl; } int main() { int a = 100; std::thread t(print, a); // a 被拷贝一份传给线程, 线程内部用的是副本 a = 200; // 修改主线程的 a, 不影响线程内的副本 t.join(); return 0; } 输出 100 而不是 200——线程拿到的是构造时的副本。参数在 std::thread 构造的那一刻就被拷贝/移动了,之后主线程再怎么改原变量,线程内部都不受影响。 注:拷贝发...
数据共享问题与锁
需要预先学习: 多线程编程基础 数据竞争:共享数据的问题 多线程共享内存是优势,也是问题的源头。当两个或多个线程同时访问同一内存位置,且至少有一个是写操作,就构成数据竞争(data race)。C++ 标准规定数据竞争是未定义行为(UB):程序可能输出任何结果,可能崩溃,也可能"碰巧正确"。 看一个经典例子——两个线程各做 100 万次 counter++: #include <iostream> #include <thread> int counter = 0; void add() { for (int i = 0; i < 1000000; ++i) { ++counter; // 数据竞争: 两个线程同时读写 counter } } int main() { std::thread t1(add); std::thread t2(add); t1.join(); t2.join();...
多线程编程进阶
需要预先学习: 多线程编程基础 数据共享问题与锁 线程间通信的问题 锁解决了"互斥"(同一时刻只有一个线程进临界区),但没解决"等待":线程 A 要等线程 B 完成某件事(比如数据准备好了)才能继续,A 应该怎么等? 忙等:while (!ready) {} 死循环空转,浪费 CPU,还带来数据竞争 轮询:while (!ready) { sleep(1ms); } 浪费少些,但有延迟,且 ready 的读写仍要同步 这两种方案都不优雅。标准答案有两个:条件变量(等待"事件")和 future/promise(等待"结果")。本篇文章围绕它们展开,最后实现一个线程池。 条件变量 std::condition_variable 条件变量(condition variable)让线程阻塞等待某个条件成立,由其他线程唤醒它。它必须配合 std::unique_lock(和一把互斥量)使用。 核心 API: 函数 行为 wait(lock) 阻塞,并自动解锁;被唤醒后...
原子操作
需要预先学习: 多线程编程基础 数据共享问题与锁 为什么要原子操作 回顾《数据共享问题与锁》中的计数器例子:两个线程各执行 100 万次 ++counter,结果不是 200 万。原因是 ++counter 在底层是"读 → 加 → 写"三步,两个线程的三步交错执行,更新互相覆盖。 用互斥锁可以解决,但代价不小:lock() 通常涉及系统调用(竞争时线程睡眠/唤醒)、缓存行失效、上下文切换。如果只是"把一个变量加 1"这种简单操作,锁是杀鸡用牛刀。 原子操作(atomic operation)就是答案:不可分割的操作——要么完整执行,要么完全不执行,中间不可能被其他线程插入。x = 1 这类单条机器指令天然是原子的;而"读-改-写"这类多步操作,需要硬件提供特殊指令(如 x86 的 lock 前缀指令)保证原子性。 std::atomic<T> 把这种能力封装成标准库类型。用它改写计数器: #include <atomic> #include <iostream> #incl...


