当前位置: 首页 > news >正文

【C++】C++11

目录

C++11简介

统一的列表初始化

{}初始化

std::initializer_list

声明

auto

decltype

nullptr

范围for循环

智能指针

STL中的一些变化

右值引用和移动语义

左值引用和右值引用

右值引用的意义

完美转发

lambda表达式

新的类功能

可变参数模版 

包装器

function包装器

bind

线程库

线程函数参数

lock_guard和unique_lock

原子性操作库

两个线程交替打印


C++11简介

相比于C++98/03,C++11发生了较大的变化,大约新增了约140个新特性,以及修正了约600个缺陷,这使得C++更加强大。

统一的列表初始化

{}初始化

在C++98中,允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

struct Point
{int _x;int _y;
};int main()
{int array1[] = { 1,2,3,4,5 };Point p = { 1,2 };return 0;  
}

C++11扩大了用大括号括起的列表的使用范围,使其可用于所有的内置类型和用户自定义类型,列表初始化时,可以添加等号(=),也可以不添加,但是不太建议去掉

int arr1[]{ 1,2,3,4,5 };
int arr2[5]{ 0 };
Point p2{ 1,2 };

对于自定义类型创建对象时也可以用列表初始化方式调用构造函数初始化:

class A
{
public:A(int x, int y):_x(x), _y(y){}A(int x):_x(x), _y(x){}
private:int _x;int _y;
};
int main()
{A a5(6);//单参数的隐式类型转换A a3 = 1;A a4 {2};//多参数的隐式类型转换//C++支持的列表初始化,这里{1,2}先调用构造函数初始化,再调用拷贝构造赋值给a1   A a1 = { 1,2 };A a2 { 3,4 };return 0;
}

std::initializer_list

//vector可以这样初始化
vector<int> v1(10, 1);
//但是vector不能用{}初始化
vector<int> v2 = {1,2,3,4,5};

为什么vector用不了{}这样构造呢?{...}里面可能有1/2/3/4/....个元素,但是vector的构造函数参数列表不知道到底该设置多少个,要写无数个构造函数才行,就像下面这样:

所以,为了支持这样的初始化,引入了initializer_list,

因此,我们现在可以将{...}的常量数组转化为initializer_list。

initializer_list的本质是两个指针,一个first指向第一个元素,一个last指向最后一个元素的下一个,为了验证这样一个事实,我们算一下i1的大小,在32位平台下,i1的大小是8(2个指针的大小):

initializer_list也支持简单的迭代器begin和end,迭代器的类型是const T*,由于它指向常量数组,所以是const T*,不支持修改。 

所以,为了解决本节刚开始提出的问题,C++11使用了这样的方式解决:

vector(initializer_list<T> i1);

这个构造一劳永逸的解决了问题,不用像上面那样麻烦的方式解决。

initializer_list可以接收常量数组,本质是两个指针,一个指向数组的开始,一个指向数组最后的下一个。这样,就算{...}里有5个8个,都可以传给initializer_list。

当  X自定义 = Y类型,这个时候就会隐式类型转换,需要有 X(Y mm),即X支持Y为参数类型的构造就可以。

在上面图中,{1,2,3,4,5}会被识别成initializer_list,下面的那个,等号右侧生成一个临时对象,再去拷贝构造给v4。  

再举一个例子,创建map对象时,也可以用{}进行创建:

声明

auto

在C++11中,auto用于自动推断类型,这样要求必须进行显示初始化。

int main()
{int i = 10;auto p = &i;cout << typeid(p).name() << endl;map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };//map<string, string>::iterator it = dict.begin();auto it = dict.begin();return 0;
}

decltype

关键字decltype将变量的类型声明为表达式指定的类型。它的功能和typeid有点像,typeid可以拿到对象的类型,但是得到的只是一个单纯的字符串,不能用来定义对象,如果想定义一个和typeid一样的值,这就做不到了。

但是decltype可以做到:

但是,有人可能会认为,我用一个auto不也可以吗?但是,我们换一个场景:

我想用ret3的返回值类型去初始化class B的模版参数,这时候typeid就不行了,但是可以用decltype来确定ret3的类型,

B<decltype(ret3)> b;

虽然auto和decltype虽然可以方便我们写代码,但是有些地方增加代码读起来的难度,所以慎用auto和decltype!

nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

范围for循环

我们在之前的学习中,已经多次使用了范围for,这里不再赘述。

只有一点需要注意,我们最好把&加上,即for(auto& e:v)或者for(const auto& e:v)(在不改变值的情况下加const),这样可以防止深拷贝,提高效率。

