[C.C++] C++11特性

120 0
Honkers 2026-8-8 16:35:21 来自手机 | 显示全部楼层 |阅读模式

auto & decltype

关于C++11新特性,最先提到的肯定是类型推导,C++11引入了auto和decltype关键字,使用他们可以在编译期就推导出变量或者表达式的类型,方便开发者编码也简化了代码。

  • auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型。
    • 规则1:值拷贝 → 去掉顶层 const
    • 规则 2:带 & → 变成引用类型
    • 规则 3:带 && → 变成万能引用 / 右值引用
    • 规则 4:数组 / 函数退化为指针(取引用可以保留数组、函数类型)
  1. const int a = 10;
  2. const int& b = a;
  3. int c;
  4. //带 & → 变成引用类型,带引用的auto推导会保留const
  5. auto& la = a;//const int&
  6. auto& lb = b;//const int&
  7. auto& lc = c;//int&
  8. /*
  9. 看到规律的嘛,auto& 相当于把auto替换为等号右边值的类型
  10. auto& la = a;
  11. 其中a为const int;
  12. 所以推导出 const int& la = a;
  13. 至于lb为什么不是 const int& & 类型需要了解引用折叠
  14. */
  15. //带 && → 变成万能引用 / 右值引用
  16. /* 见拓展 */
  17. auto&& la = a;//const int&
  18. auto&& lb = b;//const int&
  19. auto&& lc = c;//int&
  20. auto&& ld = 666;//int&&
  21. //规则1:值拷贝 → 去掉顶层 const
  22. auto va = a;//int
  23. auto vb = b;//int
  24. auto vc = c;//int
  25. const auto cva = a//const int
  26. /*值拷贝推导,因为两者之间会发生拷贝或者移动构造,这是一个全新的值,不需要保留原值的const属性,如果需要const自己加上*/
  27. //规则 4:数组 / 函数退化为指针
  28. int arr[] = {1,2,3};
  29. auto a = arr; // int*
  30. void* func(int a,int b);
  31. auto a = func;//void (*)(int,int)
  32. class A{
  33. public:
  34. void func();
  35. static bool operator();
  36. };
  37. auto a = &A::func;//void (A::*)(),非静态成员函数类型必须取地址
  38. A b;
  39. b->*a();// 调用a指针,需要绑定A类型实例对象
  40. auto a = A::operator();//void (*)()
  41. auto l = [](){};//lambda
  42. auto list = {1,2,3};//std::initializer_list<int>
  43. //auto 除了可以推导类型,还可用于形参、返回值等推导,非常方便!
复制代码

引用折叠

C++ 语法禁止 “引用的引用”,但是实际上编译器再模板和auto推导时经常出现‘引用的引用’这种情况,就比如我上面提到了lb为什么不是const int& &类型,这是因为编译器为了防止引用的引用这种非法情况,而引入了引用折叠规则,其规则如下

T& & → 折叠成 T&

T& && → 折叠成 T&

T&& & → 折叠成 T&

T&& && → 折叠成 T&&

一句话概括就有& 保留 &

拓展

要理解万能引用部分需要自行去了解左值右值等内容;

上面代码中 a、b、c都是具名变量,所以都是左值,故而万能引用的auto推导出的类型是&类型

而666是字面量,为右值类型,故而推导出为int&&

一个&&却能够推导出&或者&&两种引用类型,这就是大名鼎鼎的万能引用,其实万能引用也不玄乎,其实就是因为引用折叠规则,使得当&&加上&或&&会折叠出不同类型罢了

🧐新手容易搞混万能引用和右值引用,其实万能引用只出现在需要引用折叠的情况,也就是auto推导和模板中,当你看到&&前面不是模板或者auto,而是一个具体的类型(int),这个时候就是右值引用,反之万能引用

  • decltype:相对于auto用于推导变量类型,而decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。
    • 存在一个语法糖 decltype(auto) 可以自动推导出完整的类型
  1. const int &i = 1;int a = 2;decltype(i) b = 2; // b是const int&
  2. decltype(auto) d = i;//const int&
  3. //但是使用decltype(auto)有时候会产生难以预料的问题
  4. decltype(auto) d = a;//int
  5. decltype(auto) d = (a);//int&
  6. //仅仅添加了一个(),推导出来的类型缺不一样,这是因为括号表达式具名,是左值
复制代码

深入了解请翻阅《Effective Modern C++》 Item1-4

左值右值

众所周知C++11新增了右值引用,这里涉及到很多概念:

  • 左值:可以取地址并且有名字的东西就是左值。

    能取地址、有名字、能放在等号左边的值

    1. int a = 10; // a 是左值
    2. a = 20; // 左值可以被赋值
    3. &a; // 可以取地址
    复制代码
  • 右值:不能取地址的没有名字的东西就是右值。

    不能取地址、没名字、只能放在等号右边的值

    1. int b = 10; // 10 是右值
    2. int c = a + b; // a+b 运算结果是右值
    3. // &10; ❌ 报错,不能取地址
    复制代码
  • 纯右值:运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。

    纯粹的临时值,用完立刻销毁

    • 字面量:10、true、"hello"
    • 运算结果:a+b、x*y
    • 非引用返回的函数值:func() 返回 int
  • 将亡值:可以理解为即将要销毁的值。

    “马上就要死,但还没死” 的值

    • 用 std::move 转化后的左值
    • 即将被销毁、准备转移资源的对象

    一句话:纯右值 = 天生临时;将亡值 = 本来是左值,被标记成临时准备销毁

  • 左值引用:对左值进行引用的类型。

    只能绑定左值,给左值起别名

    1. int& ref = a; // ✅ 绑定左值
    2. // int& ref = 10; ❌ 报错,不能绑右值
    复制代码
  • 右值引用:对右值进行引用的类型。

    只能绑定右值,专门用来 “接管” 临时值

    1. int&& rref = 10; // ✅ 绑定右值
    2. int&& rref2 = a + b; // ✅ 绑定右值
    3. // int&& rref3 = a; ❌ 报错,不能绑左值
    复制代码
  • 移动语义:转移资源所有权,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用。

    一句话定义

    把临时对象 / 将亡对象的资源 “偷过来”,不拷贝,直接转移所有权。

    为什么要用?

    • 普通拷贝:复制数据 → 慢、浪费内存
    • 移动语义:直接接管指针 / 资源 → 超快、零拷贝
    1. string s1 = "hello";
    2. string s2 = move(s1);
    3. // s1 的资源转给 s2,s1 变成空
    复制代码

    ✅ 作用:性能爆炸提升,尤其用在大对象、容器(vector/string)上。

  • 完美转发:可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参。

    一句话定义

    写一个通用模板函数,把传入的参数原封不动转发给另一个函数:

    • 是左值 → 转发成左值
    • 是右值 → 转发成右值

    核心工具

    std::forward + 万能引用 T&&

    1. template<typename T>
    2. void forward(T&& arg) {
    3. // 完美转发,保持值类型不变
    4. other_func(std::forward<T>(arg));
    5. }
    复制代码
  • 返回值优化:当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。

返回值优化(RVO)是 C++ 编译器的一种强制 / 可选优化,核心是消除函数返回对象时的临时拷贝,直接在调用者内存位置构造对象,大幅减少大型对象的拷贝开销

  • 价值:对 大对象(如 string、vector、自定义结构体)性能提升显著,可减少 2 次构造 + 2 次析构。

为了理解返回值优化,我们需要知道C++的函数是怎么管理内存的。

  1. Return_type func(Param a){
  2. return Return_value;
  3. }
  4. //我们调用func函数
  5. Param a;
  6. auto ret = func(a);
  7. //函数内会发生下面内容
  8. {
  9. Param temp_a = a;
  10. //所以不管我们传入的a是左值还是右值,在函数体内它都是左值,因为函数体内用的是temp_a是具名值,所以知道为什么要用万能转发了嘛宝子们,因为不万能转发,在函数内a是左值!
  11. return Return_value;
  12. }
  13. //不管我们传入的a是什么类型,实际上都会在函数内发生Param temp_a = a(除非传入的a是一个引用),当函数涉及模板时,temp_a的类型就由模板推导出的形参类型决定(注意绝对不是template<typename T>中T的类型,详细请看《Effective Modern C++》 Item1-4)
  14. //那么当我们运行完是如何返回的呢,见下面
  15. Return_type ret_val = Return_value;
  16. auto ret = ret_val;
  17. //可以看到函数返回值会在调用处创建一个临时的ret_val,然后用return处的Return_value来初始化这个临时值,再用这个临时的值来初始化我们需要用的ret
  18. //这之中发生了2次不必要的构造和析构,为此编译器提供了下面的优化手段
复制代码
1. URVO(Unnamed RVO,匿名返回值优化)
  • 场景:函数返回无名临时对象(直接构造返回)。
  • C++ 标准C++17 强制要求,必须优化。
  • 示例:
  1. std::string create() {
  2. return std::string("hello"); // 返回临时对象,URVO必触发
  3. }
复制代码
2. NRVO(Named RVO,具名返回值优化)
  • 场景:函数返回具名局部变量
  • C++ 标准编译器可选(主流编译器如 GCC/Clang/MSVC 默认开启)。
  • 示例:
  1. std::string create() {
  2. std::string s = "hello"; // 具名局部变量
  3. return s; // NRVO:编译器可选优化
  4. }
复制代码

通过上面两种优化手段

  1. //编译器不再
  2. Return_type ret_val = Return_value;
  3. auto ret = ret_val;
  4. //而是直接相当于直接把ret的地址传入函数,在函数体内构造ret
  5. // 无优化:2次构造+2次析构
  6. T f() { T t; return t; } // 局部t构造→拷贝到临时→临时拷贝到obj
  7. int main() { T obj = f(); }
  8. void f(T* hidden) { new (hidden) T; } // 直接在obj地址构造
  9. int main() { T obj; f(&obj); }
复制代码

列表初始化

在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。

通过std::initializer_list实现的,有兴趣去读源码

  1. // 普通变量
  2. int a{10};
  3. double b{3.14};
  4. // 类对象
  5. class A{
  6. public:
  7. A(){}
  8. A(int x){}
  9. };
  10. A obj1{}; // 无参构造
  11. A obj2{5}; // 带参构造
  12. // 解决默认构造歧义
  13. A func(); // 函数声明
  14. A obj(); // 本意是想构造一个A对象但是被解析成函数声明(坑)
  15. A obj{}; // ✅ 明确创建对象,无歧义
  16. A obj;
  17. A obj({});
  18. //数组 / 容器初始化
  19. int arr[]{1,2,3};
  20. vector<int> v{1,2,3};
  21. //聚合类初始化
  22. struct Stu{
  23. int id;
  24. string name;
  25. };
  26. Stu s{101,"张三"};
  27. //禁止窄转换(安全特性)
  28. int x{3.9}; // ❌ 编译报错,浮点转整型属于窄转换
  29. int y(3.9); // ⚠️ 允许截断,隐患
复制代码

std::function & std::bind & lambda表达式

function

  • 可存储多种可调用对象
    • 普通函数(Free Function)
    • Lambda 表达式
    • 仿函数(Functor,即重载了 operator() 的类)
    • 成员函数(需结合 std::bind 或 Lambda)
    • 静态成员函数
  • 类型安全:在编译期检查调用签名。
  • 可拷贝、可赋值,适合作为回调函数、事件处理器等。
  • 可判断是否为空(operator bool)。

std::function 基本用法

(1)包含头文件
  1. #include <functional> // 必须包含此头文件
复制代码
(2)声明 std::function

std::function 的模板参数是函数签名(返回类型 + 参数类型):

  1. std::function<返回类型(参数类型1, 参数类型2, ...)> func;
复制代码

例如:

  1. std::function<int(int, int)> func; // 可调用对象:接受两个 int,返回 int
复制代码

(3)存储不同可调用对象
① 存储普通函数
  1. int add(int a, int b) {
  2. return a + b;
  3. }
  4. std::function<int(int, int)> func = add;
  5. std::cout << func(2, 3); // 输出 5
复制代码
② 存储 Lambda 表达式
  1. auto multiply = [](int a, int b) { return a * b; };
  2. std::function<int(int, int)> func = multiply;
  3. std::cout << func(2, 3); // 输出 6
复制代码
③ 存储仿函数(Functor)
  1. struct Subtract {
  2. int operator()(int a, int b) const {
  3. return a - b;
  4. }
  5. };
  6. Subtract sub;
  7. std::function<int(int, int)> func = sub;
  8. std::cout << func(5, 3); // 输出 2
复制代码
④ 存储成员函数(需结合 std::bind)
  1. class Calculator {
  2. public:
  3. int divide(int a, int b) { return a / b; }
  4. };
  5. Calculator calc;
  6. // 使用 std::bind 绑定对象和成员函数
  7. std::function<int(int, int)> func = std::bind(&Calculator::divide, &calc, std::placeholders::_1, std::placeholders::_2);
  8. 也可以用更简单lambda auto func = [&calc](int a,int b){calc.divide(a,b);}
  9. std::cout << func(6, 3); // 输出 2
复制代码
⑤ 存储静态成员函数
  1. class MathUtils {
  2. public:
  3. static int square(int x) { return x * x; }
  4. };
  5. std::function<int(int)> func = MathUtils::square;
  6. std::cout << func(4); // 输出 16
复制代码

(4)检查 std::function 是否为空
  1. std::function<void()> emptyFunc;
  2. if (!emptyFunc) {
  3. std::cout << "emptyFunc 是空的!";
  4. }
复制代码

(5)std::function 作为函数参数

std::function 常用于回调机制,例如:

  1. void process(int x, int y, std::function<int(int, int)> op) {
  2. std::cout << "结果: " << op(x, y) << std::endl;
  3. }
  4. int main() {
  5. process(10, 5, add); // 输出 15
  6. process(10, 5, multiply); // 输出 50
  7. return 0;
  8. }
复制代码

std::function 高级用法

(1)结合 std::bind 绑定参数
  1. #include <functional>
  2. void printSum(int a, int b) {
  3. std::cout << a + b << std::endl;
  4. }
  5. int main() {
  6. // 绑定部分参数
  7. auto bindFunc = std::bind(printSum, 10, std::placeholders::_1);
  8. bindFunc(20); // 相当于 printSum(10, 20),输出 30
  9. // 存储到 std::function
  10. std::function<void(int)> func = bindFunc;
  11. func(30); // 输出 40
  12. return 0;
  13. }
复制代码
(2)用于事件系统(回调机制)
  1. #include <functional>
  2. #include <vector>
  3. class Button {
  4. public:
  5. void onClick(std::function<void()> callback) {
  6. callbacks.push_back(callback);
  7. }
  8. void click() {
  9. for (auto &cb : callbacks) {
  10. cb(); // 触发所有回调
  11. }
  12. }
  13. private:
  14. std::vector<std::function<void()>> callbacks;
  15. };
  16. int main() {
  17. Button btn;
  18. btn.onClick([]() { std::cout << "按钮被点击了!\n"; });
  19. btn.onClick([]() { std::cout << "执行另一个操作...\n"; });
  20. btn.click(); // 触发所有回调
  21. return 0;
  22. }
复制代码

输出:

  1. 按钮被点击了!
  2. 执行另一个操作...
复制代码

实现

手搓代码

  1. #include <iostream>
  2. #include <memory>
  3. namespace func {
  4. template <class T, class... Args> struct funcWrap {
  5. // 通过一个抽象接口
  6. // 实现类型擦除
  7. virtual T invoke(Args &&...args) = 0;
  8. virtual ~funcWrap() = default;
  9. virtual std::unique_ptr<funcWrap>
  10. clone() const = 0; // 用于后续Function内实现深拷贝
  11. };
  12. //在function中存储的是基类funcWrap的unique_ptr,但是我们传入的实际上是funcImpl,这样可以隐藏可调用对象的具体类型
  13. template <class R, class T, class... Args>
  14. struct funcImpl : public funcWrap<T, Args...> {
  15. // 通过这个桥接类,我们实际存储可调用对象
  16. R realFunc; // 通过层层转发,我们存储的实际是可调用对象的副本
  17. //用模板扩大构造函数接收类型范围
  18. template <class U> explicit funcImpl(U &&f) : realFunc(std::forward<U>(f)) {}
  19. virtual T invoke(Args &&...args) override {
  20. return realFunc(std::forward<Args>(args)...);
  21. }
  22. virtual std::unique_ptr<funcWrap<T, Args...>> clone() const override {
  23. return std::make_unique<funcImpl>(realFunc);
  24. }
  25. };
  26. template <class sig> struct MyFunction; // 提前声明主模板,用于后面模板的特化
  27. template <class T, class... Args>
  28. class MyFunction<T(Args...)> { // 用特化版本,和函数声明的格式类似,便于阅读
  29. public:
  30. MyFunction() noexcept = default;
  31. template <class R> MyFunction(R &&f) { assign(std::forward<R>(f)); }
  32. // 用clone函数来拷贝而不用拷贝构造函数,因为拷贝构造函数需要具体的类型
  33. // 但是我们这funcWrap已经将其类型擦除了,所以不知道调用哪个子类的拷贝构造
  34. /*
  35. 如果我们再拷贝构造函数里面拷贝
  36. _func = std::make_unique<???>(other._func);这里面的类型我们不知道
  37. 要么我们由用一长串的if else来判断,:-) 显然太麻烦
  38. 所以我们统一用clone来拷贝,类型由调用的对象来决定
  39. */
  40. MyFunction(const MyFunction &other)
  41. : _func(other._func ? other._func->clone() : nullptr) {}
  42. MyFunction(MyFunction &&other) noexcept = default;
  43. MyFunction &operator=(const MyFunction &other) {
  44. if (this != &other) {
  45. _func = other._func ? other._func.clone() : nullptr;
  46. }
  47. return *this;
  48. }
  49. MyFunction &operator=(MyFunction &&) noexcept = default;
  50. T operator()(Args... args) {
  51. if (!_func)
  52. throw std::runtime_error("目标函数为空");
  53. return _func->invoke(std::forward<Args>(args)...);
  54. }
  55. explicit operator bool() const noexcept { return static_cast<bool>(_func); }
  56. void swap(MyFunction &other) noexcept { _func.swap(other._func); }
  57. private:
  58. //R是可调用对象的类型,这个R我们通过桥接类隐藏了,这样就可以存储不同类型的调用对象,只需要调用对象签名相同即可
  59. template <class R> void assign(R &&f) {
  60. // 这里用decay_t去除引用和const修饰符,获得裸属性,防止_func变为引用类型,而不是对象副本
  61. _func = std::make_unique<funcImpl<std::decay_t<R>, T, Args...>>(
  62. (std::forward<R>(f)));
  63. }
  64. std::unique_ptr<funcWrap<T, Args...>> _func;
  65. };
  66. } // namespace func
  67. using namespace func;
  68. int main() {
  69. MyFunction<int(int, int)> mytest = [](int a, int b) { return a + b; };
  70. std::cout << mytest(1, 4);
  71. auto cp = mytest;
  72. std::cout << std::endl << cp(2, 5) << std::endl;
  73. auto cp2 = std::move(mytest);
  74. std::cout << cp2(1, 1);
  75. MyFunction<void(void)> emp;
  76. if (!emp)
  77. printf("null\n");
  78. int (*func_ptr)(int, int) = [](int a, int b) {
  79. printf("ok");
  80. return a - b;
  81. };
  82. cp2 = MyFunction<int(int, int)>(func_ptr);
  83. std::cout << cp2(1, 1);
  84. }
复制代码

简易版实现

  1. template<typename R,typename...Args>
  2. struct ICallable{
  3. virtual R invoke(Args&& ...args) = 0;
  4. virtual ~ICallable(){};
  5. };
  6. template<typename T,typename R,typename...Args>
  7. class ICallabelImpl : public ICallable<R,Args ...>{
  8. T callable;
  9. public:
  10. ICallabelImpl(T&& c) : callable(c){}
  11. R invoke(Args&& ... args) override{
  12. return callable(std::forward<Args>(args)...);
  13. }
  14. };
  15. template<typename Signature>
  16. class myFunction;
  17. template<typename R,typename...Args>
  18. class myFunction<R(Args...)>{
  19. std::unique_ptr<ICallable<R,Args...>> funcPtr;
  20. public:
  21. template<typename T>
  22. myFunction(T&& callable)
  23. {
  24. funcPtr = std::make_unique<ICallabelImpl<T,R,Args...>>(std::forward<T>(callable));
  25. }
  26. R operator()(Args... args)const{
  27. return funcPtr->invoke(std::forward<Args>(args)...);
  28. }
  29. };
复制代码

std::bind

使用std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何需要的时候。

std::bind通常有两大作用:

  • 将可调用对象与参数一起绑定为另一个std::function供调用
  • 将n元可调用对象转成m(m < n)元可调用对象,绑定一部分参数,这里需要使用std::placeholders

具体示例:

  1. #include <functional>
  2. #include <iostream>
  3. #include <memory>
  4. void f(int n1, int n2, int n3, const int& n4, int n5) {
  5. std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << std::endl;
  6. }
  7. int g(int n1) { return n1; }
  8. struct Foo {
  9. void print_sum(int n1, int n2) { std::cout << n1 + n2 << std::endl; }
  10. int data = 10;
  11. };
  12. int main() {
  13. using namespace std::placeholders; // 针对 _1, _2, _3...
  14. // 演示参数重排序和按引用传递
  15. int n = 7;
  16. // ( _1 与 _2 来自 std::placeholders ,并表示将来会传递给 f1 的参数)
  17. auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
  18. n = 10;
  19. f1(1, 2, 1001); // 1 为 _1 所绑定, 2 为 _2 所绑定,不使用 1001
  20. // 进行到 f(2, 42, 1, n, 7) 的调用
  21. // 嵌套 bind 子表达式共享占位符
  22. auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
  23. f2(10, 11, 12); // 进行到 f(12, g(12), 12, 4, 5); 的调用
  24. // 绑定指向成员函数指针
  25. Foo foo;
  26. auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
  27. f3(5);
  28. // 绑定指向数据成员指针
  29. auto f4 = std::bind(&Foo::data, _1);
  30. std::cout << f4(foo) << std::endl;
  31. // 智能指针亦能用于调用被引用对象的成员
  32. std::cout << f4(std::make_shared<Foo>(foo)) << std::endl;
  33. }
复制代码

lambda表达式

lambda表达式可以说是c++11引用的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:

  1. auto func = [capture] (params) opt -> ret { func_body; };
复制代码

其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类), ret是返回值类型,func_body是函数体。

一个完整的lambda表达式:

  1. auto func1 = [](int a) -> int { return a + 1; };
  2. auto func2 = [](int a) { return a + 2; };
  3. cout << func1(1) << " " << func2(2) << endl;
复制代码

如上代码,很多时候lambda表达式返回值是很明显的,c++11允许省略表达式的返回值定义。

