c++ 学习

参考手册

https://www.cplusplus.com/

https://www.apiref.com/cpp-zh/index.html   c++ 中文参考手册

https://zh.cppreference.com/w/cpp  c++ 参考手册

基本概念

https://zh.cppreference.com/w/cpp/language/basic_concepts

本节定义了描述 C++ 编程语言时所使用的一些专门的术语与概念。

一个 C++ 程序是一个含有声明的文本文件(通常为头文件与源文件)序列。它们被翻译成一个可执行程序,C++ 实现通过调用其主 (main) 函数执行这一程序。

在 C++ 程序中,一些被称为关键词的词语有着特殊的含义。其它词语可以被用作标识符。在翻译的过程中,注释会被忽略。程序中的某些特定字符必须通过转义序列表示。

C++ 程序中的实体包括值、对象引用、 结构化绑定 (C++17 起)、函数枚举项类型、类成员、模板模板特化命名空间形参包。预处理器不是 C++ 实体。

声明可以引入实体,将它们与名字关联起来,并定义其属性。能够定义使用一个实体所需的所有属性的声明是定义。对任何被 ODR 使用的非内联函数或变量,程序中必须只含有其一个定义。

函数的定义通常包括一系列的语句,其中部分会包含表达式。表达式指定了程序需要进行的运算。

程序中遇到的名字通过名字查找与引入它们的声明关联起来。每个名字都只在称为其作用域的程序部分中有效。有些名字有链接,这使得它们即使出现在不同的作用域或翻译单元时也代表相同的实体。

C++ 中的每一个对象、引用、函数和表达式都会关联一个类型。类型可以分为基础类型,复合类型,或自定义类型,以及完整或不完整的类型等。

被声明的且不是非静态数据成员的对象和引用是变量

记录

1. 模板

模板概念 :https://zh.cppreference.com/w/cpp/language/templates

模板类继承 

1.模板类从一个父模板类继承后,不能访问其内部的protected成员变量,提示:not declare;

2. 普通类从一个父模板类继承后,可以访问其内部的protected成员变量,可正常编译和使用;

对于第1个现象,如果想正常使用需要加上父模板类的域名; 

具体实例 参考博文 : C++ 模板类继承,成员访问问题_gtl_csdn的博客-CSDN博客

2. virtual 函数说明符

https://zh.cppreference.com/w/cpp/language/virtual

virtual 说明符指定非静态成员函数函数并支持动态调用派发。它只能在非静态成员函数的首个声明(即当它于类定义中声明时)的 声明说明符序列 中出现。

解释

虚函数是可在派生类中覆盖其行为的成员函数。与非虚函数相反,即使没有关于该类实际类型的编译时信息,仍然保留被覆盖的行为。当使用到基类的指针或引用来处理派生类时,对被覆盖的虚函数的调用,将会调用定义于派生类中的行为。当使用有限定名字查找(即函数名出现在作用域解析运算符 :: 的右侧)时,此行为被抑制。

#include <iostream>
struct Base {
   virtual void f() {
       std::cout << "base\n";
   }
};
struct Derived : Base {
    void f() override { // 'override' 可选
        std::cout << "derived\n";
    }
};
int main()
{
    Base b;
    Derived d;
 
    // 通过引用调用虚函数
    Base& br = b; // br 的类型是 Base&
    Base& dr = d; // dr 的类型也是 Base&
    br.f(); // 打印 "base"
    dr.f(); // 打印 "derived"
 
    // 通过指针调用虚函数
    Base* bp = &b; // bp 的类型是 Base*
    Base* dp = &d; // dp 的类型也是 Base*
    bp->f(); // 打印 "base"
    dp->f(); // 打印 "derived"
 
    // 非虚函数调用
    br.Base::f(); // 打印 "base"
    dr.Base::f(); // 打印 "base"
}

每个虚函数都有其最终覆盖函数,它是进行虚函数调用时所执行的函数,不管是否可见或者不可访问。基类 Base 的虚成员函数 vf 是最终覆盖函数,除非派生类声明或(通过多重继承)继承了覆盖 vf 的另一个函数。

3. 函数包装器

std::function

std::function - cppreference.com

定义于头文件 <functional>

template< class >
class function; /* 未定义 */

(C++11 起)

template< class R, class... Args >
class function<R(Args...)>;

(C++11 起)

类模板 std::function 是通用多态函数封装器。 std::function 的实例能存储、复制及调用任何可复制构造 (CopyConstructible) 可调用 (Callable) 目标——函数、 lambda 表达式、 bind 表达式或其他函数对象,还有指向成员函数指针和指向数据成员指针。

存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为。调用 std::function 的目标导致抛出 std::bad_function_call 异常。

std::function 满足可复制构造 (CopyConstructible) 可复制赋值 (CopyAssignable) 

#include <functional>
#include <iostream>
 
struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_+i << '\n'; }
    int num_;
};
 
void print_num(int i)
{
    std::cout << i << '\n';
}
 
struct PrintNum {
    void operator()(int i) const
    {
        std::cout << i << '\n';
    }
};
 
