C++11重要知识点介绍

目录

一.右值引用和移动语义

1.左值引用和右值引用

(1)左值引用

(2)右值引用

2.左值引用和右值引用比较

(1)左值引用:

(2)右值引用

3.右值引用使用场景(移动构造和移动赋值)

4.万能引用和完美转发

(1)万能引用 

(2)完美转发

二.类的新功能

1.默认成员函数 

2.成员初始化

3.关键字

(1)default(强制生成默认函数)

(2)delete(禁止生成默认函数)

(3)final和override

三.lambda表达式

1.出现原因

2.lambda表达式

3.lambda表达式语法

(1)lambda表达式各部分

(2)捕获列表说明

(3)lambda底层

四.包装器

1.function包装器

(1)包装器语法:

(2)包装器使用

(3)OJ实战: 

(4)bind (了解)

五 .线程库

1.thread测试总代码

2.thread

3.线程函数参数

4.原子性线程库

5.lock_guard与unique_lock

(1)mutex的种类

(2)lock_guard

(3)unique_lock

(4)支持两个线程交替打印,一个打印奇数,一个打印偶数


前言:这里包括了上一篇中所没有介绍的一些重要的C++11知识点。

C++11第1篇:C++11一些零碎的知识点介绍_糖果雨滴a的博客-CSDN博客

一.右值引用和移动语义

1.左值引用和右值引用

        首先无论是左值引用还是右值引用都是给对象取别名。

(1)左值引用

        左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,并且可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main()
{
    // 以下的p、b、c、*p都是左值
    int* p = new int(0);
    int b = 1;
    const int c = 2;

    // 以下几个是对上面左值的左值引用
    int*& rp = p;
    int& rb = b;
    const int& rc = c;
    int& pvalue = *p;

    return 0;
}

(2)右值引用

        右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边右值不能取地址。右值引用就是对右值的引用,给右值取别名。

int main()
{
    double x = 1.1, y = 2.2;

    // 以下几个都是常见的右值
    10;
    x + y;
    fmin(x, y);

    // 以下几个都是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);

    // 这里编译会报错,因为左操作数必须为左值
    10 = 1;
    x + y = 1;
    fmin(x, y) = 1;

    return 0;
}

        需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。这个特性并不重要。

int main()
{
    double x = 1.1, y = 2.2;
    int&& rr1 = 10;
    const double&& rr2 = x + y;
    rr1 = 20;
    rr2 = 5.5; // 报错

    return 0;
}

2.左值引用和右值引用比较

(1)左值引用:

① 左值引用只能引用左值,不能引用右值

② 但是const修饰的左值引用既可以引用左值,也可以引用右值

int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a; // ra为a的别名
    //int& ra2 = 10; // 编译失败,因为10是右值

    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;

    return 0;
}

(2)右值引用

① 右值引用只能引用右值,不能引用左值

② 但是右值引用可以引用move以后的左值

int main()
{
    // 右值引用只能右值,不能引用左值。
    int&& r1 = 10;

    // message : 无法将左值绑定到右值引用
    int a = 10;
    int&& r2 = a;

    // 右值引用可以引用move以后的左值
    int&& r3 = std::move(a);

    return 0;
}

3.右值引用使用场景(移动构造和移动赋值)

        在以往的左值引用中,对string类成员我们可以通过在返回值中加引用的方式来避免进行深拷贝,而导致效率低的问题,但是如果遇到下面这种情况,怎么办呢?

        如果我们在函数中创建了一个局部变量(临时的string类对象),而我们又需要返回这个类对象时,我们就不能(左值引用返回)在返回值加引用了,因为这个对象在出函数就要销毁,这就需要(传值返回)深拷贝了,就会导致效率低下。

        因此,C++11添加了右值引用来解决这种情况。

        我们只需要添加一个移动构造和一个移动赋值即可。

(1)移动构造

void swap(string& s)
{
    ::swap(_str, s._str);
    ::swap(_size, s._size);
    ::swap(_capacity, s._capacity);
}

// 移动构造
string(string&& s)
    :_str(nullptr)
    ,_size(0)
    ,_capacity(0)
{
    cout << "string(string&& s) -- 移动语义" << endl;
    swap(s);
}