智能指针

我们之后单独学习这块。

STL中的一些变化

新容器 

array:对越界检查更严格,但是可以用vector来替代,所以用处不大,是一个挺失败的设计。

forward_list:单向链表,相比list节省一个指针,但是也不好用。

容器内部

几乎在每个容器内部,都增加了initializer_list、移动构造(这个很有用),

还有emplace、empalce_back:

右值引用和移动语义

左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

什么是左值和右值

左值和右值的区分关键,是能否取地址。可以取地址的是左值,不能取地址的是右值。

因此,a、b、c均是左值。

左值不一定是值,也可能是表达式。如*p也是左值,因为它可以取地址。

类似的,v[1]也是左值,它可以取地址。

右值也是一个表达数据的表达式,它不能取地址,如字面常量、匿名对象、临时对象。

什么是左值引用和右值引用

引用就是取别名,左值引用就是给左值取别名,右值引用就是给右值取别名,

右值引用就是用&&表示: 

自定义的类型右值主要有两种,一类是匿名对象,一类是传值返回的临时对象。

左值引用不能给右值取别名,但是const 左值引用可以,

const string& ref1 = string("1111");

右值引用也不能给左值取别名,但是可以给move以后得左值取别名。

左值引用总结:

1. 左值引用只能引用左值,不能引用右值。

2. 但是const左值引用既可引用左值,也可引用右值。

右值引用总结:

1. 右值引用只能右值,不能引用左值。

2. 但是右值引用可以move以后的左值。

右值引用的意义

引用的意义是减少拷贝,提高效率, 比如引用传参

void func1(const string& s);

如果不用引用,那传参时就要拷贝,像map这种大一点的容器拷贝的代价很大,

还有传引用返回

string& func2();

左值引用的场景主要就是引用传参和传引用返回。但是,左值引用返回值的问题没有彻底解决,因为如果是func2中局部对象,不能用引用返回,它的生命周期就在func2里面,出了作用域就销毁了,同时,也不能用右值引用返回,右值引用也不能解决生命周期的问题,

例如,bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

to_string的返回值用左值引用和右值引用都不太行,因为返回值str出了作用域都会被销毁,在C++11中,另辟蹊径,增加了一个参数为右值引用的构造函数--移动构造,(对比:参数为左值引用的构造函数为拷贝构造),移动构造拷贝构造构成函数重载。

在没有移动构造时,拷贝构造string(const string& s)既可以接收左值,又可以接收右值;但是在有了移动构造时,编译器会找最匹配的。

在C++11中,将右值做了区分,分为纯右值将亡值,内置类型的那种是纯右值,自定义类型的那种是将亡值,在移动构造中,可以这样写:

//移动构造
//右值(将亡值)
string(string&& s):_str(nullptr)
{cout << "string(string&& s) -- 移动构造" << endl;swap(s);
}

既然已经是将亡值,那么不妨把我的和将亡值交换一下,移动将亡值的资源,这样就不用拷贝构造了,效率就上来了    

有人说右值引用延长了str的生命周期,这种说法是不正确的,str出了作用域就会被销毁,确切的说是延长了资源的生命周期。

如果是下面这种形式,如果没有移动拷贝和移动赋值,那就是一次拷贝构造和一次赋值拷贝,编译器不敢将这两步合二为一(因为这是两个不同的操作),

如果有了移动拷贝和移动赋值,那就是一次移动拷贝和一次移动赋值,

移动构造、移动赋值和我们之前拷贝构造的现代写法有点像,但是这两种有本质的区别,现代写法是让一个临时对象去构造并转移它的资源,并没有提高效率,而移动构造、移动赋值给我的右值就是一个将亡值,直接转移这个将亡值的资源,代价很小。

我们再来看一些关于右值引用的其他问题:

我们知道,std::string("111111111111")本身是右值,但是右值引用本身(s1)是左值,因为只有右值引用本身处理成左值,才能实现移动构造和移动赋值,转移资源(右值不能转移资源)。这样的意思,是为了移动构造和移动赋值的转移资源的逻辑是自洽的。

我们来看一下C++11中其他地方用到右值引用的,

由于s1是左值,所以push_back不能调用移动拷贝,只能做深拷贝,

但是,如果想上图这样,将匿名对象放到push_back的参数中,就会调用移动构造。在push_back这类函数时,使用匿名对象就更好了,这样就不会设计到深拷贝,只需要移动构造,提高效率。可见,右值引用不仅在返回值有意义,也在参数值有意义