int main()
{
    // 存储自由函数
    std::function<void(int)> f_display = print_num;
    f_display(-9);
 
    // 存储 lambda
    std::function<void()> f_display_42 = []() { print_num(42); };
    f_display_42();
 
    // 存储到 std::bind 调用的结果
    std::function<void()> f_display_31337 = std::bind(print_num, 31337);
    f_display_31337();
 
    // 存储到成员函数的调用
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    const Foo foo(314159);
    f_add_display(foo, 1);
    f_add_display(314159, 1);
 
    // 存储到数据成员访问器的调用
    std::function<int(Foo const&)> f_num = &Foo::num_;
    std::cout << "num_: " << f_num(foo) << '\n';
 
    // 存储到成员函数及对象的调用
    using std::placeholders::_1;
    std::function<void(int)> f_add_display2 = std::bind( &Foo::print_add, foo, _1 );
    f_add_display2(2);
 
    // 存储到成员函数和对象指针的调用
    std::function<void(int)> f_add_display3 = std::bind( &Foo::print_add, &foo, _1 );
    f_add_display3(3);
 
    // 存储到函数对象的调用
    std::function<void(int)> f_display_obj = PrintNum();
    f_display_obj(18);
 
    auto factorial = [](int n) {
        // 存储 lambda 对象以模拟“递归 lambda ”,注意额外开销
        std::function<int(int)> fac = [&](int n){ return (n < 2) ? 1 : n*fac(n-1); };
        // note that "auto fac = [&](int n){...};" does not work in recursive calls
        return fac(n);
    };
    for (int i{5}; i != 8; ++i) { std::cout << i << "! = " << factorial(i) << ";  "; }
}

std::function 经常作为回调函数使用。

1.  作为回调函数

C++ 回调函数的实现,以及function,bind,lambda表达式的使用

https://blog.csdn.net/fantasysolo/article/details/90698874?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-0.base&spm=1001.2101.3001.4242

C++11的无固定形参的回调

https://blog.csdn.net/qq_28107929/article/details/89680924

4. lambda 表达式

https://zh.cppreference.com/w/cpp/language/lambda

构造闭包:能够捕获作用域中的变量的无名函数对象。

语法

[ 捕获 ] <模板形参>(可选)
(C++20) ( 形参 ) lambda-说明符 { 函数体 }
(1)
[ 捕获 ] ( 形参 ) 尾随返回类型 { 函数体 }(2)
[ 捕获 ] ( 形参 ) { 函数体 }(3)
[ 捕获 ] lambda-说明符(可选)
(C++23) { 函数体 }
(4)

1) 完整声明。

2) const lambda 的声明:复制捕获的对象在 lambda 体内为 const。

3) 省略 尾随返回类型 :闭包的 operator() 的返回类型从 return 语句推导,如同对于声明返回类型为 auto 的函数的推导一样。

4) 省略形参列表:不接收实参的函数,如同形参列表是 ()。仅当完全不使用 constexprconstevalmutable、异常说明、属性或尾随返回类型时才能使用此形式。 (C++23 前)

解释

捕获-零或更多捕获符的逗号分隔列表,可选地以 默认捕获符(capture-default) 起始。

有关捕获符的详细描述,见下文

若变量满足下列条件,则 lambda 表达式可以不捕获就使用它

  • 该变量为非局部变量,或具有静态或线程局部存储期(该情况下无法捕获该变量),或者
  • 该变量为以常量表达式初始化的引用。

若变量满足下列条件,则 lambda 表达式可以不捕获就读取其值

  • 该变量具有 const 而非 volatile 的整型或枚举类型,并已用常量表达式初始化,或者
  • 该变量为 constexpr 且无 mutable 成员。

Lambda 捕获

捕获 是零或更多捕获符的逗号分隔列表,可选地以 默认捕获符 开始。仅有的默认捕获符是

  • &(以引用隐式捕获被使用的自动变量)和
  • (以复制隐式捕获被使用的自动变量)。

当出现任一默认捕获符时,都能隐式捕获当前对象(*this)。当它被隐式捕获时,始终被以引用捕获,即使默认捕获符是 = 也是如此。当默认捕获符为 = 时,*this 的隐式捕获被弃用。 (C++20 起)

捕获 中单独的捕获符的语法是

标识符(1)
标识符 ...(2)
标识符 初始化器(3)(C++14 起)
& 标识符(4)
& 标识符 ...(5)
& 标识符 初始化器(6)(C++14 起)
this(7)
* this(8)(C++17 起)
... 标识符 初始化器(9)(C++20 起)
& ... 标识符 初始化器(10)(C++20 起)

1) 简单以复制捕获

2) 作为包展开的简单以复制捕获

3) 带初始化器的以复制捕获

4) 简单以引用捕获

5) 作为包展开的简单引用捕获

6) 带初始化器的以引用捕获

7) 当前对象的简单以引用捕获

8) 当前对象的简单以复制捕获

9) 用作为包展开的初始化器以复制捕获

10) 用作为包展开的初始化器以引用捕获

当默认捕获符是 & 时,后继的简单捕获符必须不以 & 开始。

struct S2 { void f(int i); };
void S2::f(int i)
{
    [&]{};          // OK:默认以引用捕获
    [&, i]{};       // OK:以引用捕获,但 i 以值捕获
    [&, &i] {};     // 错误:以引用捕获为默认时的以引用捕获
    [&, this] {};   // OK:等价于 [&]
    [&, this, i]{}; // OK:等价于 [&, i]
}

当默认捕获符是 = 时,后继的简单捕获符必须以 & 开始,或者为 *this (C++17 起) 或 this (C++20 起)。

struct S2 { void f(int i); };
void S2::f(int i)
{
    [=]{};          // OK:默认以复制捕获
    [=, &i]{};      // OK:以复制捕获,但 i 以引用捕获
    [=, *this]{};   // C++17 前:错误:无效语法
                    // C++17 起:OK:以复制捕获外围的 S2
    [=, this] {};   // C++20 前:错误:= 为默认时的 this
                    // C++20 起:OK:同 [=]
}

任何捕获符只可以出现一次:

struct S2 { void f(int i); };
void S2::f(int i)
{
    [i, i] {};        // 错误:i 重复
    [this, *this] {}; // 错误:"this" 重复 (C++17)
}

5. std::bind

定义于头文件 <functional>

(1)

template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );

(C++11 起)
(C++20 前)

template< class F, class... Args >
constexpr /*unspecified*/ bind( F&& f, Args&&... args );