(2)移动赋值

void swap(string& s)
{
    ::swap(_str, s._str);
    ::swap(_size, s._size);
    ::swap(_capacity, s._capacity);
}


// 移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动语义" << endl;
    swap(s);
    return *this;
}

        这里的移动构造和移动赋值其实就是一种现代写法,通过利用别人和自己进行交换,从而避免了进行深拷贝。

全部代码实现如下: 

namespace hb
{
    class string
    {
    public:
        typedef char* iterator;
        iterator begin()
        {
            return _str;
        }

        iterator end()
        {
            return _str + _size;
        }

        string(const char* str = "")
            :_size(strlen(str))
            , _capacity(_size)
        {
            //cout << "string(char* str)" << endl;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }

        // s1.swap(s2)
        void swap(string& s)
        {
            ::swap(_str, s._str);
            ::swap(_size, s._size);
            ::swap(_capacity, s._capacity);
        }

        // 拷贝构造
        string(const string& s)
            :_str(nullptr)
        {
            cout << "string(const string& s) -- 深拷贝" << endl;
            string tmp(s._str);
            swap(tmp);
        }

        // 赋值重载
        string& operator=(const string& s)
        {
            cout << "string& operator=(string s) -- 深拷贝" << endl;
            string tmp(s);
            swap(tmp);
            return *this;
        }
        // 移动构造
        string(string&& s)
            :_str(nullptr)
            ,_size(0)
            ,_capacity(0)
        {
            cout << "string(string&& s) -- 移动语义" << endl;
            swap(s);
        }

        // 移动赋值
        string& operator=(string&& s)
        {
            cout << "string& operator=(string&& s) -- 移动语义" << endl;
            swap(s);
            return *this;
        }

        ~string()
        {
            delete[] _str;
            _str = nullptr;
        }

        char& operator[](size_t pos)
        {
            assert(pos < _size);
            return _str[pos];
        }

        void reserve(size_t n)
        {
            if (n > _capacity)
            {
                char* tmp = new char[n + 1];
                strcpy(tmp, _str);
                delete[] _str;
                _str = tmp;
                _capacity = n;
            }
        }

        void push_back(char ch)
        {
            if (_size >= _capacity)
            {
                size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newcapacity);
            }
            _str[_size] = ch;
            ++_size;
            _str[_size] = '\0';
        }

        //string operator+=(char ch)
        string& operator+=(char ch)
        {
            push_back(ch);
            return *this;
        }

        const char* c_str() const
        {
            return _str;
        }

    private:
        char* _str;
        size_t _size;
        size_t _capacity; // 不包含最后做标识的\0
    };
}

实现原理:

        如果没有右值引用,那么会进行拷贝构造,而这里实际上是两次拷贝构造,一次是在return的时候创建一个临时变量,将其拷贝构造进这个临时变量中,然后再拷贝构造传回去。不过呢,现在一般的编译器都对其进行了优化,优化后就变成了一次拷贝构造。

        而移动构造和移动赋值其实也是如此,在返回参数时,原本左值被强制move(这里不需要我们去move,而是编译器会帮我们),会去调用移动构造或者移动赋值函数,而不会去调用拷贝构造或者拷贝赋值,并且也是需要调用两次。不过编译器一般也是对其优化成了一次。

move

        如果需要用右值去引用左值实现移动语义,那么就可以通过move函数将左值转化为右值。

4.万能引用和完美转发

(1)万能引用 

template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);
}

① 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值
② 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
③ 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值
④ 我们希望能够在传递过程中保持它的左值或者右值的属性,就需要用到完美转发

(2)完美转发

        完美转发可以在在传参的过程中保留对象原生类型的属性(右值不会退化为左值),想要使用完美转发就要用forward

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }

// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
    Fun(std::forward<T>(t));
}
int main()
{
    PerfectForward(10); // 右值
    int a;
    PerfectForward(a); // 左值
    PerfectForward(std::move(a)); // 右值
    const int b = 8;
    PerfectForward(b); // const 左值
    PerfectForward(std::move(b)); // const 右值

    return 0;
}

        通过这里的输出函数,我们可以发现左值调用的就是左值,右值调用的就是右值,这就是用了完美转发forward。

