目录
一. 统一的列表初始化 {} 适用于各种STL容器
二. 类型推导 auto 和 decltype的出现
三. 右值引用移动语义 (特别重要的新特性)
四. 万能引用 + 完美转发
五. 可变参数模板 (参数包)
六. emplace_back 的出现和对比分析 push_back接口 emplace_back 是 结合这 可变模板参数出现的
C++开发交流+赀料君羊:666064665
C++开发交流+赀料君羊:666064665
七. Lambda表达式
八. 包装器 (适配器) (function包装器)
九. 线程库
十. 原子操作
十一. 总结本章
一. 统一的列表初始化 {} 适用于各种STL容器
- C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自 定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
- 创建对象时也可以使用列表初始化方式调用构造函数初始化
struct Date {
int _year;
int _month;
int _day;
};
int main() {
int a{ 2 }; //支持使用{}的统一初始化了
vector <int > intv{ 1, 2, 3, 5 };
vector <Date > datev{ { 2001, 10, 9 }, Date{ 2001, 10, 21 } };
//上述的这些方式都是支持的了 {} 其实是调用的构造函数
return 0;
}
- 为什么可以支持 {} 这种方式来调用各种容器的构造函数?
- 本质原因: 支持了 std::initializer_list 作为参数的构造函数的产生
在容器中构造函数中的出现例子:
- 从此以后终于知道为啥引入 initializer_list头文件之后就可以进行各种容器的{} 初始化形式了.
光光知道还不够, 我们一定要去看看它的底层实现是怎样的, 如下
namespace tyj {
template <class T>
class vector {
typedef T* iterator;
typedef const T* const_iterator;
public:
//如何进行一个初始化, 范围形式的初始化
vector(initializer_list<T>& l) {
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator sit = _start;
//然后就是范围形式的构造了
//如下是方式1: 基于范围的实现
/*for (auto& e : l) {
*sit++ = e;
}*/
//然后是第二种形式, 使用迭代器进行赋值, 其实也就是上述范围的赋值的底层
typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end()) {
*sit++ = *lit++;
}
}
//针对这个 operator = 赋值运算符的重载 还是复用上述的构造函数
vector<T>& operator=(initializer_list<T> l) {
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
int main() {
//测试上述的东西:
//断点测试, 进去查看其中的内存即可
tyj::vector<int> intv = initializer_list<int>{ 1, 2, 3, 4, 5 };
return 0;
}
二. 类型推导 auto 和 decltype的出现
- auto 关键字的作用在编译阶段对于=右边的对象进行自动的类型推导
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
既然有了auto 可以自动推导=右边的类型, 为啥需要decltype呢????
decltype的出现是为了补齐auto 不支持对于表达式的类型推导的缺陷的, 经常适用于后置返回类型的推导. 使用形式如下: 如下包含了万能引用, 完美转发lambda表达式等等知识点, 后序会一一讲解清除
template<class T, class U>
auto Add(T&& t, U&& u)
->decltype(std::forward<T>(t) +std::forward<T>(u)) {
return std::forward<T>(t) +std::forward<T>(u);
}
int main() {
auto func = [](int a, double b)->decltype(a + b){ return a + b; };
cout << Add(2, 5);
while (1);
return 0;
}
三. 右值引用移动语义 (特别重要的新特性)
故名思意, 对左值的引用就是左值引用, 对于右值的引用就是右值引用
定义左值和右值的区别, 可否进行取地址, 可以取地址的就是左值, 不可以取地址的就是右值
定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址, 所以本质还是左值.
int main() {
int a = 10;
int& b = a; //此处是左值引用
int&& c = 2; //此处是右值引用
const int& d = 100; //突然发现此处也是可行的?
const int& e = a;
int&& h = std::move(a); //std::move 作用 将左值引用转换为右值引用
//先引出结论: const 左值引用既可以引用左值也可以引用右值
//右值引用 就只能引用右值不可以引用左值
return 0;
}
- const 左值引用既可以引用左值也可以引用右值.
- std::move() 方法可以将左值转换为右值
注意点: 我们不可以对于右值进行一个取地址, 但是一旦给右值取别名之后, 会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量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;
}
左值引用作为参数和返回值完美解决了深拷贝的问题, 但是存在一些情况像是局部对象的返回, 就没办法以左值引用返回, 这个时候需要进行深拷贝, 于是右值引用的出现使得这个问题的解决成为可能, 有没有什么办法可以将局部对象作为返回值的这个深拷贝也优化掉?????
首先移动构造的本质: 本质是一种资源窃取, 资源转移…..
比如 return str; str 如果是一个局部对象的话, 它出不了函数, 一旦函数调用结束, 就会随着栈帧一起释放掉, 但是它的底层存在 char* _str 这样一个字符串数组的成员. 要是一同释放掉着实浪费, 我们是否可以将其利用起来??????
移动构造的本质就是 将 即将返回的局部对象的所有底层的堆区资源进行转移 窃取, 反正函数调用结束你即将消亡, 然鹅我拷贝构造做深拷贝正好需要的也是这个, 于是将这个即将消亡的 str 的 底层的 堆区 资源转移,进行移动构造出新的对象
void swap(string& s) {
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
} //提供swap方法方便转移资源
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0) {
swap(s); //直接通过交换, 转移财产, 我的全是空, 和你换
//反正你即将死亡, 不如将你的资源换给我助我快速构造加以利用
std::cout << "string(string&& s) ---移动构造" << std::endl;
}
string& operator= (string&& s) {
swap(s);
std::cout << "string& operator= (string&& s) ---移动赋值" << std::endl;
return *this;
}
tyj::string to_string(int val) {
tyj::string s;
bool flag = 0;
if (val < 0) {
val *= -1;
flag = 1;
}
while (val) {
s.push_back(val % 10);
val /= 10;
}
if (flag) s.push_back('-');
std::reverse(s.begin(), s.end());
return s; // 返回即将消亡的局部对象, 如何优化掉这个深拷贝?
}
图解分析, 存在移动构造和不存在移动构造的区别:
移动构造相对于拷贝构造: 比较区别????
移动构造和拷贝构造本质都是构造一个对象: 只是两者采取的构造方式不一样, 拷贝构造的话如果是深拷贝, 也就是底层存在堆区数据, 存在指针, 就需要新开堆区空间, 且需要进行堆区数据的拷贝, 效率低… 移动构造, 我还是需要堆区空间存储数据, 但是我不自己新开辟, 我直接将拷贝对象的堆区资源转移过来成为我的即可.,…. 不需要new 空间 + 数据转移, 效率提高
注意: 移动构造 和 拷贝构造相比, 它的高效仅仅体现在深拷贝 上面, 如果不存在深拷贝. 仅仅只是栈区数据的拷贝, 两者效率是相同的
深拷贝: 存在堆区空间的拷贝…. 也就是存在底层存储数据的空间的拷贝
移动构造高效就高效在了这个底层存储数据空间的获取上面, 不是从新申请空间 + 拷贝数据的方式来获取的, 而是直接的获取对方的现有空间 + 数据
全部代码如下, 可以测试上述推论: 分别测试存在移动语义和不存在的情况看看调用如何??
namespace tyj {
class string {
typedef char* iterator;
typedef const char* const_iterator;
public:
const_iterator begin() const {
return _str;
}
const_iterator end() const {
return _str + _size;
}
iterator begin() {
return _str;
}
iterator end() {
return _str + _size;
}
string(const char* s = "")
: _size(strlen(s))
, _capacity(_size)
, _str(new char[_size]) {
//std::cout << "string(const char* s) ---构造对象" << std::endl;
}
//提供一个swap 函数 一切都是为了方便后序的资源转移拷贝构造等等复用代码
void swap(string& s) {
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0) {
//复用代码
string tmp(s._str);
swap(tmp);
std::cout << "string(const string& s) --- 深拷贝" << std::endl;
}
string& operator=(const string& s) {
//复用拷贝构造代码
string tmp(s);
swap(tmp);
std::cout << "string& operator=(string s) --- 深拷贝" << std::endl;
return *this;
}
//然后右值引用出现了, 出现了另外一种方式, 叫做移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0) {
swap(s);
std::cout << "string(string&& s) ---移动构造" << std::endl;
}
string& operator= (string&& s) {
swap(s);
std::cout << "string& operator= (string&& s) ---移动赋值" << std::endl;
return *this;
}
void reserve(size_t n) { //扩容
if (n <= _capacity) return;
char* pTemp = new char[n];
_capacity = n;
memcpy(pTemp, _str, _size + 1); //拷贝_size + 1个过去, 结束字符也拷贝过去
delete[]_str;
_str = pTemp;
}
void push_back(char c) {
if (_size == _capacity) {
reserve(_capacity > 0 ? (_capacity << 1) : 8);
}
_str[_size++] = c; //放入数据
_str[_size] = 0; //后序制结束
}
private:
size_t _size;
size_t _capacity;
char* _str;
};
//来一个函数, 方便测试右值引用使用案例
tyj::string to_string(int val) {
tyj::string s;
bool flag = 0;
if (val < 0) {
val *= -1;
flag = 1;
}
while (val) {
s.push_back(val % 10);
val /= 10;
}
if (flag) s.push_back('-');
std::reverse(s.begin(), s.end());
return s;
}
}
int main() {
tyj::string s1 = tyj::to_string(123456);
tyj::string s2;
s2 = tyj::to_string(23456);
return 0;
}
- STL 容器中全部都是怎加了移动构造和移动赋值的: 如下图:
就连 STL的 push_back 等等这种接口上都是同样增添了右值引用版本的:
四. 万能引用 + 完美转发
万能引用: 就是 既可以引用左值 也可以引用右值 模板中的&& 万能引用
为了引出完美转发 首先先看如下的一段代码
void f(int& a) {
std::cout << "左值引用" << endl;
}
void f(const int& a) {
std::cout << "const 左值引用" << endl;
}
void f(int&& a) {
std::cout << "右值引用" << endl;
}
void f(const int&& a) {
std::cout << "const 右值引用" << endl;
}
template <class T>
void PerfectForward(T&& t) {
f(t);
}
int main() {
PerfectForward(2);
int a = 10;
PerfectForward(a);
PerfectForward(move(a));
return 0;
}
结果不尽人意, 全部都是左值引用????? 为啥 仅仅经过了一次参数 t 接收之后t 退化了 退化成了左值?????
前面我们学过 右值一旦 被引用之后就可以取地址了, 其实也就自然退化为左值了, 这个时候需要调用 std::forward<类型>() 进行完美转发, 保持之前的类型属性不退化
void PerfectForward(T&& t) {
//先尝试一下不是完美转发
//f(t);
//然后进行完美转发
f(std::forward<T>(t)); //转发之后效果就恢复正常了
}
- 万能引用 + 完美转发, 在 过程中保持住 右值属性不退化
- 接下来就是完美转发在实际案例中的使用场景
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<tyj::string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
while (1);
return 0;
}
//上述所有的 传入 && 右值引用作为参数的地方后序进一步传参全部需要使用forward<>()完美转发
//完美转发保持之前原有的类型属性不变, 如果不使用完美转发效果就是后序全部变成左值引用退化了
//可以取地址的就是左值了, 不可以取地址的才是右值, 右值一旦被变量接收其实也就退化成左值了
//如果想要继续保持右值的属性就需要完美转发
五. 可变参数模板 (参数包)
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改 进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
简单的理解可以理解为一个参数包, 可以支持传入数量不固定的参数, 而且还是模板, 使用起来更加的灵活
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
模板参数包的简单使用. 第一种解包方式, 递归解包
//设置递归终点, 当参数包解包完全, 适配空包
void ShowList() {
cout << endl;
}
//递归形式调用解包, 每一次解出一个参数
template<class T, class ...Args >
void ShowList(T& val, Args... args) {
cout << val << endl;
ShowList(args...);
}
int main() {
ShowList("dsadsa", 2, 5, 6, "edsad");
return 0;
}
- 解包方式2: 利用数组结合 {} 初始化的方式 进行解包
template<class T>
int PrintArg(T& val) {
cout << val << endl;
return 0;
}
template<class ...Args>
void ShowList(Args... args) {
int arr[] = { PrintArg(args)... };
}
int main() {
ShowList(1, 43, 6, 7, 8, "dfsads", "dsaw", 'a');
return 0;
}
六. emplace_back 的出现和对比分析 push_back接口 emplace_back 是 结合这 可变模板参数出现的
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的tyj::string,来试试
// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实效率也还好, 差别不算很大
std::list< std::pair<int, tyj::string> > mylist;
mylist.emplace_back(10, "sort");
mylist.emplace_back(make_pair(20, "sort"));
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort" });
return 0;
}
七. Lambda表达式
如下, 先进行一个简单的使用
struct Cars {
int carnum;
int price;
string name;
};
struct cmp {
bool operator()(Cars& c1, Cars& c2) {
return c1.price < c2.price;
}
};
bool cmp2(const Cars& c1, const Cars& c2) {
return c1.price < c2.price;
}
int main() {
auto fun = [] {}; //这个是最为简单的lambda表达式啥都不干
fun(); //调用, 使用方式像极了无参仿函数调用
auto add = [](double a, double b)->double { return a + b; };
cout << add(2.7, 3.7) << endl;
//然后是常用方式: 代替 仿函数使用
Cars cars[] = {
{100, 150000, "长城"}
, {55, 20000, "宝马摩托"}
, {455, 1000, "小电瓶"}
, {1000, 500, "自行车"}};
//形式1:
sort(cars, cars + sizeof(cars) / sizeof(Cars)
, [](Cars& c1, Cars& c2)->bool {return c1.price < c2.price; });
//形式2:
sort(cars, cars + sizeof(cars) / sizeof(Cars), cmp()); //传入匿名可调用对象
//形式3:
sort(cars, cars + sizeof(cars) / sizeof(Cars), cmp2); //传入函数指针
return 0;
}
上述我们利用了 lambda 表达式来代替了仿函数的使用, 这个也是lambda表达式的常用形式之一, 在很多时候都可以见到上述的这种使用场景… 但是有没有思考过为什么吗?
可调用对象(仿函数). 函数指针, lambda表达式 底层处理是否是类似 或者说甚至一样的???
其实 lambda表达式的底层处理就是完全处理成了仿函数的.
其实本质上 lambda的底层只是有很多的修饰, 如果把修饰看成是类名 本质就完全是类的 operator() 的重载 仿函数的底层处理形式了
lambda底层的时候也曾阐述 lambda 的底层处理就是按照仿函数, 当作类来进行处理的
针对对于自定义对象的 sort 还有一点点小小的技巧, 可以在我们需要 sort 的自定义类中去重载一下operator < 函数, 直接不需要在自己传入排序规则了…….
why??? 上述 重载一下 operator < 就可以达到重建排序规则 () 的效果
因为默认调用的其实就是 less<>{} 说白了就是 operator < 验证如下代码: 可以达到同上述一毛一样的效果.
struct Cars {
int carnum;
int price;
string name;
bool operator<(Cars& c) const {
return price < c.price;
}
};
int main() {
Cars cars[] = {
{100, 150000, "长城"}
, {55, 20000, "宝马摩托"}
, {455, 1000, "小电瓶"}
, {1000, 500, "自行车"} };
sort(cars, cars + sizeof(cars) / sizeof(Cars));
return 0;
}
八. 包装器 (适配器) (function包装器)
function 是 C++中的类模板, 也是一个包装器.
说到包装器, 首先就要思考 函数指针, 仿函数, Lambda表达式
上章就提到了 三者底层可能差不大多, 使用的情景也是各有雷同, 包装器 其实就可以算是将上述三者进行一个统一, 适配成一个东西 如下 : function 包装器可以实现对三者的统一包装
//函数指针
int add1(int a, int b) {
return a + b;
}
//仿函数
struct Add {
int operator()(int a, int b) {
return a + b;
}
int a, b;
};
int main() {
auto add2 = [](int a, int b){ return a + b; }; //当然可以在()->指定后置返回类型
//auto add2 = [](int a, int b)->int { return a + b; };
function<int(int, int) > func1 = add1; //函数名
function<int(int, int) > func2 = Add(); //函数对象
function<int(int, int) > func3 = add2; //lambda表达式
std::cout << func1(3, 5) << std::endl;
std::cout << func2(3, 5) << std::endl;
std::cout << func3(3, 5) << std::endl;
while (1);
return 0;
}
- 思考一个问题, 为什么需要 function 这个包装器, 直接使用三者不可以吗???
包装器的好处???? 统一了可调用对象的类型, 并且指定了参数和返回值类型
\1. 简化了函数指针这样的复杂指针的使用, 函数指针复杂难以理解
\2. 方便了作为参数时候的传入
\3. 仿函数是一个类名没有指定参数和返回值需要知道就需要去看这个operator () 重载获取
\4. lambda 在语法层, 看不到类型, 底层存在类型, 但是也是lambda_uuid, 也很难看
我觉得function 出现的 最最重要的原因就是有了一个确切的类型, 使用简单方便,
解决函数指针使用复杂的问题, 解决仿函数不能指定参数类型的问题, 要知道参数类型还要跑去看哪个 operator() 以及解决 lambda没有具体类型的问题.
未使用 function代码:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(int i=0;i<tokens.size();i++){
if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/"){
int rhs=st.top();
st.pop();
int lhs=st.top();
st.pop();
switch(tokens[i][0]){
case '+':st.push(lhs+rhs);break;
case '-':st.push(lhs-rhs);break;
case '*':st.push(lhs*rhs);break;
case '/':st.push(lhs/rhs);break;
}
continue;
}
st.push(stoi(tokens[i]));
}
//然后就是最后的结果了
return st.top();
}
};
使用 function 的代码:
class Solution {
//使用包装器进行复用??? 如何利用包装器??
//需要的是 function 和 对应的 op 对应起来...
//如何对应 使用的就是map 对应 map<string, function<int(int, int) > > opmap
//逆波兰表达式: 左 右 op
//遇到 op 的时候 说明前面的就是 l + r
//每一个运算结果需要重新入栈
public:
int evalRPN(vector<string>& tokens) {
stack<int > numst;
map<string, function<int(int, int) > > opmap = {
{"+", [](int a, int b)->int{ return a + b;}} ,
{"-", [](int a, int b)->int{ return a - b;}} ,
{"*", [](int a, int b)->int{ return a * b;}} ,
{"/", [](int a, int b)->int{ return a / b;}}
};
for (auto& e : tokens) {
if (e == "+" || e == "-" || e == "*" || e == "/") {
int r = numst.top(); numst.pop();
int l = numst.top(); numst.pop(); //先提取的是r 后 l
numst.push(opmap[e](l, r));
} else {
numst.push(stoi(e));
}
}
return numst.top();
}
};
九. 线程库
简单的用起来
int main() {
size_t n = 100;
thread t1([n]{
for (size_t i = 0; i < n; i += 2) {
cout << i << endl;
}
});
cout << t1.get_id() << endl; //线程id
thread t2([n]{
for (size_t i = 1; i < n; i += 2) {
cout << i << endl;
}
});
cout << t2.get_id() << endl; //线程id
t1.join();
t2.join(); //主线程阻塞等待子线程的死亡
while (1); //等待他们结束
return 0;
}
- 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的 状态。
- 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。 线程函数一般情况下可按照以下三种方式提供: 函数指针 lambda表达式 函数对象
void TFun() {
cout << "函数指针" << endl;
}
struct TF {
void operator()() {
cout << "函数对象" << endl;
}
};
int main() {
thread t1(TFun); //传入函数指针
TF tf;
thread t2(tf); //可调用对象(仿函数)
thread t3([]() {cout << "Lambda" << endl; });
t1.join();
t2.join();
t3.join(); //join 主线程挂起等待三个线程结束返回
return 0;
}
thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个 线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象
线程已经调用jion或者detach结束
线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在 线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。 (线程函数参数传入是以值拷贝的形式拷贝到栈空间中的, 所以既是是引用类型, 在线程中修改后外部实参也是无法修改的) 如何处理这个问题, 如下代码解释
std::ref(); 使用这个函数转换之后传入的线程函数参数才是真正的引用, 线程中改变, 外面也会改变
class Fun {
public:
void operator()() {
cout << "operator()" << endl;
}
};
void ThreadFunc1(int& x)
{
x += 10;
}
void ThreadFunc2(int* x)
{
*x += 10;
}
int main() {
//测试一波:
int a = 10;
thread t1(ThreadFunc1, a); //传入a
cout << a << endl; //?? a 是否改变?
//上述发现 a 没有改变
//如何可以使得传入的数据不需要进行拷贝, 而是原有数据?
如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t3(ThreadFunc1, std::ref(a));//才不会传入拷贝本
cout << a << endl;
thread t2(ThreadFunc2, &a); //这样看一看???
cout << a << endl; //a改变了, 因为这个传入的是地址进去
t1.join();
t2.join();
return 0;
}
十. 原子操作
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
//多线程对于共享数据的写操作带来的问题…
unsigned long num = 0L; //先定义全局的数据
void tf(size_t n) {
for (size_t i = 0; i < n; ++i) {
num += 1;
}
}
int main() {
thread t1(tf, 10000000);
thread t2(tf, 10000000);
t1.join();
t2.join();
cout << num << endl;
return 0;
}
- 发现一个大问题, 上述根本无法获取我们想要的结果甚至, 每一次运行结果都是不一样的
解决上述问题的方式1: 在 C++98 中采取的是加锁的方式实现避免函数的重入问题,
lock();
操作临界资源 (写入操作)
unlock();
unsigned long num = 0L; //先定义全局的数据
mutex mtx;
void tf(size_t n) {
for (size_t i = 0; i < n; ++i) {
mtx.lock();
num += 1;
mtx.unlock();
}
}
int main() {
thread t1(tf, 10000000);
thread t2(tf, 10000000);
t1.join();
t2.join();
cout << num << endl;
return 0;
}
加锁确实是可以解决上述的问题, 但是不停的解锁解锁, 效率会变得特别低, 时间消耗也会大大增加, 不停的加锁解锁, 虽然也解决了问题, 保护了临界资源.. 但是程序运行时延性大大增加, 而且对于锁控制不好还会死锁, 于是C++11 搞出来一个原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。
atomic_long num{ 0 };//定义全局的原子操作数据
void tf(size_t n) {
for (size_t i = 0; i < n; ++i) {
num += 1;
}
}
int main() {
thread t1(tf, 10000000);
thread t2(tf, 10000000);
t1.join();
t2.join();
cout << num << endl;
return 0;
}
有了原子操作数据, 确实针对这些数据的操作不再需要加锁保护了, 但是如果是一段代码段的原子操作, 就还是不得不使用锁来实现, 但是只要设计到锁就可能发生死锁, C++11为了预防死锁, C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。
unique_lock 和 lock_guard 都是对于锁的一种封装模板类, 实现对于锁的管理,
然后接下来是一个简单的实现
// RAII
namespace tyj
{
template<class Lock>
class lock_guard
{
public:
lock_guard(Lock& lock)
:_lock(lock)
{
_lock.lock();
cout << "加锁" << endl;
}
/*void lock()
{
_lock.lock();
}
void unlock()
{
_lock.unlock();
}*/
//对于lock_guard是没有上述操作的, 它仅仅只是做垃圾回收
//出作用域自动回收锁, 调用析构解锁
~lock_guard()
{
_lock.unlock();
cout << "解锁" << endl;
}
lock_guard(const lock_guard<Lock>& lock) = delete;
private:
Lock& _lock;
};
}
只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数 成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁 问题。
向比较 lock_guard 而言, unique_lock 提供了更多的操作
上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有 权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
条件变量引入以及条件变量对象和互斥对象配合实现一个案例
要求 : 支持两个线程交替打印,一个打印奇数,一个打印偶数
int main() {
mutex mtx; //定义锁 为后面的完成要求做准备
bool flag = 1;//flag = 1 打印偶数 flag = 0 打印奇数 配合 condion_variable使用
condition_variable _cond; //定义条件变量为后序相互耦合关联式打印埋伏笔
thread t1([&](){
unique_lock<mutex> _lock(mtx);
int i = 0;
while (i < 100) {
while (!flag) _cond.wait(_lock);//不满足flag 一直等
//说明满足了
cout << "i: " << i << endl;
flag = 0; //修改让t2去打印
_cond.notify_one();//唤醒t2打印奇数了
i += 2;
}
});
thread t2([&](){
unique_lock<mutex> _lock(mtx);
int j = 1;
while (j < 100) {
while (flag) _cond.wait(_lock);//满足flag 说明这个时候在打印偶数
//说明满足了
cout << "j: " << j << endl;
flag = 1; //修改让t2去打印
_cond.notify_one();//唤醒t2打印奇数了
j += 2;
}
});
t1.join();
t2. join();
return 0;
}
十一. 总结本章
首先本章介绍了初始化参数列表{} 进行统一的初始化
{} 的本质是一个类型 叫做 initializer_list , 支持使用{} 构造的本质是支持传入initializer_list做参数的构造函数
然后引入了右值引用, 可以取地址的是左值, 右值是不可以去地址的值, 一旦给右值取别名, 右值就会退化, 就会分配空间 + 地址 退化为左值
然后通过右值引用引出移动构造, 移动构造相比拷贝构造好处体现在深拷贝上面, 他和深拷贝不同的是不需要重新开底层的存储空间 + 转移数据, 直接窃取右值的底层空间
模板右值引用: 万能引用, 引用接收之后所有的右值会退化为左值, 想要保持住右值属性不退化, 需要进行 std::forward<>()完美转发, 保持右值属性
然后是 lambda表达式的引出, 捕获列表->后置返回类型{函数体} 且lambda表达式的底层处理就是 类的可调用对象 operator() 运算符重载
function 包装器 对于 函数指针 仿函数 lambda表达式的统一封装…. 包装: 好处, 使用起来更加方便, 指定好了参数和返回值类型, 作为参数传入也更加方便灵活…
thread 线程类库, C++11 支持的线程库, 参数的传入以值拷贝形式, 要想传入的是真正的引用 必须进行 std::ref()处理
原子操作: 创建了一套原子操作数据类型 atomic_long 等等以atomic开头的支持原子操作的数据类型, 相比 使用mutex 更加高效, 且不会死锁
但是由于对于代码段的原子操作, 原子操作的数据类型 无能为力, 只能使用 mutex, 使用锁为了避免死锁, C++11 产生了 锁的管理模板类 unique_lock 和 lock_guard 进行管理锁, 在 对象结束的时候调用析构解锁, 不至于一直死锁
因为一直使用锁, 效率极低, 所以 可以使用 condition_variable 配合锁使用完成一些特殊的要求, 以及提高效率, 不至于让系统一直不停的加锁解锁, 因为加锁解锁 耗费CPU资源
原文作者:小杰312
原文地址:https://blog.csdn.net/weixin_53695360/article/details/122969157