(C++20 起)
(2)

template< class R, class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );

(C++11 起)
(C++20 前)

template< class R, class F, class... Args >
constexpr /*unspecified*/ bind( F&& f, Args&&... args );

(C++20 起)

函数模板 bind 生成 f 的转发调用包装器。调用此包装器等价于以一些绑定到 args 的参数调用 f 。

参数

f-可调用 (Callable) 对象(函数对象、指向函数指针、到函数引用、指向成员函数指针或指向数据成员指针)
args-要绑定的参数列表,未绑定参数为命名空间 std::placeholders 的占位符 _1, _2, _3... 所替换

返回值

返回类型: 成员对象,构造函数

注解

可调用 (Callable) 中描述,调用指向非静态成员函数指针或指向非静态数据成员指针时,首参数必须是引用或指针(可以包含智能指针,如 std::shared_ptr 与 std::unique_ptr),指向将访问其成员的对象。

到 bind 的参数被复制或移动,而且决不按引用传递,除非包装于 std::ref 或 std::cref 。

允许同一 bind 表达式中的多重占位符(例如多个 _1 ),但结果仅若对应参数( u1 )是左值或不可移动右值才良好定义。

示例

#include <random>
#include <iostream>
#include <memory>
#include <functional>
 
void f(int n1, int n2, int n3, const int& n4, int n5)
{
    std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << '\n';
}
 
int g(int n1)
{
    return n1;
}
 
struct Foo {
    void print_sum(int n1, int n2)
    {
        std::cout << n1+n2 << '\n';
    }
    int data = 10;
};
 
int main()
{
    using namespace std::placeholders;  // 对于 _1, _2, _3...
 
    // 演示参数重排序和按引用传递
    int n = 7;
    // ( _1 与 _2 来自 std::placeholders ,并表示将来会传递给 f1 的参数)
    auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
    n = 10;
    f1(1, 2, 1001); // 1 为 _1 所绑定, 2 为 _2 所绑定,不使用 1001
                    // 进行到 f(2, 42, 1, n, 7) 的调用
 
    // 嵌套 bind 子表达式共享占位符
    auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
    f2(10, 11, 12); // 进行到 f(12, g(12), 12, 4, 5); 的调用
 
    // 常见使用情况:以分布绑定 RNG
    std::default_random_engine e;
    std::uniform_int_distribution<> d(0, 10);
    std::function<int()> rnd = std::bind(d, e); // e 的一个副本存储于 rnd
    for(int n=0; n<10; ++n)
        std::cout << rnd() << ' ';
    std::cout << '\n';
 
    // 绑定指向成员函数指针
    Foo foo;
    auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
    f3(5);
 
    // 绑定指向数据成员指针
    auto f4 = std::bind(&Foo::data, _1);
    std::cout << f4(foo) << '\n';
 
    // 智能指针亦能用于调用被引用对象的成员
    std::cout << f4(std::make_shared<Foo>(foo)) << '\n'
              << f4(std::make_unique<Foo>(foo)) << '\n';
}

6. 使用std::function、std::bind和Lambda实现c++的回调函数

下文皆参考一下几篇博文

参考文档: https://blog.csdn.net/fantasysolo/article/details/90698874

使用std::function作为函数入参
https://www.jianshu.com/p/c4c84b073413

C++使用模板、函数指针、接口和lambda表达式这四种方法做回调函数的区别比较

https://www.cnblogs.com/kanite/p/8299147.html

c++ 传统回调函数实现

1. 函数指针

// CppTest.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdlib.h>
#include <math.h>

class Result;

typedef void (Result::*CallbackPtr)(int);

class MathCallBack
{
    int ops1,ops2;
    int result;
public:
    void Add(int a,int b,Result *caller,CallbackPtr callback)
    {
        ops1 = abs(a);   /* 实际上这个函数可能非常复杂,非常耗时,这样回调更突显作用*/
        ops2 = abs(b);

        result = ops1+ops2;

        (caller->*callback)(result);
    }
};

class Result
{
public:
    void showResult(int res)
    {
        printf("result = %d\n",res);
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    Result reShow;
    MathCallBack math;

    math.Add(1,3,&reShow,&Result::showResult);

    system("pause");

    return 0;
}

2. 接口类

// CppTest.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdlib.h>
#include <math.h>

template<typename T>
class MathTemplate
{
    int ops1,ops2;
    int result;
public:
    void Add(int a,int b,T callback)
    {
        ops1 = abs(a);   /* 实际上这个函数可能非常复杂,非常耗时,这样回调更突显作用*/
        ops2 = abs(b);

        result = ops1+ops2;

        callback.showResult(result);
    }
};

class Result
{
public:
    void showResult(int res)
    {
        printf("result = %d\n",res);
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    Result reShow;
    MathTemplate<Result> math;
    math.Add(1,3,reShow);

    system("pause");
    return 0;
}

3. 模板

// CppTest.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdlib.h>
#include <math.h>

template<typename T>
class MathTemplate
{
    int ops1,ops2;
    int result;
public:
    void Add(int a,int b,T callback)
    {
        ops1 = abs(a);   /* 实际上这个函数可能非常复杂,非常耗时,这样回调更突显作用*/
        ops2 = abs(b);

        result = ops1+ops2;

        callback.showResult(result);
    }
};

class Result
{
public:
    void showResult(int res)
    {
        printf("result = %d\n",res);
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    Result reShow;
    MathTemplate<Result> math;
    math.Add(1,3,reShow);

    system("pause");
    return 0;
}

这三种方法的确定是 耦合度比较高,因此需要使用C++ 11 提供的std::function 方法。

4. 利用std::function 和Lambda 实现回调函数

// CppTest.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdlib.h>
#include <math.h>
#include <iostream>
#include <functional>

class MathCallBack
{
    int ops1,ops2;
    int result;

public:
    void Add(int a,int b,std::function<void (int)> func)
    {
        ops1 = abs(a);   /* 实际上这个函数可能非常复杂,非常耗时,这样回调更突显作用*/
        ops2 = abs(b);

        result = ops1+ops2;
        func(result);
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    MathCallBack math;


    math.Add(1,3,[](int result) -> void {
            printf("result = %d\n",result);
        });

    system("pause");

    return 0;
}

6. 使用std::function和std::bind实现回调

参见 std::bind 示例代码。

现代C++的回调技术--使用std::bind和std::function
https://blog.csdn.net/will_free/article/details/61622122

#include<iostream>
#include<functional>

typedef std::function<void()> Functor;

class Blas
{
    public:
        void setCallBack(const Functor& cb)
        {functor = cb;};