使用场景:

template<class T>
struct ListNode
{
    ListNode* _next = nullptr;
    ListNode* _prev = nullptr;
    T _data;
};

template<class T>
class List
{
    typedef ListNode<T> Node;
public:
    List()
    {
        _head = new Node;
        _head->_next = _head;
        _head->_prev = _head;
    }

    void PushBack(T&& x)
    {
        //Insert(_head, x);
        Insert(_head, std::forward<T>(x));
    }

    void PushFront(T&& x)
    {
        //Insert(_head->_next, x);
        Insert(_head->_next, std::forward<T>(x));
    }

    void Insert(Node* pos, T&& x)
    {
        Node* prev = pos->_prev;
        Node* newnode = new Node;
        newnode->_data = std::forward<T>(x); // 关键位置
        // prev newnode pos
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }

    void Insert(Node* pos, const T& x)
    {
        Node* prev = pos->_prev;
        Node* newnode = new Node;
        newnode->_data = x; // 关键位置
        // prev newnode pos
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }
    private:
        Node* _head;
};

int main()
{
    List<bit::string> lt;
    lt.PushBack("1111");
    lt.PushFront("2222");

    return 0;
}

注意:如果用来万能引用,要用完美转发,那么在每一个函数传参后都要使用,否则右值依旧会退化成左值。

二.类的新功能

1.默认成员函数 

        在C++98中只有6个默认成员函数:

① 构造函数

② 析构函数

③ 拷贝构造函数

④ 拷贝赋值重载函数

⑤ 取地址重载函数

⑥ const取地址重载函数

        而C++11中右值引用的出现,有新增了两个默认成员函数:

⑦ 移动构造函数

⑧ 移动赋值函数

        针对移动构造函数和移动赋值函数我们应该注意如下几点:

(1)如果没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

(2)如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。
默认生成的移动构造函数,对于内
置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似)

(3)如果提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

2.成员初始化

        C++11允许在类定义时给成员变量初始缺省值,默认生成的构造函数会使用这些缺省值初始化。

class A
{
public:

private:
    int a = 0;
}

        这里的0就是缺省值。

3.关键字

(1)default(强制生成默认函数)

        如果想要使用某个默认的函数,但是因为某些原因这个函数没有默认生成,这时我们就可以使用default使之强制生成。(例如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字强制移动构造生成)

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}

    Person(const Person& p)
        :_name(p._name)
        ,_age(p._age)
    {}

    Person(Person&& p) = default;

private:
    hb::string _name;
    int _age;
};

int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);

    return 0;
}

(2)delete(禁止生成默认函数)

        如果想要限制某些默认函数的生成,就可以使用delete关键字。

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}

    Person(const Person& p) = delete;

private:
    hb::string _name;
    int _age;
};

int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);

    return 0;
}

(3)final和override

        这个是用在继承和多态中的关键字。

① final作用:修饰虚函数,表示该虚函数不能再被重写。

② override作用:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写会编译报错。

三.lambda表达式

1.出现原因

        函数指针是在C语言中,不过因为过于麻烦,因此C++中新出了仿函数。sort这个函数就可以根据我们自定义的仿函数来进行对不同的元素排序,不过如果我们需要对很多种自定义类型进行排序,那么就会很麻烦,因此C++11中就又出现了lambda表达式,使之进一步简化。而lambda的底层其实就是仿函数。

2.lambda表达式

sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
	return g1._price < g2._price; });
	
cout << endl;

sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
	return g1._price > g2._price; });

cout << endl;

sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
	return g1._evaluate < g2._evaluate; });

cout << endl;

sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
	return g1._evaluate > g2._evaluate; });

        以上就是使用lambda表达式来代替仿函数。

3.lambda表达式语法

(1)lambda表达式各部分

① []:捕捉列表。该列表总数出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文的变量供lambda函数使用

② ():参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略

③ mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)

④ ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导

⑤ {}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量

注意:参数列表和返回值类型都是可写可不写的,而捕捉列表和函数体可以为空。因此最简单的lambda表达式为:[]{}

        lambda表达式可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。