也可以这样,这样就会先发生隐式类型转换,将const char*转换为string,转换时会产生临时对象,是右值,临时对象具有常性,会调用移动构造。

最后一个问题,如果list里的值类型是int(内置类型)或者日期类(浅拷贝自定义类型),还会涉及到移动拷贝和移动构造吗?

不会涉及,上面效率提升,针对的是自定义类型的深拷贝的类,因为深拷贝的类才有转移资源的移动系列函数;对于内置类型和浅拷贝自定义类型,没有移动系列函数。

完美转发

模版中的&&万能转发

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; }//函数模版里面,这里叫做万能引用
template<typename T>
void PerfectForward(T&& t)
{Func(t);
}void PerfectForward(int& t)
{Fun(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;
}

函数模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。

但是,我们看程序运行的结果,

因为在函数参数t接收后,后续都退化成了左值,所以都会调用Fun(int& x)和Fun(const int& x)这两个函数,但是这不是我们想要的结果,我们希望能够在传递的过程中保持它的左值或者右值的属性,那么就需要用到完美转发

std::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; }//函数模版里面,这里叫做万能引用
//实参传左值,就推成左值引用,实参传右值,就推成右值引用
template<typename T>
void PerfectForward(T&& t)
{//std::forward<T>(t)在传参的过程中保持了t的原生类型属性Fun(std::forward<T>(t));
}void PerfectForward(int& t)
{Fun(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;
}

什么时候用完美转发呢?在函数模版里,想要达到传什么就保持它的属性就用完美转发。这是一道常见的面试题。

lambda表达式

在C++98中,如果想要对一个自定义类型进行排序,需要用到仿函数,用户自定义排序的比较规则,比如,有这样一个自定义类型,

struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};

我们有存储Goods类型的vector,想要对这个vector按照价格排序,既可以排升序,又可以排降序,可以使用algorithm这个头文件中的sort算法,但是如果需要对商品按照它的某一项属性排序,如价格,就需要自己写一个类来定义仿函数,

struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};struct ComparePriceLess
{bool operator()(const Goods& g1, const Goods& g2){return g1._price < g2._price;}
};struct ComparePriceGreater
{bool operator()(const Goods& g1, const Goods& g2){return g1._price > g2._price;}
};int main()
{vector<Goods> v = { {"苹果",2.3,2},{"香蕉",3.2,4}, {"西瓜",0.9,3} };sort(v.begin(), v.end(), ComparePriceGreater());return 0;
}

但是,上面的方法有点复杂,每次都要自己写一个类,所以C++为了解决这样的问题,引入了lambda表达式

lambda表达式实际是一个匿名函数,lambda表达式书写格式:

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

各部分说明:

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

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

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

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

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

注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

对捕捉列表和mutable的进一步说明:

为了解决传值捕捉在函数内部改变不会影响外面的问题,引入了引用捕捉,引用捕捉是在捕捉列表变量前加&(这其实和取地址&有些冲突,但是在lambda表达式捕捉列表中我们认为&是引用捕捉) :  

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

[var]:表示传值捕捉变量var

[=]:表示传值方式捕捉父作用域的所有变量

[&var]:表示引用方式捕捉变量var

[&]:表示引用方式捕捉父作用域的所有变量

[this]:表示传值方式捕捉当前的this指针

 除了上面传值和传引用捕捉方式以外,还有混合捕捉(一部分传值捕捉,一部分传引用捕捉):

因此,我们可以写lambda表达式作为一个匿名函数传给sort进行排序:

int main()
{vector<Goods> v = { {"苹果",2.3,2},{"香蕉",3.2,4}, {"西瓜",0.9,3} };auto priceLess = [](const Goods g1, const Goods g2)->bool {return g1._price < g2._price; };sort(v.begin(), v.end(), priceLess);//也可以直接传lambda表达式sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._price < g2._price;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._price > g2._price;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._evaluate < g2._evaluate; });sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._evaluate > g2._evaluate;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._name < g2._name;});sort(v.begin(), v.end(), [](const Goods g1, const Goods g2){return g1._name > g2._name;});return 0;
}

其中,priceLess的类型,我不知你不知只有编译器知,见下图,是随机生成的类型。

其实,lambda表达式就是仿函数,lambda编译时,会生成对应的仿函数,上面的随机名字就是仿函数类的名称。

实际上,lambda表达式和范围for很类似,范围for替代成了迭代器,lambda替代成了仿函数

新的类功能