        void printFunctor()
        {functor();};
    private:
        Functor functor;
};

class Atlas
{
    public:
        Atlas(int x_) : x(x_)
        {
            //使用当前类的静态成员函数

           blas.setCallBack(std::bind(&addStatic,x,2));
        //使用当前类的非静态成员函数 
                  blas.setCallBack(std::bind(&Atlas::add,this,x,2));
        }

        void print()
        {
            blas.printFunctor();
        }
    private:
        void add(int a,int b)
        {
            std::cout << a+b << std::endl;
        }

        static void addStatic(int a,int b)
        {
            std::cout << a+b << std::endl;
        }

        Blas blas;
        int x;
};

int main(int argc,char** argv)
{
    Atlas atlas(5);
    atlas.print();
    return 0;
}

7. 多线程

https://zh.cppreference.com/w/cpp/thread

线程

https://zh.cppreference.com/w/cpp/thread/thread

std::thread

定义于头文件 <thread>

class thread;

(C++11 起)

类 thread 表示单个执行线程。线程允许多个函数同时执行。

线程在构造关联的线程对象时立即开始执行(等待任何OS调度延迟),从提供给作为构造函数参数的顶层函数开始。顶层函数的返回值将被忽略,而且若它以抛异常终止,则调用 std::terminate 。顶层函数可以通过 std::promise 或通过修改共享变量(可能需要同步,见 std::mutex 与 std::atomic )将其返回值或异常传递给调用方。

std::thread 对象也可能处于不表示任何线程的状态(默认构造、被移动、 detach 或 join 后),并且执行线程可能与任何 thread 对象无关( detach 后)。

没有两个 std::thread 对象会表示同一执行线程; std::thread 不是可复制构造 (CopyConstructible) 可复制赋值 (CopyAssignable) 的,尽管它可移动构造 (MoveConstructible) 可移动赋值 (MoveAssignable) 

std::thread

thread() noexcept;

(1)(C++11 起)

thread( thread&& other ) noexcept;

(2)(C++11 起)

template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );

(3)(C++11 起)

thread( const thread& ) = delete;

(4)(C++11 起)

构造新的 thread 对象。

1) 构造不表示线程的新 thread 对象。

2) 移动构造函数。构造表示曾为 other 所表示的执行线程的 thread 对象。此调用后 other 不再表示执行线程。

3) 构造新的 std::thread 对象并将它与执行线程关联。新的执行线程开始执行

std::invoke(decay_copy(std::forward<Function>(f)), 
            decay_copy(std::forward<Args>(args))...);

其中 decay_copy 定义为

template <class T>
std::decay_t<T> decay_copy(T&& v) { return std::forward<T>(v); }

除了 decay_copy 的调用在调用方语境求值,故而任何求值和复制/移动参数中抛出的异常被抛到当前线程,而不用开始新线程。

 构造函数的调用完成同步于(定义于 std::memory_order )新的执行线程上 f 副本的调用开始。

 若 std::decay<Function>::type 与 std::thread 为同一类型,则此构造函数不参与重载决议。

4) 复制构造函数被删除; thread 不可复制。没有二个 std::thread 对象可表示同一执行线程。

参数

other-用以构造此 thread 的另一 thread 对象
f-执行于新线程的可调用 (Callable)
args...-传递给新函数的参数

注解

移动或按值复制线程函数的参数。若需要传递引用参数给线程函数,则必须包装它(例如用 std::ref 或 std::cref )忽略来自函数的任何返回值.。若函数抛异常,则调用 std::terminate 。为将返回值或异常传递回调用方线程,可使用 std::promise 或 std::async 。

示例

#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
 