(2)捕获列表说明

        捕获列表决定了上下文中哪些数据可以被lambda使用,以及使用的方式是传值还是传引用。

① [var]:表示值传递方式捕捉变量var
② [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
③ [&var]:表示引用传递捕捉变量var
④ [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
⑤ [this]:表示值传递方式捕捉当前的this指针

注意:

        a. 父作用域只包含lambda函数的语句块

        b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
        比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

        c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
        比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a就重复传递了

        d. 在块作用域以外的lambda函数捕捉列表必须为空。

        e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。

        f. lambda表达式之间不能相互赋值,即使看起来类型相同

int main()
{
	int a = 0, b = 200;
	// [capture-list] (parameters) mutable -> return-type { statement }
	// 捕捉列表  参数  返回值  函数体

	// 一般是局部匿名函数  也可以写到全局
	auto Add1 =  [](int x, int y)->double{return (x + y) /3.0; };
	auto Add2 = [](int x, int y)->int{return (x + y) /3.0; };
	auto Add3 = [a, b]{return (a+b) / 3.0; };

	cout << Add1(a, b) << endl;
	cout << Add2(a, b) << endl;
	cout << Add3() << endl;

	auto Swap1 = [](int& x, int& y){
		int tmp = x;
		x = y;
		y = tmp;
	};

	Swap1(a, b);
	cout << a << " " << b << endl;

	//mutable 只是让传值捕捉变量const属性去掉了
	/*auto Swap2 = [a, b]()mutable{
		int tmp = a;
		a = b;
		b = tmp;
	};*/
	// 用引用的方式捕捉
	auto Swap2 = [&a, &b]{
		int tmp = a;
		a = b;
		b = tmp;
	};

	Swap2();
	cout << a << " " << b << endl;

	int c =2, d=3, e=4, f=5, g=6, ret;
	// 传值捕捉全部对象
	auto Func1 = [=]{
		return c + d*e / f + g;
	};

	cout << Func1() << endl;

	// 传引用捕捉全部对象
	auto Func2 = [&]{
		ret = c + d*e / f + g;
	};

	Func2();
	cout << ret << endl;

	// 混着捕捉
	auto Func3 = [c, d, &ret]{
		ret = c + d;
	};

	Func3();
	cout << ret << endl;

	// ret传引用捕捉 其他全部传值捕捉
	auto Func4 = [=, &ret]{
		ret = c + d*e / f + g;
		//c = 1;
	};

	Func4();
	cout << ret << endl;

	return 0;
}

(3)lambda底层

        函数对象,又称为仿函数,可以像函数一样使用的对象。就是在类中重载了operator()运算符的类对象。

class Rate
{
public:
    Rate(double rate): _rate(rate)
    {}

    double operator()(double money, int year)
    { 
        return money * _rate * year;
    }
private:
    double _rate;
};

int main()
{
    // 函数对象
    double rate = 0.49;
    Rate r1(rate);
    r1(10000, 2);

    // lambda表达式
    auto r2 = [=](double monty, int year)->double{return monty*rate*year;};
    r2(10000, 2);

    return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。

        在底层编译器对于lambda表达式的处理方式完全就是按照函数对象的方式处理:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator。

四.包装器

1.function包装器

        function包装器也叫适配器。C++中的function本质是一个类模板,也是一个包装器。

        在C++中,有很多类型,既可以是函数,也可以是成员函数、静态成员函数、仿函数,又或者是lambda表达式等等,如此多的类型使用模板就会导致效率低下,因此有了function包装器。

(1)包装器语法:

std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参

(2)包装器使用

int f(int a, int b)
{
	return a + b;
}

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};

class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}

	double plusd(double a, double b)
	{
		return a + b;
	}
};

int main()
{
	std::function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;

	std::function<int(int, int)> func2 = Functor();
	cout << func2(10, 20) << endl;

	std::function<int(int, int)> func3 = &Plus::plusi;
	cout << func3(100, 200) << endl;

	// 非静态成员函数包装
	std::function<double(Plus, double, double)> func4 = &Plus::plusd;
	cout << func4(Plus(), 100.11, 200.11) << endl;

	std::function<int(int, int)> func5 = [](int a, int b) {return a + b; };
	cout << func5(100, 200) << endl;

	return 0;
}