在前面我们学习了右值引用,其实,右值引用的特点是和左值引用的特点进行了区分,是左值就匹配左值引用,是右值就匹配右值引用。移动语义是,当右值匹配到右值引用的时候,会调用移动构造和移动拷贝。它们针对的是深拷贝的自定义类型对象,如string、vector、list等,可以转移资源,提高效率。

原来的C++类中,有6个默认成员函数,构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载,前4个最重要,后两个用处不大。

C++11 新增了两个:移动构造函数和移动赋值运算符重载。

对于移动构造函数和移动赋值运算符重载,有一些需要注意的:

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

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

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

 我们调试以下代码可以得到验证:

class Person
{
public:Person(const char* name = "张三", int age = 18):_name(name), _age(age){}/*~Person(){}*/
private:ghs::string _name;int _age;
};int main()
{Person s1;//默认拷贝构造Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}

 强制生成默认函数的关键字default

C++可以更好地控制要使用的默认函数。假设要使用某个默认的函数,但是因为某些原因这个函数没有默认生成。比如:当我们提供了拷贝构造,就不会生成移动构造,那么就可以使用default关键字指定移动构造生成。

class Person
{
public:Person(const char* name = "张三", int age = 18):_name(name), _age(age){}//强制生成Person(Person& p) = default;Person& operator=(Person& p) = default;Person(Person&& p) = default;Person& operator=(Person&& p) = default;~Person(){}
private:ghs::string _name;int _age;
};int main()
{Person s1;//默认拷贝构造Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}

禁止生成默认函数的关键字delete:

 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

继承和多态中的final与override关键字
这个我们在继承和多态章节已经进行了详细学习。

可变参数模版 

在C++11之前,模版中的参数数量是固定的,但是在C++11中引入了可变参数模版,这无疑是一大进步。

我们先来看一个基本可变参数的函数模版:

template <class ...Args>
void ShowList(Args... args)
{}

其中,Args是一个模版参数包,代表0~N个类型,args是根据模版参数包定义的一个形参参数包。上面的参数Args前面有省略号,所以它是一个可变模版参数,我们把带省略号的参数成为参数包,包含0~N个模版参数。但是我们无法直接获取参数包args中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数。由于语法不支持args[i]的方式获取可变参数,所以我们用一些方法来获取参数包的每个值。

递归函数展开参数包

void _CppPrintf()
{cout << endl;
}
template<class T,class ... Args>
void _CppPrintf(const T& val, Args... args)
{cout << val << endl;_CppPrintf(args...);
}
template <class ... Args>
void CppPrintf(Args... args)
{_CppPrintf(args...);
}
int main()
{CppPrintf(1, 'A', std::string("sort"));return 0;
}

其编译时递归推导过程如下: 

还有另外一个奇葩的推导方式

STL中的emplace相关接口函数

可以看出,emplace_back对深拷贝类型有一定的优化,但是不那么明显,效率没有提升很多

其实,emplace_back对需要浅拷贝的效率更高,因为push_back浅拷贝类型,只能调用拷贝构造,即需要一次构造+一次拷贝构造,而emplace_back只需要调用一次构造就行。

总之,emplace_back可以直接构造,而无需调用拷贝构造!

当然,emplace_back除了上面的用多个参数进行构造,也可以用单参数构造,

因此,当使用emplace时,实参建议的选择顺序是:参数包 > 右值 > 左值。

包装器

function包装器

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

ret = func(x);

上面的func可能是什么呢?可能是函数名、函数指针、仿函数或者lambda表达式,这些都是可调用对象

但是,它们都多少有些问题:

函数指针       ->    类型定义复杂

仿函数对象   ->    要定义一个类,用的时候有点麻烦,不适合类型统一

lambda         ->    没有类型概念     

template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数名cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lamber表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}

运行以上程序,发现useF函数模版实例化了三份,这看起来有点麻烦,为了解决以上问题,引出了包装器。

std::function在头文件<functional>

//类模版原型
template <class Ret, class... Args>
class function<Ret(Args...)>;

模版参数说明:

Ret被调用函数返回值

Args被调用函数的形参

实际上,function的底层是仿函数,在调用时会调用operator()。

int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};//不是定义可调用对象,而是包装可调用对象
int main()
{//空对象function<int(int, int)> fc1;//包装函数指针function<int(int, int)> fc2 = f;//包装仿函数对象function<int(int, int)> fc3 = Functor();//包装lambdafunction<int(int, int)> fc4 = [](int x, int y) {return x + y; };cout << fc2(1, 2) << endl;//fc2本质是调用了operator()cout << fc2.operator()(1, 2) << endl;cout << fc3(1, 2) << endl;cout << fc4(1, 2) << endl;return 0;
}