void f1(int n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 1 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
void f2(int& n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 2 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
class foo
{
public:
    void bar()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 3 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
 
class baz
{
public:
    void operator()()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 4 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
 
int main()
{
    int n = 0;
    foo f;
    baz b;
    std::thread t1; // t1 不是线程
    std::thread t2(f1, n + 1); // 按值传递
    std::thread t3(f2, std::ref(n)); // 按引用传递
    std::thread t4(std::move(t3)); // t4 现在运行 f2() 。 t3 不再是线程
    std::thread t5(&foo::bar, &f); // t5 在对象 f 上运行 foo::bar()
    std::thread t6(b); // t6 在对象 b 的副本上运行 baz::operator()
    t2.join();
    t4.join();
    t5.join();
    t6.join();
    std::cout << "Final value of n is " << n << '\n';
    std::cout << "Final value of f.n (foo::n) is " << f.n << '\n';
    std::cout << "Final value of b.n (baz::n) is " << b.n << '\n';
}

互斥

互斥算法避免多个线程同时访问共享资源。这会避免数据竞争,并提供线程间的同步支持。

定义于头文件 <mutex>

mutex

(C++11)

提供基本互斥设施
(类)

timed_mutex

(C++11)

提供互斥设施,实现有时限锁定
(类)

recursive_mutex

(C++11)

提供能被同一线程递归锁定的互斥设施
(类)

recursive_timed_mutex

(C++11)

提供能被同一线程递归锁定的互斥设施,并实现有时限锁定
(类)

定义于头文件 <shared_mutex>

shared_mutex

(C++17)

提供共享互斥设施
(类)

shared_timed_mutex

(C++14)

提供共享互斥设施并实现有时限锁定
(类)

通用互斥管理

定义于头文件 <mutex>

lock_guard

(C++11)

实现严格基于作用域的互斥体所有权包装器
(类模板)

scoped_lock

(C++17)

用于多个互斥体的免死锁 RAII 封装器
(类模板)

unique_lock

(C++11)

实现可移动的互斥体所有权包装器
(类模板)

shared_lock

(C++14)

实现可移动的共享互斥体所有权封装器
(类模板)

defer_lock_ttry_to_lock_tadopt_lock_t

(C++11)(C++11)(C++11)

用于指定锁定策略的标签类型
(类)

defer_locktry_to_lockadopt_lock

(C++11)(C++11)(C++11)

用于指定锁定策略的标签常量
(常量)

通用锁定算法

try_lock

(C++11)

试图通过重复调用 try_lock 获得互斥体的所有权
(函数模板)

lock

(C++11)

锁定指定的互斥体,若任何一个不可用则阻塞
(函数模板)

单次调用

once_flag

(C++11)

确保 call_once 只调用函数一次的帮助对象
(类)

call_once

(C++11)

仅调用函数一次,即使从多个线程调用

(函数模板)

https://zh.cppreference.com/w/cpp/thread/mutex

https://zh.cppreference.com/w/cpp/thread/unique_lock

https://zh.cppreference.com/w/cpp/thread/lock_guard

通常不直接使用 std::mutex ,使用 std::unique_lock 、 std::lock_guard 或 std::scoped_lock (C++17 起)以更加异常安全的方式管理锁定。

std::lock_guard 是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。

创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。

std::unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。

类 unique_lock 可移动,但不可复制——它满足可移动构造 (MoveConstructible) 可移动赋值 (MoveAssignable) 但不满足可复制构造 (CopyConstructible) 可复制赋值 (CopyAssignable) 

类 unique_lock 满足基本可锁定 (BasicLockable) 要求。若 Mutex 满足可锁定 (Lockable) 要求,则 unique_lock 亦满足可锁定 (Lockable) 要求(例如:能用于 std::lock ) ;若 Mutex 满足可定时锁定 (TimedLockable) 要求,则 unique_lock 亦满足可定时锁定 (TimedLockable) 要求。

std::unique_lock 比std::mutex 的锁定方式更多,增加可定时锁定。

条件变量

条件变量是允许多个线程相互交流的同步原语。它允许一定量的线程等待(可以定时)另一线程的提醒,然后再继续。条件变量始终关联到一个互斥。

定义于头文件 <condition_variable>

condition_variable

(C++11)

提供与 std::unique_lock 关联的条件变量
(类)

condition_variable_any

(C++11)

提供与任何锁类型关联的条件变量
(类)

notify_all_at_thread_exit

(C++11)

安排到在此线程完全结束时对 notify_all 的调用
(函数)

cv_status

(C++11)

列出条件变量上定时等待的可能结果
(枚举)

std::condition_variable

https://zh.cppreference.com/w/cpp/thread/condition_variable

定义于头文件 <condition_variable>

class condition_variable;

(C++11 起)

condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。

有意修改变量的线程必须

  1. 获得 std::mutex (常通过 std::lock_guard )
  2. 在保有锁时进行修改
  3. 在 std::condition_variable 上执行 notify_one 或 notify_all (不需要为通知保有锁)

即使共享变量是原子的,也必须在互斥下修改它,以正确地发布修改到等待的线程。

任何有意在 std::condition_variable 上等待的线程必须

  1. 在与用于保护共享变量者相同的互斥上获得 std::unique_lock<std::mutex>
  2. 执行下列之一:
  1. 检查条件,是否为已更新或提醒它的情况
  2. 执行 wait 、 wait_for 或 wait_until ,等待操作自动释放互斥,并悬挂线程的执行。
  3. condition_variable 被通知时,时限消失或虚假唤醒发生,线程被唤醒,且自动重获得互斥。之后线程应检查条件,若唤醒是虚假的,则继续等待。

或者

  1. 使用 wait 、 wait_for 及 wait_until 的有谓词重载,它们包揽以上三个步骤

std::condition_variable 只可与 std::unique_lock<std::mutex> 一同使用;此限制在一些平台上允许最大效率。 std::condition_variable_any 提供可与任何基本可锁定 (BasicLockable) 对象,例如 std::shared_lock 一同使用的条件变量。

condition_variable 容许 wait 、 wait_for 、 wait_until 、 notify_one 及 notify_all 成员函数的同时调用。

类 std::condition_variable 是标准布局类型 (StandardLayoutType) 。它非可复制构造 (CopyConstructible) 可移动构造 (MoveConstructible) 可复制赋值 (CopyAssignable) 可移动赋值 (MoveAssignable) 

通知

notify_one

通知一个等待的线程
(公开成员函数)

notify_all

通知所有等待的线程
(公开成员函数)

等待

wait

阻塞当前线程,直到条件变量被唤醒
(公开成员函数)

wait_for

阻塞当前线程,直到条件变量被唤醒,或到指定时限时长后
(公开成员函数)

wait_until

阻塞当前线程,直到条件变量被唤醒,或直到抵达指定时间点
(公开成员函数)

示例

与 std::mutex 组合使用 condition_variable ,以促进线程间交流。

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
 
void worker_thread()
{
    // 等待直至 main() 发送数据
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});
 
    // 等待后,我们占有锁。
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    // 发送数据回 main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    // 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
    lk.unlock();
    cv.notify_one();
}
 
int main()
{
    std::thread worker(worker_thread);
 
    data = "Example data";
    // 发送数据到 worker 线程
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
 
    // 等候 worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';
 
    worker.join();
}

std::condition_variable::wait 

https://zh.cppreference.com/w/cpp/thread/condition_variable/wait

std::condition_variable::wait_for

https://zh.cppreference.com/w/cpp/thread/condition_variable/wait_for

std::condition_variable::wait_until

https://zh.cppreference.com/w/cpp/thread/condition_variable/wait_until

template< class Clock, class Duration >

std::cv_status
    wait_until( std::unique_lock<std::mutex>& lock,

                const std::chrono::time_point<Clock, Duration>& timeout_time );
(1)(C++11 起)
template< class Clock, class Duration, class Pred >

bool wait_until( std::unique_lock<std::mutex>& lock,
                 const std::chrono::time_point<Clock, Duration>& timeout_time,

                 Pred pred );
(2)(C++11 起)

wait_until 导致当前线程阻塞直至通知条件变量、抵达指定时间或虚假唤醒发生,可选的循环直至满足某谓词。

1) 原子地释放 lock ,阻塞当前线程,并将它添加到等待在 *this 上的线程列表。将在执行 notify_all() 或 notify_one() 时,或抵达绝对时间点 timeout_time 时解除阻塞线程。亦可能虚假地解除阻塞。解除阻塞时,无关缘由,重获得 lock 并退出 wait_for() 。

2) 等价于