上面的包装器就分别有函数、仿函数、静态成员函数、成员函数和lambda表达式。

(3)OJ实战: 

150. 逆波兰表达式求值

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<long long> st;
        map<string, function<int(int, int)>> opFuncMap = 
        {
            { "+", [](long long x, long long y){ return x + y; }},
            { "-", [](long long x, long long y){ return x - y; }},
            { "*", [](long long x, long long y){ return x * y; }},
            { "/", [](long long x, long long y){ return x / y; }}
        };

        for(auto& str : tokens)
        {
            // 操作符
            if(opFuncMap.count(str))
            {
                long long right = st.top();
                st.pop();
                long long left = st.top();
                st.pop();

                st.push(opFuncMap[str](left, right));
            }
            // 操作数
            else
            {
                st.push(stoll(str));
            }
        }

        return st.top();
    }
};

(4)bind (了解)

        std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。

语法:

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

        可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

        调用bind的一般形式:auto newCallable = bind(callable,arg_list);

        其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

        arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

int f(int a, int b)
{
	return a - b;
}

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};


class Plus
{
public:
	Plus(int x = 2)
		:_x(x)
	{}

	int plusi(int a, int b)
	{
		return (a + b) * _x;
	}
private:
	int _x;
};

int main()
{
	std::function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;

	std::function<int(int, int)> func2 = Functor();
	cout << func2(10, 20) << endl;

	// 3个参数
	std::function<int(Plus, int, int)> func3 = &Plus::plusi;
	cout << func3(Plus(), 100, 200) << endl;

	// 调整可调用对象的参数个数和顺序
	// _1 _2 _3... 表示你要自己传的那些参数,_1表示第一个参数传给_1
	// 调整个数
	// 2个参数
	std::function<int(int, int)> func4 = std::bind(&Plus::plusi, Plus(10), placeholders::_1, placeholders::_2);

	// 1个参数
	std::function<int(int)> func5 = std::bind(&Plus::plusi, Plus(10),
		10, placeholders::_1);

	cout << func5(200) << endl;

	// 调整顺序 -- 用处不大
	std::function<int(int, int)> func6 = std::bind(f, placeholders::_2, placeholders::_1);
	cout << func1(66, 77) << endl;
	cout << func6(66, 77) << endl;

	map<string, std::function<int(int, int)>> opFuncMap =
	{
		{ "普通函数指针", f },
		{ "函数对象", Functor() },
		{ "成员函数指针", std::bind(&Plus::plusi, Plus(10), placeholders::_1, placeholders::_2) }
	};

	cout << opFuncMap["普通函数指针"](1, 2) << endl;
	cout << opFuncMap["函数对象"](1, 2) << endl;
	cout << opFuncMap["成员函数指针"](1, 2) << endl;


	return 0;
}

五 .线程库

1.thread测试总代码

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <atomic>

using namespace std;

void Print(int n, int& x, mutex& mtx)
{
	for (int i = 0; i < n; ++i)
	{
		mtx.lock();

		cout <<this_thread::get_id()<<":"<<i << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
		++x;

		mtx.unlock();
	}

}

int main()
{
	mutex m;
	int count = 0;
	thread t1(Print, 10, ref(count), ref(m));
	thread t2(Print, 10, ref(count), ref(m));

	t1.join();
	t2.join();

	cout << count << endl;

	return 0;
}

int main()
{
	mutex mtx;
	int x = 0;
	int n = 10;
	thread t1([&](){
		for (int i = 0; i < n; ++i)
		{
			mtx.lock();

			cout << this_thread::get_id() << ":" << i << endl;
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			++x;

			mtx.unlock();
		}
	});

	thread t2([&](){
		for (int i = 0; i < n; ++i)
		{
			mtx.lock();

			cout << this_thread::get_id() << ":" << i << endl;
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			++x;

			mtx.unlock();
		}
	});

	t1.join();
	t2.join();

	cout << x << endl;

	return 0;
}

