꧁ 大家好,我是
兔7
,一位努力学习C++的博主~ ꧂
☙ 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步❧
🚀 如有不懂,可以随时向我提问,我会全力讲解~💬
🔥 如果感觉博主的文章还不错的话,
希望大家关注、点赞、收藏三连支持一下博主哦
~!👀
🔥 你们的支持是我创作的动力!⛅
🧸
我相信现在的努力的艰辛,都是为以后的美好最好的见证!⭐
🧸 人的心态决定姿态!⭐
🚀 本文章CSDN首发!✍
目录
0. 前言
此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。
大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~
感谢大家对我的支持,感谢大家的喜欢,
兔7
祝大家在学习的路上一路顺利,生活的路上顺心顺意~!
1. C++11简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得 C++03 这个名字已经取代了 C++98 称为 C++11 之前的最新 C++ 标准名称。不过由于 TC1 主要是对 C++98 标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为 C++98/03 标准。从 C++0x 到 C++11 , C++ 标准 10 年磨一剑,第二个真正意义上的标准珊珊来迟。相比于 C++98/03 ,C++11 则带来了数量可观的变化,其中包含了约 140 个新特性,以及对 C++03 标准中约 600 个缺陷的修正,这使得 C++11 更像是从 C++98/03 中孕育出的一种新语言。相比较而言,C++11 能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
2. 列表初始化
2.1 C++98中{}的初始化问题
在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1,2,3,4,5 };
int array2[5] = { 0 };
//依次赋值给_x _y
Point p = { 1, 2 };
return 0;
}
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector<int> v{1,2,3,4,5};
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
2.2 内置类型的列表初始化
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2022, 8, 9);
Date d3 = (2021, 3, 21);
Date d4{ 2002,9,13 };
Date d5 = { 2021,3,21 };
return 0;
}
注意:列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别。
当然还可以像 d4 、 d5这么用,但是不建议这么用,因为这种用法是为其它地方设置的,这么用是可以的,但是不建议~!
2.3 initializer_list
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
auto il1 = { 10, 20, 30 };
std::initializer_list<int> il2 = { 1,2,3,4 };
cout << typeid(il1).name() << endl;
vector<int> v = { 1,2,3,4,5 };
list<int> lt = { 10,20,30 };
vector<Date> vd = { Date(2022,8,9) , Date{2022,8,9 } , { 2022,8,9 } };
map<string, int> dict = { make_pair("sort", 1), {"insert", 2} };
return 0;
}
那么 initializer_list 用在什么地方呢?
其实 list vector map 支持这种 {} 宽泛的初始化就是因为 initializer_list 。
所以其实 {} 列表就是一个 initializer_list 类型,支持到 list 就是依次从 {} 中取出来值然后给 list ,因为:
有 size begin end ,支持迭代器,那样去使用。
namespace twotwo
{
template<class T>
class vector {
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
/*typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}*/
for (auto e : l)
*vit++ = e;
}
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()
{
twotwo::vector<int> tv = { 1,2,3,4,5 };
tv = { 10, 20 ,30 };
return 0;
}
大概思路就是通过上面的方式进行实现的。
3. 变量类型推导
3.1 auto
int main()
{
int i = 10;
auto p = &i;
auto pf = string();
map<string, int> dict = { make_pair("sort", 1), {"insert", 2} };
// map<string, int>::iterator it = dict.begin();
auto it = dict.begin();
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
cout << typeid(it).name() << endl;
return 0;
}
auto 可以直接帮忙推类型,在写迭代器的那里会轻松不少。
3.2 decltype
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret1; // ret的类型是double
//auto ret2 = x*y;
decltype(&x) p; // p的类型是int*
cout << typeid(ret1).name() << endl;
cout << typeid(p).name() << endl;
F(1, 2.2);
return 0;
}
我们知道 typeid().name() 可以看到一个变量的类型,但是:
我们不能它去做一个变量的类型。
那么如果就像通过其它变量的类型去推,可以这样:
还可以这样使用:
当然也可以通过模板去推:
decltype是不可以做返回值的,但是:
这里要想做返回值只能这么写。
3.3 nullptr
由于C++中 NULL 被定义成字面量 0,这样就可能带回来一些问题,因为 0 既能指针常量,又能表示整形常量。
所以出于清晰和安全的角度考虑,C++11 中新添了 nullptr,用于表示空指针。
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(NULL);
f(nullptr);
return 0;
}
4 范围for循环
也是这里提供的,下面是我写的博客,那里讲了迭代器,和迭代器相关的一些问题,大家可以去看看。
5. 智能指针
这个我会在后面非常详细的讲解,这个会出一篇博客,所以现在就不讲呢。
6. STL 中一些变化
新容器
其中 unordered_map 和 unordered_set 非常有意义。而且出了博客去供大家去看。
array 这个容器定义出来的是在栈上的。
forward_list 是单链表,因为单链表也不单独拿出来用,就很…..鸡肋。。。
7. 右值引用
7.1 右值引用概念
传统的 C++ 语法中就有引用的语法,而 C++ 中新增了的右值引用语法特性,所以从现在开始之前学的引用就叫做左值引用。
无论是左值引用还是右值引用,都是给对象取别名
。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),
一般情况下,我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。
定义时 const 修饰符后的左值(特殊情况),不能给它赋值,但是可以取它的地址,左值引用就是给左值的引用,给左值取别名。
什么是右值?什么是右值引用?
左值也是一个表示数据的表达式,如:字面常量、表达式返回值,传值返回函数的返回值(这个不能是左值引用返回)等等,
右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。
右值引用就是对右值的引用,给右值取别名。
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);
cout << &rr1 << endl;
rr1 = 20;
cout << &rr1 << endl;
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
//10 = 1;
//x + y = 1;
//fmin(x, y) = 1;
return 0;
}
我们可以看到右值引用这样是可以使用的。
而且我们知道右值是不可以改的,但是右值引用可以修改:
临时变量就是有人接收的话就将临时变量传过去,没有人接收就丢了,不需要存起来。
而右值引用是开了一块空间,将这些变量存起来了,
所以这里虽然右值不能取地址,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。
但是其实右值引用也不是这么用的~!
7.2 左值与右值
普通类型的变量,因为有名字,可以取地址,都认为是左值。
const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是 const 类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间), C++11 认为其是左值。
如果表达式的运行结果是一个临时变量或者对象,认为是右值。
如果表达式运行结果或单个变量是一个引用则认为是左值。
总结
不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断。
能得到引用的表达式一定能够作为引用,否则就用常引用。
C++11对右值进行了严格的区分:
C语言中的纯右值,比如:a+b, 100
将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
7.3 引用与右值引用比较
左值引用 -> 左值、右值引用 -> 右值。
那么 左值引用 -> 右值? 右值引用 -> 左值?
int main()
{
int a = 10;
int& ra1 = a; // ra为a的别名
// int& ra2 = 10; // 编译失败,因为10是右值
const int& ra2 = 10;
const int& ra3 = 10 + 20;
return 0;
}
所以其实左值不能直接引用右值,因为属于权限的放大,本来不能改,引用了之后可以改了,但是 const 左值可以引用右值。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
//int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
右值引用不可以引用左值。
但是也不是完全不可以,C++又添加了一个特性是 move() 。
move(a) 之后,a 还是左值。
所以右值引用不能引用左值,但是可以引用 move 之后的左值。
7.4 右值引用使用场景和意义(移动语义
)
namespace twotwo
{
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)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
this->swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
this->swap(s);
return *this;
}
// 赋值重载(如果用到移动赋值就只能用这个,下面的用不了)
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
赋值重载(现代写法)
//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
};
twotwo::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
twotwo::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
void func1(twotwo::string s)
{}
void func2(twotwo::string& s)
{}
int main()
{
//twotwo::to_string(1234);
twotwo::string ret1;
ret1 = twotwo::to_string(1234);
return 0;
}
左值引用的使用场景:
-
做参数
-
做返回值
我们可以看到传值和传引用的差别是很大的,一个进行了深拷贝,一个就是传过去了个别名。深拷贝的代价是很大的。
所以这里左值引用做参数做的很彻底,没有问题。但是做返回值却是一个短板。
我们可以看到,如果是传值返回的话就会产生一次深拷贝,传引用是不会的,那么我们就可以无脑使用传引用么?其实是不是的,因为这里是可以使用传引用返回的,但是有些场景是不可以使用的。
我们可以看到 to_string 不能用左值引用返回,因为函数的返回值出了作用域就不在了,就不能使用引用返回,就会存在拷贝!
那么这时就需要用到右值引用了~!
当然,这里可不是将传值返回直接搞成右值引用的形式就可以了,这里还需要引入
移动构造
。
因为拷贝构造:
这里的 const string& 可以接收左值,也可以接收右值,我们引入移动构造就是想将右值引用到移动构造中:
移动构造的思路就是将将亡值转移走。
我们可以看到,现在就是调用的移动构造了,这样结构越复杂,优化的程度就越大,因为这里就相当于只将它们的指针(其实也就是两个空间)交换一下,代价是非常小的~!
但是这里其实挺复杂的,接下来解析:
我们在用深拷贝的时候,其实本应该两次深拷贝的,但是编译器进行了优化,所以直接拷贝给了 ret1。
如果加入了移动构造,而且没有优化的情况下,这里应该是发生一次拷贝构造,一次移动构造。
但是这里要清楚的是 to_string 里的 str 在 to_string 里还是一个左值,但是 C++ 编译器极度追求效率,所以在识别的时候,它会将 str 往右值去识别。所以这里即使不优化的话,也其实是两次移动构造,其实优化没优化的影响不少特别大了。
我们看一下是不是转移了:
这个就是它的有用场景之一。
当没有变量接收时,有移动构造调用该移动构造,没有移动构造调用拷贝构造,也就是说,不管有没有变量接收,这里一定会拷贝(移动)到那一段内存(如果返回值占用空间小的话就是寄存器),也就是说,不管有没有变量接收,肯定是有返回值的,只是看调用方接收不接收这个返回值。
其实移动构造在库里也是添加了的:
那么如果有人这么写:
这里就是一次拷贝构造+一次 operator= ,
这里不会优化,因为不是一个函数,如果是构造再拷贝构造,或者是拷贝构造再拷贝构造才会被优化
。
我们会发现,确实没有优化,执行了两次拷贝。
那么我们使用移动构造:
我们这时看到就是一次移动拷贝一次拷贝构造了。
要避免拷贝构造,那么就可以引进一个
移动赋值
了,跟引入移动构造的意义是相同的~
我们可以看到,现在就是进行了一次移动构造一次移动赋值。
右值引用的真正意义在移动构造、移动赋值这些地方最大的价值就是跟左值引用进行区分,是左值就匹配左值引用,是右值就匹配右值引用。是右值的话就进行资源的转移,这样就大大提高了效率。
像库里的也添加了移动赋值,这里我就不去看了,大家不放心可以去看看。
总结
右值引用出来以后,并不是直接使用右值引用去减少拷贝,提高效率,而是指针深拷贝的类提供移动构造和移动赋值,这时这些类的对象进行传参返回或者是参数为右值时,则可以用移动构造和移动赋值,转移资源,避免深拷贝,从而提升效率。
7.5 右值引用引用左值及其一些更深入的使用场景分析
我们可以看到,一个是拷贝构造,一个是移动构造,那个移动构造如果没有实现,那么就还是拷贝构造,没有问题。
我们再使用 move 的时候,一定要知道自己在做什么,虽然我们转移了,但是被转移的左值就不能用了。
我们可以看到这里都用到了右值引用,显然用右值引用是为了提高效率。
我们可以看到,第一个是拷贝构造,后面三个是移动构造。
这里是因为 s1 是左值值引用接收的,但是当链接到链表中的时候,是将 s1 里的内容拷贝到链表中的 Data 中。
下面的是右值引用接收的,当链接到链表中的时候,是将右值移动构造到链表中的 Data 中。
所以第一个是拷贝构造,后面三个是移动构造。
所以,以后我们如果能构造临时对象、匿名对象,我们就构造临时对象、匿名对象,这样就可以减少拷贝提高效率啦~!
总结
右值引用使用场景二,还可以使用在容器插入接口函数中,如果实参是右值,则可以转移它的资源,减少拷贝提高效率。
7.6 完美转发
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)
{
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;
}
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
我们可以看到,正常来说,就像我红框框起来的那样,应该分别按照框起来的格式去打印。
但是我们发现和我们想想的是不一样的,而且我们看到的都是匹配的左值,这是因为引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。
但是我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。
我们可以看到,只要用到了完美转发 std::forward<T>();就可以完美的保持它本来的属性。
namespace twotwo
{
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)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
this->swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
this->swap(s);
return *this;
}
// 赋值重载(如果用到移动赋值就只能用这个,下面的用不了)
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
赋值重载(现代写法)
//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
};
twotwo::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
twotwo::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
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 = (Node*)malloc(sizeof(Node));
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(const T& x)
{
Insert(_head, x);
}
void PushBack(T&& x)
{
//cout << &x << endl;
// 这里x属性退化为左值,其他对象再来引用x,x会识别为左值
//Insert(_head, x);
// 这里就要用完美转发,让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); // 关键位置
Node* newnode = (Node*)malloc(sizeof(Node));
new(&newnode->_data)T(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; // 关键位置
Node* newnode = (Node*)malloc(sizeof(Node));
new(&newnode->_data)T(x);
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<twotwo::string> lt;
twotwo::string s1("1111");
lt.PushBack(s1);
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}
我先说一下,如果这么写,那么这里右边的 PushBack(T&& x) 的 T&& 不是万能引用了,因为左边已经实例化出对象 lt 了,那么 PushBack(T&& x) 就只能接收右值引用了。
我们可以看到,我们实现的最后这里调的是移动赋值,而我们前面的写的那个调用的是移动构造,这是为什么呢?
其实 STL 内存申请不是 new 出来的,它是内存池,走的空间配置器,内存池只开了空间,也就是说这个节点是没有 new 的,如果 new 的话:
这里因为 T 就是 string ,所以 new 的话就会调用它的构造函数,所以如果是 new 出来的,那么这里就:
如果是 new 出来的,那么 newnode->_data 就是已经存在的对象,那就直接给它赋值了,如果是内存池出来的,内存池是只开了空间没有初始化,就相当于 malloc 出来的一样。
所以我们实现的这里调用的是移动赋值而不是移动构造。
为什么右值引用引用了右值以后,在后面属性就会退化成左值?
前面说过,右值是不可以取地址的,但是给右值取别名后后,会导致右值被存储到特定位置,且可以取到该位置的地址。
也就是说可以取到地址了。
所以这里 x 属性退化为左值,其他对象再来引用x,x会识别为左值。
所以这里就要用到完美转发,保持它的右值属性:
我们可以看到,这样就调用到了右值引用的 Insert 了,但是在 Insert 中也是一个右值引用,所以在我画黄线的那里也要用完美转发继续保持优质属性。也就是在右值引用中只要传参就要用完美转发。
我们可以看到现在都是调用的移动赋值了,但是我们知道这里还是有点问题,因为这里本应该都是移动构造。所以如果我们想看到和 STL 里一样的结果,我们在代码中就不能用 new 了,我们只要将 new 换位 malloc 就可以了。
但是这里要注意的是:
我们 malloc 后 newnode 的 next 和 prev 都会初始化,所以我们不用处理,但是我们想把 x 构造到它这个对象上去我们就要用到 定位new 。
在一个已经存在的对象,去调用它的构造函数。
我们可以看到这样就可以了,和 STL 基本就是一模一样了。
8. 新的类功能
在C++中对于空类编译器会生成一些默认的成员函数
构造函数、
拷贝构造函数
运算符重载、
析构函数
取地址(&)的重载
const 取地址(const &)的重载
移动构造函数
移动赋值运算符重载
最重要的是前四个,五、六用处不大,默认成员函数就是我们不写,编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点:
namespace twotwo
{
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)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
this->swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
this->swap(s);
return *this;
}
// 赋值重载(如果用到移动赋值就只能用这个,下面的用不了)
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
赋值重载(现代写法)
//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
};
twotwo::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
twotwo::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//Person() = default;
//Person(Person&& p) = default;
//Person(const Person& p)
// :_name(p._name)
// , _age(p._age)
//{}
/*Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
/*~Person()
{}*/
private:
twotwo::string _name;
int _age = 0;
};
int main()
{
Person s1;
Person s2;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
我们可以看到调用的是移动构造,这里内置类型完成值拷贝,自定义类型如果实现了移动构造就调用移动构造,没有实现移动构造就调用拷贝构造。
类成员变量初始化
C++11 允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个很简单,我就不说了。
强制生成默认函数的关键字 default
C++可以让你更好的控制要使用的默认函数,假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成构造函数、移动构造了,那么我们可以使用 default 关键字显示指定构造函数、移动构造的生成。
禁止生成默认函数的关键字 delete
如果想要限制某些默认函数的生成,在 C++98 中,是该函数设置成 private ,并且只声明就可以,这样只要其它人想要调用就会报错。
在 C++11 中更简单,只需要在该函数声明上加上=delete 即可,该语法指示编译器不生成对应函数的默认版本,称 =delete 修饰的函数为删除函数。
继承和多态中的 final 和 override 关键字
final 修饰类不可被继承。
final 修饰虚函数,这个虚函数就不能被重写。
override 修饰子类重写的虚函数,检查是否完成重写,如果没有就报错。
一般纯虚函数才是要求子类强制重写,如果子类不重写,子类依旧是抽象类,不能实例化对象。
9. 可变参数模板
就像是这样,ShowList 可以传多个参数,也可以不传参数,可变参数模板会自动去推。
其实用的话不是很难,但是自己实现这个就比较麻烦了。
首先我们可以通过 sizeof…(args) 来获取参数包的个数,但是不能可以通过 arg[i] 的方式将里面的参数取出来:
template <class ...Args>
void ShowList(Args... args)
{
//cout << sizeof...(args) << endl;
for (int i = 0; i < sizeof...(args); ++i)
{
cout << args[i] << " "; // 不支持
}
}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
不过我们可以这样设计:
算是递归式的,就是新添加一个模板,然后通过 ShowList(args…) 传参,然后传给 value 和 args ,这样一直递归下去,就依次取完了。
但是还是有问题,就是参数包为 0 的时候还会继续,这时就会报错。那么我们可不可以:
其实是不可以的,因为 if 那一段是逻辑代码,但是它这个是模板,模板推演的时候虽然推出来了,但是不会走逻辑这一份代码,而是继续走 ShowList(args) 往下传,因为这个是模板。
就相当于,if 那一块是运行时逻辑,而展开参数包是一个编译时逻辑,所以是不可以的。
所以我们可以这样:
我们可以定义一个没有参数的,这样如果是空包的话就会去执行上面的空包了,因为会匹配最合适的,这样就停止了。
当然还有人写成一个参数的:
当然如果是一个参数的话,那么就不可以传空的了,所以其实传空参数的还是比较好的。
还可以这样去推:
但是这样只能推整数,所以还是有些问题,其实我们还是有办法的,可以用逗号表达式:
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
template <class ...Args>
void ShowList(Args... args)
{
// 列表初始化
// {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... )
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1, 2, 3);
std::string s("111");
ShowList(1,2,'A',"ssss", s);
ShowList(23, 11.11, "121313");
int a[] = { 1,2,3,4 };
return 0;
}
当然我们也可以不选择使用逗号表达式,也可以这样做:
template <class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
// 列表初始化
// {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... )
int arr[] = { PrintArg(args)... };
cout << endl;
}
int main()
{
ShowList(1, 2, 3);
std::string s("111");
ShowList(1, 2, 'A', "ssss", s);
ShowList(23, 11.11, "121313");
int a[] = { 1,2,3,4 };
return 0;
}
当然这里也可以用万能引用:
都说 emplace_back 比 push_back 高效,但是为什么高效?在什么时候才会高效?
我们可以看到,传左值的时候,是一次构造一次拷贝构造。传右值的时候是一次构造和一次移动构造。传参数包的时候就只是一次构造。
我们可以看到,传左值和右值的时候,无论是 emplace_back 还是 push_back 都是一样的,没有区别,只有再用参数包的时候,才会有区别。
为什么这个区别,具体可以看一下这个图:
10 lambda 表达式
10.1 C++98中的一个例子
struct Goods
{
string _name;
double _price;
int _num;
// ...
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
struct CompareNumLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._num < gr._num;
}
};
struct CompareNumGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._num > gr._num;
}
};
// 可调用对象类型
// 仿函数
// 函数指针
// lamber
int main()
{
vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100}, { "橙子", 2.2 , 1000}, { "菠萝", 1.5, 1} };
// 要求分别按名字、价格、数量进行排序,升序或降序
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
sort(v.begin(), v.end(), CompareNumLess());
sort(v.begin(), v.end(), CompareNumGreater());
return 0;
}
在C++98 中,如果想要对一个数据集合中的元素进行排序,可以使用 std::sort 方法,如果待排序元素为自定义类型,需要用户定义排序时的比较规则,那么我们就可以调用一个类模板。
所以可调用对象的类型就有:仿函数、函数指针,现在还有了一个 lambda。
10.2 lambda 表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
1. lambda表达式各部分说明
[capture-list] :
捕捉列表
,该列表总是出现在lambda函数的开始位置,
编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。
与普通函数的参数列表一致
,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:
返回值类型
。用
追踪返回类型形式声明函数的返回值类型
,没有返回值时此部分可省略。
返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
。
{statement}:
函数体
。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在lambda函数定义中,
参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空
。 因此C++11中
最简单的lambda函数
为:[]{}; 该lambda函数不能做任何事情。
我们这样就实现了一个相加的 lambda 表达式。
当然这里我没有用捕捉列表,直接用的传参的方式,接下来就用一下捕捉列表:
并且还可以不需要参数和返回值(返回值可以经过推演):
所以我们就可以通过这个方式来写最开始的调用:
我们可以看到,我们也可以写成一行,也可以分开写,如果长的话就可以这么写。
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表达式之间不能相互赋值
,即使看起来类型相同。
接下来就用 lambda 表达式实现 swap,其中还是有很多细节的:
int main()
{
int a = 1, b = 2;
auto swap1 = [](int& x, int& y)->void
{
int ret = x;
x = y;
y = ret;
};
swap1(a, b);
// 尝试利用捕捉列表,捕捉当前局部域的变量,
//这样就不用传参或者减少传参,省略参数和返回值
//这里传值方式捕捉,拷贝外面的a和b给lambda里面的a、b
//lambda里面的a、b的改变不会影响外面
auto swap2 = [a, b]()mutable
{
int tmp = a;
a = b;
b = tmp;
};
auto swap3 = [&a,&b]()//->void
{
int ret = a;
a = b;
b = ret;
};
swap3();
return 0;
}
其实捕捉不是直接将那个变量拿过来用,捕捉的本质还是传参,就像范围 for 是自动判断结束,自动取里面的数据,但底层是还是迭代器。
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);
// lamber
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
return 0;
}
我们可以看到,仿函数调用的就是运算符重载。
lambda 表达式底层被编译器转化成了仿函数
。
然后我们看一下为什么捕捉的不能直接用,而且捕捉的默认是 const 的,加了 mutable 就可以把 const 去掉就可以用了。
然后 lambda 表达式不能相互赋值,就比方说上面的 swap1 swap2 不能相互赋值,这是因为它们就不少相同的类型,而且它们的名称也不一样嘛,
在 vs2013 2019 编译器上这里可能是:
这样的名称,那一长串就是 lambda_uuid ,uuid 就是会生成不一样的串,就保证了名称是唯一的。
11. 包装器
可调用对象的类型:函数指针、仿函数(函数对象)、lambda
想一下 ret = func(x); 中的 func 可能是什么呢?
可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?
所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!为什么呢?我们继续往下看:
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 func(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(func, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double{ return d / 4; }, 11.11) << endl;
return 0;
}
通过这里我们可以看到地址都是不同的, count 都是1, 所以说它实例化了三次。
所以这样可能会导致模板效率低下,那么这里就有了包装器来解决这个问题。
std::function 在头文件<functional>
// 类模板原型如下
template<class T> function; //undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret:被调用函数的返回类型
Args...:被调用函数的形参
接下来就包装一下:
int f1(int a, int b)
{
return a + b;
}
struct Functor1
{
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)> ff1 = f1;
cout << ff1(1, 2) << endl;
// 包装仿函数
std::function<int(int, int)> ff2 = Functor1();
cout << ff2(1, 2) << endl;
// 包装静态成员函数
std::function<int(int, int)> ff3 = Plus::plusi;
cout << ff3(1, 2) << endl;
// 包装非静态成员函数
std::function<double(Plus, double, double)> ff4 = &Plus::plusd;
cout << ff4(Plus(),1.1, 2.2) << endl;
// 包装lambda表达式
auto f5 = [](int a, int b){return a + b; };
std::function<int(int, int)> ff5 = f5;
cout << ff5(1, 2) << endl;
}
我们可以看到,我们将函数指针、仿函数、成员函数、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);
}
第一此时包装之后传给 f 之后不是函数指针什么的,而是包装器,那么它们推出来的 f 就不是三个了,就是一个了。
第二包装之后看待每个包装起来的函数都是一样的,不用去看具体的实现细节了。
用同一的类型去管理,更方便。
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);
}
int main()
{
// 函数名
std::function<double(double)> f1 = func;
cout << useF(f1, 11.11) << endl;
// 函数对象
std::function<double(double)> f2 = Functor();
cout << useF(f2, 11.11) << endl;
// lamber表达式
std::function<double(double)> f3 = [](double d)->double{ return d / 4; };
cout << useF(f3, 11.11) << endl;
return 0;
}
我们可以看到,我们用了包装器之后,模板实例化就只实例化了一份。
我们接下来通过这个方式改一道题:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(const auto& str : tokens)
{
int left, right;
if(str == "+" || str == "-" || str == "*" || str == "/")
{
right = st.top();
st.pop();
left = st.top();
st.pop();
switch(str[0])
{
case '+':
st.push(left + right);
break;
case '-':
st.push(left - right);
break;
case '*':
st.push(left * right);
break;
case '/':
st.push(left / right);
break;
}
}
else
{
st.push(stoi(str));
}
}
int ret = st.top();
return ret;
}
};
这个是这道题的正确写法,但是实在是有点麻烦。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
map<string, std::function<int(int, int)>> opMap =
{
{"+", [](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(const auto& str : tokens)
{
int left, right;
if(str == "+" || str == "-" || str == "*" || str == "/")
{
right = st.top();
st.pop();
left = st.top();
st.pop();
// opMap[std] 取出来符号,然后()调用lambda表达式
st.push(opMap[str](left, right));
}
else
{
st.push(stoi(str));
}
}
int ret = st.top();
return ret;
}
};
然后当我们要添加方法的时候,直接在 opMap 里面直接添加就可以了。
总结
bind
bind 基本上都是用来进行参数调节。
上面的下面两个才是真正的应用场景。
如果没有绑定固定的调用对象,就像我们前面写过的,也就是下面的代码所示,是需要这么调用的。
// 包装非静态成员函数
std::function<double(Plus, double, double)> ff4 = &Plus::plusd;
cout << ff4(Plus(),1.1, 2.2) << endl;
这就是 bind 的应用场景。
12. 线程库
因为我在我写的 Linux 专栏中对多线程写了特别特别详细的讲解,所以下面我直接用就好了,写几个样例,解释解释,下面的链接就是我写的多线程,大家可以去看看,多线程非常非常的重要!
【Linux】多线程(重中之重)(学习兼顾复习)_兔7的博客-CSDN博客
void f(int N)
{
for (int i = 0; i < N; ++i)
{
cout <<this_thread::get_id()<<":" <<i << endl;
}
}
int main()
{
int n;
cin >> n;
vector<thread> vthreads;
vthreads.resize(n);
for (auto& td : vthreads)
{
td = thread(f, 100);
cout << td.get_id() << endl;
}
for (auto& td : vthreads)
{
td.join();
}
return 0;
}
这就是普通的使用,其中 this_thread::get_id() 是独立出来的,用来获取线程 id 的。
接下来加锁:
看一下,锁是加在里面更高效,还是加在外面更高效?
其实在外面是更高效的,因为如果在里面加锁的话,会导致一直加锁解锁,而且多个线程频繁的被唤醒,可能会频繁的切换上下文。
所以放在外面的更高效的。
而且其实将
锁定义到全局的很不好的
,所以我们可以用到 lambda 表达式。
int main()
{
int n;
cin >> n;
vector<thread> vthreads;
vthreads.resize(n);
mutex mtx;
int N = 1000000;
int x = 0;
for (auto& td : vthreads)
{
td = thread([&mtx, &N, &x]
{
mtx.lock();
for (int i = 0; i < N; ++i)
{
++x;
}
mtx.unlock();
});
}
for (auto& td : vthreads)
{
td.join();
}
printf("%d个线程并行对x++了%d次,x:%d\n", n, N, x);
return 0;
}
当然我们还可以用原子包装的类:
int main()
{
int n;
cin >> n;
vector<thread> vthreads;
vthreads.resize(n);
mutex mtx;
int N = 1000000;
atomic<int> x = 0;
//atomic_int x = { 0 };
for (auto& td : vthreads)
{
td = thread([&mtx, &N, &x]
{
//mtx.lock();
for (int i = 0; i < N; ++i)
{
++x;
}
//mtx.unlock();
});
}
for (auto& td : vthreads)
{
td.join();
}
cout << x << endl;
return 0;
}
这样就不用互斥锁了,x++ 就是原子性的了。
普通的锁,如果一个函数只加锁了,出去后没有解锁,然后所有线程进来都会在申请锁的位置挂起,这样进程就相当于卡死了。
所以这是可以用到递归锁,递归锁是如果你第一次申请了,没有释放锁,你再进来可以凭借锁继续进入,就不会卡死。
lock_guard
其实我们在这里这样加锁解锁是很不好的,因为:
突然因为返回,导致没有解锁,最后导致其它线程进来的时候,全都阻塞到申请锁上。就卡死了。
所以我们在 return;之前肯定是要 unlock() 一下。
这点如果很注意也是可以解决的,但是如果是抛异常的话,就根本来不及 unlock() ,因为抛异常直接就到了调用它的地方,然后进入 catch 里进行处理。
那我们如果保证这里的锁一定解锁了呢?
这里其实就要牵扯到智能指针中的 RAII 技术了。RAII是一种利用对象生命周期来控制程序资源。
namespace twotwo
{
template<class Lock>
class lock_guard
{
public:
lock_guard(Lock& lock)
:_lock(lock)
{
_lock.lock();
cout << "加锁" << endl;
}
~lock_guard()
{
_lock.unlock();
cout << "解锁" << endl;
}
lock_guard(const lock_guard<Lock>& lock) = delete;
private:
Lock& _lock;
};
}
mutex mtx;
void func()
{
//mtx.lock();
twotwo::lock_guard<mutex> lg(mtx);
// ...
FILE* fout = fopen("test.txt", "r");
if (fout == nullptr)
{
// ....
//mtx.unlock();
return;
}
}
int main()
{
func();
return 0;
}
所以现在无论是正常执行结束、还是中途返回、还是抛异常,这里 lg 都会在出了 func 函数作用域后调用自己的析构函数,就保证一定能解锁。
如果我们想在中途解锁,也就是只保护一部分资源:
我们直接用 {} 控制作用域就行了。
所以这里的 lock_guard 是很纯粹的,不能自己加锁解锁,如果想的话可以使用 unique_lock 。它里面可以用 lock 和 unlock ,其实实现很简单,就是在类里再多实现一下:
void lock()
{
_lock.lock();
}
void unlock()
{
_lock.unlock();
}
支持两个线程交替打印,一个打印奇数一个打印偶数
这里就是利用的原子性进行的打印。
int main()
{
int n = 100;
mutex mtx;
condition_variable cv;
bool flag = true;
// 奇数
thread t1([&]() {
int i = 1;
for (; i < n;)
{
// 创建的时候自动加锁,也可以给定参数不加锁
// 构造加锁
// unique_lock<mutex> lock(mtx, std::defer_lock);
unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flag]()->bool {return flag; }); // true
cout << i << endl;
i += 2;
flag = false;
cv.notify_one();
// 锁可以释放可以不释放,unique_lock可以自己释放
// 析构解锁
}
});
// 偶数
thread t2([&](){
int j = 2;
for (; j < n;)
{
// 创建的时候自动加锁,也可以给定参数不加锁
// 构造加锁
unique_lock<mutex> lock(mtx);
cv.wait(lock, [&flag]()->bool{return !flag; }); // false
cout << j << endl;
j += 2;
flag = true;
cv.notify_one();
// 锁可以释放可以不释放,unique_lock可以自己释放
// 析构解锁
}
});
t1.join();
t2.join();
return 0;
}
如上就是
C++11引入的新玩法
的所有知识,如果大家喜欢看此文章并且有收获,可以支持下
兔7
,给
兔7
三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!
再次感谢大家观看,感谢大家支持!