while (!pred()) {
    if (wait_until(lock, timeout_time) == std::cv_status::timeout) {
        return pred();
    }
}
return true;

示例:

#include <iostream>
#include <atomic>
#include <condition_variable>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
 
std::condition_variable cv;
std::mutex cv_m;
std::atomic<int> i{0};
 
void waits(int idx)
{
    std::unique_lock<std::mutex> lk(cv_m);
    auto now = std::chrono::system_clock::now();
    if(cv.wait_until(lk, now + idx*100ms, [](){return i == 1;}))
        std::cerr << "Thread " << idx << " finished waiting. i == " << i << '\n';
    else
        std::cerr << "Thread " << idx << " timed out. i == " << i << '\n';
}
 
void signals()
{
    std::this_thread::sleep_for(120ms);
    std::cerr << "Notifying...\n";
    cv.notify_all();
    std::this_thread::sleep_for(100ms);
    i = 1;
    std::cerr << "Notifying again...\n";
    cv.notify_all();
}
 
int main()
{
    std::thread t1(waits, 1), t2(waits, 2), t3(waits, 3), t4(signals);
    t1.join(); 
    t2.join();
    t3.join();
    t4.join();
}

Future

标准库提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常。这些值在共享状态中传递,其中异步任务可以写入其返回值或存储异常,而且可以由持有该引用该共享态的 std::future 或 std::shared_future 实例的线程检验、等待或是操作这个状态。

定义于头文件 <future>

promise

(C++11)

存储一个值以进行异步获取
(类模板)

packaged_task

(C++11)

打包一个函数,存储其返回值以进行异步获取
(类模板)

future

(C++11)

等待被异步设置的值
(类模板)

shared_future

(C++11)

等待被异步设置的值(可能为其他 future 所引用)
(类模板)

async

(C++11)

异步运行一个函数(有可能在新线程中执行),并返回保有其结果的 std::future
(函数模板)

launch

(C++11)

指定 std::async 所用的运行策略
(枚举)

future_status

(C++11)

指定在 std::future 和 std::shared_future 上的定时等待的结果
(枚举)

std::promise

定义于头文件 <future>

template< class R > class promise;

(1)(C++11 起)

template< class R > class promise<R&>;

(2)(C++11 起)

template<>          class promise<void>;

(3)(C++11 起)

1) 空模板

2) 非 void 特化,用于在线程间交流对象

3) void 特化,用于交流无状态事件

类模板 std::promise 提供存储值或异常的设施,之后通过 std::promise 对象所创建的 std::future 对象异步获得结果。注意 std::promise 只应当使用一次。

每个 promise 与共享状态关联,共享状态含有一些状态信息和可能仍未求值的结果,它求值为值(可能为 void )或求值为异常。 promise 可以对共享状态做三件事:

  • 使就绪: promise 存储结果或异常于共享状态。标记共享状态为就绪,并解除阻塞任何等待于与该共享状态关联的 future 上的线程。
  • 释放: promise 放弃其对共享状态的引用。若这是最后一个这种引用,则销毁共享状态。除非这是 std::async 所创建的未就绪的共享状态,否则此操作不阻塞。
  • 抛弃: promise 存储以 std::future_errc::broken_promise 为 error_code 的 std::future_error 类型异常,令共享状态为就绪,然后释放它。

promise 是 promise-future 交流通道的“推”端:存储值于共享状态的操作同步于(定义于 std::memory_order )任何在共享状态上等待的函数(如 std::future::get )的成功返回。其他情况下对共享状态的共时访问可能冲突:例如, std::shared_future::get 的多个调用方必须全都是只读,或提供外部同步。

成员函数

获取结果

get_future

返回与承诺的结果关联的 future
(公开成员函数)

设置结果

set_value

设置结果为指定值
(公开成员函数)

set_value_at_thread_exit

设置结果为指定值,同时仅在线程退出时分发提醒
(公开成员函数)

示例

此示例展示能如何将 promise<int> 用作线程间信号。

#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>
 
void accumulate(std::vector<int>::iterator first,
                std::vector<int>::iterator last,
                std::promise<int> accumulate_promise)
{
    int sum = std::accumulate(first, last, 0);
    accumulate_promise.set_value(sum);  // 提醒 future
}
 