那我们不禁要问,为什么要给函数指针、仿函数、lambda外面套一个壳再使用呢,它们本身也是可以调用的啊。

包装器的一些玩法:逆波兰表达式求解

class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;//命令->动作(函数)map<string,function<int(int,int)>> m={{"+",[](int x,int y){return x+y;}},{"-",[](int x,int y){return x-y;}},{"*",[](int x,int y){return x*y;}},{"/",[](int x,int y){return x/y;}},};for(auto& e : tokens){if(m.count(e)) {function<int(int,int)> f = m[e];//操作符运算int right = st.top();st.pop();int left = st.top();st.pop();st.push(f(left,right));}else{//操作数入栈st.push(stoi(e));}} return st.top();   }
};

另外,如果我们想要包装成员函数指针,需要&类型::函数名这样调用。成员函数指针又分静态成员函数和非静态成员函数:

int main()
{//成员函数的函数指针 &类型::函数名//包装静态成员函数function<int(int, int)> f1 = &Plus::plusi;cout << f1(1, 2) << endl;//包装非静态成员函数,不能直接Plus::plusd,需要在前面加取地址符&,静态函数前可加可不加&/*function<double(double, double)> f2 = &Plus::plusd;cout << f2(1.1, 2.2) << endl;*///包装器的参数要和成员函数的参数一致,成员函数第一个参数是一个隐含的this指针function<double(Plus*, double, double)> f3 = &Plus::plusd;Plus plus;cout << f3(&plus,1.1, 2.2) << endl;//但是,上面包装非静态的成员函数有点麻烦,还需要定义一个类对象//是通过指针&plus或者对象Plus()去调用plusd,所以这里传指针和对象都可以function<double(Plus, double, double)> f4 = &Plus::plusd;cout << f4(Plus(), 1.1, 2.2) << endl;return 0;
}

bind

std::bind是一个函数模版,用于调整可调用对象的参数个数或者顺序

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

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

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

fn是要绑定的函数,args是要对应的参数,包含像_1、_2这样的名字,这些名字是占位符,_1为newCallable中第一个参数,_2为newCallable中第二个参数,依次类推。Ret是返回值的类型,我们可以显示实例化bind,以此来控制返回值的类型。

但是,调整顺序的意义不大,了解即可。

除了调整顺序以外,我们还可以调整参数个数:

class Sub
{
public:int sub(int a, int b){return a - b;}
};int main()
{//绑定,调整参数个数,把第一个参数用Sub()绑定死auto f4 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);cout << f4(10, 5);cout << endl;auto f5 = bind(&Sub::sub, &sub, placeholders::_1, placeholders::_2);cout << f5(10, 5);return 0;
}

这样,我们得到绑定后的对象为f4,调用f4时,只需要传两个未绑定的参数a和b即可。

再来看一个例子:

void fx(const string& s, int x, int y)
{cout << s << " -> [血量:" << x << "  蓝:" << y <<"]" << endl;
}
int main()
{fx("赵云", 80, 46);fx("赵云", 78, 34);fx("赵云", 54, 13);return 0;
}

我们可以这样调fx,这很正常,但是调fx时每次都要传“赵云”这个参数,其实,我们可以绑定第一个参数一直是“赵云”,这样只需要输入参数x和y即可:

int main()
{auto f1 = bind(fx1, "赵云", placeholders::_1, placeholders::_2);f1(100, 89);f1(98, 76);return 0;
}

除了绑定第一个参数,我们还可以绑定第二个参数,比如在游戏中开挂,无论使用哪个角色,血量一直保持在100:

int main()
{auto f2 = bind(fx1, placeholders::_1, 100, placeholders::_2);f2("孙尚香", 46);f2("关羽", 93);return 0;
}

我们需要记住的是,_1代表第一个实参,_2代表第二个实参。

实际上,bind的返回值是一个类,里面重载了operator(),实际上会调用仿函数。

bind的返回除了传给auto,也可以传给function(因为bind的返回值是一个类,里面重载了仿函数),

线程库

我们之前了解的多线程问题,都是和平台相关的,比如windows和linux下都有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在多线程编程时不需要依赖第三方库。要使用标准库中的线程,必须包含<thread>头文件。

注意:

1.当创建线程对象后,如果使用无参构造,没有提供线程函数,该对象没有对应任何线程。

get_id()的返回值类型是id类型,id类型为std::thread命名空间下疯转的一个类。