int main()
{
	mutex mtx;
	int x = 0;
	int n = 10;
	int m;
	cin >> m;

	vector<thread> v(m);
	//v.resize(m);

	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				mtx.lock();

				cout << this_thread::get_id() << ":" << i << endl;
				std::this_thread::sleep_for(std::chrono::milliseconds(100));
				++x;

				mtx.unlock();
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}


int main()
{
	mutex mtx;
	int x = 0;
	int n = 1000000;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				// 并行
				mtx.lock();
				++x;
				mtx.unlock();
			}
		});

		//v[i] = thread([&](){
		//	// 串行
		//	mtx.lock();
		//	for (int i = 0; i < n; ++i)
		//	{
		//		++x;
		//	}
		//	mtx.unlock();
		//});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}


int main()
{
	mutex mtx;
	atomic<int> x = 0;
	//int x = 0;
	int n = 1000000;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				// t1 t2 t3 t4
				++x;
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}

 RAII
template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lk)
		:_lock(lk)
	{
		_lock.lock();
		cout << "thread:" << this_thread::get_id() << "加锁" << endl;
	}

	~LockGuard()
	{
		cout << "thread:" << this_thread::get_id() << "解锁" << endl << endl;
		_lock.unlock();
	}
private:
	Lock& _lock;
};

int main()
{
	mutex mtx;
	atomic<int> x = 0;
	//int x = 0;
	int n = 100;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				//mtx.lock();

				 如果存在抛异常就死锁了
				//try {
				//	cout << this_thread::get_id() << ":" << i << endl;
				//	std::this_thread::sleep_for(std::chrono::milliseconds(100));
				//}
				//catch (...)
				//{
				//	mtx.unlock();
				//	throw;
				//}

				//mtx.unlock();

				//mtx.lock();
				//LockGuard<mutex> lk(mtx);

				{
					lock_guard<mutex> lk(mtx);
					// 如果存在抛异常就死锁了
					cout << this_thread::get_id() << ":" << i << endl;
				}

				std::this_thread::sleep_for(std::chrono::milliseconds(100));
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}

2.thread

        在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接
口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在
并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的
线程,必须包含<thread>头文件。

函数名功能
thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn,
args1, args2,
...)
构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的
参数
get_id()获取线程id
jionable()线程是否还在执行,joinable代表的是一个正在执行中的线程。
jion()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离
的线程变为后台线程,创建的线程的"死活"就与主线程无关

注意:
1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的
状态。

2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

#include <thread>

int main()
{
    std::thread t1;
    cout << t1.get_id() << endl;
    return 0;
}

        get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中
包含了一个结构体:

typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;

3. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。
线程函数一般情况下可按照以下三种方式提供:

#include <iostream>
#include <thread>

using namespace std;

void ThreadFunc(int a)
{
	cout << "Thread1" << a << endl;
}

class TF
{
public:
	void operator()()
	{
		cout << "Thread3" << endl;
	}
};

int main()
{
	// 线程函数为函数指针
	thread t1(ThreadFunc, 10);
	// 线程函数为lambda表达式
	thread t2([] {cout << "Thread2" << endl; });
	// 线程函数为函数对象
	TF tf;
	thread t3(tf);

	t1.join();
	t2.join();
	t3.join();

	cout << "Main thread!" << endl;

	return 0;
}

4. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个
线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。

5. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效

采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象
线程已经调用jion或者detach结束

并行与并发的区别?

int main()
{
	mutex mtx;
	int x = 0;
	int n = 1000000;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				// 并行
				mtx.lock();
				++x;
				mtx.unlock();
			}
		});

		//v[i] = thread([&](){
		//	// 串行
		//	mtx.lock();
		//	for (int i = 0; i < n; ++i)
		//	{
		//		++x;
		//	}
		//	mtx.unlock();
		//});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}

        如上代码,并行是将加锁和解锁写在for循环内;并发是将加锁和解锁写在循环外。即,并行是这个线程执行一下任务之后再换到另一个线程去执行任务;并发是当前线程的任务都执行完之后再换到另一个线程执行完他的任务。

        这里在测试时,只要数据稍大些,可以很明显的看出并行的速度要比并发的慢。因为并行需要进行很多次的加锁解锁,如果是两个线程要循环1000次,那么并行就要进行2000次加锁解锁,而并发仅仅需要2次加锁解锁。

