extern "C"
需要预先了解: extern、static与inline关键字总结 函数 extern “C” 的使用 extern “C” 的作用是告诉 C++ 编译器:这个名字(函数或变量)按 C 语言的规则处理链接。它解决的是 C++ 程序调用 C 语言库的问题。 为什么需要 extern “C” 现实中有大量优秀的 C 库——OpenGL、SDL、zlib、sqlite 等。C++ 程序直接 #include 它们的头文件并调用函数时,链接阶段往往会失败,报出 undefined reference 错误。原因在于:C 编译器和 C++ 编译器给同一个函数生成的符号名不一样(详见本篇后半部分)。 extern "C" 就是给 C++ 编译器的一条指令:“这个函数的符号,按 C 的方式生成。” 基本用法 1. 单个声明: extern "C" int add(int a, int b); 2. 代码块(多个声明一次搞定): extern "C" { int add(int a, int b); const char* ...
完美转发
需要预先了解: 引用与左右值 移动语义与std::move 模版基础 转发的问题 写一个包装函数(wrapper)时,你常常希望它把参数"原样"转交给另一个函数——参数的类型、值类别(左值/右值)、const 属性都保持不变。这件事比听起来难,先看直接的做法有什么问题。 void target(int& x) { /* 左值版本 */ } void target(int&& x) { /* 右值版本 */ } template <typename T> void wrapper(T arg) // 按值传递 { target(arg); } 按值传递有两个问题: 参数被拷贝了一份(右值时走移动构造,但左值时一定是深拷贝),大对象开销明显 无论调用者传的是左值还是右值,arg 在函数内部都是一个有名字的左值——target(arg) 永远调用左值版本,值类别信息丢失 改用引用传递呢?T& 只能绑定左值,...
Traits(类型萃取)
需要预先学习: 模版基础 Traits(类型萃取) 写泛型代码时,你迟早会遇到一个问题:模版参数 T 到底是个什么类型? 是指针还是普通对象?是整数还是浮点数?去掉引用之后是什么?这些信息在编译期是存在的,但到了运行期已经"擦除"——if 只能根据值做判断,判断不了类型。 **Traits(类型萃取)**就是解决这个问题的技术:定义一类专门的 traits 类,把"关于类型的信息"编码成类成员——你给它一个类型,它告诉你一个编译期的事实(“是不是指针”),或给出一个关联的类型(“去掉指针后剩下的类型”)。“萃取”(extraction)指的正是从类型中提取这些隐藏信息的过程。 注:typeid 虽然能在运行期给出类型名,但泛型代码需要的是能参与编译期计算的答案——用来开分支、声明变量、约束模版,这些 typeid 都做不到。 用偏特化实现编译期分支 从一个需求出发:写一个泛型函数 reset——如果 T 是指针,就 delete 并置空;否则赋一个默认值。这个判断必须发生在编译期,而普通的 if 做不到:它的两个分支都要...
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();...