2.当创建一个线程对象后,并且给线程关联函数,该线程就会被启动,与主线程一起运行。线程函数一般有3种提供方式:

  • 函数指针
  • lambda表达式
  • 函数对象

函数指针:

void print(int n, int k)
{for (int i = k; i < n; i++){std::cout << i << " ";}std::cout << std::endl;
}int main()
{std::thread t1(print, 100, 0);std::thread t2(print, 200, 100);std::cout << t1.get_id() << std::endl;std::cout << t2.get_id() << std::endl;t1.join();t2.join();std::cout << std::this_thread::get_id << std::endl;return 0;
}

lambda表达式:

int main()
{int x = 0;std::mutex mtx;std::thread t1([&] {mtx.lock();for (int i = 0; i < 10000; i++){++x;}mtx.unlock();});std::thread t2([&]{mtx.lock();for (int i = 0; i < 10000; i++){++x;}mtx.unlock();});t1.join();t2.join();std::cout << x << std::endl;return 0;
}

可以使用this_thread类中的get_id来获取主线程id。

3.线程不支持拷贝,不允许拷贝构造和赋值,但是支持移动构造和移动赋值。

线程函数参数

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

如果想要通过形参改变外部实参,必须借助std::ref()函数或者传入指针。

lock_guard和unique_lock

在多线程中,为了保证线程安全,需要通过锁的方式控制。 

 如果把锁放到for循环里面,也是线程安全的,但是这样会导致线程之间频繁切换,效率低。

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

那如果在func中前半部分希望加锁,而后半部分不希望加锁,只需要用{}把前半部分括住,定义一个局部域,LockGuard的生命周期就在这个{}局部域了。

而unique_lock和lock_guard类似,只不过功能更丰富一些,支持手动加锁解锁。

原子性操作库

传统解决线程安全的方法是对共享资源进行加锁保护,虽然加锁可以解决问题,但是加锁的缺陷是,只要要有一个线程在对sum++,其他线程就会被阻塞,影响效率,而且可能造成死锁。

为此,C++11中引入了原子操作,需要包含<atomic>库。

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

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

两个线程交替打印

要使两个线程交替打印,需要使用到条件变量: 

条件变量condition_variable用于进行进程之间的互相通知。

为了使得线程交替打印,要保证线程1先执行,线程2后执行(哪怕把线程2放到前面)。

这里利用了wait,

由于条件变量wait不是线程安全的,因此要给wait传互斥锁,调用wait的线程被阻塞,直到被notified,wait的作用是使进程间做到同步。在wait阻塞进程时,当前进程会先把锁解掉,允许在这个锁上阻塞的线程继续走。当这个进程被唤醒后,这个进程会解阻塞,并获取到这个锁。

notify_one会唤醒在这个条件变量上等待的一个线程,如果没有现成在上面等待,什么都不做,如果有多个线程在等待,会选择其中任意一个。

下面是两个线程交替打印的代码:

{std::mutex mtx;std::condition_variable c;int n = 100;bool flag = true;std::thread t1([&]() {int i = 0;while (i < n){std::unique_lock<std::mutex> lock(mtx);//flag=false t1就阻塞//flag=true t1就不会阻塞while (!flag){c.wait(lock);}std::cout << i << std::endl;flag = false;i += 2; // 偶数c.notify_one();}});std::thread t2([&]() {int j = 1;while (j < n){std::unique_lock<std::mutex> lock(mtx);//只要flag==true,t2就阻塞//只要flag==false,t2就不会阻塞while (flag)c.wait(lock);std::cout << j << std::endl;j += 2; // 奇数flag = true;c.notify_one();}});t1.join();t2.join();return 0;
}

相关文章:

【C++】C++11

目录 C11简介 统一的列表初始化 {}初始化 std::initializer_list 声明 auto decltype nullptr 范围for循环 智能指针 STL中的一些变化 右值引用和移动语义 左值引用和右值引用 右值引用的意义 完美转发 lambda表达式 新的类功能 可变参数模版 包装器 func…...

k8sollama部署deepseek-R1模型,内网无坑

这是目录 linux下载ollama模型文件下载到本地,打包迁移到k8s等无网络环境使用下载打包ollama镜像非k8s环境使用k8s部署访问方式非ollama运行deepseek模型linux下载ollama 下载后可存放其他服务器 curl -L https://ollama.com/download/ollama-linux-amd64.tgz -o ollama-linu…...

mysql8 C++源码中创建表函数,表字段最大数量限制,表行最大存储限制

在 MySQL 8 的 C 源码中&#xff0c;表的最大字段数量限制体现在 MAX_FIELDS 宏定义中。这个宏定义了表中可以拥有的最大字段数量。 代码中的体现 在 mysql_prepare_create_table 函数中&#xff0c;有以下代码段检查表的字段数量是否超过最大限制&#xff1a; cpp if (alt…...

胜任力冰山模型:深入探索职业能力的多维结构

目录 1、序言 2、什么是胜任力&#xff1f; 3、任职资格和胜任力的区别 4、胜任力冰山模型&#xff1a;职场能力的多维展现 4.1、冰山水面上的部分 4.2、冰山水面下的部分 4.3、深层的个人特质与价值观 5、如何平衡任职资格与胜任能力 6、结语 1、序言 在快速发展的I…...

什么是三层交换技术?与二层有什么区别?

什么是三层交换技术&#xff1f;让你的网络飞起来&#xff01; 一. 什么是三层交换技术&#xff1f;二. 工作原理三. 优点四. 应用场景五. 总结 前言 点个免费的赞和关注&#xff0c;有错误的地方请指出&#xff0c;看个人主页有惊喜。 作者&#xff1a;神的孩子都在歌唱 大家好…...

Linux+Docer 容器化部署之 Shell 语法入门篇 【Shell 替代】

&#x1f380;&#x1f380;Shell语法入门篇 系列篇 &#x1f380;&#x1f380; LinuxDocer 容器化部署之 Shell 语法入门篇 【准备阶段】LinuxDocer 容器化部署之 Shell 语法入门篇 【Shell变量】LinuxDocer 容器化部署之 Shell 语法入门篇 【Shell数组与函数】LinuxDocer 容…...

DeepSeek LLM(初代)阅读报告

概况 这个是deepseek发布的第一版模型对应的技术报告&#xff0c;模型发布于23年11月&#xff0c;本报告发布于24年1月。 模型有7B和67B两个版本。 虽然本报告中还没有用上后面V2/V3和R1中的关键技术例如MLA、MTP、GRPO&#xff0c;但是报告中已经指明了MoE、强化学习等未来…...

JAVA异步的TCP 通讯-服务端

一、服务端代码示例 import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousServerSocketChannel; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.Completion…...

高效协同,Tita 助力项目管理场景革新

在当今快节奏、高度竞争的商业环境中&#xff0c;企业面临着前所未有的挑战&#xff1a;如何在有限资源下迅速响应市场变化&#xff0c;确保多个项目的高效执行并达成战略目标&#xff1f;答案就在于优化项目集程管理。而在这个过程中&#xff0c;Tita项目管理产品以其独特的优…...

【AIGC魔童】DeepSeek v3提示词Prompt书写技巧

【AIGC魔童】DeepSeek v3提示词Prompt书写技巧 &#xff08;1&#xff09;基础通用公式&#xff08;适用80%场景&#xff09;&#xff08;2&#xff09;问题解决公式&#xff08;决策支持&#xff09;&#xff08;3&#xff09;创意生成公式&#xff08;4&#xff09;学习提升公…...

Vue | 透传 Attributes(非 prop 的 attribute )

文章目录 引言I Attribute 继承II 禁用 attribute 继承禁用 attribute 继承的常见场景通过将 inheritAttrs 选项设置为 false从 3.3 开始可在 `<script setup>` 中使用defineOptions例子引言 “透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emi…...

启明星辰发布MAF大模型应用防火墙产品,提升DeepSeek类企业用户安全

2月7日&#xff0c;启明星辰面向DeepSeek等企业级大模型业务服务者提供的安全防护产品——天清MAF&#xff08;Model Application Firewall&#xff09;大模型应用防火墙产品正式发布。 一个新赛道将被开启…… DeepSeek的低成本引爆赛道规模 随着DeepSeek成为当前最热的现象级…...

Vuex 解析:从 Vue 2 到 Vue 3 的演变与最佳实践

Vuex 是 Vue.js 中的状态管理模式&#xff0c;广泛应用于 Vue 2 和 Vue 3 中&#xff0c;其内部实现存在一些差异。 1. 什么是 Vuex &#xff1f; Vuex 是 Vue.js 官方提供的状态管理库&#xff0c;用于集中管理应用的所有组件的状态。主要是通过一种集中化的方式来管理共享状…...

一文解释nn、nn.Module与nn.functional的用法与区别

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;零基础入门PyTorch框架_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 …...

日志统计(acWing,蓝桥杯)

题目&#xff1a; 1238. 日志统计 题目 提交记录 讨论 题解 视频讲解 小明维护着一个程序员论坛。现在他收集了一份”点赞”日志&#xff0c;日志共有 NN 行。 其中每一行的格式是&#xff1a; ts id 表示在 tsts 时刻编号 idid 的帖子收到一个”赞”。 现在小明想…...

3个DeepSeek隐藏玩法

大家最近是不是都被DeepSeek-R1刷屏了 这款号称“中国版O1”的模型&#xff0c;不仅在数学和编程领域表现出色&#xff0c;中文写作能力也很强。 最重要的是&#xff0c;它在理解提示词方面有了很大突破&#xff0c;只要你能打字&#xff0c;它就能理解你的意思。 不过&…...

部署LLM模型到云端

文章目录 1 ECS 云服务器部署2 函数计算FC3 人工智能平台PAI-EAS4 大模型服务平台百炼压测实验结果显示,由于本地设备算力有限,本地部署的模型服务无法满足低延迟和高并发的需求。针对这类线上业务,可以考虑云端部署。 下面先来看看本地部署和云端部署的特点对比。 由上可…...

Python连接不同数据库的总结

Python连接不同数据库的总结 在数据驱动的现代应用开发中&#xff0c;Python凭借其丰富的库和强大的生态系统&#xff0c;成为连接各种数据库的理想编程语言。本文将深入探讨Python连接不同类型数据库的方法、常用库以及关键注意事项。 一、连接MySQL数据库 MySQL是广泛使用…...

web直播弹幕抓取分析 signature

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 前言 最近遇到太多难点了卡了很久&am…...

Linux ftrace 内核跟踪入门

文章目录 ftrace介绍开启ftraceftrace使用ftrace跟踪指定内核函数ftrace跟踪指定pid ftrace原理ftrace与stracetrace-cmd 工具KernelShark参考 ftrace介绍 Ftrace is an internal tracer designed to help out developers and designers of systems to find what is going on i…...

web vue 项目 Docker化部署

Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段&#xff1a; 构建阶段&#xff08;Build Stage&#xff09;&#xff1a…...

什么是库存周转?如何用进销存系统提高库存周转率?

你可能听说过这样一句话&#xff1a; “利润不是赚出来的&#xff0c;是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业&#xff0c;很多企业看着销售不错&#xff0c;账上却没钱、利润也不见了&#xff0c;一翻库存才发现&#xff1a; 一堆卖不动的旧货…...

【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表

1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...

学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”

2025年#高考 将在近日拉开帷幕&#xff0c;#AI 监考一度冲上热搜。当AI深度融入高考&#xff0c;#时间同步 不再是辅助功能&#xff0c;而是决定AI监考系统成败的“生命线”。 AI亮相2025高考&#xff0c;40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕&#xff0c;江西、…...

《C++ 模板》

目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板&#xff0c;就像一个模具&#xff0c;里面可以将不同类型的材料做成一个形状&#xff0c;其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式&#xff1a;templa…...

MySQL 知识小结(一)

一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库&#xff0c;分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷&#xff0c;但是文件存放起来数据比较冗余&#xff0c;用二进制能够更好管理咱们M…...

从面试角度回答Android中ContentProvider启动原理

Android中ContentProvider原理的面试角度解析&#xff0c;分为​​已启动​​和​​未启动​​两种场景&#xff1a; 一、ContentProvider已启动的情况 1. ​​核心流程​​ ​​触发条件​​&#xff1a;当其他组件&#xff08;如Activity、Service&#xff09;通过ContentR…...

云原生安全实战:API网关Envoy的鉴权与限流详解

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关 作为微服务架构的统一入口&#xff0c;负责路由转发、安全控制、流量管理等核心功能。 2. Envoy 由Lyft开源的高性能云原生…...

Win系统权限提升篇UAC绕过DLL劫持未引号路径可控服务全检项目

应用场景&#xff1a; 1、常规某个机器被钓鱼后门攻击后&#xff0c;我们需要做更高权限操作或权限维持等。 2、内网域中某个机器被钓鱼后门攻击后&#xff0c;我们需要对后续内网域做安全测试。 #Win10&11-BypassUAC自动提权-MSF&UACME 为了远程执行目标的exe或者b…...

[KCTF]CORE CrackMe v2.0

这个Reverse比较古老&#xff0c;已经有20多年了&#xff0c;但难度确实不小。 先查壳 upx压缩壳&#xff0c;0.72&#xff0c;废弃版本&#xff0c;工具无法解压。 反正不用IDA进行调试&#xff0c;直接x32dbg中&#xff0c;dump内存&#xff0c;保存后拖入IDA。 这里说一下…...