1.C++11简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(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能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节课程
主要讲解实际中比较实用的语法。
C++11reference
小故事:
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫
C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。
2、列表初始化
2.1 C++98中{}的初始化问题
在C++98中,标准允许使用花括号()对数组元素进行统一的列表初始值设定,比如:
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector<int> v{1,2,3,4,5}
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。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;
}
2.2 内置类型的列表初始化
int main()
{
// 内置类型变量
int x1 = {10};
int x2{10};
int x3 = 1+2;
int x4 = {1+2};
int x5{1+2};
// 数组
int arr1[5] {1,2,3,4,5};
int arr2[]{1,2,3,4,5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]{1,2,3,4,5};
// 标准容器
vector<int> v{1,2,3,4,5};
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
return 0;
}
注意:列表初始化可以在{}之前使用等号,其效果与不使用=没什么区别
2.3 自定义类型的列表初始化
- 标准库支持单个对象的列表初始化
class Point
{
public:
Point(int x = 0, int y = 0): _x(x), _y(y)
{}
private:
int _x;
int _y;
};
int main()
{
Pointer p{ 1, 2 };
return 0;
}
- 多个对象的列表初始化
多个对象想要支持列表初始化,**需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。**注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。
initializer_list介绍文档
#include <initializer_list>
template<class T>
class Vector {
public:
// ...
Vector(initializer_list<T> l): _capacity(l.size()), _size(0)
{
_array = new T[_capacity];
for(auto e : l)
_array[_size++] = e;
}
Vector<T>& operator=(initializer_list<T> l)
{
delete[] _array;
size_t i = 0;
for (auto e : l)
_array[i++] = e;
return *this;
}
// ...
private:
T* _array;
size_t _capacity;
size_t _size;
};
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。关于auto的详细介绍可以参考
C++初阶课件。
std::initializer_list使用场景
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值
list
vector
map
vector/operator=
int main()
{
vector<int> v = { 1,2,
list<int> lt = { 1,2 }
// 这里{"sort", "排序"}
map<string, string> di
// 使用大括号对容器赋值
v = {10, 20, 30};
return 0;
让模拟实现的vector也支持{}初始化和赋值
namespace bit
{
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;
};
}
3.声明
3.1 auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
3.2 decltype
3.2.1 为什么需要decltype
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就
无能为力
3.2.2 decltype
decltype是根据表达式的实际类型推演出定义变量时所用的类型,比如:
- 推演表达式类型作为变量的定义类型
int main()
{
int a = 10;
int b = 20;
// 用decltype推演a+b的实际类型,作为定义c的类型
decltype(a+b) c;
cout<<typeid(c).name()<<endl;
return 0;
}
- 推演函数返回值的类型
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() <<endl;
return 0;
}
3.3 nullptr
由于C++中NULL被定义成字面量0,这样就可能会带来一些问题,因为0既能表示指针常量又能表示整形常量。所以出于清晰和安全的考虑,C++11中新增了nullptr,用于表示空指针。
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifdef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,所以出于清晰和安全的考虑,C++11中新增了nullptr,用于表示空指针。
4 范围for循环
范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。 - 迭代的对象要实现++和==的操作。(范围for底层就是迭代器)
5 智能指针
6 STL中的一些变化
新容器
用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和
unordered_set。
容器中的一些新方法
实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本:
emplace_back
push_back
insert
emplace
但是这些接口到底意义在哪?网上都说他们能提高效率,他们是如何提高效率的?
请看下面的右值引用和移动语义章节的讲解。另外emplace还涉及模板的可变参数,也需要再继续深入学习后面章节的知识。
7 右值引用和移动语义
7.1 左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值*,==左值可以出现赋值符号的左边,==右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
左值引用可以直接减少拷贝(1、左值引用传参;2、传引用返回)。
左值引用未解决的问题:
1、局部对象返回问题。
2、插入接口,对象拷贝问题。
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;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名
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);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
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;
}
关于右值引用如何解决了左值引用没有解决的问题,请看下图
7.2 左值引用与右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是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;
}
右值引用总结:
- 右值引用只能引用右值,不能引用左值。
- 但是右值引用可以引用move以后的左值。
int main()
{
// 右值引用只能引用右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
现在我们来看一段代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<list>
#include<assert.h>
using namespace std;
name space Maria
{
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(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
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;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
Maria::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
Maria::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;
}
}
int main()
{
Maria::string s1("hello world");
Maria::string ret1 = s1;
Maria::string ret2 = (s1+'!');
Maria::string ret3 = move(s1);
return 0;
}
我们现在着重研究一下ret2。可以发现在等号右边(tmp的地址)与等号左边(ret2的地址)是相同的
可以看到我们上述的情况就是下图的第二种情况。通过打印的信息我们也可以推测,move一个左值的效果与将亡值相似。
在不支持c++11的编译器里对于临时变量传值返回可能会有如下优化
在支持右值引用的c++11的编译器里
接下类再来看看这段代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<list>
#include<string>
#include<assert.h>
using namespace std;
namespace Maria
{
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(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// s1 = 将亡值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
//cout << "~string()" << endl;
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;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
//const Maria::string& to_string(int value)
Maria::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
Maria::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;
}
}
int main()
{
Maria::string s1;
s1 = Maria::to_string(1234);
}
运行结果
将移动拷贝和移动赋值相关代码屏蔽之后,运行结果如下:
补充:const &可以延长对象生命周期
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<list>
#include<string>
#include<assert.h>
using namespace std;
namespace Maria
{
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(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// s1 = 将亡值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
cout << "~string()" << endl;
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;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
//const Maria::string& to_string(int value)
Maria::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
Maria::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;
}
}
int main()
{
Maria::string("hello world");
const Maria::string& ref1 = Maria::string("hello world");
Maria::string s2;
}
通过调试,我们可以发现,匿名对象的生命周期是本行,运行完本行之后就析构。但是有了const & 之后,匿名对象的生命周期就跟着ref1的生命周期一样延长到了整个函数体
我们再来观察下面的代码
运行结果
可以看到运行到最后,move(s1)之后,s1中的资源被转移到了s3中,而自己没有资源。
接下来继续看一段代码
在既有拷贝构造又有移动构造的类中,编译器会调用最匹配的构造函数。
总结:左值引用直接减少拷贝,提高效率
右值引用间接减少拷贝。识别左值还是右值,如果是右值,则不再进行深拷贝,直接移动拷贝,提高效率。
7.3 完美转发
模版中的&&万能引用
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(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;
}
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;
}
std::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<Maria::string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}
5.final与override
8 新的类功能
默认成员函数
源来c++类中,有6个默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const取地址重载
最后重要的是前四个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下: - 如果你没有实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝复制重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
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& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
/*~Person()
{}*/
private:
Maria::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
在将Person类里面的构造、拷贝构造、析构函数都屏蔽掉之后,也就是上面所示代码。运行可以得到下面的结果。
将Person类里面的构造、拷贝构造、析构函数都放出来,运行可以得到下面的结果。
可以看到我们自己编写的Person类的构造、拷贝构造、析构函数影响了移动拷贝移动构造函数的生成。
强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用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:
Maria::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
运行结果:
在前面我们已经自己编写了Person类的构造、拷贝构造、析构函数的基础上,我们想要生成移动构造和移动赋值函数,可以使用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& operator=(const Person& p)
//{
// if(this != &p)
// {
// _name = p._name;
// _age = p._age;
// }
// return *this;
//}
~Person()
{
cout << "~Person()" << endl;
}
//强制生成移动构造和移动赋值
Person(Person&& p) = default;
Person& operator=(Person && p) = default;
private:
Maria::string _name;
int _age;
};
int main()
{
Person s1;
cout << endl;
Person s2 = s1;
cout << endl;
Person s3 = std::move(s1);
cout << endl;
Person s4;
cout << endl;
s4 = std::move(s2);
cout << endl;
return 0;
}
运行结果:
可以看到这个结果与我们最开始没有给出Person类的构造、拷贝构造、析构函数编译器自己生成默认的移动构造移动拷贝函数运行结果是一样的。
禁止生成默认函数的关键字delete
如果能想要限制某些函数的生成,在C++11当中,只需在该函数声明前加上=delete即可,改语法知识编译器不生成对应的函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
Maria::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
可以看到Person的拷贝构造函数在声明的时候delete之后,我们就无法自动生成拷贝构造函数,所有需要调用拷贝构造的语句都会出错
9 可变参数模版
C++11的新特性可变参数模版能够让您创建可以接受可变参数的函数模版和类模板,相比C++98/03,类模板和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模版特性就够用了,所以这里我们点到为止。
下面就是一个基本可变参数的函数模版
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0~N个模版参数。我们无法直接获取参数包args中的每一个参数的,只能通过展开参数包的方式来获取参数包内的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样的方式获取可变参数,所以我们得用一些奇招来一一获取参数包的值。
递归函数方式展开参数包
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
//或者写成
//void ShowList()
//{
// cout << endl;
//}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value <<" ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
运行结果:
逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
//template <class T>
//int PrintArg(T t)
//{
// cout << t << " ";
// return 0;
//}
展开函数
//template <class ...Args>
//void ShowList(Args... args)
//{
// int arr[] = { PrintArg(args)... };
// cout << endl;
//}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
运行结果:
STL容器中的emplace相关接口函数
STL容器中的emplace相关接口函数
https://cplusplus.com/reference/vector/vector/emplace_back
https://cplusplus.com/reference/list/list/emplace_back
emplace_back支持模版的可变参数和万能引用。那么empalce系列函数到底有什么优势呢?
通过下面的代码来观察emplace_back和push_back之间有什么异同
int main()
{
std::list<Maria::string> mylist;
Maria::string s1("1111");
mylist.push_back(s1);
mylist.emplace_back(s1);
cout << endl;
Maria::string s2("2222");
mylist.push_back(move(s1));
mylist.emplace_back(move(s2));
return 0;
}
可以看到运行结果相同
看接下来的这段代码
int main()
{
std::list<Maria::string> mylist;
mylist.push_back("3333"); //构造匿名对象+移动构造
cout << endl;
mylist.emplace_back("3333"); //直接构造
return 0;
}
下面来看这段代码
结论:emplace_back比push_back的效率有时更高,是理论上的。
在实际实践中,还是要根据看传递的实参类型、容器种类、插入位置、容器持有类型构造函数的异常安全性、容器是否禁止相同元素的插入等情况,对两者性能进行基准测试。
根据经验,如果下列情况都成立,那么emplace_back几乎总是比push_back的效率更高:
待添加的值是以构造而非赋值方式加入容器,这个大多数标准容器都满足
传递的实参类型与容器持有之物的类型不同,这时emplace_back不要求创建和析构临时对象
容器不太可能由于出现重复情况而拒绝待添加的值,要么容器允许重复值,要么添加的大部分值满足唯一性。因为emplace_back会创建临时节点与容器内值比较,如果因为重复值被拒绝,构造与析构就付出了成本。
除此之外,如果要使用emplace_back,还有两个问题需要处理:
涉及到资源管理。push_back会存在临时对象,当构造抛出异常时,临时对象可以正常析构释放对象
与带有explicit声明饰词的构造函数之间的互动。在使用emplace_back,要特别小心去保证传递了正确的实参,因为即使是带有explicit声明饰词的构造函数也会被编译器纳入考虑范围,因为它会尽力找到某种方法来解释你的代码使它合法(即使不符合逻辑)。
以上,在emplace_back不适合使用的时候,就是push_back的用武之地了。
10 Lambda表达式
10.1 C++98中的一个例子
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。
#include <algorithm>
#include <functional>
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
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& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
return 0;
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
10.2 Lambda表达式
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
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; });
return 0;
}
上述代码就是使用C++11中的Lambda表达式来解决,可以看出Lambda表达式实际是一个匿名函数。
10.3 lambda表达式书写
1、lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}
- lambda表达式各部分说明[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
注意
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{};该lambda函数不能做任何事情。
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[]{};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c){b = a + c; };
fun1(10)
cout<<a<<" "<<b<<endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int{return b += a+ c; };
cout<<fun2(10)<<endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
int main()
{
size_t m;
cin >> m;
vector<thread> vthreads(m);
//创建m个进程分别打印0~(n-1)
for (size_t i = 0; i < m; i++)
{
size_t n;
cin >> n;
vthreads[i] = thread([i, n]() {
for (int j = 0; j < n; j++)
{
cout << i << " " << j << endl;
}
cout << endl;
});
}
for (auto& t : vthreads)
{
t.join();
}
}
2、捕捉列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意: - a.父作用域包含lambda函数的语句块
- b.语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
int main()
{
int x = 0, y = 1;
auto swap1 = [](int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
};
swap1(x, y);
cout << x << " " << y << endl;
//传值捕捉
auto swap2 = [x,y]()mutable
{
int tmp = x;
x = y;
y = tmp;
};
cout << x << " " << y << endl;
//引用捕捉
auto swap3 = [&x, &y]()
{
int tmp = x;
x = y;
y = tmp;
};
swap3();
cout << x << " " << y << endl;
return 0;
}
运行结果
可以看到捕捉列表与以前的形式参数很相似,只在局部范围有效,同时是实参的临时拷贝。
- c.捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复 - d.在块作用域中的lambda函数捕捉列表必须为空。
- e.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用于或者非局部变量都会导致编译报错。
- f.lambda表达式之间不能相互赋值
即使看起来类型相同,但是由于命名规则的原因(lambda+uuid),使得他们具有唯一的类名。也就是说不同lambda表达式具有不同的类型,也就不能相互赋值。
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
3.关于lambda的大小
int main()
{
int x = 0, y = 1;
int m = 0, n = 1;
auto swap1 = [](int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
};
cout << sizeof(swap1) << endl;
}
运行结果:
10.4 函数对象与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(1000, 2);
//lambda
auto r2 = [=](double money, int year)->double {return money * rate * year; };
r2(1000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器就会自动生成一个类,在该类中重载了operator()。
所以lambda表达式底层就是仿函数,通过lambda+uuid(Universally Unique Identifier)为之命名。仿函数类是没有成员变量的空类,但是为了标识唯一的类,编译器给了空类一个字节来标识这个类。
11 原子性操作库(atomic)
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
#include <iostream>
using namespace std;
#include <thread>
unsigned long sum = 0L;
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;
}
C++98中传统的解决方式:可以对共享修改的数据可以加锁保护。
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
std::mutex m;
unsigned long sum = 0L;
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 <iostream>
using namespace std;
#include <thread>
#include <atomic>
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;
}
更为普遍的,我们可以使用atomi类模板,定义出需要的任意原子类型
#include<mutex>
int main()
{
int n = 100000;
//atomic<int> x = 0;
//atomic<int> x = { 0 };
atomic<int> x{ 0 };
mutex mtx;
size_t begin = clock();
thread t1([&, n] {
for (int i = 0; i < n; i++)
{
++x;
}
});
thread t2([&, n] {
for (int i = 0; i < n; i++)
{
++x;
}
});
t1.join();
t2.join();
size_t end = clock();
cout << x << endl;
cout << "运行时间:"<<end - begin << endl;
return 0;
}
运行结果:
12 线程库
12.1 thread类的简单介绍
在C++11之前,涉及到多线程的问题,都是和平台有关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程库进行支持了,使得c++在并行编程时不需要以来第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
C++11中线程类
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1,args2,…) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数 |
get_id() | 获取线程id |
joinable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程 |
join() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"与主线程无关 |
注意:
- 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的
状态。 - 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
#include <thread>
int main()
{
std::thread t1;
cout << t1.get_id() << endl;
return 0;
}
12.2 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
12.3 lock_guard与unique_lock
- lock_guard
在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性。那么就只能通过所得方式来进行控制
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lk)
:_lk(lk)
{
_lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
private:
Lock& _lk;
};
void Func(int n)
{
for (int i = 0; i < n; i++)
{
try
{
//不能这样写,如果发生了抛异常,由于没有解锁,当程序再次进入这段语句会造成死锁
//mtx.lock();
LockGuard<mutex> lock(mutex);
++x;
//抛异常
if (rand() % 3 == 0)
{
throw exception("抛异常");
}
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
}
int main()
{
Func(10);
Func(20);
return 0;
}
通过上述代码可以看到,lock_guard类模版主要是通过RALL的方式,对其管理的互斥量进行了封装在需要枷锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域之前,lock_guard对象要销毁,调用析构函数自动解锁,可以有效避免死锁问题
__lock_guard的缺陷:太单一,__用户没有办法对该锁进行控制,因此C++11又提供了unique_lock
- unique_guard
与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所管理的互斥量的指针)。
代码: 两个线程交替打印,t1打印奇数,t2打印偶数
//两个线程交替打印,t1打印奇数,t2打印偶数
#include<mutex>
#include<iostream>
#include<condition_variable>
using namespace std;
int main()
{
mutex mtx;
condition_variable cv;
int n = 100;
int x = 1;
//如何保证t1先运行,t2阻塞
//如何防止一个线程不断运行
thread t1([&, n] {
while(x<n)
{
unique_lock<mutex> lock(mtx);
if (x % 2 == 0)
{
//偶数阻塞,奇数就打印
cv.wait(lock);
}
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
thread t2([&, n] {
while (x < n)
{
unique_lock<mutex> lock(mtx);
if (x % 2 != 0)
{
//奇数就阻塞,偶数打印
cv.wait(lock);
}
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
t1.join();
t2.join();
cout << x << endl;
return 0;
}
13.包装器
function包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
那么我们来看看,我们为什么需要function呢
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lambda表达式对象?
#include<functional>
#include<map>
#include<iostream>
using namespace std;
int f(int a, int b)
{
cout << "int f(int a, int b)" << endl;
return a + b ;
}
struct Functor
{
public:
int operator()(int a, int b)
{
cout << "int operator()(int a, int b)" << endl;
return a + b;
}
};
int main()
{
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {
cout << "[](int a, int b)" << endl;
return a + b;
};
cout << f1(1, 2) << endl;
cout << f2(10, 20) << endl;
cout << f3(100, 200) << endl;
map<string, function<int(int, int)>> opFuncMap;
opFuncMap["函数指针"] = f1;
opFuncMap["仿函数"] = f2;
opFuncMap["lambda"] = f3;
cout << opFuncMap["lambda"](1, 2) << endl;
return 0;
}
通过下面的例子我们发现,可以用相同的调用方式来调用函数,仿函数对象,lambda对象,将调用一般化。
下面来看看包装器的其他应用场景:
逆波兰表达式求值
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
map<string,function<int(int,int)>> opFuncMap = {
{"+",[](int a,int b){return a+b;}},
{"-",[](int a,int b){return a-b;}},
{"*",[](int a,int b){return a*b;}},
{"/",[](int a,int b){return a/b;}}
};
for(auto str:tokens)
{
if(opFuncMap.count(str))
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(opFuncMap[str](left,right));
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
bind
std::bind定义在头文件中,是一个函数模版,她就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这样做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
1、调整参数顺序
#include<iostream>
#include<functional>
using namespace std;
//调整参数顺序
void Print(int a, int b)
{
cout << a << " ";
cout << b << endl;
}
int main()
{
Print(1, 2);
auto forwordPrint=bind(Print,placeholders:: _1, placeholders::_2);
auto reversePrint=bind(Print, placeholders::_2, placeholders::_1);
forwordPrint(1, 2);
reversePrint(1, 2);
return 0;
}
运行结果:
2、调整参数个数
//调整参数个数
#include<functional>
class Mul
{
public:
Mul(int rate)
:_rate(rate)
{}
int func(int a, int b)
{
return (a - b) * _rate;
}
private:
int _rate;
};
int main()
{
function<int(Mul, int, int)> fMul1 = &Mul::func;
cout << fMul1(Mul(1), 10, 5) << endl;
function<int(int, int)> fMul2 = bind(&Mul::func,Mul(1),placeholders::_1,placeholders::_2);
cout<<fMul2(10, 5) << endl;
return 0;
}