void do_work(std::promise<void> barrier)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    barrier.set_value();
}
 
int main()
{
    // 演示用 promise<int> 在线程间传递结果。
    std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
    std::promise<int> accumulate_promise;
    std::future<int> accumulate_future = accumulate_promise.get_future();
    std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
                            std::move(accumulate_promise));
 
    // future::get() 将等待直至该 future 拥有合法结果并取得它
    // 无需在 get() 前调用 wait()
    //accumulate_future.wait();  // 等待结果
    std::cout << "result=" << accumulate_future.get() << '\n';
    work_thread.join();  // wait for thread completion
 
    // 演示用 promise<void> 在线程间对状态发信号
    std::promise<void> barrier;
    std::future<void> barrier_future = barrier.get_future();
    std::thread new_work_thread(do_work, std::move(barrier));
    barrier_future.wait();
    new_work_thread.join();
}

std::packaged_task

定义于头文件 <future>

template< class > class packaged_task; // 不定义

(1)(C++11 起)

template< class R, class ...Args >
class packaged_task<R(Args...)>;

(2)(C++11 起)

类模板 std::packaged_task 包装任何可调用 (Callable) 目标(函数、 lambda 表达式、 bind 表达式或其他函数对象),使得能异步调用它。其返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。

正如 std::function , std::packaged_task 是多态、具分配器的容器:可在堆上或以提供的分配器分配存储的可调用对象。

获取结果

get_future

返回与承诺的结果关联的 std::future
(公开成员函数)

执行

operator()

执行函数
(公开成员函数)

make_ready_at_thread_exit

执行函数,并确保结果仅在一旦当前线程退出时就绪
(公开成员函数)

reset

重置状态,抛弃任何先前执行的存储结果
(公开成员函数)

示例

#include <iostream>
#include <cmath>
#include <thread>
#include <future>
#include <functional>
 
// 避免对 std::pow 重载集消歧义的独有函数
int f(int x, int y) { return std::pow(x,y); }
 
void task_lambda()
{
    std::packaged_task<int(int,int)> task([](int a, int b) {
        return std::pow(a, b); 
    });
    std::future<int> result = task.get_future();
 
    task(2, 9);
 
    std::cout << "task_lambda:\t" << result.get() << '\n';
}
 
void task_bind()
{
    std::packaged_task<int()> task(std::bind(f, 2, 11));
    std::future<int> result = task.get_future();
 
    task();
 
    std::cout << "task_bind:\t" << result.get() << '\n';
}
 
void task_thread()
{
    std::packaged_task<int(int,int)> task(f);
    std::future<int> result = task.get_future();
 
    std::thread task_td(std::move(task), 2, 10);
    task_td.join();
 
    std::cout << "task_thread:\t" << result.get() << '\n';
}
 
int main()
{
    task_lambda();
    task_bind();
    task_thread();
}

std::future

定义于头文件 <future>

template< class T > class future;

(1)(C++11 起)

template< class T > class future<T&>;

(2)(C++11 起)

template<>          class future<void>;

(3)(C++11 起)

类模板 std::future 提供访问异步操作结果的机制:

  • 然后,异步操作的创建者能用各种方法查询、等待或从 std::future 提取值。若异步操作仍未提供值,则这些方法可能阻塞。
  • 异步操作准备好发送结果给创建者时,它能通过修改链接到创建者的 std::future 的共享状态(例如 std::promise::set_value )进行。

注意, std::future 所引用的共享状态不与另一异步返回对象共享(与 std::shared_future 相反)。

获取结果 

get

返回结果
(公开成员函数)

状态

valid

检查 future 是否拥有共享状态
(公开成员函数)

wait

等待结果变得可用
(公开成员函数)

wait_for

等待结果,如果在指定的超时间隔后仍然无法得到结果,则返回。
(公开成员函数)

wait_until

等待结果,如果在已经到达指定的时间点时仍然无法得到结果,则返回。
(公开成员函数)

示例

#include <iostream>
#include <future>
#include <thread>
 
int main()
{
    // 来自 packaged_task 的 future
    std::packaged_task<int()> task([](){ return 7; }); // 包装函数
    std::future<int> f1 = task.get_future();  // 获取 future
    std::thread(std::move(task)).detach(); // 在线程上运行
 
    // 来自 async() 的 future
    std::future<int> f2 = std::async(std::launch::async, [](){ return 8; });
 
    // 来自 promise 的 future
    std::promise<int> p;
    std::future<int> f3 = p.get_future();
    std::thread( [&p]{ p.set_value_at_thread_exit(9); }).detach();
 
    std::cout << "Waiting..." << std::flush;
    f1.wait();
    f2.wait();
    f3.wait();
    std::cout << "Done!\nResults are: "
              << f1.get() << ' ' << f2.get() << ' ' << f3.get() << '\n';
}

线程池

https://github.com/progschj/ThreadPool/blob/master/ThreadPool.h

https://blog.csdn.net/weixin_41074793/article/details/104302557
线程池-学习笔记(ThreadPool源代码内容详细解读)

https://www.cnblogs.com/ailumiyana/p/10016965.html
基于C++11实现线程池的工作原理

https://zhuanlan.zhihu.com/p/374242822
C/C++手撕线程池(线程池的封装和实现)


https://zhuanlan.zhihu.com/p/92632090
面试官:来!聊聊线程池的实现原理以及使用时的问题

在线程池中存在几个概念:核心线程数、最大线程数、任务队列。核心线程数指的是线程池的基本大小;最大线程数指的是,同一时刻线程池中线程的数量最大不能超过该值;任务队列是当任务较多时,线程池中线程的数量已经达到了核心线程数,这时候就是用任务队列来存储我们提交的任务。 与其他池化技术不同的是,线程池是基于生产者-消费者模式来实现的,任务的提交方是生产者,线程池是消费者。当我们需要执行某个任务时,只需要把任务扔到线程池中即可。线程池中执行任务的流程如下图如下。