3.线程函数参数

        线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

#include <thread>
void ThreadFunc1(int& x)
{
	x += 10;
}

void ThreadFunc2(int* x)
{
	*x += 10;
}

int main()
{
	int a = 10;
	// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
	// 引用的是线程栈中的拷贝
	thread t1(ThreadFunc1, a);
	t1.join();
	cout << a << endl;
	// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
	thread t2(ThreadFunc1, std::ref(a);
	t2.join();
	cout << a << endl;
	// 地址的拷贝
	thread t3(ThreadFunc2, &a);
	t3.join();
	cout << a << endl;

	return 0;
}
void Print(int n, int& x, mutex& mtx)
{
	for (int i = 0; i < n; ++i)
	{
		mtx.lock();

		cout <<this_thread::get_id()<<":"<<i << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
		++x;

		mtx.unlock();
	}

}

int main()
{
	mutex m;
	int count = 0;
	thread t1(Print, 10, ref(count), ref(m));
	thread t2(Print, 10, ref(count), ref(m));

	t1.join();
	t2.join();

	cout << count << endl;

	return 0;
}

注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。

4.原子性线程库

        多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。比如:

#include <iostream>
#include <thread>

using namespace std;

unsigned long sum = 0;

void fun(size_t num)
{
    for (size_t i = 0; i < num; ++i)
    sum++;
}

int main()
{
    cout << "Before joining,sum = " << sum << std::endl;
    thread t1(fun, 10000000);
    thread t2(fun, 10000000);
    t1.join();
    t2.join();

    cout << "After joining,sum = " << sum << std::endl;

    return 0;
}

        传统的解决方法是加锁:

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

unsigned long sum = 0;
std::mutex m;

void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
	{
		m.lock();
		sum++;
		m.unlock();
	}
}

int main()
{
	cout << "Before joining,sum = " << sum << std::endl;
	thread t1(fun, 10000000);
	thread t2(fun, 10000000);

	t1.join();
	t2.join();

	cout << "After joining,sum = " << sum << std::endl;
	
	return 0;
}

        虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。

        因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。

 注意:需要使用以上原子操作变量时,必须添加头文件:#include <atomic>

#include <iostream>
#include <thread>
#include <atomic>

using namespace std;

atomic_long sum{ 0 };

void fun(size_t num)
{
    for (size_t i = 0; i < num; ++i)
    sum ++; // 原子操作
}

int main()
{
    cout << "Before joining, sum = " << sum << std::endl;
    thread t1(fun, 1000000);
    thread t2(fun, 1000000);

    t1.join();
    t2.join();

    cout << "After joining, sum = " << sum << std::endl;

    return 0;
}

        在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。
        更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。

atmoic<T> t; // 声明一个类型为T的原子类型变量t

        注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及
operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算
符重载默认删除掉了。

#include <atomic>
int main()
{
    atomic<int> a1(0);
    //atomic<int> a2(a1); // 编译失败
    atomic<int> a2(0);
    //a2 = a1; // 编译失败

    return 0;
}

5.lock_guard与unique_lock

        在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。
        比如:一个线程对变量number进行加一100次,另外一个减一100次,每次操作加一或者减一之后,输出number的结果,要求:number最后的值为1。

#include <thread>
#include <mutex>

int number = 0;
mutex g_lock;

int ThreadProc1()
{
	for (int i = 0; i < 100; i++)
	{
		g_lock.lock();
		++number;
		cout << "thread 1 :" << number << endl;
		g_lock.unlock();
	}
	return 0;
}

int ThreadProc2()
{
	for (int i = 0; i < 100; i++)
	{
		g_lock.lock();
		--number;
		cout << "thread 2 :" << number << endl;
		g_lock.unlock();
	}

	return 0;
}

int main()
{
	thread t1(ThreadProc1);
	thread t2(ThreadProc2);

	t1.join();
	t2.join();

	cout << "number:" << number << endl;

	return 0;
}

        上述代码的缺陷:锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。

(1)mutex的种类

在C++11中,Mutex总共包了四个互斥量的种类:

1. std::mutex

        C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:

函数名函数功能
lock()上锁:锁住互斥量
unlock()解锁:释放对互斥量的所有权
try_lock()尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻


注意,线程函数调用lock()时,可能会发生以下三种情况:

        如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直拥有该锁
        如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
        如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

线程函数调用try_lock()时,可能会发生以下三种情况:

        如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用unlock释放互斥量
        如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
        如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

2. std::recursive_mutex

        其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,
        std::recursive_mutex 的特性和 std::mutex 大致相同。

3. std::timed_mutex

        比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() 。

try_lock_for():

        接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until():

        接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

4. std::recursive_timed_mutex

(2)lock_guard

std::lock_gurad 是 C++11 中定义的模板类。定义如下:

template<class _Mutex>

class lock_guard
{
public:
	// 在构造lock_gard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}

	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		: _MyMutex(_Mtx)
	{}

	~lock_guard() _NOEXCEP
	{
		_MyMutex.unlock();
	}

	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	_Mutex& _MyMutex;
};

        通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

        lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。

(3)unique_lock

        与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。

        与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

        上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
        修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)

        获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

(4)支持两个线程交替打印,一个打印奇数,一个打印偶数

第一种方法:

int main()
{
	int i = 0;
	int n = 100;

	thread t1([&](){
		while (i < n)
		{
			while (i % 2 != 0)
			{
				this_thread::yield();
			}

			cout <<this_thread::get_id()<<":"<<i << endl;
			i += 1;
		}
	});


	thread t2([&](){
		while(i < n)
		{
			while (i % 2 == 0)
			{
				this_thread::yield();
			}

			cout << this_thread::get_id() << ":" << i << endl;
			i += 1;
		}
	});

	t1.join();
	t2.join();

	return 0;
}

        这个就是直接通过while循环的i %2 是否为0来使线程交替打印,这个是可以的,但是如果情况再复杂一些就不行了。

另一种方法:

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;

	thread t1([&](){
		while (i < n)
		{
			mtx.lock();

			cout << this_thread::get_id() << ":" << i << endl;
			i += 1;

			mtx.unlock();
		}
	});

	this_thread::sleep_for(chrono::microseconds(100));

	thread t2([&](){
		while (i < n)
		{
			mtx.lock();

			cout << this_thread::get_id() << ":" << i << endl;
			i += 1;

			mtx.unlock();
		}
	});

	t1.join();
	t2.join();

	return 0;
}

        这个方法是不行的,只用加锁和解锁是可能出现某一个线程连续执行多次任务的,不满足题意的交替打印。

第二种方法:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <atomic>

using namespace std;

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;
	condition_variable cv;
	bool ready = true;

	// t1打印奇数
	thread t1([&](){
		while (i < n)
		{
			{
				unique_lock<mutex> lock(mtx);
				cv.wait(lock, [&ready](){return !ready; });

				cout << "t1--" << this_thread::get_id() << ":" << i << endl;
				i += 1;

				ready = true;

				cv.notify_one();
			}

			//this_thread::yield();
			this_thread::sleep_for(chrono::microseconds(100));
		}
	});

	// t2打印偶数
	thread t2([&]() {
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&ready](){return ready; });

			cout <<"t2--"<<this_thread::get_id() << ":" << i << endl;
			i += 1;
			ready = false;

			cv.notify_one();
		}
	});

	this_thread::sleep_for(chrono::seconds(3));

	cout << "t1:" << t1.get_id() << endl;
	cout << "t2:" << t2.get_id() << endl;

	t1.join();
	t2.join();

	return 0;
}

        最好的方法,使用条件变量,使用条件变量就要有头文件:#include <condition_variable>

        这里定义了条件变量cv,还有一个很关键的ready,这个设置为true还是false是决定线程1先开始还是线程2先开始的。接下来在lambda表达式中,wait这个函数很关键,这里wait是根据当前ready来判断是等待还是不等待,如果不等待,那么线程执行完之后,要将ready置为和另一个线程相反的(线程1是true执行,线程2是false执行,那么线程1执行完就将ready变为false,线程2执行完就将ready变成true)。

        最后打印出来的结果一定是两个线程交替打印的,一个打印奇数,一个打印偶数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰果滴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值