lambda表达式允许捕获一定范围内的变量:

  • []不捕获任何变量

  • [&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用

  • [=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用

  • [=, &a]值捕获外部作用域所有变量,按引用捕获a变量

  • [a]只值捕获a变量,不捕获其它变量

  • [this]捕获当前类中的this指针

lambda表达式示例代码:

  1. int a = 0;
  2. auto f1 = [=](){ return a; }; // 值捕获a
  3. cout << f1() << endl;
  4. auto f2 = [=]() { return a++; }; // 修改按值捕获的外部变量,error
  5. auto f3 = [=]() mutable { return a++; };
复制代码

代码中的f2是编译不过的,因为修改了按值捕获的外部变量,其实lambda表达式本质是一个仿函数,这个访函数operator()默认是const的,所以不能修改成员变量,而加了mutable,相当于去掉const属性。

lambda表达式有一个常见的坑:this悬空

C++ 中按值捕获 this([=]/[this])时,lambda 拷贝的是 this 指针本身,不是指针指向的对象

  1. class A{
  2. int val = 0;
  3. public:
  4. auto getLambda(){
  5. return [=](){ val++;
  6. }
  7. }
  8. };
  9. A a;
  10. auto lambda = A.getLambda();
  11. //由于lambda表达式存储的是a的this指针,而不是val对象本身,当a析构后,在调用lambda对象时,会错误的访问到this
  12. //C++17引入了*this捕获,可以直接捕获对象本身,不再回捕获this
  13. [*this](){
  14. val++;
  15. } //这是对的
复制代码

模板的改进

C++11关于模板有一些细节的改进:

  • 模板的右尖括号

    • C++11之前是不允许两个右尖括号出现的,会被认为是右移操作符,所以需要中间加个空格进行分割,避免发生编译错误。

      1. int main() {
      2. std::vector<std::vector<int>> a; // error before C++11
      3. std::vector<std::vector<int> > b; // ok
      4. }
      复制代码
  • 函数模板的默认模板参数

    • C++11之前只有类模板支持默认模板参数,函数模板是不支持默认模板参数的,C++11后都支持。

      1. template <typename T, typename U=int>
      2. class A {
      3. T value;
      4. };
      5. template <typename T=int, typename U> // error
      6. class A {
      7. T value;
      8. };
      复制代码

      类模板的默认模板参数必须从右往左定义,而函数模板则没有这个限制。

      1. template <typename R, typename U=int>
      2. R func1(U val) {
      3. return val;
      4. }
      5. template <typename R=int, typename U>
      6. R func2(U val) {
      7. return val;
      8. }
      9. int main() {
      10. cout << func1<int, double>(99.9) << endl; // 99
      11. cout << func1<double, double>(99.9) << endl; // 99.9
      12. cout << func1<double>(99.9) << endl; // 99.9
      13. cout << func1<int>(99.9) << endl; // 99
      14. cout << func2<int, double>(99.9) << endl; // 99
      15. cout << func1<double, double>(99.9) << endl; // 99.9
      16. cout << func2<double>(99.9) << endl; // 99.9
      17. cout << func2<int>(99.9) << endl; // 99
      18. return 0;
      19. }
      复制代码

可变参数模板

C++11 引入的可变参数模板(Variadic Templates) 是 C++ 模板编程的核心特性,它允许你定义接收任意数量、任意类型参数的模板类 / 函数,彻底解决了传统模板只能处理固定参数的局限。

核心概念

  1. 参数包(Parameter Pack):用 typename... 或 class... 声明,用来承载0 个或多个模板参数
  2. 函数参数包:模板参数包可以对应函数的任意数量实参
  3. 展开参数包:可变参数模板必须递归展开参数包(C++17 有折叠表达式简化,但 C++11 只能用递归)

1. 基础语法

模板声明

  1. // 可变参数函数模板
  2. template <typename... Args> // Args 是模板参数包
  3. void func(Args... args); // args 是函数参数包
  4. // 可变参数类模板
  5. template <typename... Args>
  6. class MyClass;
复制代码
  • ... 是核心语法,用来声明 / 展开参数包

2. C++11递归展开参数包

C++11 没有折叠表达式,必须用递归 + 边界条件展开参数包,这是最经典的用法。

示例 1:打印任意数量、任意类型的参数

  1. #include <iostream>
  2. using namespace std;
  3. // 【递归边界】:0个参数时调用,终止递归
  4. void print() {
  5. cout << "递归结束" << endl;
  6. }
  7. // 【递归函数】:处理1个参数 + 剩余参数包
  8. template <typename T, typename... Args>
  9. void print(T first, Args... rest) {
  10. // 打印第一个参数
  11. cout << first << " ";
  12. // 递归调用:展开剩余参数包
  13. print(rest...);
  14. }
  15. int main() {
  16. // 支持任意数量、任意类型参数
  17. print(1, 2.5, "hello", 'A', true);
  18. return 0;
  19. }
复制代码

执行流程

  1. 1 2.5 hello A 1 递归结束
复制代码
  • 每次递归剥离第一个参数,剩余参数包继续传递
  • 无参数时调用边界函数,递归终止

3. 常用技巧:获取参数包大小

使用 sizeof...(参数包名) 可以编译期获取参数数量

  1. template <typename... Args>
  2. void getSize(Args... args) {
  3. // 编译期计算参数个数
  4. cout << "参数个数:" << sizeof...(Args) << endl;
  5. cout << "函数参数个数:" << sizeof...(args) << endl;
  6. }
  7. // 调用:getSize(1, "abc", 3.14); → 输出 3
复制代码

来看C++17 折叠表达式怎么优雅的解决问题吧

  1. template <typename... Args>
  2. void print(Args... args) {
  3. (cout << ... << args) << endl;
  4. }
复制代码
类型写法展开结果说明
一元左折叠(... + args)((a + b) + c)从左往右算
一元右折叠(args + ...)(a + (b + c))从右往左算
二元左折叠(init + ... + args)(((init + a) + b) + c)最常用
二元右折叠(args + ... + init)(a + (b + (c + init)))有初始值右折叠

搭配逗号表达式甚至可以批量执行代码块

  1. template<typename... Args>
  2. void exec(Args&&... args)
  3. {
  4. {
  5. ([&]{
  6. std::cout << args << std::endl;
  7. }(), ...);
  8. }
  9. }
复制代码

4. 可变参数类模板

常用于通用容器、元编程工具,比如模拟 std::tuple:

  1. // 主模板
  2. template <typename... Args>
  3. class MyTuple;
  4. // 递归特化:拆分第一个类型 + 剩余类型
  5. template <typename First, typename... Rest>
  6. class MyTuple<First, Rest...> {
  7. public:
  8. First value;
  9. MyTuple<Rest...> rest;
  10. MyTuple(First f, Rest... r) : value(f), rest(r...) {}
  11. };
  12. // 边界特化:0个参数
  13. template <>
  14. class MyTuple<> {};
复制代码

5. 完美转发 + 可变参数(进阶实用)

结合 std::forward 实现参数完美转发,避免类型丢失、拷贝开销,这是 C++11 可变参数模板的最高频实用场景(比如工厂函数、线程构造):

  1. #include <iostream>
  2. #include <utility> // std::forward
  3. using namespace std;
  4. template <typename T, typename... Args>
  5. T* createObject(Args&&... args) {
  6. // 完美转发所有参数给构造函数
  7. return new T(std::forward<Args>(args)...);
  8. }
  9. // 测试类
  10. class Test {
  11. public:
  12. Test(int a, string b) {
  13. cout << a << " " << b << endl;
  14. }
  15. };
  16. int main() {
  17. Test* p = createObject<Test>(10, "test");
  18. delete p;
  19. return 0;
  20. }
复制代码
  • Args&&:万能引用,接收左值 / 右值
  • std::forward(args)...:展开并完美转发所有参数

属性

C++11 的属性(Attribute)是用 [[...]] 写在声明上的 “编译器提示”,用于控制警告、优化、静态检查等,不影响语法但影响编译行为。

[[noreturn]](C++11)

函数永不返回(死循环、直接 abort/exit),编译器可做优化并警告 “不可达代码”。

  1. [[noreturn]] void fatal() {
  2. std::cerr << "err\n";
  3. std::abort();
  4. }
复制代码
[[nodiscard]](后面的都是C++17)

不可忽略返回值,否则编译警告;适合返回错误码、资源句柄、智能指针等。

  1. [[nodiscard]] int open_file(const char* path);
  2. open_file("a.txt"); // 警告:忽略 nodiscard 返回值
  3. int a = open_file("a.txt");//不警告
复制代码
[[maybe_unused]]

变量 / 参数可能不用,消除 “未使用” 警告。

  1. void f([[maybe_unused]] int debug) { }
复制代码
[[fallthrough]]

switch 中允许穿透,消除 “无 break” 警告。

  1. switch (n) {
  2. case 1: ...; [[fallthrough]];
  3. case 2: ...; break;
  4. }
复制代码

noexcept

noexcept 是 C++11 引入的异常说明符,用于声明函数 “绝不抛出异常”;违约抛异常会直接 std::terminate,添加noexcept会让系统优化函数,提神性能。

作为函数说明符(声明 / 定义处)
  1. // 无条件:承诺不抛异常
  2. void func() noexcept;
  3. void func() noexcept { /* ... */ }
  4. // 等价写法
  5. void func() noexcept(true);
  6. // 允许抛异常(默认)
  7. void func() noexcept(false);
复制代码
作为运算符(编译期判断)
  1. // 判断表达式是否不抛异常,返回 bool 常量
  2. static_assert(noexcept(1 + 2));
  3. static_assert(!noexcept([]{ throw 1; }()));
复制代码
  • 编译期求值,不执行表达式。

现代C++错误处理

本来是打算讲system_error库的,但是想想单单这个库好像很难理解为什么要有这个库,所以我打算就C++错误处理方案的演变来讲解,和过去的C++比较一下

1. 返回 -1 法(传统 C 风格)

核心思想:在正常返回值域之外选一个特殊值(如 -1)表示出错。

  1. int sqrt(int x) {
  2. if (x < 0) {
  3. return -1; // -1 表示"非法输入"
  4. } else {
  5. for (int i = 0; ; ++i) {
  6. if (i * i >= x) return i;
  7. }
  8. }
  9. }
  10. int main() {
  11. int ret = sqrt(-4);
  12. if (ret == -1) {
  13. fmt::println("错误:负数不能开平方");
  14. } else {
  15. fmt::println("结果:{}", ret);
  16. }
  17. }
复制代码

注意事项

要点说明
错误码在正常值域之外若返回 int,可选超出 INT_MIN/INT_MAX 的值;若返回 long long 则此法失效
并行调用下错误信息杂乱多个线程同时返回 -1,调用方无法区分错误来源
不要用 exit(-1)进程直接退出,无法做重试/回滚等补救操作

2. std::optional(C++17)

适用场景:只需区分"有效/无效",不需要多种错误码。

  1. #include <optional>
  2. std::optional<int> sqrt(int x) {
  3. if (x < 0) {
  4. return std::nullopt; // 空值表示出错
  5. } else {
  6. for (int i = 0; ; ++i) {
  7. if (i * i >= x) return i;
  8. }
  9. }
  10. }
  11. int main() {
  12. auto ret = sqrt(-4);
  13. if (!ret.has_value()) { // 可简写为 if (!ret)
  14. fmt::println("错误:负数不能开平方");
  15. } else {
  16. fmt::println("结果:{}", ret.value()); // 若为 nullopt 调用 .value() 会抛异常
  17. }
  18. }
复制代码

优点:类型安全,nullopt 时调用 .value() 会直接报错,方便调试。

缺点:只能表达"成功/失败",无法携带具体错误原因。


3. 多错误码:pair / 输出参数

适用场景:需要区分多种错误类型。

3.1 pair 方案

  1. std::pair<int, bool> sqrt(int x) {
  2. if (x < 0) return {-1, false};
  3. // ...
  4. return {result, true};
  5. }
复制代码

缺点:bool 只能表示成功/失败,若要多种错误码则需改用 pair,语义不清晰。

3.2 输出参数方案

将结果通过引用传出,返回值专用于错误码:

  1. int sqrt(int x, int &ret) {
  2. if (x < 0) {
  3. return -1; // 错误码 -1:负数输入
  4. } else if (x == 4) {
  5. return -2; // 错误码 -2:其他自定义错误
  6. } else {
  7. for (int i = 0; ; ++i) {
  8. if (i * i >= x) {
  9. ret = i;
  10. return 0; // 0 表示成功
  11. }
  12. }
  13. }
  14. }
  15. int main() {
  16. int ret;
  17. int err = sqrt(-4, ret);
  18. if (err == -1) {
  19. fmt::println("错误:负数不能开平方");
  20. } else if (err == -2) {
  21. fmt::println("其他错误...");
  22. } else {
  23. fmt::println("结果:{}", ret);
  24. }
  25. }
复制代码

4. thread_local 全局错误变量

模拟 C 标准库 的做法,但用 thread_local 保证线程安全。

  1. #include <cerrno>
  2. #include <cstring>
  3. #include <clocale>
  4. thread_local int my_errno;
  5. int sqrt(int x) {
  6. if (x < 0) {
  7. my_errno = -1;
  8. return -1;
  9. } else if (x == 4) {
  10. my_errno = -2;
  11. return -1;
  12. } else {
  13. for (int i = 0; ; ++i) {
  14. if (i * i >= x) {
  15. my_errno = 0;
  16. return i;
  17. }
  18. }
  19. }
  20. }
  21. int main() {
  22. int ret = sqrt(-4);
  23. if (my_errno == -1) {
  24. // 使用 strerror 风格:
  25. // setlocale(LC_ALL, "zh-CN.UTF-8");
  26. // const char *str = strerror(my_errno);
  27. fmt::println("错误:负数不能开平方");
  28. } else if (my_errno == -2) {
  29. fmt::println("其他错误...");
  30. } else {
  31. fmt::println("结果:{}", ret);
  32. }
  33. }
复制代码

额外工具

  • perror("前缀信息") → 自动输出前缀 + 错误描述
  • setlocale(LC_ALL, "zh-CN.UTF-8") + strerror(errno) → 中文错误信息

缺点:仍需牺牲一个正常返回值来表示"出错",且全局变量在复杂调用链中容易遗漏检查。


5. 自定义错误码枚举体系

5.1 定义枚举 + 错误信息映射

  1. enum class LoginErrc {
  2. success = 0,
  3. not_login,
  4. not_valid_pass,
  5. };
  6. const char* LoginStrError(LoginErrc err) {
  7. switch (err) {
  8. case LoginErrc::success: return "成功";
  9. case LoginErrc::not_login: return "未登录";
  10. case LoginErrc::not_valid_pass: return "密码错误";
  11. default: return "未知错误";
  12. }
  13. }
复制代码

5.2 传递枚举错误码

  1. int sqrt(int x, LoginErrc &err) {
  2. if (x < 0) {
  3. err = LoginErrc::not_login;
  4. return -1;
  5. } else if (x == 4) {
  6. err = LoginErrc::not_valid_pass;
  7. return -1;
  8. } else {
  9. // ...
  10. return 0;
  11. }
  12. }
  13. int main() {
  14. LoginErrc err = LoginErrc::success; // 必须初始化
  15. int ret = sqrt(4, err);
  16. if (err == LoginErrc::success) {
  17. fmt::println("结果:{}", ret);
  18. } else {
  19. fmt::println("错误:{}", LoginStrError(err));
  20. }
  21. }
复制代码

5.3 抽象 ErrorCode / ErrorCategory

问题:自定义错误码与系统错误码(errno)混在一起难以区分。

方案:引入 category 指针来标记错误码的"命名空间"。

  1. struct ErrorCategory {
  2. virtual const char* message(int val) = 0;
  3. virtual ~ErrorCategory() = default;
  4. };
  5. struct StdErrorCategory : ErrorCategory {
  6. const char* message(int val) override {
  7. return strerror(val);
  8. }
  9. };
  10. struct LoginErrorCategory : ErrorCategory {
  11. const char* message(int val) override {
  12. return LoginStrError(static_cast<LoginErrc>(val));
  13. }
  14. };
  15. // 单例模式:防止悬空指针和内存泄漏
  16. LoginErrorCategory* loginErrorCategory() {
  17. static LoginErrorCategory instance;
  18. return &instance;
  19. }
  20. struct ErrorCode {
  21. int val;
  22. ErrorCategory* category;
  23. bool operator==(const ErrorCode &) const = default;
  24. const char* message() const {
  25. return category->message(val);
  26. }
  27. };
  28. // 便捷构造
  29. ErrorCode makeErrorCode(LoginErrc err) {
  30. return {static_cast<int>(err), loginErrorCategory()};
  31. }
  32. // 使用
  33. int main() {
  34. ErrorCode ec = makeErrorCode(LoginErrc::not_login);
  35. fmt::println("{}", ec.message());
  36. }
复制代码

6. std::error_code(system_error)

标准库 已内置上述抽象,无需手动实现。

  1. #include <system_error>
  2. // 自定义错误类别(单例)
  3. auto const& login_category() {
  4. //匿名结构体
  5. static const struct : std::error_category {
  6. std::string message(int val) const override {
  7. switch (static_cast<LoginErrc>(val)) {
  8. case LoginErrc::success: return "成功";
  9. case LoginErrc::not_login: return "未登录";
  10. case LoginErrc::not_valid_pass: return "密码错误";
  11. default: return "未知错误";
  12. }
  13. }
  14. const char* name() const noexcept override {
  15. return "login";
  16. }
  17. } instance;
  18. return instance;
  19. }
  20. std::error_code make_error_code(LoginErrc ec) {
  21. return {static_cast<int>(ec), login_category()};
  22. }
  23. // 函数实现
  24. int sqrt(int x, std::error_code &ec) {
  25. if (x < 0) {
  26. ec = make_error_code(std::errc::invalid_argument);
  27. return -1;
  28. }
  29. if (x == 4) {
  30. ec = make_error_code(LoginErrc::not_login);
  31. return -1;
  32. }
  33. // ...
  34. ec.clear(); // 成功
  35. return 0;
  36. }
  37. int main() {
  38. std::error_code ec;
  39. int ret = sqrt(4, ec);
  40. if (!ec) {
  41. fmt::println("结果:{}", ret);
  42. } else {
  43. fmt::println("错误:{}", ec.message()); // 等价于 ec.category().message(ec.value())
  44. if (ec == make_error_code(LoginErrc::not_login)) {
  45. // 处理特定错误
  46. }
  47. }
  48. }
复制代码

要点:std::error_code 本质上就是 {int, error_category*} 的封装,通过 category 指针区分错误来源(系统 vs 自定义),彻底解决了自定义错误码与系统错误码混淆的问题。


7. std::variant(C++17)

将成功值和错误码放在同一个返回值中,调用方通过 index() 或 std::holds_alternative 区分。

  1. #include <variant>
  2. #include <system_error>
  3. std::variant<int, std::error_code> sqrt(int x) {
  4. if (x < 0) {
  5. return make_error_code(std::errc::invalid_argument);
  6. }
  7. if (x == 4) {
  8. return make_error_code(LoginErrc::not_login);
  9. }
  10. for (int i = 0; ; ++i) {
  11. if (i * i >= x) return i;
  12. }
  13. }
  14. int main() {
  15. auto ret = sqrt(5);
  16. if (ret.index() == 0) { // 等价于 std::holds_alternative<int>(ret)
  17. fmt::println("结果:{}", std::get<0>(ret)); // 等价于 std::get<int>(ret)
  18. } else {
  19. fmt::println("错误:{}", std::get<1>(ret).message());
  20. }
  21. }
复制代码

优点:成功值和错误码无缝共存于一个返回值。

缺点:index() 不够语义化,调用方代码读起来不如 has_value() 直观。


8. std::expected(C++23,终极方案)

专为"成功或错误"语义设计,是目前最优雅的解决方案。

  1. #include <expected>
  2. #include <system_error>
  3. std::expected<int, std::error_code> sqrt(int x) {
  4. if (x < 0) {
  5. return std::unexpected{make_error_code(std::errc::invalid_argument)};
  6. }
  7. if (x == 4) {
  8. return std::unexpected{make_error_code(LoginErrc::not_login)};
  9. }
  10. for (int i = 0; ; ++i) {
  11. if (i * i >= x) return i;
  12. }
  13. }
  14. int main() {
  15. auto ret = sqrt(5);
  16. if (ret.has_value()) {
  17. fmt::println("结果:{}", ret.value());
  18. } else {
  19. fmt::println("错误:{}", ret.error().message());
  20. }
  21. // --- 单子(Monadic)操作 ---
  22. // 成功时转换值
  23. auto doubled = ret.map([](int x) { return x * 2; });
  24. // 失败时转换错误
  25. auto mapped = ret.map_error([](std::error_code ec) {
  26. if (ec == make_error_code(LoginErrc::not_login)) {
  27. return make_error_code(std::errc::permission_denied);
  28. }
  29. return ec;
  30. });
  31. // 成功时执行副作用
  32. ret.and_then([](int x) -> std::expected<void, std::error_code> {
  33. fmt::println("计算成功:{}", x);
  34. return {};
  35. });
  36. // 错误传播(短路逻辑)
  37. auto result = sqrt(4)
  38. .map([](int x) { return x * 2; })
  39. .map_error([](auto ec) {
  40. fmt::println("出错了:{}", ec.message());
  41. return ec;
  42. });
  43. }
复制代码

核心优势

特性说明
类型安全编译期区分成功/错误,不会误用
单子操作.map() .map_error() .and_then() .or_else() 链式处理
错误传播类似 Rust 的 ? 操作符,自动短路
语义清晰has_value() / error() 一目了然

type_traits

C++11 的 `` 是编译期类型信息工具集,核心价值是在模板代码里做类型判断、关系查询、类型转换与条件启用

三大核心能力(C++11 全覆盖)

1. 类型判断 Traits(Unary)

查询类型 “是不是什么”,通过 ::value 取编译期布尔值。

基础类型分类

  • is_void:是否 void
  • is_integral:是否整型(含 bool/char)
  • is_floating_point:是否浮点型
  • is_array:是否数组
  • is_pointer:是否裸指针
  • is_enum:是否枚举
  • is_class:是否非联合类 / 结构体
  • is_union:是否联合

类型属性

  • is_const:是否 const 修饰

  • is_volatile:是否 volatile 修饰

  • is_reference:是否左值 / 右值引用

  • is_trivially_copyable:是否可平凡拷贝(无自定义拷贝构造)

    示例

    1. static_assert(std::is_integral<int>::value, "int 是整型");
    2. static_assert(!std::is_floating_point<int>::value, "int 不是浮点");
    3. static_assert(std::is_const<const int>::value, "const int 带 const");
    复制代码
    2. 类型关系 Traits(Binary)

    查询两个类型间的关系。

    • is_same:T 与 U 完全相同(含 cv 与引用)
    • is_base_of:Base 是 Derived 的可访问基类
    • is_convertible:T 可隐式转换为 U

    示例

    1. static_assert(std::is_same<int, int>::value, "相同类型");
    2. static_assert(!std::is_same<int, const int>::value, "cv 修饰不同");
    3. struct B {};
    4. struct D : B {};
    5. static_assert(std::is_base_of<B, D>::value, "B 是 D 基类");
    复制代码
    3. 类型转换 Traits

    从旧类型生成新类型,用于模板参数调整。

    修饰符操作

    • remove_const:移除顶层 const
    • remove_volatile:移除顶层 volatile
    • remove_cv:同时移除 const/volatile
    • add_const:添加顶层 const
    • add_volatile:添加顶层 volatile
    • add_cv:添加 const+volatile

    引用 / 指针操作

    • remove_reference:移除左值 / 右值引用
    • add_lvalue_reference:添加左值引用
    • add_rvalue_reference:添加右值引用
    • remove_pointer:移除一层指针
    • decay:数组→指针、函数→函数指针、移除 cv 与引用(模拟传参退化)

    示例

    1. // 移除 const
    2. using T1 = std::remove_const<const int>::type; // int
    3. // 移除引用
    4. using T2 = std::remove_reference<int&>::type; // int
    5. // 添加右值引用
    6. using T3 = std::add_rvalue_reference<int>::type; // int&&
    复制代码

    核心工具:std::enable_if(SFINAE 开关)C++20后有requires 比这个更好用更easy

    作用:编译期条件启用模板函数 / 类,是 SFINAE 最常用工具。

    1. 定义(C++11)
    1. template <bool Cond, typename T = void>
    2. struct enable_if {}; // 条件 false 时无 type,触发 SFINAE
    3. template <typename T>
    4. struct enable_if<true, T> { using type = T; }; // 条件 true 时有 type
    复制代码
    2. 用法:函数模板重载
    1. // 仅整型可用
    2. template <typename T>
    3. typename std::enable_if<std::is_integral<T>::value, void>::type
    4. foo(T) { std::cout << "整型\n"; }
    5. // 仅浮点可用
    6. template <typename T>
    7. typename std::enable_if<std::is_floating_point<T>::value, void>::type
    8. foo(T) { std::cout << "浮点\n"; }
    9. foo(10); // 整型
    10. foo(3.14); // 浮点
    复制代码
    3. 用法:模板类特化
    1. template <typename T, typename = void>
    2. struct MyTrait : std::false_type {};
    3. template <typename T>
    4. struct MyTrait<T, typename std::enable_if<std::is_class<T>::value>::type>
    5. : std::true_type {};
    6. static_assert(MyTrait<int>::value == false, "int 不是类");
    7. static_assert(MyTrait<std::string>::value == true, "string 是类");
    复制代码

using

using别名声明比传统typedef语法更直观、支持模板别名,现代 C++ 优先选用。

1. 基础写法对比

  1. // typedef 旧式
  2. typedef int MyInt;
  3. typedef std::vector<int> IntVec;
  4. // using 新式
  5. using MyInt = int;
  6. using IntVec = std::vector<int>;
复制代码
  • using赋值式写法,左右逻辑清晰,可读性更强

2. 函数指针差异

typedef 嵌套晦涩,using 一目了然

  1. // 函数指针:返回int,参数两个int
  2. typedef int (*FuncPtr)(int, int);
  3. using FuncPtr = int (*)(int, int);
复制代码

3. 关键优势:支持模板别名

typedef 不能模板化,using 可以

  1. // 模板别名,C++11起支持
  2. template<typename T>
  3. using Vec = std::vector<T>;
  4. Vec<int> v;
  5. Vec<string> s;
复制代码

4. 无法直接 typedef 模板容器

  1. // 错误写法
  2. template<typename T>
  3. typedef std::vector<T> Vec;
复制代码

5. 数组别名观感

  1. typedef int Arr[5];
  2. using Arr = int[5];
复制代码

6. 类型萃取场景适配

模板元编程中,using 别名能完美配合std::remove_const等萃取类,typedef 书写别扭。

std::thread

想要深入C++并发的同学请查看 《C++并发编程实战》这本经典之作

c++11之前可能使用pthread_xxx来创建线程,繁琐且不易读,c++11引入了std::thread来创建线程,支持对线程join或者detach

std::thread 核心接口
  1. // 构造
  2. thread(); // 空线程(不可join)
  3. thread(F&& f, Args&&... args); // 创建线程并执行 f(args...)
  4. // 禁用拷贝,只允许移动
  5. thread(const thread&) = delete;
  6. thread& operator=(const thread&) = delete;
  7. thread(thread&&) noexcept;
  8. thread& operator=(thread&&) noexcept;
  9. // 成员函数
  10. bool joinable() const; // 是否可join(未join/detach)
  11. void join(); // 阻塞等待线程结束
  12. void detach(); // 分离为后台线程
  13. id get_id() const; // 获取线程ID
复制代码
std::this_thread 命名空间
  1. namespace this_thread {
  2. thread::id get_id(); // 获取当前线程ID
  3. void sleep_for(duration); // 休眠指定时间
  4. void sleep_until(time_point); // 休眠到指定时间点
  5. void yield(); // 让出CPU,重新调度
  6. }
复制代码

创建线程的 4 种方式

1. 普通函数
  1. void func(int a, const string& b) {
  2. cout << "线程:" << a << ", " << b << endl;
  3. }
  4. int main() {
  5. thread t(func, 10, "hello"); // 传参:值+const引用
  6. t.join();
  7. return 0;
  8. }
复制代码
2. Lambda 表达式(最常用)
  1. int main() {
  2. int x = 100;
  3. thread t([x]() { // 捕获变量
  4. cout << "Lambda线程:" << x << endl;
  5. });
  6. t.join();
  7. return 0;
  8. }
复制代码
3. 类成员函数
  1. class Task {
  2. public:
  3. void run(int id) {
  4. cout << "成员线程:" << id << endl;
  5. }
  6. };
  7. int main() {
  8. Task obj;
  9. thread t(&Task::run, &obj, 1); // 传:成员函数指针+对象指针+参数
  10. t.join();
  11. return 0;
  12. }
复制代码
4. 仿函数(重载 operator ())
  1. struct Functor {
  2. void operator()(int a) {
  3. cout << "仿函数线程:" << a << endl;
  4. }
  5. };
  6. int main() {
  7. thread t(Functor(), 20);
  8. t.join();
  9. return 0;
  10. }
复制代码

发起线程

任何可调用对象都可以作为thread的入口,只需要将其传入thread的构造函数即可。

将函数对象传递给thread时需要额外注意,如果将函数对象传入构造函数会造成二义性。

  1. std::thread t(func());
复制代码

上面代码会有两者解释

  • 我们的本意,用func()构造thread
  • 编译器误以为这是一个函数声明,函数的返回值是thread,函数的参数是一个返回值是func,没有参数的无名函数指针

解决方法是对临时的函数对象命名,或者使用列表初始化方法、加一个括号、使用lambda

  1. std::thread t((func()));
  2. std::thread t{func()};
  3. std::thread t([]{
  4. func();
  5. });
复制代码

当启动了线程之后,就需要明确是要等待线程结束(join),还是由他独立运行(detach),如果两者都没调用,那么如果在父线程已经结束,管理子线程的thread对象已经被销毁时,线程函数仍未结束时,管理子线程的thread对象的析构函数会调用std::terminate()终止整个程序。

C++ 标准规定:std::thread 对象析构时,必须处于以下两种状态之一:

  1. 线程已执行完毕(join() 已被调用)
  2. 线程已被分离(detach() 已被调用)

若都未满足,析构函数会触发 std::terminate 终止程序,目的是防止线程成为 “孤儿线程” 后资源无法回收。

等待线程

线程在创建后需要决定其归宿(决定何时回收其资源和由谁回收),而要回收这些资源需要等待该线程执行完毕,所以除非我们detach该线程(将其交给系统守护线程),我们都需要等待该线程的结束,否则在父线程执行完毕后该未完成的子线程将无线程回收其资源而成为孤儿线程
孤儿线程会造成许多危害,最明显的就是资源泄露,由于没有回收资源,该孤儿线程占有的内存、句柄、锁资源都不会得到释放,其次会引起逻辑错误,由于父线程先结束,子线程可能依赖于父线程上的一些资源,这会导致子线程访问到已经回收的资源,或者如果父线程需要使用由子线程处理过后的数据,而父线程已经退出,最后导致处理结果丢失,最后由于父线程已经关闭,其子线程如果发生错误将难以定位错误,将无法通过其父线程的ID来确认错误的业务模块
一般来说多线程都是由一个主线程来创建其他线程,主线程统一join其他线程,这种情况主线程将未能有效的进行其他工作,有时候可能希望主线程也可以同时进行其他工作,这时候我们需要使用future和条件变量来创建线程池,由线程池来管理线程

异常情况下

我们都知道在thread对象被销毁前,必须join或者detach,但是如果在join或者detach之前,父线程发生了异常,那join操作会被略过,导致子线程没有被成功回收。
我们想当然的会想到使用try/catch来解决,但是当该代码更加复杂,我们难免无法考虑到所有可能的退出路径。更优雅的我们应该使用RAII技术来解决,很可惜,C++11的thread竟然不带有RAII,直到C++20才引入了自带RAII的std::jthread

分离线程

与join一样,在detach之前也需要使用joinable来检查,只有关联了执行对象的线程才能够detach,我们一定要小心,如果该线程函数非常简单,如果再去detach之前该线程就执行完毕,再detach会出现问题
考虑一个使用场景(如果QQ群聊),我们需要通过群聊,通过点击群友头像,来打开多个与群友的私聊,我们可以使用detach来实现,这样就不会因为关闭了群聊而导致私聊界面也被关闭

带参数启动线程

线程具有内部存储空间,参数默认情况下会被复制到该处,然后这些参数才会以右值的方式传入新线程的执行函数,这两步是分开完成的,所以这会造成一些问题

  1. void func(std::string const& s);
  2. void oops(){
  3. char buff[114514];
  4. std::thread t(f,buff);
  5. t.detach();
  6. }
复制代码

上面代码中,buff作为char*传入f,我们本意是buff会转换为std::string,然后传入f,但是由于在实际开始线程之前,thread会将buff拷贝到线程空间,此时buff是指针,要等到buff实际传入f函数的时候,才会转换为string,如果在这时,oops函数执行返回,buff已经被回收了,再将其转换为string就会出错,这与我们的本意相悖了
所以当传入线程的参数对象是指针类型时,必须显式的将其转化为你需要的对象类型
还有一种情况,我们想用非const的引用的形式传入f,但是thread会错误的先以拷贝的方式将其拷入线程空间{

因为设计时考虑到线程异步执行,无法保证实参生命周期:主线程局部变量可能提前销毁,若直接传原生引用,子线程访问会野指针崩溃。标准库为规避悬空引用风险,统一做参数副本保存。std::thread 的构造函数内部,强制把所有参数的 “引用属性” 去掉了,变成了纯值类型,然后拷贝 / 移动存到线程内部。所以导致引用失效了

},这样我们再线程里面实际修改的是这个拷贝,而不是我们传入的对象本身(其实根本就无法编译通过),为此我们必须使用std::ref将该引用对象进行包装,才能事实上真正的引用传入

std::ref核心作用是:将左值对象包装为可拷贝的
“引用代理”,解决「需要传递引用但语境要求拷贝」的场景(如 std::thread 构造、std::bind
绑定等),std::ref(obj) 会返回一个 std::reference_wrapper 类型的对象(可拷贝),这个
“包装器” 本质上持有 obj 的指针,当被传递时,拷贝的是 “包装器”
而非原始对象,最终在目标语境中会自动解包为原始对象的引用。

_由于thread的构造函数传参需要拷贝,如果你传入的参数禁止拷贝,似乎就无法进行了,这时候就需要使用移动语义,通过移动的方式传入,thread会通过万能转发,将参数移动到临时空间,再移动到可执行对象上,以此解决,除此之外还可以使用智能指针传入

_如果要将某个类的成员函数设置为线程函数,则应传入一个函数指针

  1. class X{
  2. public:func(){
  3. }
  4. } my_x;
  5. std::thread t(&X::func,&my_x);
复制代码

线程归属权的转移

_有时候我们想要在一个函数里面创建线程,但是不在这个函数等待,而在另一个函数内等待,因为thread不允许拷贝操作,所以就需要使用移动来转移资源

  1. void func1();
  2. void func2();
  3. using std::thread;
  4. thread t1(func1);
  5. thread t2(std::move(t1));
  6. t1 = thread(func2);
  7. thread t3 = std::move(t2);
  8. t1 = std::move(t3); //⚠️ 错误,由于t1掌握的原线程资源没有join或者detach,被析构后会调用terminate终止程序
复制代码

joining_thread(std::jthread C++20)

std::mutex相关

1. std::mutex(最基础、最常用)

特点

  • 独占式互斥量
  • 不可递归上锁(同一个线程重复锁会死锁
  • 不可拷贝、不可移动
  • 性能最好,优先使用

核心方法

  • lock():加锁(阻塞)
  • unlock():解锁
  • try_lock():尝试加锁,不阻塞

2. std::recursive_mutex(递归互斥量)

特点

  • 同一个线程可以多次上锁(不会死锁)
  • 内部有引用计数,解锁次数 = 上锁次数才会真正释放
  • 比普通 mutex 慢一点

适用场景递归函数、多层嵌套函数需要加锁

3. std::timed_mutex(带超时的互斥量)

特点

  • 普通 mutex + 超时等待功能
  • 不会永久阻塞

新增方法

  • try_lock_for(时间):等待一段时间
  • try_lock_until(时间点):等到某个时间点

4. std::recursive_timed_mutex(递归 + 超时)

特点

  • 结合了递归 + 超时
  • 功能最全,但性能最差

使用案例

拿一个std::mutex和std::timed_mutex举例,别的都是类似的使用方式:

std::mutex:

  1. #include <iostream>
  2. #include <mutex>
  3. #include <thread>
  4. using namespace std;
  5. std::mutex mutex_;
  6. int main() {
  7. auto func1 = [](int k) {
  8. mutex_.lock();
  9. for (int i = 0; i < k; ++i) {
  10. cout << i << " ";
  11. }
  12. cout << endl;
  13. mutex_.unlock();
  14. };
  15. std::thread threads[5];
  16. for (int i = 0; i < 5; ++i) {
  17. threads[i] = std::thread(func1, 200);
  18. }
  19. for (auto& th : threads) {
  20. th.join();
  21. }
  22. return 0;
  23. }
复制代码

std::timed_mutex:

  1. #include <iostream>
  2. #include <mutex>
  3. #include <thread>
  4. #include <chrono>
  5. using namespace std;
  6. std::timed_mutex timed_mutex_;
  7. int main() {
  8. auto func1 = [](int k) {
  9. timed_mutex_.try_lock_for(std::chrono::milliseconds(200));
  10. for (int i = 0; i < k; ++i) {
  11. cout << i << " ";
  12. }
  13. cout << endl;
  14. timed_mutex_.unlock();
  15. };
  16. std::thread threads[5];
  17. for (int i = 0; i < 5; ++i) {
  18. threads[i] = std::thread(func1, 200);
  19. }
  20. for (auto& th : threads) {
  21. th.join();
  22. }
  23. return 0;
  24. }
复制代码

​ 实际开发中几乎不直接调用 lock ()/unlock (),因为容易忘记解锁导致死锁。推荐搭配 RAII 锁管理器

  • std::lock_guard:自动加锁、自动解锁(最简单)
  • std::unique_lock:更灵活,支持延迟加锁、超时、手动解锁

RAII 锁管理器

里主要介绍两种RAII方式的锁封装,可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。

c++11主要有std::lock_guard和std::unique_lock两种方式,使用方式都类似,如下:

  1. #include <iostream>
  2. #include <mutex>
  3. #include <thread>
  4. #include <chrono>
  5. using namespace std;
  6. std::mutex mutex_;
  7. int main() {
  8. auto func1 = [](int k) {
  9. // std::lock_guard<std::mutex> lock(mutex_);
  10. std::unique_lock<std::mutex> lock(mutex_);
  11. for (int i = 0; i < k; ++i) {
  12. cout << i << " ";
  13. }
  14. cout << endl;
  15. };
  16. std::thread threads[5];
  17. for (int i = 0; i < 5; ++i) {
  18. threads[i] = std::thread(func1, 200);
  19. }
  20. for (auto& th : threads) {
  21. th.join();
  22. }
  23. return 0;
  24. }
复制代码

std::lock_gurad相比于std::unique_lock更加轻量级,少了一些成员函数,std::unique_lock类有unlock函数,可以手动释放锁,所以条件变量都配合std::unique_lock使用,而不是std::lock_guard,因为条件变量在wait时需要有手动释放锁的能力,具体关于条件变量后面会讲到。

易错点

意外泄漏保护数据

_使用锁并不能一劳永逸,尽管你编写的函数保护了数据,但是如果函数中传入或者运行着某些不安全的函数,将保护数据泄露了,仍会造成危险

  1. class data{
  2. void do(){
  3. }
  4. };
  5. class data_wrapper{
  6. private:
  7. data my_data;
  8. std::mutex mtx;
  9. public:
  10. template<class func>
  11. void process_data(func invoker){
  12. std::lock_guard<std::mutex> lock(mtx);
  13. invoker(my_data);
  14. }
  15. };
  16. //尽管上面类是安全的,但是如果传入的可调用对象不安全,仍不能保证线程安全
  17. data* unprotected_data;
  18. void linked(data& linked_data){
  19. unprotected_data = &linked_data;
  20. }
  21. //如果process_data中传入了上面linked函数,则会导致保护数据泄漏到了unprotected_data中,这个数据会变得不安全
复制代码

_当函数中涉及到指针和引用时,尽管使用了锁,仍然不能保证一定安全

接口内线程安全不代表接口之间线程安全

_以栈为例,尽管栈内的接口是线程安全的,但是多个线程调用其接口时,并不能保证其栈容器整体的线程安全,以empty和size接口为例,线程A调用接口后,紧接着B线程调用pop或者push,会导致A线程得到的结果过期而不可用

  1. stack<int> st;
  2. if(!st.empty()){
  3. //当线程A运行到这,线程B调用了pop使容器为空,导致错误
  4. const int val = s.top();
  5. st.pop();
  6. val->do();
  7. }
  8. //另一种情形
  9. //thread A thread B
  10. if(!st.empty())
  11. if(!st.empty())
  12. int const val = st.top();
  13. int const val = st.top();
  14. st.pop();
  15. st.pop()
  16. ...
  17. //可以看到上面两个线程最后获取的都是同一个栈顶元素,而第二个元素未被处理而丢弃了
复制代码

_为了解决接口之间引起的多线程问题,有必要将其包装成一个原子的操作,或者对整个容器包装,使其同一时间只能被一个线程访问
_然而将接口包装包装成原子不合理时也会造成问题,我们将pop函数包装成弹出并返回栈顶元素,如果在return时发生异常会导致数据丢失

  1. auto pop(){
  2. const auto val = st.top();
  3. st.pop();
  4. return val;
  5. //如果return时返回数据的拷贝过程中内存不足而抛出异常,数据丢失
  6. }
复制代码

_为解决上面问题有三个方法

  • 传入引用

    _上面问题本质是返回数据是由于内存不足而导致的数据丢失,如果提前分配好内存,就可以避免,为此我们提前创建数据对象,然后将其引用传递进函数,再将top赋值给他

  • 为该数据提供不抛出异常的拷贝构造函数或者不抛出异常的移动构造函数

    _虽然这种方法安全,但是也限定了容器的可用范围,为包装容器的线程安全,容器的存储数据一定要提供上面两种构造函数,这需要再编译器使用std::is_nothrow_copy_constructible和std::is_nothrow_move_constructible判断

  • 返回指针

    _这需要我们注意内存安全,返回智能指针是个不错的选择

  • 结合方法1-3或1-2

死锁

_死锁是两个或多个函数以不同顺序请求锁资源造成的,平时编码时当需要多个锁资源时尽量按照相同顺序请求

_除此外如果能够同时将两个锁锁住,也可以解决,正好C++标准库提供了std::lock()函数

  1. class X{
  2. std::mutex mtx;
  3. data _data;
  4. public:
  5. friend void func(X& l,X& r){
  6. if(&l==&r) return;
  7. std::lock(l.mtx,r.mtx);//同时锁住两个锁,避免锁的先后顺序引起的死锁问题
  8. std::lock_guard<std::mutex> lock_l(l.mtx,std::adopt_lock);//std::adopt_lock 转交锁的所有权
  9. std::lock_guard<std::mutex> lock_r(r.mtx,std::adopt_lock);
  10. ...
  11. }
  12. };
复制代码

std::lock()的原理是对所有锁调用try_lock,如果不是所有锁都加锁成功,就将已经获得的锁释放

_针对上情景,C++17还提供scoped_lock<>,其相当于lock+lock_guard,可用自动管理多个锁的生命周期

  1. std::scoped_lock<std::mutex,std::mutex> lock(l.mtx,r.mtx);
  2. //根据C++17的自动模板推理,可简写为下面
  3. std::scoped_lock lock(l.mtx,r.mtx);
复制代码

_当然如果我们的需求是分开获得多个锁,上面方法就不可用了,还是得靠经验避免死锁,在代码的调试器可用引入死锁检测器,辅助排除死锁

_当然死锁不只有使用锁时会遇到,在任何等待资源的情景都可能发生,例如:两个线程互相等待,同样也是一种死锁

避免死锁的注意事项

  • 避免嵌套锁

    _同一线程只能持有一个锁,不要尝试获得第二个锁,实在不行就使用scoped_lock,一次性获得锁,当然即使这样也不能排除两个线程互相等待造成的死锁

  • 以固定顺序获取锁

  • 按层级获取锁

  • 避免调用未知的接口,无法知道该接口是否尝试获取锁

  • 将锁准则拓广到锁操作以外
    对于线程来说,简单的操作是由一个线程统一负责其他线程的创建和汇合

unique_lock

_unique_lock相较于lock_guard使用可更加灵活,lock_guard创建即上锁,unique_lock可用选择随后上锁,方法就是传入第二个构造参数,除了std::adopt_lock以转交锁的所有权外,通过std::defer_lock即可延迟上锁。
_不过为实现延迟上锁,unique_lock比lock_guard占用更多空间,也更慢一些,其需要维持一个标志来代表目前是否上锁,来决定析构时是否调用unlock,通过成员函数owns_lock可以查看这个标志

转移锁的拥有权

  1. using std::unique_lock<>
  2. unique_lock<std::mutex> get_lock(){
  3. extern std::mutex mtx;
  4. unique_lock lk(mtx);
  5. do();
  6. return lk;
  7. }
  8. void foo(){
  9. unique_lock lk(get_lock());
  10. do_();
  11. }
复制代码

保护共享数据的初始化

_某些资源如网络类,其创建的开销非常大,对于这种资源一般需要单例化并且进行延迟初始化,但是在多线程的环境下,由谁来初始化这个资源,怎么创建这个资源是个问题。

  1. std::shared_ptr<data> data_ptr;
  2. void foo(){
  3. if(!data_ptr){
  4. //1
  5. data_ptr.reset(new data);
  6. }
  7. data_ptr->do();
  8. }
  9. //单线程情况下上面代码是正确的,多线程下如果A线程已经运行到1处,此时线程B获得时间片并运行了完整的函数,此时A就会重复对data_ptr进行初始化
  10. 为此有了下面的版本
  11. void foo2(){
  12. if(!data_ptr){
  13. std::lock_guard<std::mutex> lk(mtx);
  14. if(!data_ptr){
  15. data_ptr.reset(new data);//1
  16. }
  17. }
  18. data_ptr->do();
  19. }
  20. //表面上通过双重验证避免了重复的初始化,但是有问题
  21. //假设A线程正在进行1段代码,但是由于编译器的指令重排,dara_ptr存储的指针可能先被赋值后初始化
  22. //而这是B线程进入此函数,看到了data_ptr不为空,就直接绕过了锁的保护,调用do函数,而此时数据并为完全初始化
复制代码

_正确做法

  1. 1.call_once
  2. //c++11提供了call_once函数和std::once_flag类,其能保证代码只被调用一次(由flag标记)
  3. template <class Callable, class... Args>
  4. void call_once(std::once_flag& flag, Callable&& f, Args&&... args);
  5. 2.静态全局变量
  6. 3.c++20中的用atomic<>对智能指针进行包装
复制代码

读写锁

_c++17提供shared_mutex,shared_timed_mutex,shared_lock

三者是 C++ 读写锁组件,shared_mutex 基础读写锁、shared_timed_mutex 支持超时、shared_lock 以 RAII 托管共享读锁,适配读多写少并发场景。

线程同步相关库

condition_variable

一、基本概念

1. 作用
  • 线程协作:当一个线程需要等待某个条件(如 “数据准备好”)才能继续执行时,可使用 condition_variable 进入等待状态。
  • 避免忙等待:线程等待时会释放锁,不消耗 CPU 资源,直到被其他线程唤醒。
2. 核心函数
  • wait(lock):释放锁并阻塞线程,直到被唤醒。唤醒后会重新获取锁。
  • notify_one():唤醒一个等待中的线程。
  • notify_all():唤醒所有等待中的线程。

二、使用方法

1. 经典范式:生产者 - 消费者模型
  1. #include <condition_variable>
  2. #include <mutex>
  3. #include <queue>
  4. std::queue<int> data_queue;
  5. std::mutex mtx;
  6. std::condition_variable cv;
  7. bool data_ready = false;
  8. // 生产者线程
  9. void producer() {
  10. for (int i = 0; i < 10; ++i) {
  11. std::unique_lock<std::mutex> lock(mtx);
  12. data_queue.push(i);
  13. data_ready = true;
  14. cv.notify_one(); // 通知消费者
  15. lock.unlock(); // 提前释放锁
  16. }
  17. }
  18. // 消费者线程
  19. void consumer() {
  20. while (true) {
  21. std::unique_lock<std::mutex> lock(mtx);
  22. // 使用 lambda 防止虚假唤醒
  23. cv.wait(lock, []{ return data_ready; });
  24. // 处理数据
  25. int data = data_queue.front();
  26. data_queue.pop();
  27. data_ready = !data_queue.empty();
  28. lock.unlock();
  29. // 处理数据(无锁状态)
  30. }
  31. }
复制代码

伪唤醒

_线程调用wait()后被其他线程通过notify唤醒了,但是需要满足的等待条件未达到,或者因为系统中断导致线程唤醒,为此wait不能仅仅检查等待条件一次,而是循环检查,防止被错误唤醒

  1. std::unique_lock lock(mtx);
  2. if(!cond){
  3. cv.wait(lock);
  4. }
  5. //⚠️错误
  6. while(!cond){
  7. cv.wait(lock);
  8. }
  9. //✔️正确
  10. cv.wait(lock,[]{return is_ready;});//简化写法
复制代码

使用条件变量构建简单线程安全队列

  1. //部分代码
  2. std::shared_ptr<T> try_pop(){
  3. std::lock_guard<std::mutex> lock(mtx);
  4. if(data.empty()) return std::shared_ptr<T>();
  5. std::shared_ptr<T> res(std::make_shared<T>(data.front()));
  6. data.pop();
  7. return res;
  8. }
  9. std::shared_ptr<T> wait_and_pop(){
  10. std::unique_lock<std::mutex> lock(mtx);
  11. cv.wait(lock,[]{return !data.empty();});
  12. ....
  13. }
复制代码

future

_假如我们需要获取一个结果,但是不知道这个结果什么时候准备好,如果使用条件变量的话,则需要一个专门的锁来保护,而一个锁仅仅只在这一个地方起作用实在有点浪费了,为此可以使用future来解决

_future库中存在两种future,分别为future和shared_future,为了使用库的功能,只需要对我们需要异步获取的结果进行包装,如果没有需要获取的异步结果,我们也可以使用特化的future和shared_future

此外,除了future库,c++还提供std::experimental命名空间,这个命名空间对标准库里面的future功能进行了扩展,但是并未被c++标准正式采纳,要使用这些工具,需要引入

一、基本概念

1. 异步操作的结果管理
  • std::future:存储异步操作的结果,可通过 get() 获取(只能调用一次)。
  • std::promise:设置异步操作的结果,与 future 关联。
  • std::packaged_task:包装可调用对象,自动关联 future。
  • std::async:直接启动异步任务并返回 future。
2. 工作流程
  1. 异步操作(线程/任务) → 设置结果(promise/packaged_task) → 获取结果(future)
复制代码

二、核心用法

1. 使用 std::async 启动异步任务
  1. #include <future>
  2. #include <iostream>
  3. int compute() {
  4. // 模拟耗时计算
  5. std::this_thread::sleep_for(std::chrono::seconds(2));
  6. return 42;
  7. }
  8. int main() {
  9. // 启动异步任务
  10. std::future<int> fut = std::async(std::launch::async, compute);
  11. // 主线程继续执行其他操作...
  12. // 获取结果(阻塞直到任务完成)
  13. int result = fut.get();
  14. std::cout << "Result: " << result << std::endl;
  15. }
复制代码
2. 使用 std::promise 手动控制结果
  1. void worker(std::promise<int>&& prom) {
  2. // 模拟工作
  3. int value = 100;
  4. // 设置结果
  5. prom.set_value(value);
  6. }
  7. int main() {
  8. std::promise<int> prom;
  9. std::future<int> fut = prom.get_future();
  10. // 启动线程并传递 promise
  11. std::thread t(worker, std::move(prom));
  12. // 获取结果
  13. int result = fut.get();
  14. t.join(); // 等待线程结束
  15. return 0;
  16. }
复制代码
3. 使用 std::packaged_task 包装任务
  1. int add(int a, int b) {
  2. return a + b;
  3. }
  4. int main() {
  5. // 包装任务
  6. std::packaged_task<int(int, int)> task(add);
  7. std::future<int> fut = task.get_future();
  8. // 执行任务(可在线程中执行)
  9. task(3, 4);
  10. // 获取结果
  11. std::cout << "Sum: " << fut.get() << std::endl;
  12. return 0;
  13. }
复制代码

三、高级特性

1. 超时等待
  1. std::future<int> fut = std::async(compute);
  2. // 等待结果,最多 3 秒
  3. if (fut.wait_for(std::chrono::seconds(3)) == std::future_status::ready) {
  4. int result = fut.get();
  5. std::cout << "Result ready: " << result << std::endl;
  6. } else {
  7. std::cout << "Timeout or deferred" << std::endl;
  8. }
复制代码
2. 异常处理
  1. int risky_compute() {
  2. throw std::runtime_error("Something went wrong");
  3. return 0;
  4. }
  5. int main() {
  6. std::future<int> fut = std::async(risky_compute);
  7. try {
  8. int result = fut.get();
  9. } catch (const std::exception& e) {
  10. std::cout << "Exception: " << e.what() << std::endl;
  11. }
  12. return 0;
  13. }
复制代码
3. 组合多个 future
  1. // 启动多个异步任务
  2. auto fut1 = std::async([]{ return 1; });
  3. auto fut2 = std::async([]{ return 2; });
  4. auto fut3 = std::async([]{ return 3; });
  5. // 等待所有任务完成
  6. int sum = fut1.get() + fut2.get() + fut3.get();
复制代码

四、注意事项

1. get() 只能调用一次
  1. std::future<int> fut = std::async(compute);
  2. int a = fut.get(); // 正确
  3. int b = fut.get(); // 错误:std::future_error
复制代码
2. 生命周期管理
  • future的状态必须在线程完成前保持有效:

    1. void bad_example() {
    2. std::promise<int> prom;
    3. std::future<int> fut = prom.get_future();
    4. // 错误:prom 在线程执行前被销毁
    5. std::thread([p = std::move(prom)]() mutable {
    6. p.set_value(42);
    7. }).detach();
    8. }
    复制代码
3. std::launch 策略
  • std::async的启动策略:

    1. // 强制创建新线程
    2. std::async(std::launch::async, compute);
    3. // 允许延迟执行(可能在调用 get() 时同步执行)
    4. std::async(std::launch::deferred, compute);
    5. // 默认策略(std::launch::async|std::launch::deferred,由编译器决定如何启动)
    6. std::async(compute);
    复制代码

五、常见应用场景

1. 并行计算
  1. auto fut1 = std::async([]{ return heavy_computation1(); });
  2. auto fut2 = std::async([]{ return heavy_computation2(); });
  3. // 合并结果
  4. int result = fut1.get() + fut2.get();
复制代码
2. 异步 IO
  1. std::future<std::string> read_file_async(const std::string& path) {
  2. return std::async(std::launch::async, [path] {
  3. std::ifstream file(path);
  4. std::string content((std::istreambuf_iterator<char>(file)),
  5. std::istreambuf_iterator<char>());
  6. return content;
  7. });
  8. }
复制代码
3. 实现异步 API
  1. class AsyncService {
  2. public:
  3. std::future<int> perform_task() {
  4. return std::async(std::launch::async, &AsyncService::task_impl, this);
  5. }
  6. private:
  7. int task_impl() { /* 实现 */ }
  8. };
复制代码

六、C++17 扩展:std::shared_future

std::future 的结果只能被获取一次,而 std::shared_future 允许多次获取:

  1. std::promise<int> prom;
  2. std::shared_future<int> shared_fut = prom.get_future().share();
  3. // 多个线程可同时获取结果
  4. auto t1 = std::thread([&] { std::cout << shared_fut.get(); });
  5. auto t2 = std::thread([&] { std::cout << shared_fut.get(); });
  6. prom.set_value(42);
  7. t1.join();
  8. t2.join();
复制代码

线程同步拓展

利用future进行函数式编程

_应该注意,async函数如果不显示指定策略,就无法预测其执行方式,不知道其何时执行,如果其依赖某一种顺序执行,肯会引发错误,或者期望其异步并行,结果实际同步执行了,造成性能的波动。如果大量async会创建过多线程,消耗系统资源,导致线程饱和。往后应使用线程池优化

消息传递进行同步

_CSP模型

experiment库(C++20引入)

then

_then函数的核心作用是为future绑定一个 “延续任务”(回调函数),当future关联的前序任务执行完成(状态变为就绪)时,延续任务会自动触发执行,无需手动调用get()/wait()阻塞等待前序结果。

手动模拟 then 函数(基于标准库)
由于 C++ 标准库无原生then,可通过 “线程 +future阻塞等待” 手动模拟延续任务的效果,核心思路是:创建一个新线程,在新线程中阻塞等待前序future就绪,然后执行延续任务。

  1. #include <iostream>
  2. #include <future>
  3. #include <chrono>
  4. #include <thread>
  5. #include <functional>
  6. // 手动实现then函数:接收前序future和延续任务,返回新的future
  7. template <typename T, typename Continuation>
  8. auto then(std::future<T> pre_fut, Continuation&& cont) -> std::future<decltype(cont(std::move(pre_fut)))> {
  9. // 封装延续任务到packaged_task
  10. using ResultType = decltype(cont(std::move(pre_fut)));
  11. std::packaged_task<ResultType()> task([pre_fut = std::move(pre_fut), cont = std::forward<Continuation>(cont)]() mutable {
  12. return cont(std::move(pre_fut)); // 等待前序任务完成,执行延续任务
  13. });
  14. // 获取新future并启动线程执行
  15. std::future<ResultType> cont_fut = task.get_future();
  16. std::thread t(std::move(task));
  17. t.detach(); // 分离线程(或手动管理线程生命周期)
  18. return cont_fut;
  19. }
  20. // 前序任务
  21. int calculate() {
  22. std::cout << "前序任务执行线程ID:" << std::this_thread::get_id() << std::endl;
  23. std::this_thread::sleep_for(std::chrono::seconds(1));
  24. return 50;
  25. }
  26. // 延续任务
  27. int multiply(std::future<int> fut) {
  28. int val = fut.get();
  29. std::cout << "延续任务执行线程ID:" << std::this_thread::get_id() << std::endl;
  30. return val * 3;
  31. }
  32. int main() {
  33. // 启动前序任务
  34. std::future<int> pre_fut = std::async(std::launch::async, calculate);
  35. // 手动调用then绑定延续任务
  36. std::future<int> cont_fut = then(std::move(pre_fut), multiply);
  37. // 获取延续任务结果
  38. std::cout << "延续任务结果:" << cont_fut.get() << std::endl;
  39. return 0;
  40. }
复制代码

✔️ 为了允许后续函数的多参数运行,有下面方法

1.lambda表达式捕获

  1. // 1. 前序任务
  2. boost::future<int> pre_fut = boost::async(boost::launch::async, pre_computation);
  3. // 2. 自定义参数
  4. std::string task_name = "【数据翻倍任务】";
  5. int multiplier = 3;
  6. bool need_log = true;
  7. // 3. then的延续任务:第一个参数是future,后续通过lambda捕获自定义参数
  8. boost::future<int> cont_fut = pre_fut.then([task_name, multiplier, need_log](boost::future<int> pre_result_fut) {
  9. ...
  10. });
复制代码

2.std::bind

  1. std::string process_data(boost::future<std::string> pre_fut,
  2. const std::string& prefix,
  3. int repeat_times,
  4. bool to_upper);
  5. auto bound_cont = std::bind(process_data,
  6. std::placeholders::_1, // 占位符:对应future参数
  7. prefix, // 自定义参数1
  8. repeat, // 自定义参数2
  9. upper); // 自定义参数3
复制代码

3.多参数运行的then手动实现

  1. template <typename T, typename Continuation, typename... Args>
  2. auto then(std::future<T> pre_fut, Continuation&& cont, Args&&... args)
  3. -> std::future<decltype(cont(std::move(pre_fut), std::forward<Args>(args)...))> {
  4. using ResultType = decltype(cont(std::move(pre_fut), std::forward<Args>(args)...));
  5. // 封装任务:绑定所有参数
  6. std::packaged_task<ResultType()> task(
  7. [pre_fut = std::move(pre_fut),
  8. cont = std::forward<Continuation>(cont),
  9. ... args = std::forward<Args>(args)]() mutable {
  10. return cont(std::move(pre_fut), std::forward<Args>(args)...);
  11. }
  12. );
  13. std::future<ResultType> cont_fut = task.get_future();
  14. std::thread t(std::move(task));
  15. t.detach(); // 实际项目建议用线程池管理
  16. return cont_fut;
  17. }
复制代码

_后续函数可以连环调用,并且在最后一个调用函数处进行异常处理,但是会注意到,每一个then操作都需要上层函数返回的结果,这并不是真正的异步,依旧会造成阻塞,为了解决这个问题,我们不对前一个then返回的future进行get操作,而是将其加入其他线程运行进行异步(如使用async)

⚠️⚠️⚠️⚠️⚠️ 需要注意,上面给出的then示例代码于标准库有误,标准库同步运行后续函数,若需要异步则需要在后续函数内完成相关逻辑,而我给出的代码默认开启一个线程实际非常不安全,除此外,后续函数内的异步逻辑尽量使用线程池,而不是async

when_all&when_any

std::when_all(C++20 引入的核心异步工具),它是用于等待多个std::future(或std::shared_future)同时就绪的批量同步操作,能高效实现 “多任务并行等待”,解决了手动逐个wait()/get()的繁琐与低效问题。

一、std::when_all的核心定义与特性

  1. 核心功能:接收多个独立的future对象作为参数,返回一个新的std::future,该新future会在所有输入future都就绪(完成执行 / 设置结果)后才就绪,不会因为某个future提前就绪而阻塞等待。
  2. 返回值类型
    • 若输入多个不同类型的future(如future、future),返回future>(元组类型,保存所有输入future的结果);
    • 若输入多个相同类型的future(如多个future),返回future>(向量类型,保存所有输入future的结果);
    • 自动处理嵌套future(利用future自动展开特性),无需手动解包。
  3. 非阻塞特性:std::when_all本身调用时不会阻塞,它只是返回一个 “批量等待凭证”(新future),只有对这个新future调用get()/wait()时,才会阻塞等待所有输入future就绪。
  4. 异常处理:若任意一个输入future存储了异常,当对when_all返回的future调用get()时,会抛出该异常(其余future的结果仍可正常获取,不会丢失)。

std::when_any(C++20 引入的异步批量处理工具),它与std::when_all相对应,核心是实现 “多个std::future中任意一个就绪(完成执行 / 设置结果)即返回”,适用于 “先到先处理”“超时兜底”“多源竞争” 等场景。

一、std::when_any的核心定义与特性

  1. 核心功能:接收多个独立的future(或shared_future)作为参数,返回一个新的std::future。该新future会在任意一个输入future就绪时立即就绪,无需等待其他未完成的任务,是 “竞争式” 批量等待工具。

  2. 返回值类型

    :固定返回

    1. std::future<std::pair<size_t, std::tuple<Futures...>>>
    复制代码

    (或对应重载的适配类型),其中:

    • size_t:就绪future在输入序列中的索引(标记哪个任务先完成);
    • std::tuple:包含所有输入future的元组(已就绪的future可获取结果,未就绪的仍可继续等待);
    • 支持自动展开嵌套future,也支持相同类型future的批量传入(适配vector迭代器)。
  3. 非阻塞特性:std::when_any本身调用时不阻塞,仅返回 “竞争等待凭证”;只有对返回的future调用get()/wait()时,才会阻塞至第一个future就绪。

  4. 异常处理:若某个输入future因异常就绪(存储了异常),std::when_any会将该future判定为 “就绪” 并立即返回,调用get()时会抛出该异常。

对比维度std::when_anystd::when_all
就绪条件任意一个输入future就绪即返回所有输入future就绪后才返回
返回值核心标记首个就绪任务的索引 + 所有future封装所有任务结果的tuple/vector
核心用途先到先处理、超时兜底、多源竞争(如多节点请求选最快)批量结果汇总、多任务协同(如多文件下载后合并)
等待耗时等于最快任务的耗时等于最慢任务的耗时
后续处理可继续处理剩余未完成的任务所有任务已完成,直接汇总结果
latch&&barrier

std::latch(C++20 引入的同步原语),是一种一次性计数同步工具,核心用于 “等待多个线程完成某一阶段任务后,再统一进入下一阶段”,常被称为 “门闩shuan”—— 一旦门闩打开(计数归 0),便无法再次关闭,具有不可复用性。

一、std::latch的核心定义与特性

  1. 核心功能:初始化时设置一个非负计数,线程可以调用wait()阻塞等待计数归 0;也可以调用count_down()将计数减 1(支持批量减n);当计数降至 0 时,所有阻塞在wait()上的线程会被同时唤醒,继续执行后续逻辑。
  2. 不可复用性:这是std::latch的关键特性 —— 计数归 0 后,无法重新设置计数,后续调用count_down()或wait()均无效,属于 “一次性同步工具”。
  3. 轻量级特性:std::latch是无锁实现(多数平台下),性能远高于std::mutex+std::condition_variable的手动计数实现,适合高性能并发场景。
  4. 核心接口
    • 构造函数:std::latch latch(count)(初始化计数,count为非负整数);
    • count_down(n = 1):将计数减少n(n不能超过当前计数),计数归 0 时唤醒所有等待线程;
    • wait():阻塞当前线程,直到计数归 0;
    • try_wait():非阻塞检查计数是否为 0,返回bool(true表示计数已归 0);
    • arrive_and_wait(n = 1):等价于count_down(n) + wait(),即 “先减计数,再等待计数归 0”。

std::barrier(C++20 引入的同步原语),是一种可复用的阶段式同步工具,常被称为 “屏障” 或 “栅栏”。它的核心作用是让一组线程在执行过程中,必须在指定的 “屏障点” 等待所有参与线程到达后,才能共同进入下一阶段,且支持多轮重复同步,完美适配流水线、循环阶段任务等场景。

一、std::barrier的核心定义与特性

  1. 核心功能:初始化时指定参与同步的线程数量(“参与数”),每个线程执行到屏障点时调用arrive_and_wait()(或相关接口),会阻塞等待其他所有线程到达;当最后一个线程到达屏障后,所有阻塞线程会被同时唤醒,且屏障会自动重置计数,支持下一轮同步,具备可复用性。
  2. 可复用性:这是std::barrier与std::latch的核心区别 —— 屏障不会因计数归 0 而失效,每轮所有线程到达后自动恢复初始参与数,可无限次重复使用。
  3. 阶段回调特性:构造std::barrier时可指定一个 “阶段完成回调函数”,当每一轮所有线程都到达屏障后,会自动执行该回调(由最后一个到达屏障的线程触发),用于处理每轮阶段的收尾 / 准备工作。
  4. 灵活的线程管理:支持通过arrive_and_drop()接口,让某个线程在到达屏障后 “退出同步组”,后续轮次的屏障参与数会永久减少 1,适配线程动态退出的场景。
  5. 核心接口
    • 构造函数:std::barrier barrier(participant_count, completion_func = {})(指定参与线程数和阶段回调,CompletionFunction为回调类型);
    • arrive_and_wait():当前线程到达屏障,阻塞等待其他线程,所有线程到达后唤醒并重置屏障;
    • arrive_and_drop():当前线程到达屏障,且后续不再参与同步,屏障参与数永久减 1,阻塞等待其他线程;
    • wait():阻塞当前线程,直到当前轮次所有线程到达屏障(较少使用,通常直接用arrive_and_wait())。
对比维度std::barrierstd::latch
复用性可复用(每轮同步后自动重置计数,支持多轮阶段任务)一次性(计数归 0 后失效,无法重置复用)
计数特性构造时指定参与线程数,支持arrive_and_drop动态减计数构造时固定计数,后续无法修改或调整
阶段回调支持内置阶段完成回调(最后一个线程到达时触发)无任何回调功能
核心用途多轮阶段同步(流水线、循环计算、迭代式任务)一次性等待(线程初始化、分块任务汇总)
性能开销略高(需维护重置逻辑和回调状态)更轻量(无复用 / 回调逻辑,无额外开销)
线程退出支持动态退出(arrive_and_drop)不支持动态调整,需确保计数匹配
flex_barrier(未纳入标准)

flex_barrier的本质是 “可动态调整参与数的可复用屏障”,它继承了std::barrier的阶段同步、可复用特性,同时弥补了std::barrier参与数固定(仅能通过arrive_and_drop永久减少)的局限性,核心特性如下:

  1. 灵活重置参与数:这是flex_barrier的核心优势 —— 阶段回调函数可以返回一个非负整数,该整数会作为下一轮屏障的参与线程数,支持每轮动态调整(增加、减少或保持不变),无需线程永久退出。
  2. 可复用性:与std::barrier一致,每轮所有线程到达屏障后,自动触发同步并重置,支持多轮阶段任务。
  3. 阶段回调增强:回调函数不再是 “无返回值”,而是通过返回值控制下一轮参与数,回调仍由最后一个到达屏障的线程触发。
  4. 兼容基础屏障接口:支持arrive_and_wait()(到达 + 等待)、arrive_and_drop()(到达 + 永久退出)等核心接口,用法与std::barrier兼容。
  5. 轻量级扩展:在std::barrier的基础上增加了参与数动态调整逻辑,性能开销略高于std::barrier,但远低于手动实现的灵活同步方案。

原子操作相关

原子操作定义

原子操作 (Atomic Operation)不可被中断的一个或一系列操作,从 CPU 指令层面保证,这个操作的执行过程中,不会被任何其他线程的指令打断,要么完整执行完毕,要么完全不执行,不存在「执行一半」的中间状态。

原子操作 vs 互斥锁

两者都是解决多线程数据竞争的核心方案,但适用场景和特性完全不同,是互补关系而非替代关系:

特性维度原子操作 std::atomic互斥锁 std::mutex/std::lock_guard
操作粒度细粒度(单个变量的读写 / 自增 / 比较等)粗粒度(一段代码块 / 多步操作的整体保护)
底层实现CPU 硬件级指令支持(无内核态切换)操作系统内核提供的同步机制(有内核态切换)
性能开销极小,几乎无额外开销较大,锁的加解锁、线程阻塞 / 唤醒都有开销
阻塞特性非阻塞,线程不会挂起,自旋等待阻塞式,竞争失败的线程会被挂起,让出 CPU
适用场景单个共享变量的并发读写(计数、标记位、状态量)多步操作组成的临界区(比如先判断变量值,再修改 + 赋值)

std::atomic

  1. std::atomic 封装的共享变量,所有成员方法都是原子操作,对该变量的读写 / 修改操作天然无数据竞争;
  2. 支持大部分基础数据类型:bool/char/short/int/long/long long、指针类型 T*,以及无符号版本;
  3. C++20 支持了更多复合类型,如double/float
  4. 不允许拷贝构造 / 拷贝赋值,因为拷贝是「读 + 写」两步操作,无法原子化,编译器会直接禁止。

原子类型别名

为了简化代码,C++ 标准库提供了常用原子类型的typedef 别名

  1. #include <atomic> // 必须包含的头文件
  2. // 等价关系:别名 = std::atomic<基础类型>
  3. std::atomic_bool flag; // std::atomic<bool>
  4. std::atomic_int cnt; // std::atomic<int>
  5. std::atomic_long val; // std::atomic<long>
  6. std::atomic_ulong uval; // std::atomic<unsigned long>
  7. std::atomic_uint64_t id; // std::atomic<uint64_t>
  8. std::atomic<int*> ptr; // std::atomic<int*>
复制代码

原子操作的核心:内存序

内存序的由来

CPU 存在指令重排、编译器存在代码优化重排,同时多线程的变量修改存在缓存一致性问题:线程 A 修改的变量值,线程 B 可能无法立刻看到(线程有自己的缓存,未同步到主存)。

内存序(std::memory_order):是原子操作的「约束规则」,定义了原子操作的执行顺序操作的可见性禁止编译器 / CPU 重排的范围,是原子操作中最难但最核心的知识点。

内存序的核心原则

所有原子操作的成员方法,都有一个可选的内存序参数,格式如下:

  1. // 以原子自增为例,memory_order是可选参数
  2. atomic_var.fetch_add(1, std::memory_order_seq_cst);
复制代码

默认规则:如果不指定内存序,所有原子操作默认使用 std::memory_order_seq_cst(顺序一致性),这是最强的内存序约束,也是最安全、最易理解的内存序。

3 种常用的内存序

(1) 顺序一致性 std::memory_order_seq_cst
  • 核心规则:全局顺序一致。所有线程看到的所有原子操作的执行顺序,都和代码书写的顺序完全一致,无任何重排;线程 A 修改的原子变量,线程 B 立刻可见。
  • 优点:逻辑最简单,绝对不会出错,无任何内存可见性问题;
  • 缺点:约束最强,编译器 / CPU 优化最少,性能相对略低(但比互斥锁高很多);
  • 适用场景:绝大多数业务场景,对性能要求不是极致苛刻的场景。
(2) 释放 - 获取 std::memory_order_release + std::memory_order_acquire
  • 核心规则:配对使用,读和写分离约束:

    • 写操作(比如store()、fetch_add())用 release:线程 A 的写操作完成后,所有在该写操作之前的内存操作,都不会被重排到该操作之后,且修改会同步到主存;
    • 读操作(比如load()、fetch_sub())用 acquire:线程 B 的读操作完成前,所有在该读操作之后的内存操作,都不会被重排到该操作之前,且会从主存读取最新值;
  • 优点:约束比 seq_cst 弱,编译器 / CPU 优化更多,性能更好;

  • 缺点:必须配对使用,否则会有可见性问题;

  • 适用场景:生产者 - 消费者模型、单写多读的原子变量场景。

(3) 宽松内存序 std::memory_order_relaxed
  • 核心规则:仅保证当前原子操作本身是原子的,不做任何顺序约束,不保证可见性,编译器 / CPU 可以任意重排该操作前后的指令,线程间的变量修改可能延迟可见;
  • 优点:性能最优,无任何额外约束;
  • 缺点:风险极高,可能出现「操作完成但其他线程看不到」的情况,仅保证无数据竞争;
  • 适用场景:纯计数场景(比如统计请求量、访问次数),只关心最终结果是否正确,不关心中间过程的顺序和可见性。

std::atomic 核心成员

1. 基础读写操作

(1) 原子赋值:store(val)

原子的将 val 写入原子变量,覆盖原有值,等价于 a = val(重载了=运算符,底层是 store)

  1. a.store(10); // 原子赋值 a = 10
  2. a = 20; // 等价写法,推荐简洁写法
复制代码
(2) 原子读取:load()

原子的读取原子变量的当前值,等价于 (int)a(重载了类型转换运算符,底层是 load)

  1. int x = a.load(); // 原子读取 a 的值
  2. int y = a; // 等价写法,推荐简洁写法
复制代码

2. 算术 / 位运算

这类方法是读 - 改 - 写原子操作:先读取值 → 执行运算 → 写入新值,返回运算前的旧值

✅ 方法命名规则:fetch_xxx 表示「获取旧值并执行 xxx 操作」

  1. a.fetch_add(1); // 原子自增:a = a + 1,返回自增前的旧值 → 等价于 a++
  2. a.fetch_sub(2); // 原子自减:a = a - 2,返回自减前的旧值
  3. a.fetch_and(3); // 原子按位与:a = a & 3,返回旧值
  4. a.fetch_or(4); // 原子按位或:a = a | 4,返回旧值
  5. a.fetch_xor(5); // 原子按位异或:a = a ^5,返回旧值
复制代码

💡 语法糖(更简洁):C++ 重载了算术运算符,推荐使用:

  1. a++; // 等价于 a.fetch_add(1),后置自增,返回旧值
  2. ++a; // 等价于 a.fetch_add(1),前置自增,返回新值
  3. a += 5; // 等价于 a.fetch_add(5)
  4. a -= 2; // 等价于 a.fetch_sub(2)
复制代码

3. 最核心的原子操作:compare_exchange_weak / compare_exchange_strong (CAS 操作)

(1) 核心名称:CAS = Compare And Swap(比较并交换),是无锁编程的基石
(2) 核心逻辑(原子的执行以下三步,不可中断):
  1. 比较:原子变量的当前值 vs 预期值 expected;
  2. 交换:如果两个值相等,则将原子变量的值更新为目标值 desired;
  3. 返回:成功返回true,失败返回false(失败时,会将原子变量的真实值写入expected,更新预期值)。
(3) 两个版本的区别
  • compare_exchange_strong(expected, desired):强 CAS,比较失败一定是「值不相等」,无虚假失败,推荐优先使用;
  • compare_exchange_weak(expected, desired):弱 CAS,可能出现「值相等但返回 false」的虚假失败(CPU 的伪唤醒),但性能略高,适合在循环中使用。
(4) 经典使用场景(必背):循环 CAS 实现原子的「条件修改」
  1. std::atomic<int> a(0);
  2. int expected = 0;
  3. int desired = 10;
  4. // 原子的判断:如果a的值等于expected(0),则将a改为desired(10)
  5. // 成功返回true,失败返回false,且expected会被更新为a的真实值
  6. while (!a.compare_exchange_weak(expected, desired)) {
  7. // 失败时,重新循环:expected已经被自动更新为a的真实值,无需手动赋值
  8. }
复制代码

✅ CAS 的核心价值:无锁实现「判断 + 修改」的原子操作,替代互斥锁保护的临界区,性能拉满。

4. 其他常用方法

(1) exchange(val):原子交换

原子的将原子变量的值改为val,返回修改前的旧值,等价于「原子赋值 + 原子读取」

  1. int old_val = a.exchange(100); // a变为100,返回a原来的值
复制代码
(2) is_lock_free():判断是否是无锁实现

返回bool值:true表示该原子类型是硬件级无锁实现(推荐),false表示是编译器用互斥锁模拟的原子操作(性能差)。

  1. if (a.is_lock_free()) {
  2. std::cout << "无锁原子操作,性能最优" << std::endl;
  3. }
复制代码

💡 规律:所有基础类型(int/long/bool/ 指针)的原子操作,在现代 CPU 上都是 lock-free 的。

注意事项

1. 原子操作的适用边界:仅支持「单个变量」

std::atomic 只能保证单个原子变量的操作是原子的,无法保证多个原子变量的组合操作是原子的

错误示例(有数据竞争,未定义行为):

  1. std::atomic<int> a(0), b(0);
  2. // 两个原子操作的组合,整体不是原子的!线程A执行a++时,线程B可能执行b++
  3. void func() { a++; b++; }
复制代码

如果需要保护多步操作 / 多个变量,必须用互斥锁 (std::mutex) 包裹,这是原子操作的「能力天花板」。

2. 原子类型不可拷贝 / 赋值

std::atomic 禁用了拷贝构造函数和拷贝赋值运算符,以下代码编译报错:

  1. std::atomic<int> a(10);
  2. std::atomic<int> b = a; // 错误:拷贝构造被禁用
  3. std::atomic<int> c;
  4. c = a; // 错误:拷贝赋值被禁用
复制代码

通过load()和store()原子读写实现值传递:

  1. std::atomic<int> a(10);
  2. std::atomic<int> b;
  3. b.store(a.load()); // 正确,原子读+原子写
复制代码

4. 原子操作的性能优势是「相对的」

原子操作比互斥锁快,但比普通变量的操作慢(因为要做硬件级的原子保证),不要把所有变量都声明为原子类型,只有「多线程共享读写」的变量才需要原子化。

atomic_flag

std::atomic_flag 是 C++11 原子布尔类型

std::atomic_flag 是无默认构造函数的,也不支持赋值初始化,C++ 标准规定:它只能通过宏 ATOMIC_FLAG_INIT 进行初始化,且初始化后固定为「清除状态 (clear)」,也就是初始值等价于false。

  1. #include <atomic>
  2. // ✅ 正确初始化:唯一合法写法,初始状态为 清除(clear/false)
  3. std::atomic_flag flag = ATOMIC_FLAG_INIT;
  4. // ❌ 错误写法1:无默认构造,编译报错
  5. std::atomic_flag flag1;
  6. // ❌ 错误写法2:不支持赋值初始化,编译报错
  7. std::atomic_flag flag2(false);
  8. // ❌ 错误写法3:不支持拷贝初始化,编译报错
  9. std::atomic_flag flag3 = flag;
复制代码

1. bool test_and_set( memory_order order = memory_order_seq_cst ) volatile noexcept;

核心语义:「测试并置位」,原子的执行两个操作(不可中断)

测试:读取当前atomic_flag的状态;

置位:将当前atomic_flag的状态强制设置为 置位 (set/true)

返回值:返回本次操作前的旧状态(重点)。

核心特点
  • 不管当前状态是什么,执行后一定会变成置位状态 (true)
  • 返回值是「操作前的值」,这是实现自旋锁的核心关键
代码示例
  1. std::atomic_flag flag = ATOMIC_FLAG_INIT; // 初始状态:clear(false)
  2. bool ret1 = flag.test_and_set(); // 返回旧值 false,flag现在变为 set(true)
  3. bool ret2 = flag.test_and_set(); // 返回旧值 true,flag依然是 set(true)
复制代码

2. void clear( memory_order order = memory_order_seq_cst ) volatile noexcept;

核心语义:「原子清除」
  • 无返回值,原子的将atomic_flag的状态设置为 清除状态 (clear/false)
  • 唯一作用就是把置位的标志复位,是test_and_set()的反向操作。
代码示例
  1. std::atomic_flag flag = ATOMIC_FLAG_INIT; // clear(false)
  2. flag.test_and_set(); // flag变为 set(true)
  3. flag.clear(); // flag变回 clear(false)
复制代码

C++20 对 std::atomic_flag 的增强(扩展知识点)

C++20 为了弥补std::atomic_flag功能的极简性,新增了 3 个便利的成员函数,完全兼容原有用法,让它的使用更友好,不需要再手动组合test_and_set和clear,新增方法如下(均支持内存序参数):

  1. #include <atomic>
  2. std::atomic_flag flag = ATOMIC_FLAG_INIT;
  3. // 1. test():原子读取当前状态,返回bool,不修改状态(弥补无load的缺陷)
  4. bool cur_state = flag.test(std::memory_order_seq_cst);
  5. // 2. wait(bool old_val):阻塞线程,直到标志的状态不等于old_val
  6. flag.wait(false); // 线程阻塞,直到flag变为true
  7. // 3. notify_one()/notify_all():唤醒等待该标志的一个/所有线程
  8. flag.notify_one();
复制代码

atomic<>的参数要求

一、std::atomic 对 T 的【硬性编译要求】

✅ 核心约束 1:T 必须是 可平凡复制 (Trivially Copyable) 类型

这是std::atomic对T的最核心、最基础的要求,C++ 标准强制规定,没有任何例外。

什么是「可平凡复制 (Trivially Copyable)」?
  • 通俗理解:类型的对象可以通过 内存二进制逐字节拷贝 完成复制,不需要调用自定义的拷贝构造 / 拷贝赋值 / 析构函数;
  • 技术定义:满足「平凡拷贝构造 + 平凡拷贝赋值 + 平凡析构」,且没有虚函数 / 虚基类;
  • 编译器提供的默认拷贝 / 赋值,就是「平凡的」。
✅ 满足该条件的常见类型(合法类型)

这类类型可以无条件作为std::atomic的模板参数,编译完全通过:

  1. 所有基础算术类型:bool/char/short/int/long/long long、无符号版本unsigned int等;float/double/long double;

  2. 所有指针类型:任意T*,比如int*、char*、std::string*、自定义类指针等;

  3. 所有枚举类型(enum/enum class);

  4. 所有

    平凡的聚合体 / 结构体 / 联合体

    :无自定义构造 / 拷贝 / 析构、无虚函数、成员均为平凡类型。

    1. // ✅ 合法:平凡结构体,可作为atomic的参数
    2. struct Point { int x; int y; };
    3. std::atomic<Point> p{};
    复制代码
❌ 违反该条件的常见类型(非法类型,编译报错)

这类类型绝对不能作为std::atomic的模板参数,编译器直接抛错,无任何兼容写法

  1. C++ 标准库的绝大多数容器:std::string、std::vector、std::list、std::map等(都有自定义拷贝 / 析构);

  2. 自定义拷贝构造 / 拷贝赋值 / 析构函数的类 / 结构体;

  3. 虚函数 / 虚基类的类(有虚表指针,拷贝逻辑非平凡);

  4. 带引用成员的类 / 结构体(引用不可赋值);

    1. // ❌ 全部编译报错,都是非平凡复制类型
    2. std::atomic<std::string> a_str; // std::string非平凡
    3. std::atomic<std::vector<int>> a_vec; // std::vector非平凡
    4. struct A { std::string s; }; std::atomic<A> a; // 成员非平凡,结构体也非平凡
    5. struct B { virtual void f(){} }; std::atomic<B> b; // 带虚函数
    复制代码

✅ 核心约束 2:T 必须是 可默认构造 类型

std::atomic 内部需要初始化封装的T类型对象,因此T必须支持默认构造(无参构造)。

  • 基础类型、指针类型、枚举类型:天然支持默认构造;

  • 自定义结构体:如果是聚合体,编译器生成默认构造;如果手动写了带参构造但没写默认构造,则编译报错。

    1. struct Test1 { int a; };
    2. std::atomic<Test1> t1; // ✅ 合法,编译器生成默认构造
    3. struct Test2 {
    4. int a;
    5. Test2(int x) : a(x) {} // 仅带参构造,无默认构造
    6. };
    7. std::atomic<Test2> t2; // ❌ 编译报错,Test2无默认构造
    复制代码

✅ 核心约束 3:T 必须是 可赋值 (Assignable) 类型

std::atomic 提供了store()、赋值重载operator=等写操作,因此T必须支持赋值操作(operator=)。

  • 违反场景极少:只有带

    1. const
    复制代码

    成员的结构体 / 类,或引用成员的类型,会被禁止赋值;

    1. struct Test { const int a; };
    2. std::atomic<Test> t; // ❌ 编译报错,Test的成员是const,不可赋值
    复制代码

二、std::atomic 对 T 的【隐式功能要求】(编译通过,但有坑)

这类约束的特点是:编译器不报错,但使用时功能受限 / 性能极差 / 无意义,是「语法合法、语义非法」的场景,也是 90% 的开发者会踩的坑,比硬性约束更重要

✅ 隐式要求 1:T 的内存大小 (size) 必须满足「硬件原子支持」

核心规则

std::atomic 的原子性,本质是 CPU 提供的硬件原子指令实现,而 CPU 的原子指令只支持固定大小的内存操作(比如 1/2/4/8 字节)。

  • 对于现代 x86/x64 架构的 CPU:原子指令支持 1,2,4,8,16 字节 的原子读写 / 修改;
  • 对于 ARM/RISC-V 等架构:原子指令通常支持 1,2,4,8 字节
✅ 满足条件:sizeof(T) 是 CPU 原子指令支持的大小

此时std::atomic是 真正的无锁 (lock-free) 原子操作,is_lock_free()返回true,性能拉满,这是我们期望的场景。

  1. std::atomic<int> a; // sizeof(int)=4 ✅ lock-free
  2. std::atomic<long long> b; // sizeof=8 ✅ lock-free
  3. std::atomic<int*> c; // sizeof(指针)=8 ✅ lock-free
  4. struct S { int x; int y; }; std::atomic<S> d; // sizeof=8 ✅ lock-free
复制代码
⚠️ 不满足条件:sizeof(T) 超出 CPU 原子指令支持的大小

此时编译器不会报错,但会做一个「兜底操作」:用内部的互斥锁 (mutex) 模拟原子操作,此时std::atomic的is_lock_free()返回false。

  • 问题 1:性能暴跌!原子操作的初衷是「无锁高性能」,结果退化成了带锁操作,性能和std::mutex几乎一致;
  • 问题 2:失去原子操作的意义,还不如直接用std::mutex保护普通T类型变量,代码可读性更高。
  1. // struct Big的大小是 12字节,超出x86原子指令的常用支持范围
  2. struct Big { int a; int b; int c; };
  3. std::atomic<Big> big_atomic;
  4. // 编译通过,但运行时 big_atomic.is_lock_free() → false
  5. // 底层是用锁模拟的原子操作,性能极差
复制代码

✅ 隐式要求 2:T 是「简单类型」才有实用价值

std::atomic 只提供了通用的原子操作接口:load()/store()/exchange()/compare_exchange_weak/strong()(CAS)。

  • 如果T是基础类型 / 指针 / 枚举:这些接口完全够用,能实现所有业务需求;
  • 如果T是复杂的结构体 / 联合体:虽然语法合法,但std::atomic不会为结构体的「成员」提供原子操作,只能对结构体整体做原子读写 / 交换 / CAS,几乎没有实用价值。

举个例子:

  1. struct Point { int x; int y; };
  2. std::atomic<Point> p{{1,2}};
  3. // ✅ 合法:整体原子读取/写入
  4. Point val = p.load();
  5. p.store({3,4});
  6. // ❌ 无法实现:原子的修改 p.x 的值,而不修改 p.y
  7. // 只能先整体读取 → 修改x → 整体写入,这是三步操作,非原子!
  8. Point temp = p.load();
  9. temp.x++;
  10. p.store(temp); // 存在数据竞争,线程不安全
复制代码

结论:不要用std::atomic封装复杂结构体,如果需要原子修改结构体的成员,要么拆分成员为多个原子变量,要么用std::mutex保护整个结构体。


三、std::atomic 【特化版本的特殊规则】

C++ 标准为了优化常用类型的性能,对std::atomic做了部分基础类型的全特化 / 偏特化,这些特化版本有额外的功能,同时也有专属的规则,补充你之前学的原子操作方法,这部分是高频考点。

✅ 规则 1:算术类型特化(bool/char/int/long/float/double等)

对所有算术类型,std::atomic 特化后额外提供算术原子操作

fetch_add()、fetch_sub()、fetch_and()、fetch_or()、fetch_xor(),以及对应的运算符重载++/--/+=/-=等。

只有算术类型才有这些方法,自定义结构体 / 指针类型没有!

  1. std::atomic<int> a{0};
  2. a.fetch_add(1); // ✅ 算术特化提供的方法,合法
  3. a++; // ✅ 运算符重载,合法
  4. struct S { int x; };
  5. std::atomic<S> s;
  6. s.fetch_add(1); // ❌ 编译报错,结构体无算术特化
复制代码

✅ 规则 2:指针类型特化(T*)

对所有指针类型T*,std::atomic 特化后额外提供指针算术原子操作

fetch_add()、fetch_sub(),支持指针的原子偏移,等价于ptr += n/ptr -= n。

指针类型没有位运算 (fetch_and/or/xor),只有加减操作。

  1. int arr[10];
  2. std::atomic<int*> p{arr};
  3. p.fetch_add(2); // ✅ 合法,指针原子偏移2个int,指向arr[2]
  4. p++; // ✅ 合法,指针原子自增
  5. p.fetch_and(1); // ❌ 编译报错,指针无位运算特化
复制代码

✅ 规则 3:std::atomic 的特殊约束

std::atomic 是算术类型的特化,但禁用了所有算术 / 位运算方法,只保留核心的load()/store()/exchange()/CAS,以及赋值重载operator=。

原因:布尔类型只有true/false,算术运算无意义,位运算也没必要,C++ 标准做了语义上的优化。

  1. std::atomic<bool> flag{false};
  2. flag = true; // ✅ 合法
  3. bool val = flag; // ✅ 合法
  4. flag.fetch_add(1); // ❌ 编译报错,bool无算术操作
  5. flag++; // ❌ 编译报错
复制代码

std::atomic 【C++11/C++17/C++20 的版本兼容性规则】

C++ 标准对std::atomic的支持是逐步放宽 + 增强的,不同 C++ 版本对T的要求有细微差异,日常开发中需要注意编译器的 C++ 版本配置,这是容易被忽略的细节:

✅ C++11 版本

  1. 仅对整数类型 / 指针类型做了完整特化,提供算术 / 指针操作;
  2. 对float/double的原子支持是 可选的(部分编译器不支持);
  3. 对自定义结构体的支持有限,仅能做基础的 load/store/exchange/CAS。

✅ C++17 版本

  1. 强制要求所有编译器支持float/double/long double的原子操作,不再是可选;
  2. 放宽了对自定义平凡结构体的支持,优化了无锁判断逻辑;
  3. 所有算术类型的原子操作接口统一,无兼容性问题。

✅ C++20 版本

  1. 新增了std::atomic_ref:可以对普通变量的引用做原子操作,突破了std::atomic的类型约束;
  2. 为std::atomic新增了wait()/notify_one()/notify_all()接口,支持原子变量的条件等待;
  3. 对std::atomic_flag做了增强(你之前学的test()方法)。

非成员函数

所有非成员函数都是模板函数

必须包含头文件,函数名均以atomic_为前缀,编译器自动推导模参数T。

  1. #include <atomic> // 必须包含,缺一不可
  2. std::atomic<int> a(0);
  3. atomic_fetch_add(&a, 1); // 模板函数,自动推导T=int
复制代码

第一个参数永远是「原子对象的指针」std::atomic*

核心语法特点:非成员函数不会接收原子对象的值 / 引用,只会接收原子对象的地址(指针),这是和成员函数最直观的区别。

  1. std::atomic<int> a(0);
  2. atomic_load(&a); // ✅ 正确:传地址 &a
  3. atomic_load(a); // ❌ 错误:传值,编译报错(原子类型不可拷贝)
  4. atomic_load(&a); // ❌ 错误:语法错误,地址书写错误
复制代码

内存序参数规则和成员函数完全一致

所有非成员函数都支持可选的内存序参数,默认内存序都是 std::memory_order_seq_cst(顺序一致性,最安全),内存序的取值 / 约束 / 使用场景和你学过的成员函数一模一样:

  • std::memory_order_seq_cst:默认,全局顺序一致,新手首选;
  • std::memory_order_acquire/std::memory_order_release:释放 - 获取配对,性能优化首选;
  • std::memory_order_relaxed:宽松内存序,极致性能,慎用。

内存序参数的传参位置统一在最后一位

  1. std::atomic<int> a(0);
  2. atomic_load(&a, std::memory_order_acquire); // 读操作:acquire
  3. atomic_store(&a, 10, std::memory_order_release); // 写操作:release
  4. atomic_fetch_add(&a, 1, std::memory_order_relaxed);// 自增:relaxed
复制代码

原子非成员函数 也遵守原子类型的约束

所有非成员函数的操作对象是std::atomic,因此对T的类型要求和之前一致:T必须是可平凡复制、可赋值类型;算术特化 / 指针特化的规则也完全继承,比如只有算术类型才能调用atomic_fetch_add。

std::atomic_flag 也有专属的非成员函数

std::atomic_flag 作为独立的原子类型(无模板参数),也有对应的两个非成员函数,同样遵守上述所有规则,后文单独讲解。

基础原子读写

对应成员函数:load() / store(),最常用的非成员函数

  1. atomic_load(p):原子读

  2. atomic_store(p, val):原子写

原子交换

对应成员函数:exchange(val)

  • 函数原型:template< class T > T atomic_exchange( std::atomic* p, T val );
  • 功能:原子将val写入原子对象,返回写入前的旧值
  • 带内存序:atomic_exchange_explicit(p, val, order)

原子算术 / 位运算

对应成员函数:fetch_add/fetch_sub/fetch_and/fetch_or/fetch_xor,以及运算符重载++/--/+=等

  1. atomic_fetch_add(p, n):原子加法,等价于p->fetch_add(n)
  2. atomic_fetch_sub(p, n):原子减法,等价于p->fetch_sub(n)
  3. atomic_fetch_and(p, n):原子按位与,等价于p->fetch_and(n)
  4. atomic_fetch_or(p, n):原子按位或,等价于p->fetch_or(n)
  5. atomic_fetch_xor(p, n):原子按位异或,等价于p->fetch_xor(n)

原子 CAS 操作

对应成员函数:compare_exchange_weak / compare_exchange_strong

atomic_compare_exchange_strong(p, expected, desired)
  • 原型:template< class T > bool atomic_compare_exchange_strong( std::atomic* p, T* expected, T desired );

  • 带内存序:atomic_compare_exchange_strong_explicit(p, expected, desired, success_order, fail_order)

2. 弱 CAS:atomic_compare_exchange_weak(p, expected, desired)
  • 原型和参数和强 CAS 完全一致;
  • 特点:可能虚假失败(值相等但返回 false),但性能略高,适合在循环中使用。

⚠️ 重要语法:CAS 非成员函数的第二个参数是指针 T* expected,而成员函数是引用 T& expected,这是唯一的语法差异,也是最容易踩坑的点!

指针类型特化

对应成员函数:指针类型的fetch_add/fetch_sub

  • 函数:atomic_fetch_add(p, n) / atomic_fetch_sub(p, n)
  • 约束:仅当T是指针类型时可用,实现指针的原子偏移(比如int*指针 + 1,指向数组下一个元素);

C++20 原子等待 / 通知(

C++20 为原子操作新增了wait/notify机制,对应成员函数wait()/notify_one()/notify_all(),非成员函数如下:

  1. atomic_wait(p, old_val):阻塞线程,直到*p的值不等于old_val;
  2. atomic_notify_one(p):唤醒一个等待该原子对象的线程;
  3. atomic_notify_all(p):唤醒所有等待该原子对象的线程;
  • 带内存序:均有_explicit版本,

std::atomic_flag 的专属非成员函数(

原子标志的两个核心非成员函数

std::atomic_flag 的成员函数只有 test_and_set() / clear(),对应非成员函数如下:

  1. atomic_flag_test_and_set(p)

    • 原型:bool atomic_flag_test_and_set( std::atomic_flag* p );
    • 带内存序:atomic_flag_test_and_set_explicit(p, order)
    • 等价:p->test_and_set(order),原子测试并置位,返回旧状态,执行后标志一定为true。
  2. atomic_flag_clear(p)

    • 原型:void atomic_flag_clear( std::atomic_flag* p );
    • 带内存序:atomic_flag_clear_explicit(p, order)
    • 等价:p->clear(order),原子清除标志,置为false。

atomic_thread_fence

其作用是在fence之前的代码必定发生在fence之后的代码前

在当前线程内,栅栏order满足约束时,fence 前的所有内存访问操作,必定发生在 fence 后的所有内存访问操作之前,编译器和 CPU 都无法打破这个顺序。

注意点

1. 约束的是「内存访问操作」,而非所有代码

栅栏只会约束读 / 写内存的指令(比如变量赋值、取值、数组访问),不会约束纯寄存器操作、算术运算等不涉及内存的代码。

比如下面的代码,a++(寄存器运算)可能被重排,但x=1(内存写)一定在栅栏前,y=2(内存写)一定在栅栏后:

  1. int a = 0, x = 0, y = 0;
  2. a++; // 纯寄存器运算,可重排(比如放到栅栏后)
  3. x = 1; // 内存写,栅栏前的内存操作
  4. atomic_thread_fence(memory_order_acquire);
  5. y = 2; // 内存写,栅栏后的内存操作
  6. a++; // 纯寄存器运算,可重排(比如放到栅栏前)
复制代码

核心:栅栏的目标是解决多线程内存可见性问题,所以只对「触碰内存」的操作生效,不影响 CPU 的纯内部运算优化。

2. 不同内存序(order),约束的「方向」不同

**全栅栏(seq_cst)*的效果(前→后完全禁止重排),但实际开发中最常用的acquire/release是*单向栅栏,并非双向约束

  • release栅栏(释放序)

    仅约束「栅栏前的内存操作」不能重排到「栅栏后」(后→前的重排不限制),是向后的单向屏障

  • acquire栅栏(获取序)

    仅约束「栅栏后的内存操作」不能重排到「栅栏前」(前→后的重排不限制),是向前的单向屏障

    对应消费者:先读同步标志,再栅栏,最后读数据 —— 保证「读数据」不会跑到「读标志」前面。

std::call_once

std::call_once 是 C++11 提供的线程安全、只执行一次的工具,专门解决多线程下的单例初始化、一次性初始化问题

保证某个函数 / 代码块,在整个程序生命周期里,只被执行 1 次

天生线程安全:多个线程同时触发,只有 1 个能真正执行,其他等待完成

两个关键组件

  1. std::once_flag

    • 一个标记,用来记录 “是否已经执行过”
    • 必须全局 / 静态,不能是局部变量!
  2. std::call_once(flag, func, args...)

    • 调用函数,且只执行一次

示例

  1. #include <iostream>
  2. #include <mutex>
  3. #include <thread>
  4. using namespace std;
  5. std::once_flag g_flag;
  6. void init() {
  7. cout << "初始化操作,只执行一次!\n";
  8. }
  9. void task() {
  10. // 多个线程调用,只会执行一次 init()
  11. std::call_once(g_flag, init);
  12. cout << "线程运行\n";
  13. }
  14. int main() {
  15. thread t1(task);
  16. thread t2(task);
  17. thread t3(task);
  18. t1.join(); t2.join(); t3.join();
  19. return 0;
  20. }
  21. /*
  22. 初始化操作,只执行一次!
  23. 线程运行
  24. 线程运行
  25. 线程运行
  26. */
复制代码

智能指针

详细内容请阅读《Effective Modern C++》Item 18-22

c++11引入了三种智能指针:

  • std::shared_ptr
  • std::weak_ptr
  • std::unique_ptr

RAII 简述

RAII 是 Resource Acquisition Is Initialization(资源获取即初始化)的缩写,这个名字取的很烂,说白了就是析构时候自动释放资源,把资源回收操作(close/delete等)写在析构函数中

核心作用

  • 自动管理资源,无需手动写 free/close/delete
  • 杜绝内存泄漏、文件泄漏、锁死等问题
  • 让代码更安全、更简洁、更健壮

常用标准库 RAII 工具

C++ 已经内置了大量 RAII 工具,日常直接用即可:

  • std::unique_ptr / std::shared_ptr:自动管理动态内存
  • std::lock_guard:自动管理互斥锁
  • std::fstream:自动打开 / 关闭文件
  • std::vector / std::string:自动管理内存

三类核心智能指针

1. unique_ptr 独占指针

  • 核心:独占、移动 - only、默认开销和裸指针一样大
  • 不可拷贝(拷贝构造 / 赋值 =delete),只能 std::move。
  • 支持自定义删除器,但删除器会成为类型一部分(unique_ptr)。
  • 常用场景:工厂函数、Pimpl、局部资源管理。
  1. // 工厂返回 unique_ptr
  2. std::unique_ptr<Widget> createWidget() {
  3. return std::make_unique<Widget>();
  4. }
  5. // 自定义删除器(如 FILE*)
  6. auto fileDel = [](FILE* f) { fclose(f); };
  7. std::unique_ptr<FILE, decltype(fileDel)> fp(fopen("a.txt","w"), fileDel);
复制代码

2. shared_ptr 共享指针

  • 核心:引用计数、共享、线程安全(计数本身)、有开销
  • 拷贝计数 + 1,析构 / 赋值计数 - 1;计数 = 0 自动 delete
  • 自定义删除器不影响类型(shared_ptr 统一)。
  • 坑:循环引用会导致内存泄漏(A→B,B→A)。
  1. std::shared_ptr<int> p1 = std::make_shared<int>(42);
  2. std::shared_ptr<int> p2 = p1; // 计数 = 2
  3. p1.reset(); // 计数 = 1
复制代码

3. weak_ptr 弱指针

  • 核心:不增加引用计数、不管理生命周期、用来打破循环引用

  • 只能从 shared_ptr 构造;用 .lock() 或 shared_ptr 构造转成 shared_ptr。

  • 判空:expired()(计数 0 则 true)。

相当于shared_ptr的观察器,要转为shared_ptr需要lock

  1. std::shared_ptr<int> sp = std::make_shared<int>(42);
  2. std::weak_ptr<int> wp = sp; // 计数不变
  3. if (!wp.expired()) {
  4. std::shared_ptr<int> sp2 = wp.lock(); // 计数+1
  5. }
复制代码

打破循环引用经典写法

  1. struct B;
  2. struct A { std::shared_ptr<B> b; };
  3. struct B { std::weak_ptr<A> a; }; // weak_ptr 不计数
复制代码

最佳实践

优先用 make_unique / make_shared,而非直接 new

理由:

  1. 异常安全:避免 new 与构造之间异常导致泄漏。

    1. 因为直接new会使对象初始化和智能指针的初始化分离,如下面代码中new int(10)完成后,如果初始化unique_ptr时发生异常,而此时new int(10)已经完成,分配了内存,而智能指针创建失败,最后导致这个内存泄露了
  2. 内存紧凑:make_shared 一次分配(对象 + 控制块),更高效。

  3. 代码简洁:少写一次类型。

    1. // 推荐
    2. auto up = std::make_unique<int>(10);
    3. auto sp = std::make_shared<int>(20);
    4. // 不推荐(异常不安全、两次分配)
    5. std::unique_ptr<int> up2(new int(10));
    复制代码

基于范围的for循环

  1. vector<int> vec;
  2. for (auto iter = vec.begin(); iter != vec.end(); iter++) { // before c++11
  3. cout << *iter << endl;
  4. }
  5. for (int i : vec) { // c++11基于范围的for循环
  6. cout << i << endl;
  7. }
复制代码

委托构造函数

委托构造函数允许在同一个类中一个构造函数调用另外一个构造函数,可以在变量初始化时简化操作

不使用委托构造函数:

  1. struct A {
  2. A(){}
  3. A(int a) { a_ = a; }
  4. A(int a, int b) { // 好麻烦
  5. a_ = a;
  6. b_ = b;
  7. }
  8. A(int a, int b, int c) { // 好麻烦
  9. a_ = a;
  10. b_ = b;
  11. c_ = c;
  12. }
  13. int a_;
  14. int b_;
  15. int c_;
  16. };
复制代码

使用委托构造函数:

  1. struct A {
  2. A(){}
  3. A(int a) { a_ = a; }
  4. A(int a, int b) : A(a) { b_ = b; }
  5. A(int a, int b, int c) : A(a, b) { c_ = c; }
  6. int a_;
  7. int b_;
  8. int c_;
  9. };
复制代码
  1. //派生类委托自身构造
  2. class Base {
  3. protected:
  4. int a;
  5. public:
  6. Base(int x) : a(x) {}
  7. };
  8. class Derive : public Base {
  9. private:
  10. int b;
  11. public:
  12. // 核心构造:先初始化父类,再初始化自身
  13. Derive(int x, int y) : Base(x), b(y) {}
  14. // 委托给本类双参构造
  15. Derive(int x) : Derive(x, 0) {}
  16. // 无参委托单参
  17. Derive() : Derive(10) {}
  18. void show() {
  19. cout << a << " " << b << endl;
  20. }
  21. };
复制代码

继承构造函数

作用:子类直接复用父类所有构造函数,不用手写转发构造,简化继承初始化代码

1. 语法

  1. using 父类名::父类构造;
复制代码

2. 基础示例

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. Base() { cout << "父类无参构造\n"; }
  6. Base(int a) { cout << "父类int构造:" << a << "\n"; }
  7. Base(int a, int b) { cout << "父类双参构造:"<<a<<" "<<b<<"\n"; }
  8. };
  9. // 子类继承构造函数
  10. class Derive : public Base {
  11. public:
  12. // 直接继承父类全部构造
  13. using Base::Base;
  14. };
  15. int main()
  16. {
  17. Derive d1;
  18. Derive d2(10);
  19. Derive d3(20,30);
  20. return 0;
  21. }
复制代码

3. 核心特性

  1. 自动继承父类所有构造,参数原样匹配调用
  2. 子类新增成员默认零初始化
  3. 不会继承拷贝构造、移动构造
  4. 子类同名构造会覆盖继承来的构造

4. 子类新增成员场景

  1. class Base {
  2. protected:
  3. int x;
  4. public:
  5. Base(int a) : x(a) {}
  6. };
  7. class Derive : public Base {
  8. private:
  9. int y = 100; // 就地初始化
  10. public:
  11. using Base::Base; // 继承父类构造
  12. void show()
  13. {
  14. cout << x << " " << y << endl;
  15. }
  16. };
  17. int main()
  18. {
  19. Derive d(50);
  20. d.show(); // 50 100
  21. }
复制代码

5. 构造冲突与覆盖

子类写同参数构造,优先用子类自身,屏蔽继承构造

  1. class Derive : public Base {
  2. public:
  3. using Base::Base;
  4. // 覆盖父类单参构造
  5. Derive(int a)
  6. {
  7. cout << "子类自定义构造\n";
  8. }
  9. };
复制代码

6. 访问权限

  • public继承:using 后构造对外 public
  • protected/private继承:可控制继承构造权限

nullptr

nullptr是c++11用来表示空指针新引入的常量值,在c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL自身存在二义性(0或者空指针,但是偏向转为0)

  1. void func(void *ptr) {
  2. cout << "func ptr" << endl;
  3. }
  4. void func(int i) {
  5. cout << "func i" << endl;
  6. }
  7. int main() {
  8. func(NULL); // 编译失败,会产生二义性
  9. func(nullptr); // 输出func ptr
  10. return 0;
  11. }
复制代码

final & override

c++11关于继承新增了两个关键字,final用于修饰一个类,表示禁止该类进一步派生和虚函数的进一步重载,override用于修饰派生类中的成员函数,标明该函数重写了基类函数,如果一个函数声明了override但父类却没有这个虚函数,编译报错,使用override关键字可以避免开发者在重写基类函数时无意产生的错误。

示例代码1:

  1. struct Base {
  2. virtual void func() {
  3. cout << "base" << endl;
  4. }
  5. };
  6. struct Derived : public Base{
  7. void func() override { // 确保func被重写
  8. cout << "derived" << endl;
  9. }
  10. void fu() override { // error,基类没有fu(),不可以被重写
  11. }
  12. };
复制代码

示例代码2:

  1. struct Base final {
  2. virtual void func() {
  3. cout << "base" << endl;
  4. }
  5. };
  6. struct Derived : public Base{ // 编译失败,final修饰的类不可以被继承
  7. void func() override {
  8. cout << "derived" << endl;
  9. }
  10. };
复制代码

default

c++11引入default特性,多数时候用于声明构造函数为默认构造函数,如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数,如下代码:

  1. struct A {
  2. int a;
  3. A(int i) { a = i; }
  4. };
  5. int main() {
  6. A a; // 编译出错
  7. return 0;
  8. }
复制代码

上面代码编译出错,因为没有匹配的构造函数,因为编译器没有生成默认构造函数,而通过default,程序员只需在函数声明后加上“=default;”,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体,如下:

  1. struct A {
  2. A() = default;
  3. int a;
  4. A(int i) { a = i; }
  5. };
  6. int main() {
  7. A a;
  8. return 0;
  9. }
复制代码

拓展C++ 编译器自动生成构造 / 析构函数规则

默认构造函数(无参构造)

自动生成条件

​ 你完全没有手动写任何构造函数(包括无参、有参构造)。

不会生成的情况

​ 只要你手动写了任意一个构造函数(比如带参构造),编译器就不再生成默认构造函数。

析构函数

自动生成条件

没有手动编写析构函数

拷贝构造函数

自动生成条件

没有手动写拷贝构造函数,且没有写禁止拷贝的函数

生成的行为

浅拷贝 —— 逐字节拷贝成员变量(指针只会拷贝地址,不会拷贝指向的内存)。

不会生成的情况

  1. 手动写了拷贝构造
  2. 手动写了移动构造 / 移动赋值(C++11)
  3. 有成员变量 / 基类不可拷贝(如 std::mutex)

拷贝赋值运算符 operator=

自动生成条件

没有手动写拷贝赋值运算符

生成的行为

同样是浅拷贝,逐字节赋值成员变量。

不会生成的情况

  1. 手动写了拷贝赋值
  2. 手动写了移动构造 / 移动赋值(C++11)
  3. 有成员变量 / 基类不可赋值(const)

5. 移动构造 / 移动赋值运算符(C++11)

这两个是自动生成最严格的,必须满足所有条件才会生成:

  1. 没有手动写拷贝构造、拷贝赋值、移动构造、移动赋值
  2. 没有手动写析构函数
  3. 类中所有成员都支持移动

生成的行为

逐成员移动(对基础类型等同于拷贝)。

一句话记忆

五法则 —— 只要你手动写了拷贝 / 移动 / 析构中的任意一个,编译器就不会自动生成移动函数

delete

c++中,如果开发人员没有定义特殊成员函数,那么编译器在需要特殊成员函数时候会隐式自动生成一个默认的特殊成员函数,例如拷贝构造函数或者拷贝赋值操作符,如下代码:

  1. struct A {
  2. A() = default;
  3. int a;
  4. A(int i) { a = i; }
  5. };
  6. int main() {
  7. A a1;
  8. A a2 = a1; // 正确,调用编译器隐式生成的默认拷贝构造函数
  9. A a3;
  10. a3 = a1; // 正确,调用编译器隐式生成的默认拷贝赋值操作符
  11. }
复制代码

有时候想禁止对象的拷贝与赋值,可以使用delete修饰,如下:

  1. struct A {
  2. A() = default;
  3. A(const A&) = delete;
  4. A& operator=(const A&) = delete;
  5. int a;
  6. A(int i) { a = i; }
  7. };
  8. int main() {
  9. A a1;
  10. A a2 = a1; // 错误,拷贝构造函数被禁用
  11. A a3;
  12. a3 = a1; // 错误,拷贝赋值操作符被禁用
  13. }
复制代码

explicit

explicit专用于修饰构造函数,表示只能显式构造,不可以被隐式转换,根据代码看explicit的作用:

不用explicit:

  1. struct A {
  2. A(int value) { // 没有explicit关键字
  3. cout << "value" << endl;
  4. }
  5. };
  6. int main() {
  7. A a = 1; // 可以隐式转换
  8. return 0;
  9. }
复制代码

使用explicit:

  1. struct A {
  2. explicit A(int value) {
  3. cout << "value" << endl;
  4. }
  5. };
  6. int main() {
  7. A a = 1; // error,不可以隐式转换
  8. A aa(2); // ok
  9. return 0;
  10. }
复制代码

constexpr

constexpr为编译期常量,顾名思义要求对象能够在编译期间就求值

要理解这个就需要理解const

const相较于constexpr其实意义上更偏向于只读,用于标注这个对象不允许修改,其对象的值是在运行期间确定的(存在例外 如 const int a = 10;会被编译器优化成字面量,在编译器就确定值),所以不能用于当模板参数、全局区变长数组长度。其次const对象的const属性是可以使用const_cast摘除的

constexpr = const + 编译期可知

  • 对象加强版 const,既是只读,又保证编译期可求值。
  • 函数双模式函数—— 参数全为编译期常量时,编译期求值;否则退化到运行时执行。
  • 本质:把计算从运行时搬到编译时,减少运行时开销、增强类型安全(当然肯定会增加编译时间)。
  • 比const优点还是多的,可以尽量用

constexpr 函数

行为:编译期能算就编译期算,否则运行期算

  • 传入编译期常量实参 → 返回编译期常量
  • 传入运行期变量实参 → 返回运行期值
C++11 限制(很严)
  • 函数体只能一行(仅 return);
  • 成员函数默认隐含 const
  • 不能返回 void、不能修改成员。
  1. constexpr int square(int x) { return x * x; } // C++11 合法
复制代码
C++14 放宽(常用)
  • 允许多行、局部变量、if/for、修改成员;
  • 成员函数不再隐含 const
  • 可返回 void。
  1. constexpr int square(int x) {
  2. int res = 1;
  3. for (int i = 0; i < x; ++i) res *= x;
  4. return res;
  5. }
复制代码

constexpr 成员函数与字面类型

  • 字面类型(LiteralType):可在编译期构造 / 复制 / 析构的类型;
  • constexpr 构造函数:编译期可创建对象
  • constexpr 成员函数:编译期可调用
  1. class Point {
  2. int x_, y_;
  3. public:
  4. constexpr Point(int x, int y) : x_(x), y_(y) {}
  5. constexpr int x() const { return x_; }
  6. constexpr void setX(int x) { x_ = x; } // C++14 允许
  7. };
  8. constexpr Point p(1,2); // 编译期创建
  9. constexpr int px = p.x(); // 编译期取值
复制代码

enum class

c++11新增有作用域的枚举类型,看代码

不带作用域的枚举代码:

  1. enum AColor {
  2. kRed,
  3. kGreen,
  4. kBlue
  5. };
  6. enum BColor {
  7. kWhite,
  8. kBlack,
  9. kYellow
  10. };
  11. int main() {
  12. if (kRed == kWhite) {
  13. cout << "red == white" << endl;
  14. }
  15. return 0;
  16. }
复制代码

如上代码,不带作用域的枚举类型可以自动转换成整形,且不同的枚举可以相互比较,代码中的红色居然可以和白色比较,这都是潜在的难以调试的bug,而这种完全可以通过有作用域的枚举来规避。

有作用域的枚举代码:

  1. enum class AColor {
  2. kRed,
  3. kGreen,
  4. kBlue
  5. };
  6. enum class BColor {
  7. kWhite,
  8. kBlack,
  9. kYellow
  10. };
  11. int main() {
  12. if (AColor::kRed == BColor::kWhite) { // 编译失败
  13. cout << "red == white" << endl;
  14. }
  15. return 0;
  16. }
复制代码

使用带有作用域的枚举类型后,对不同的枚举进行比较会导致编译失败,消除潜在bug,同时带作用域的枚举类型可以选择底层类型,默认是int,可以改成char等别的类型。

  1. enum class AColor : char {
  2. kRed,
  3. kGreen,
  4. kBlue
  5. };
复制代码

非受限联合体

c++11之前union中数据成员的类型不允许有非POD类型,而这个限制在c++11被取消,允许数据成员类型有非POD类型,看代码:

  1. struct A {
  2. int a;
  3. int *b;
  4. };
  5. union U {
  6. A a; // 非POD类型 c++11之前不可以这样定义联合体
  7. int b;
  8. };
复制代码

对于什么是POD类型,大家可以自行查下资料,大体上可以理解为对象可以直接memcpy的类型。

了解即可因为c++17会使用std::varient来替代使用union

sizeof

c++11中sizeof可以用的类的数据成员上,看代码:

c++11前需要创建对象:

  1. struct A {
  2. int data[10];
  3. int a;
  4. };
  5. int main() {
  6. A a;
  7. cout << "size " << sizeof(a.data) << endl;
  8. return 0;
  9. }
复制代码

c++11后:

  1. struct A {
  2. int data[10];
  3. int a;
  4. };
  5. int main() {
  6. cout << "size " << sizeof(A::data) << endl;
  7. return 0;
  8. }
复制代码

想知道类中数据成员的大小在c++11中是不是方便了许多,而不需要定义一个对象,在计算对象的成员大小。

assertion

  1. static_assert(true/false, message);
  2. static_assert(sizeof(int)==4, "int必须4字节");
复制代码

c++11引入static_assert声明,用于在编译期间检查,如果第一个参数值为false,则打印message,编译失败。

  • 常用于约束类型、大小、模板参数

类似的还有

assert

运行期判断,表达式为假崩溃

  1. assert(x > 0);
复制代码
  • 发布版定义#define NDEBUG直接失效删除
  • 只用于调试校验,不能写业务逻辑

std::dynamic_assert (条件,信息) C++23

运行期断言,不受 NDEBUG 控制

  1. std::dynamic_assert(len > 0, "长度非法");
复制代码
  • 发布版依旧生效
  • 条件不满足触发异常 / 终止

自定义字面量

c++11可以自定义字面量,平时c++中都或多或少使用过chrono中的时间,例如:

  1. std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 100ms
  2. std::this_thread::sleep_for(std::chrono::seconds(100)); // 100s
复制代码

其实没必要这么麻烦,也可以这么写:

  1. std::this_thread::sleep_for(100ms); // c++14里可以这么使用,这里只是举个自定义字面量使用的例子
  2. std::this_thread::sleep_for(100s);
复制代码

这就是自定义字面量的使用,示例如下:

  1. struct mytype {
  2. unsigned long long value;
  3. };
  4. constexpr mytype operator"" _mytype ( unsigned long long n ) {
  5. return mytype{n};
  6. }
  7. mytype mm = 123_mytype;
  8. cout << mm.value << endl;
复制代码

关于自定义字面量,可以看下chrono的源代码。

内存对齐

什么是内存对齐

理论上计算机对于任何变量的访问都可以从任意位置开始,然而实际上系统会对这些变量的存放地址有限制,通常将变量首地址设为某个数N的倍数,这就是内存对齐。

为什么要内存对齐
  1. 硬件平台限制,内存以字节为单位,不同硬件平台不一定支持任何内存地址的存取,一般可能以双字节、4字节等为单位存取内存,为了保证处理器正确存取数据,需要进行内存对齐。
  2. 提高CPU内存访问速度,一般处理器的内存存取粒度都是N的整数倍,假如访问N大小的数据,没有进行内存对齐,有可能就需要两次访问才可以读取出数据,而进行内存对齐可以一次性把数据全部读取出来,提高效率。

alignof (查询对齐要求)

作用:获取类型 / 变量的对齐值(字节)

  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. cout << alignof(char) << endl; // 1
  5. cout << alignof(int) << endl; // 4
  6. cout << alignof(double) << endl; // 8
  7. return 0;
  8. }
复制代码

alignas (指定对齐值)

作用:强制变量 / 类型按指定字节对齐

  1. // 让 int 按 8 字节对齐
  2. alignas(8) int a;
  3. // 结构体按 16 字节对齐
  4. struct alignas(16) MyStruct { int x; };
  5. // 按某个类型的对齐值对齐
  6. alignas(double) int b;
复制代码

std::alignment_of (模板工具)

  1. #include <type_traits>
  2. std::alignment_of<int>::value; // 等价于 alignof(int)
复制代码

std::max_align_t

最大标准对齐值:所有基础类型中最大的对齐要求

  1. #include <cstddef>
  2. alignof(std::max_align_t); // 通常 8 或 16
复制代码

2. std::aligned_storage

创建指定大小 + 对齐的内存块(用于实现 placement new、内存池)

  1. using Storage = std::aligned_storage<sizeof(int), alignof(double)>::type;
复制代码

3. std::aligned_union

创建满足所有成员对齐的联合内存

  1. using U = std::aligned_union<0, int, double>::type;
复制代码

对齐内存分配函数(C++11 及以上)

std::aligned_alloc (C++17)

分配指定对齐的内存

  1. #include <cstdlib>
  2. // 分配 64 字节,对齐 16 字节
  3. void* ptr = std::aligned_alloc(16, 64);
  4. std::free(ptr);
复制代码

要求:

  • 对齐值必须是 2 的幂
  • 大小必须是对齐值的整数倍

2. posix_memalign (Linux/macOS)

  1. #include <cstdlib>
  2. void* ptr;
  3. posix_memalign(&ptr, 16, 64); // 对齐 16,大小 64
  4. free(ptr);
复制代码

3. _aligned_malloc / _aligned_free (Windows)

  1. #include <malloc.h>
  2. void* ptr = _aligned_malloc(64, 16);
  3. _aligned_free(ptr);
复制代码

检查对齐的工具函数

1. std::is_aligned (C++20)

检查指针是否满足指定对齐

  1. #include <memory>
  2. bool ok = std::is_align(16, ptr);
复制代码

2. 手动判断对齐(通用)

  1. template<size_t N>
  2. bool is_aligned(const void* ptr) {
  3. return (reinterpret_cast<uintptr_t>(ptr) % N) == 0;
  4. }
复制代码

thread_local

c++11引入thread_local,用thread_local修饰的变量具有thread周期,每一个线程都拥有并只拥有一个该变量的独立实例,一般用于需要保证线程安全的函数中。

  1. #include <iostream>
  2. #include <thread>
  3. class A {
  4. public:
  5. A() {}
  6. ~A() {}
  7. void test(const std::string &name) {
  8. thread_local int count = 0;
  9. ++count;
  10. std::cout << name << ": " << count << std::endl;
  11. }
  12. };
  13. void func(const std::string &name) {
  14. A a1;
  15. a1.test(name);
  16. a1.test(name);
  17. A a2;
  18. a2.test(name);
  19. a2.test(name);
  20. }
  21. int main() {
  22. std::thread(func, "thread1").join();
  23. std::thread(func, "thread2").join();
  24. return 0;
  25. }
复制代码

输出:

  1. thread1: 1
  2. thread1: 2
  3. thread1: 3
  4. thread1: 4
  5. thread2: 1
  6. thread2: 2
  7. thread2: 3
  8. thread2: 4
复制代码

验证上述说法,对于一个线程私有变量,一个线程拥有且只拥有一个该实例,类似于static。

基础数值类型

c++11新增了几种数据类型:long long、char16_t、char32_t等

随机数功能(了解)

c++11关于随机数功能则较之前丰富了很多,典型的可以选择概率分布类型

  1. #include <time.h>
  2. #include <iostream>
  3. #include <random>
  4. using namespace std;
  5. int main() {
  6. std::default_random_engine random(time(nullptr));
  7. std::uniform_int_distribution<int> int_dis(0, 100); // 整数均匀分布
  8. std::uniform_real_distribution<float> real_dis(0.0, 1.0); // 浮点数均匀分布
  9. for (int i = 0; i < 10; ++i) {
  10. cout << int_dis(random) << ' ';
  11. }
  12. cout << endl;
  13. for (int i = 0; i < 10; ++i) {
  14. cout << real_dis(random) << ' ';
  15. }
  16. cout << endl;
  17. return 0;
  18. }
复制代码

输出:

  1. 38 100 93 7 66 0 68 99 41 7
  2. 0.232202 0.617716 0.959241 0.970859 0.230406 0.430682 0.477359 0.971858 0.0171148 0.64863
复制代码

代码中举例的是整数均匀分布和浮点数均匀分布,c++11提供的概率分布类型还有好多,例如伯努利分布、正态分布等

正则表达式

C++11 起正式纳入 `` 标准库,提供正则编译、匹配、搜索、替换、迭代提取等完整能力

  1. using namespace std;
复制代码
1. 核心类
  • std::regex:编译后的正则模式对象(核心)。
  • std::smatch:string 匹配结果容器,存完整匹配与捕获组。
  • std::ssub_match:单个子匹配(捕获组)的内容与位置。
2. 核心函数
  • regex_match全文严格匹配(整个字符串必须符合模式)。
  • regex_search局部搜索(只要包含匹配子串即成功)。
  • regex_replace:替换匹配到的子串,支持反向引用。
  • sregex_iterator:迭代遍历所有匹配项(常用)。

正则语法(ECMAScript 默认)

1. 元字符
符号说明
.任意字符(除换行)
^字符串开头
$字符串结尾
\d数字 [0-9]
\w字母 / 数字 / 下划线
\s空白(空格 / 制表 / 换行)
\转义符(C++ 字符串需写 \\)
2. 量词
  • *:0 次或多次(贪婪)
  • +:1 次或多次
  • ?:0 次或 1 次
  • {n}:恰好 n 次
  • {n,}:至少 n 次
  • {n,m}:n~m 次
3. 捕获组
  • (pattern):捕获组 1、2…,match [0] 为全文,match [1] 为组 1。
  • (?:pattern):非捕获组(不存结果,仅分组)。

四大核心函数示例

C++ 字符串 \ 需写 \\,推荐 原始字符串 R"()" 避免转义。

1. regex_match(全文匹配)

验证手机号(简单)

  1. string phone = "13812345678";
  2. regex pat(R"(^1\d{10}$)"); // R"()" 原始字符串,免转义
  3. if (regex_match(phone, pat)) {
  4. cout << "手机号合法\n";
  5. }
复制代码
2. regex_search(局部搜索 + 捕获组)

提取日期(yyyy-mm-dd):

  1. string text = "今天是2026-05-23,明天2026-05-24";
  2. regex pat(R"((\d{4})-(\d{2})-(\d{2}))");
  3. smatch m;
  4. if (regex_search(text, m, pat)) {
  5. cout << "全文匹配:" << m[0] << endl;
  6. cout << "年:" << m[1] << " 月:" << m[2] << " 日:" << m[3] << endl;
  7. }
复制代码
3. sregex_iterator(遍历所有匹配)

提取所有数字:

  1. string text = "a1 b22 c333";
  2. regex pat(R"(\d+)");
  3. sregex_iterator it(text.begin(), text.end(), pat);
  4. sregex_iterator end;
  5. for (; it != end; ++it) {
  6. cout << "匹配:" << (*it)[0] << endl;
  7. }
复制代码
4. regex_replace(替换 + 反向引用)

日期格式转换(yyyy-mm-dd → mm/dd/yyyy):

  1. string date = "2026-05-23";
  2. regex pat(R"((\d{4})-(\d{2})-(\d{2}))");
  3. // $1$2$3 对应捕获组1、2、3
  4. string res = regex_replace(date, pat, "$2/$3/$1");
  5. cout << res; // 输出 05/23/2026
复制代码

案例

  1. //邮箱提取与验证
  2. string text = "联系我:support@company.com 或 sales@test.cn";
  3. // 邮箱正则:用户名@域名.后缀
  4. regex email_pat(R"((\w+([.-]?\w+)*)@(\w+([.-]?\w+)*\.\w{2,}))");
  5. sregex_iterator it(text.begin(), text.end(), email_pat);
  6. sregex_iterator end;
  7. for (; it != end; ++it) {
  8. cout << "邮箱:" << (*it)[0] << endl;
  9. cout << "用户名:" << (*it)[1] << " 域名:" << (*it)[3] << endl;
  10. }
复制代码

chrono

时钟类

一、核心区别总览表
特性std::system_clockstd::steady_clockstd::high_resolution_clock
时间来源系统实时时间(墙钟时间)系统单调递增时间(不受系统时间影响)实现定义(通常是 steady_clock 或 system_clock 的别名)
是否可调整是(系统时间可手动修改 / 网络同步)否(自启动后单调递增,永不回退)取决于底层实现(若为 steady_clock 则不可调,若为 system_clock 则可调)
稳定性(关键)不稳定(可能跳变 / 回退)稳定(单调递增,适合时间间隔测量)不确定(需看平台实现)
精度中等(通常毫秒 / 微秒级)中等 - 高等(取决于系统)最高(优先保证最小时间单位,如纳秒级)
能否转换为时间戳是(to_time_t() 转 Unix 时间戳)否(仅用于相对时间计算)取决于底层实现(若为 system_clock 则可)
典型用途日志时间戳、日历时间、跨设备同步程序耗时统计、性能基准测试、超时判断对精度要求极高的短时间间隔测量
二、逐个详解
1. std::system_clock:系统墙钟时间(最常用的 “真实时间”)
  • 本质:对应操作系统的 “实时时间”(即我们日常看到的日期时间),其值与世界协调时(UTC)相关联。

  • 核心特点

    • 可调整:系统管理员可手动修改,或通过 NTP(网络时间协议)自动同步(可能导致时间 “跳前” 或 “回退”);
    • 可转换:提供 std::chrono::system_clock::to_time_t() 方法,能转换为 C 语言的 time_t 类型(Unix 时间戳,秒级),方便格式化输出(如打印 2025-12-16 10:00:00);
    • 精度中等:通常支持毫秒(ms)或微秒(μs)级,满足大多数日常时间记录需求。
  • 使用场景

    • 日志打印时记录事件发生时间;
    • 保存文件的创建 / 修改时间戳;
    • 跨设备 / 跨网络的时间同步(如接口请求的时间戳)。
  • 代码示例

    1. #include <chrono>
    2. #include <ctime>
    3. #include <iostream>
    4. int main() {
    5. // 获取当前系统时间点
    6. auto now = std::chrono::system_clock::now();
    7. // 转换为 time_t(秒级时间戳)
    8. std::time_t now_time = std::chrono::system_clock::to_time_t(now);
    9. // 格式化输出(本地时间)
    10. std::cout << "当前系统时间:" << std::ctime(&now_time);
    11. return 0;
    12. }
    复制代码
2. std::steady_clock:单调稳定时钟(计时的 “黄金标准”)
  • 本质:从系统启动后开始计时,以固定速率单调递增的 “内部时钟”,不受系统时间调整的影响。

  • 核心特点

    • 不可调整:绝不会因系统时间修改而跳变、回退,始终 “向前走”;
    • 适合计时:计算两个时间点的间隔(end - start)绝对可靠,是性能测试、超时判断的首选;
    • 无时间戳转换:无法直接转换为日历时间(因为其起点是系统启动时间,而非 UTC 时间)。
  • 使用场景

    • 测量函数 / 代码块的执行耗时;
    • 实现超时逻辑(如 “等待 5 秒后超时”);
    • 高精度定时器(需稳定的时间间隔)。
  • 代码示例

    1. #include <chrono>
    2. #include <iostream>
    3. #include <thread>
    4. int main() {
    5. // 记录开始时间点
    6. auto start = std::chrono::steady_clock::now();
    7. // 模拟耗时操作(睡眠 1 秒)
    8. std::this_thread::sleep_for(std::chrono::seconds(1));
    9. // 记录结束时间点
    10. auto end = std::chrono::steady_clock::now();
    11. // 计算耗时(转换为毫秒)
    12. auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    13. std::cout << "操作耗时:" << duration.count() << " ms" << std::endl; // 输出约 1000 ms
    14. return 0;
    15. }
    复制代码
3. std::high_resolution_clock:高精度时钟(追求极限精度)
  • 本质

    C++ 标准仅要求它 “提供尽可能高的精度”,

    具体实现由编译器 / 平台决定

    • 大多数现代平台(GCC、Clang、MSVC)将其实现为 std::steady_clock 的别名(既高精度又稳定);
    • 早期部分平台(如旧版 GCC)曾将其实现为 std::system_clock 的别名(高精度但不稳定)。
  • 核心特点

    • 精度最高:优先保证最小时间单位(通常支持纳秒(ns)级);
    • 稳定性不确定:需查看平台文档确认底层实现(推荐通过代码验证是否为 steady_clock);
  • 使用场景

    • 对时间精度要求极高的短时间间隔测量(如微秒级的代码耗时);
    • 注意:若需稳定性,需先验证其是否为 steady_clock。
  • 代码示例(含稳定性验证)

    1. #include <chrono>
    2. #include <iostream>
    3. int main() {
    4. // 验证 high_resolution_clock 是否为 steady_clock(关键!)
    5. static_assert(std::is_same_v<std::chrono::high_resolution_clock, std::chrono::steady_clock>,
    6. "high_resolution_clock 不是 steady_clock,稳定性无法保证!");
    7. // 高精度计时(纳秒级)
    8. auto start = std::chrono::high_resolution_clock::now();
    9. // 模拟短耗时操作
    10. volatile int x = 0;
    11. for (int i = 0; i < 1e6; ++i) x += i;
    12. auto end = std::chrono::high_resolution_clock::now();
    13. // 转换为纳秒输出
    14. auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);
    15. std::cout << "短操作耗时:" << duration.count() << " ns" << std::endl; // 纳秒级精度
    16. return 0;
    17. }
    复制代码
三、关键选择原则(避免踩坑)
  1. 需要时间戳 / 日历时间 → 用 system_clock(唯一支持转换为真实时间的时钟);
  2. 需要测量时间间隔 / 计时 → 优先用 steady_clock(绝对稳定,无跳变风险);
  3. 需要极限精度(纳秒级) → 用 high_resolution_clock,但必须先验证其是否为 steady_clock(通过 static_assert 或文档确认);
  4. 避免的错误:用 system_clock 测量耗时(可能因系统时间调整导致结果为负或异常)。

时长类

一、核心概念与定义
1. 本质:模板类的数学表达

duration 是模板类,定义如下(简化版):

  1. template <typename Rep, typename Period = std::ratio<1>>
  2. class duration;
复制代码

两个关键模板参数:

参数作用
Rep存储时长的 “数值类型”(如 int、long long、double),必须是算术类型。
Period时长的 “时间单位”(如秒、毫秒),由 std::ratio(比例类型)指定。
关键辅助类:std::ratio

std::ratio 表示 “分子 / 分母” 的比例(默认 ratio<1> 即 1/1 秒),用于定义自定义时间单位:

  • std::ratio<1, 1>:1 秒(1/1 秒)
  • std::ratio<1, 1000>:1 毫秒(1/1000 秒)
  • std::ratio<1, 1000000>:1 微秒(1/1e6 秒)
  • std::ratio<60>:1 分钟(60/1 秒)
2. 预定义常用时长类型(推荐优先使用)

chrono 库提供了多个预定义 duration 别名,覆盖常用时间单位,避免手动写 ratio:

预定义类型对应 duration 定义时间单位适用场景
nanosecondsduration>纳秒(ns)高精度短时间间隔
microsecondsduration>微秒(μs)较高精度计时
millisecondsduration>毫秒(ms)日常计时、延迟控制
secondsduration>秒(s)普通时间间隔
minutesduration>分钟(min)较长时间间隔
hoursduration>小时(h)长时间计时(如任务耗时)
days(C++20+)duration>天(d)日期相关间隔
weeks(C++20+)duration>周(w)长周期间隔
years(C++20+)duration>年(近似)年份间隔(非闰年精确)
months(C++20+)duration>月(近似)月份间隔(平均 30.44 天)

注意:years/months 是 “平均时间单位”(如一年按 365.2425 天计算),不适合精确日历计算(需用 date 库),仅用于时长间隔。

二、核心操作(创建、访问、转换、运算)
1. 创建时长对象
方式 1:直接构造(指定数值 + 单位)
  1. #include <chrono>
  2. using namespace std::chrono; // 简化命名空间
  3. // 10 秒(默认 Rep 是 long long)
  4. seconds s1(10);
  5. // 500 毫秒
  6. milliseconds ms1(500);
  7. // 200 纳秒(高精度)
  8. nanoseconds ns1(200);
  9. // 3.5 秒(用 double 作为 Rep,支持小数时长)
  10. duration<double> d1(3.5);
  11. // 自定义单位:2 个“1/4 秒”(即 500 毫秒)
  12. duration<int, std::ratio<1, 4>> quarter_sec(2);
复制代码
方式 2:用 std::chrono_literals 字面量(C++14+,推荐)

通过字面量直接写单位(如 s/ms/us/ns),代码更简洁:

  1. #include <chrono>
  2. using namespace std::chrono_literals; // 启用字面量
  3. auto s2 = 10s; // 等价于 seconds(10)
  4. auto ms2 = 500ms; // 等价于 milliseconds(500)
  5. auto ns2 = 200ns; // 等价于 nanoseconds(200)
  6. auto d2 = 3.5s; // 等价于 duration<double>(3.5)
  7. auto min2 = 2min; // 2 分钟
  8. auto h2 = 1h; // 1 小时
复制代码
2. 访问时长数值

用 count() 方法获取时长的 “数值部分”(单位由 Period 决定):

  1. seconds s(10);
  2. std::cout << s.count(); // 输出 10(单位:秒)
  3. milliseconds ms(500);
  4. std::cout << ms.count(); // 输出 500(单位:毫秒)
  5. duration<double> d(3.5);
  6. std::cout << d.count(); // 输出 3.5(单位:秒,默认 Period)
复制代码
3. 时长转换(核心:duration_cast)

由于 duration 是类型安全的,不同单位的时长不能直接赋值,需用 std::chrono::duration_cast 显式转换(避免隐式类型错误)。

规则
  • 整数类型 Rep 转换:可能丢失精度(如秒转毫秒没问题,毫秒转秒会截断小数);
  • 浮点类型 Rep 转换:自动兼容,无需 duration_cast(直接赋值即可)。
示例
  1. // 1. 整数时长转换(需 duration_cast)
  2. auto ms = 1500ms;
  3. // 毫秒 → 秒(1500ms = 1.5s,截断为 1s)
  4. auto s = duration_cast<seconds>(ms);
  5. std::cout << s.count(); // 输出 1
  6. // 秒 → 毫秒(1s = 1000ms,无精度丢失)
  7. auto ms2 = duration_cast<milliseconds>(2s);
  8. std::cout << ms2.count(); // 输出 1000
  9. // 2. 浮点时长转换(无需 cast)
  10. duration<double, seconds::period> d1(3.5); // 3.5 秒
  11. duration<double, milliseconds::period> d2 = d1; // 自动转换为 3500.0 毫秒
  12. std::cout << d2.count(); // 输出 3500.0
  13. // 3. 高精度转低精度(可能丢失精度)
  14. auto ns = 1234567ns;
  15. auto us = duration_cast<microseconds>(ns); // 1234567ns = 1234.567us → 1234us
  16. std::cout << us.count(); // 输出 1234
复制代码
4. 时长运算(算术 / 比较)

duration 支持所有算术运算(+/-/*//)和比较运算(==/!=/

  1. using namespace std::chrono_literals;
  2. // 算术运算
  3. auto s1 = 5s + 3s; // 8s
  4. auto ms1 = 1000ms - 500ms; // 500ms
  5. auto us1 = 2us * 3; // 6us
  6. auto s2 = 10s / 2; // 5s
  7. // 不同单位运算(自动转换为“更精细的单位”)
  8. auto total_ms = 2s + 300ms; // 2s = 2000ms → 2300ms
  9. auto total_us = 500us + 1ms; // 1ms = 1000us → 1500us
  10. // 比较运算
  11. std::cout << (1s == 1000ms); // true
  12. std::cout << (500ms < 1s); // true
  13. std::cout << (2000us > 3ms); // 2000us = 2ms → false
复制代码
5. 常用工具函数
(1)std::chrono::abs(C++17+):取时长绝对值
  1. auto d = 3s - 5s; // -2s
  2. auto abs_d = std::chrono::abs(d); // 2s
复制代码
(2)std::chrono::duration_cast:显式转换(已讲)
(3)std::chrono::floor/ceil/round(C++17+):浮点时长取整
  1. duration<double> d(3.7s);
  2. auto floor_d = floor<seconds>(d); // 3s(向下取整)
  3. auto ceil_d = ceil<seconds>(d); // 4s(向上取整)
  4. auto round_d = round<seconds>(d); // 4s(四舍五入)
复制代码
三、实战场景示例
1. 测量代码耗时(结合 steady_clock)
  1. #include <chrono>
  2. #include <iostream>
  3. #include <thread>
  4. using namespace std::chrono;
  5. int main() {
  6. auto start = steady_clock::now(); // 开始时间点
  7. std::this_thread::sleep_for(1500ms); // 模拟耗时操作
  8. auto end = steady_clock::now(); // 结束时间点
  9. // 计算耗时(转换为毫秒)
  10. auto duration = duration_cast<milliseconds>(end - start);
  11. std::cout << "耗时:" << duration.count() << "ms" << std::endl; // 约 1500ms
  12. // 更高精度(纳秒)
  13. auto duration_ns = duration_cast<nanoseconds>(end - start);
  14. std::cout << "耗时:" << duration_ns.count() << "ns" << std::endl;
  15. return 0;
  16. }
复制代码
2. 自定义时间单位(如 “帧”“心跳”)
  1. #include <chrono>
  2. #include <iostream>
  3. // 自定义单位:1 帧 = 1/60 秒(适合游戏帧率控制)
  4. using frame = std::chrono::duration<int, std::ratio<1, 60>>;
  5. int main() {
  6. frame f(10); // 10 帧
  7. // 转换为毫秒(1 帧 ≈ 16.666ms)
  8. auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(f);
  9. std::cout << 10 << " 帧 = " << ms.count() << "ms" << std::endl; // 约 166ms
  10. return 0;
  11. }
复制代码
3. 浮点时长表示(如 0.5 秒)
  1. #include <chrono>
  2. #include <iostream>
  3. using namespace std::chrono_literals;
  4. int main() {
  5. // 方式1:用 duration<double>
  6. std::chrono::duration<double> d1(0.5); // 0.5 秒
  7. std::cout << d1.count() << "s" << std::endl; // 0.5s
  8. // 方式2:用字面量(C++14+)
  9. auto d2 = 0.5s; // 等价于 duration<double>(0.5)
  10. auto d3 = 1.25ms; // 1.25 毫秒
  11. // 运算
  12. auto total = d2 + d3; // 0.5s + 0.00125s = 0.50125s
  13. std::cout << total.count() << "s" << std::endl; // 0.50125s
  14. return 0;
  15. }
复制代码
四、关键注意事项
  1. 类型安全优先:避免用 int 存储时长(如 int ms = 1000),优先使用预定义 duration 类型(seconds/milliseconds),防止单位混淆;
  2. 转换精度问题:整数 duration 转换时可能截断(如 1500ms 转 seconds 为 1s),需根据场景选择 floor/ceil 或浮点 duration;
  3. C++ 版本兼容:chrono_literals(C++14+)、years/months(C++20+)、abs/floor(C++17+)需注意编译器版本;
  4. Rep 类型选择:短时间间隔用 long long(避免溢出),需要小数用 double,超大时长(如几年)可自定义 Rep 为 long long。

时间点类

一、核心概念与定义
1. 本质:模板类的核心构成

time_point 是模板类,定义如下(简化版):

  1. template <typename Clock, typename Duration = typename Clock::duration>
  2. class time_point;
复制代码

两个关键模板参数:

参数作用
Clock绑定的时钟类型(必须是 chrono 时钟,如 system_clock/steady_clock),决定了时间点的 “纪元(epoch)” 和 “时间流特性”(是否稳定、是否可调整)。
Duration时间点相对于 “时钟纪元” 的时长类型(默认是绑定时钟的 duration 类型,如 system_clock 默认用 nanoseconds),可自定义为更粗 / 更细的时长单位。
关键术语:时钟纪元(Epoch)

每个时钟都有一个固定的 “起始时刻”(纪元),time_point 的值本质是 “从该纪元到当前时刻的时长”:

  • system_clock 的纪元:通常是 Unix 纪元(1970-01-01 00:00:00 UTC)(可移植);
  • steady_clock 的纪元:系统启动时刻(不可移植,不同设备 / 重启后不同);
  • high_resolution_clock 的纪元:取决于底层实现(同其绑定的时钟)。
2. 核心特性
  • 与时钟强绑定:不同时钟的 time_point 不能直接比较 / 运算(如 system_clock::time_point 不能和 steady_clock::time_point 相加);
  • 类型安全:时长单位由 Duration 控制,避免 “时刻 + 秒” 的单位混淆;
  • 可转换性:同一时钟的 time_point 可通过 time_point_cast 转换不同 Duration 精度。
二、核心操作(创建、访问、转换、运算)
1. 创建时间点对象
方式 1:通过时钟的 now() 获取当前时刻(最常用)
  1. #include <chrono>
  2. using namespace std::chrono;
  3. // 系统当前时间点(可转换为日历时间)
  4. system_clock::time_point sys_now = system_clock::now();
  5. // 稳定时钟当前时间点(仅用于计时)
  6. steady_clock::time_point steady_now = steady_clock::now();
  7. // 高精度时钟当前时间点(取决于底层实现)
  8. high_resolution_clock::time_point high_res_now = high_resolution_clock::now();
复制代码
方式 2:通过 “纪元 + 时长” 构造(自定义时刻)

用 clock::time_point(duration) 构造,时长表示 “从纪元到该时刻的间隔”:

  1. // 1. 系统时钟:Unix 纪元后 100 秒(1970-01-01 00:01:40 UTC)
  2. system_clock::time_point sys_tp(seconds(100));
  3. // 2. 稳定时钟:系统启动后 5 分钟(仅当前设备有效)
  4. steady_clock::time_point steady_tp(minutes(5));
  5. // 3. 自定义时长精度:系统纪元后 123.456 毫秒
  6. system_clock::time_point sys_tp2(milliseconds(123456));
复制代码
方式 3:获取时钟的 “纪元时刻”(默认构造)

默认构造的 time_point 表示绑定时钟的 “纪元起始时刻”(如 system_clock 的纪元是 1970-01-01 UTC):

  1. // 系统时钟纪元时刻(1970-01-01 00:00:00 UTC)
  2. system_clock::time_point sys_epoch;
  3. // 稳定时钟纪元时刻(系统启动瞬间)
  4. steady_clock::time_point steady_epoch;
复制代码
2. 访问时间点的核心信息
(1)time_since_epoch():获取从纪元到当前时间点的时长

返回 Duration 类型(或自定义的时长类型),是 time_point 最核心的访问方法:

  1. // 系统当前时间点距离 Unix 纪元的时长(默认是 nanoseconds)
  2. auto sys_since_epoch = system_clock::now().time_since_epoch();
  3. // 转换为秒级(Unix 时间戳)
  4. auto unix_timestamp = duration_cast<seconds>(sys_since_epoch).count();
  5. std::cout << "Unix 时间戳:" << unix_timestamp << "s" << std::endl;
  6. // 稳定时钟当前时间点距离系统启动的时长(转换为分钟)
  7. auto steady_since_boot = duration_cast<minutes>(steady_clock::now().time_since_epoch());
  8. std::cout << "系统已运行:" << steady_since_boot.count() << "min" << std::endl;
复制代码
(2)min()/max():获取时钟支持的最小 / 最大时间点
  1. // 系统时钟支持的最早时刻
  2. auto sys_min = system_clock::time_point::min();
  3. // 系统时钟支持的最晚时刻
  4. auto sys_max = system_clock::time_point::max();
复制代码
3. 时间点转换(核心:time_point_cast)
(1)同一钟内的精度转换

当需要改变 time_point 的 Duration 精度时(如从纳秒级转为毫秒级),需用 std::chrono::time_point_cast(类似 duration_cast):

  1. // 1. 系统时钟:纳秒级时间点 → 毫秒级时间点
  2. auto sys_ns = system_clock::now(); // 默认 Duration 是 nanoseconds
  3. auto sys_ms = time_point_cast<milliseconds>(sys_ns); // 转换为毫秒级
  4. // 访问转换后的时长(毫秒)
  5. std::cout << sys_ms.time_since_epoch().count() << "ms" << std::endl;
  6. // 2. 稳定时钟:秒级时间点 → 微秒级时间点
  7. steady_clock::time_point steady_s(seconds(2));
  8. auto steady_us = time_point_cast<microseconds>(steady_s);
  9. std::cout << steady_us.time_since_epoch().count() << "us" << std::endl; // 2000000us
复制代码
(2)不同时钟的时间点:不可直接转换

不同时钟的 time_point 纪元、特性不同(如 system_clock 可调整,steady_clock 不可),标准不支持直接转换。若需对比,需通过 “共同的时间基准”(如系统时间戳)间接换算(不推荐,易出错):

  1. // 错误:不同时钟的 time_point 不能直接赋值/转换
  2. // steady_clock::time_point tp = system_clock::now(); // 编译报错
复制代码
4. 时间点运算(算术 / 比较)

time_point 的运算规则:

  • 时间点 ± 时长 = 新的时间点(同时钟、同时长精度);
  • 两个时间点(同时钟、同时长精度)相减 = 时长(时间间隔);
  • 支持所有比较运算(==/!=/同一时钟、同时长精度。
示例
  1. using namespace std::chrono;
  2. // 1. 时间点 + 时长 = 新时间点
  3. auto now = system_clock::now();
  4. auto later = now + 2h + 30min; // 2小时30分钟后的时刻
  5. auto earlier = now - 10s; // 10秒前的时刻
  6. // 2. 时间点 - 时间点 = 时长(时间间隔)
  7. auto start = steady_clock::now();
  8. std::this_thread::sleep_for(500ms);
  9. auto end = steady_clock::now();
  10. auto duration = end - start; // 类型为 steady_clock::duration(默认 nanoseconds)
  11. std::cout << "耗时:" << duration_cast<milliseconds>(duration).count() << "ms" << std::endl;
  12. // 3. 比较运算(同时钟)
  13. std::cout << (now < later); // true
  14. std::cout << (earlier == now); // false
  15. // 4. 不同精度的时间点运算(自动转换为更精细的精度)
  16. time_point<system_clock, seconds> tp_s(seconds(100));
  17. time_point<system_clock, milliseconds> tp_ms(tp_s + 500ms); // 100s + 0.5s = 100.5s
  18. std::cout << tp_ms.time_since_epoch().count() << "ms" << std::endl; // 100500ms
复制代码
5. 时间点与日历时间的转换(仅 system_clock 支持)

只有 system_clock 的 time_point 能转换为 “人类可读的日历时间”(因为其纪元是标准的 Unix 纪元),需借助 system_clock::to_time_t() 或 C++20+ 的 std::chrono::floor(更精确)。

示例 1:转换为本地时间(C++11+)
  1. #include <chrono>
  2. #include <ctime>
  3. #include <iostream>
  4. using namespace std::chrono;
  5. int main() {
  6. // 获取当前系统时间点
  7. system_clock::time_point now = system_clock::now();
  8. // 转换为 time_t(秒级 Unix 时间戳)
  9. std::time_t now_time = system_clock::to_time_t(now);
  10. // 转换为本地时间(如北京时间)并格式化
  11. std::tm* local_tm = std::localtime(&now_time);
  12. char buf[64];
  13. std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local_tm);
  14. std::cout << "当前本地时间:" << buf << std::endl; // 输出如 2025-12-16 15:30:45
  15. return 0;
  16. }
复制代码
示例 2:C++20+ 高精度日历时间转换(支持毫秒 / 微秒)

C++20 引入 std::chrono::floor 可保留更精细的精度(如毫秒),无需截断为秒级:

  1. #include <chrono>
  2. #include <ctime>
  3. #include <iostream>
  4. using namespace std::chrono;
  5. int main() {
  6. auto now = system_clock::now();
  7. // 转换为秒级时间点(保留整数秒)
  8. auto now_sec = floor<seconds>(now);
  9. std::time_t now_time = system_clock::to_time_t(now_sec);
  10. std::tm* local_tm = std::localtime(&now_time);
  11. // 提取毫秒部分(0-999)
  12. auto ms = duration_cast<milliseconds>(now - now_sec).count();
  13. // 格式化输出(包含毫秒)
  14. char buf[64];
  15. std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local_tm);
  16. std::cout << "当前本地时间(含毫秒):" << buf << "." << ms << std::endl; // 2025-12-16 15:30:45.123
  17. return 0;
  18. }
复制代码
三、实战场景示例
1. 测量代码执行耗时(steady_clock + time_point)

这是 time_point 最核心的场景,利用 steady_clock 的稳定性保证计时准确

  1. #include <chrono>
  2. #include <iostream>
  3. #include <vector>
  4. using namespace std::chrono;
  5. int main() {
  6. // 记录开始时间点
  7. auto start = steady_clock::now();
  8. // 模拟耗时操作:创建 100 万个元素的向量
  9. std::vector<int> vec;
  10. for (int i = 0; i < 1'000'000; ++i) {
  11. vec.push_back(i);
  12. }
  13. // 记录结束时间点
  14. auto end = steady_clock::now();
  15. // 计算耗时(转换为微秒)
  16. auto duration = duration_cast<microseconds>(end - start);
  17. std::cout << "向量初始化耗时:" << duration.count() << "us" << std::endl;
  18. return 0;
  19. }
复制代码
2. 计算未来 / 过去的时刻(system_clock + time_point)
  1. #include <chrono>
  2. #include <ctime>
  3. #include <iostream>
  4. using namespace std::chrono;
  5. // 格式化 time_point 为字符串
  6. std::string format_time(system_clock::time_point tp) {
  7. std::time_t t = system_clock::to_time_t(tp);
  8. std::tm* local_tm = std::localtime(&t);
  9. char buf[64];
  10. std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local_tm);
  11. return buf;
  12. }
  13. int main() {
  14. auto now = system_clock::now();
  15. // 计算 3 天后的时刻
  16. auto three_days_later = now + 3d; // C++20+ 支持 days 类型
  17. // C++11/14 替代:now + hours(3*24)
  18. // 计算 1 小时前的时刻
  19. auto one_hour_ago = now - 1h;
  20. std::cout << "当前时间:" << format_time(now) << std::endl;
  21. std::cout << "3 天后:" << format_time(three_days_later) << std::endl;
  22. std::cout << "1 小时前:" << format_time(one_hour_ago) << std::endl;
  23. return 0;
  24. }
复制代码
3. 超时判断(steady_clock + time_point)
  1. #include <chrono>
  2. #include <iostream>
  3. #include <thread>
  4. using namespace std::chrono;
  5. int main() {
  6. const auto timeout = 2s; // 超时时间:2 秒
  7. auto start = steady_clock::now();
  8. // 模拟任务执行(循环,直到超时或完成)
  9. bool task_done = false;
  10. while (!task_done) {
  11. // 检查是否超时
  12. auto now = steady_clock::now();
  13. if (now - start > timeout) {
  14. std::cout << "任务超时!" << std::endl;
  15. break;
  16. }
  17. // 模拟任务执行(每次循环睡眠 500ms)
  18. std::this_thread::sleep_for(500ms);
  19. std::cout << "任务执行中..." << std::endl;
  20. // 这里可以添加任务完成的条件(如 task_done = true;)
  21. }
  22. return 0;
  23. }
复制代码

新增数据结构

  • std::forward_list:单向链表,只可以前进,在特定场景下使用,相比于std::list节省了内存,提高了性能
  1. std::forward_list<int> fl = {1, 2, 3, 4, 5};
  2. for (const auto &elem : fl) {
  3. cout << elem;
  4. }
复制代码
  • std::unordered_set:基于hash表实现的set,内部不会排序,使用方法和set类似,性能比map好一些
  • std::unordered_map:基于hash表实现的map,内部不会排序,使用方法和set类似
  • std::array:数组,在越界访问时抛出异常,建议使用std::array替代普通的数组
  • std::tuple:元组类型,能同时打包多个数据,相当于用一个结构体打包多组数据,当时只适合临时用不能代替结构体,因为毕竟内部维护了很多东西,运行和编译速度都比结构体慢
  1. typedef std::tuple<int, double, int, double> Mytuple;
  2. Mytuple t(0, 1, 2, 3);
  3. std::cout << "0 " << std::get<0>(t);
  4. std::cout << "1 " << std::get<1>(t);
  5. std::cout << "2 " << std::get<2>(t);
  6. std::cout << "3 " << std::get<3>(t);
  7. //相当于
  8. struct a{
  9. int a;
  10. double b;
  11. int c;
  12. double d;
  13. };
复制代码

新增算法

  • all_of:检测表达式是否对范围[first, last)中所有元素都返回true,如果都满足,则返回true
  1. std::vector<int> v(10, 2);
  2. if (std::all_of(v.cbegin(), v.cend(), [](int i) { return i % 2 == 0; })) {
  3. std::cout << "All numbers are even\n";
  4. }
复制代码
  • any_of:检测表达式是否对范围[first, last)中至少一个元素返回true,如果满足,则返回true,否则返回false,用法和上面一样

  • none_of:检测表达式是否对范围[first, last)中所有元素都不返回true,如果都不满足,则返回true,否则返回false,用法和上面一样

  • find_if_not:找到第一个不符合要求的元素迭代器,和find_if相反

  • copy_if:复制满足条件的元素

  • itoa:对容器内的元素按序递增

  1. std::vector<int> l(10);
  2. std::iota(l.begin(), l.end(), 19); // 19为初始值
  3. for (auto n : l) std::cout << n << ' ';
  4. // 19 20 21 22 23 24 25 26 27 28
复制代码
  • minmax_element:返回容器内最大元素和最小元素位置
  1. int main() {
  2. std::vector<int> v = {3, 9, 1, 4, 2, 5, 9};
  3. auto result = std::minmax_element(v.begin(), v.end());
  4. std::cout << "min element at: " << *(result.first) << '\n';
  5. std::cout << "max element at: " << *(result.second) << '\n';
  6. return 0;
  7. }
  8. // min element at: 1
  9. // max element at: 9
复制代码
  • is_sorted:返回容器内元素是否已经排好序。
  • is_sorted_until:定位第一个无序点
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2026 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行