preview

https://github.com/progschj/ThreadPool/blob/master/ThreadPool.h

代码解释

https://www.cnblogs.com/oloroso/p/5881863.html

https://segmentfault.com/a/1190000022456590

#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

// 线程类
class ThreadPool {
public:
    ThreadPool(size_t);
// 构造函数,传入线程数目
// std::size_t 是 sizeof 运算符还有 sizeof... 运算符和 alignof 运算符 (C++11 起)所返回的
// 无符号整数类型。std::size_t 的位宽不小于 16 。(C++11 起)

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type>;
// 入队任务(传入函数和函数的参数)
// 这个函数模块只适用于c++ 11, std::result_of 在c++ 17 弃用
// 一个enqueue模板函数 适用于任何函数(变参、成员都可以),返回F(Args...)函数的运行结果,通过std::future<type>返回,然后这个type又
// 利用了运行时检测(还是编译时检测?)推断出来的
// std::future  类模板 std::future 提供访问异步操作结果的机制:这里哪里来的异步操作??
// typename C++ 关键词:typename,在模板的声明或定义内,typename 可用于声明某个待决的有限定名是类型。
// std::result_of 在编译时推导 INVOKE 表达式的返回类型。::type 是其成员类型,表示其返回的类型
// std::result_of 在c++7 中弃用,invoke_result 代替
// auto 占位符 对于变量,指定要从其初始化器自动推导出其类型
//             对于函数,指定要从其 return 语句推导出其返回类型
    ~ThreadPool();
private:
    // need to keep track of threads so we can join them
	// 工作线程
    std::vector< std::thread > workers;
    // the task queue
	// 任务队列
	// std::function<void()>  ???如何实现都是 void() 函数签名
    std::queue< std::function<void()> > tasks;
    
    // synchronization
	// 互斥
    std::mutex queue_mutex;
	// 条件变量
    std::condition_variable condition;
	// 是否停止
    bool stop;
};
 
// the constructor just launches some amount of workers
// 构造函数,传入线程数目 threads
inline ThreadPool::ThreadPool(size_t threads)
    :   stop(false)
{
    for(size_t i = 0;i<threads;++i)
        workers.emplace_back(
            [this]
			// 一个lambda,值传递方式,异步函数
            {
                for(;;)
                {
                    std::function<void()> task;

                    {
						// lock
                        std::unique_lock<std::mutex> lock(this->queue_mutex);
                        this->condition.wait(lock,
                            [this]{ return this->stop || !this->tasks.empty(); });
						// 条件变量, 
						// this->stop || !this->tasks.empty() = false时 wait阻塞
                        if(this->stop && this->tasks.empty())
                            return;  
							// stop  && empty 时 退出
							// 退出之时,哪里unlock??
                        task = std::move(this->tasks.front());
						// 从tasks 队首取的 一个task
						// std::move 
						// 左值,右值
                        this->tasks.pop();
						// 从tasks队列 删除 
                    }

                    task();
					// task 执行
                }
            }
        );
}

// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future<typename std::result_of<F(Args...)>::type>
{
    using return_type = typename std::result_of<F(Args...)>::type;
	// 得到返回类型  

    auto task = std::make_shared< std::packaged_task<return_type()> >(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );
    // auto
    // std::make_shared  可以返回一个指定类型的 std::shared_ptr
    // std::packaged_task:  使用std::packaged_task 包装 std::bind 表达式
    // std::bind    使用std::bind 绑定 函数f 和参数,
    // std::forward    
	// 
    std::future<return_type> res = task->get_future();
	//  通过task->get_future() 获得task运行结果
    {
        std::unique_lock<std::mutex> lock(queue_mutex);

        // don't allow enqueueing after stopping the pool
        if(stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        tasks.emplace([task](){ (*task)(); });
		// 将task 推入队列
    }
    condition.notify_one();
    return res;
}

// the destructor joins all threads
inline ThreadPool::~ThreadPool()
{
    {
        std::unique_lock<std::mutex> lock(queue_mutex);
        stop = true;
    }
    condition.notify_all();
    for(std::thread &worker: workers)
        worker.join();
}

#endif

workerThread

并发

值类别

https://zh.cppreference.com/w/cpp/language/value_category

每个 C++ 表达式(带有操作数的操作符、字面量、变量名等)可按照两种独立的特性加以辨别:类型值类别 (value category)。每个表达式都具有某种非引用类型,且每个表达式只属于三种基本值类别中的一种:纯右值 (prvalue)亡值 (xvalue)左值 (lvalue)

  • 泛左值 (glvalue)(“泛化 (generalized)”的左值)是其求值确定一个对象、位域或函数的个体的表达式;
  • 纯右值 (prvalue)(“纯 (pure)”的右值)是求值符合下列之一的表达式:
    • 计算某个运算符的操作数的值或为 void 表达式(这种纯右值没有结果对象
    • 初始化某个对象或位域(称这种纯右值有一个结果对象)。除 decltype 外,所有类和数组的纯右值都有结果对象,即使它被舍弃也是如此。结果对象可以是变量,由 new 表达式创建的对象,由临时量实质化创建的临时对象,或者前述三类对象的成员;
  • 亡值 (xvalue)(“将亡 (expiring)”的值)是代表其资源能够被重新使用的对象或位域的泛左值;
  • 左值 (lvalue)(如此称呼的历史原因是,左值可以出现于赋值表达式的左边)是非亡值的泛左值;
  • 右值 (rvalue)(如此称呼的历史原因是,右值可以出现于赋值表达式的右边)是纯右值或者亡值。

未完待续

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值