在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++11学习
小故事:
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。
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = {1, 2, 3, 4, 5};
int array2[5] = {0};
Point p = {1, 2};
return 0;
}
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;
}
创建对象时也可以使用列表初始化方式调用构造函数初始化。
class Date
{
public:
Date(int year, int month, int day)
: _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(2022, 1, 1); //old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{2022, 1, 2};
Date d3 = {2022, 1, 3};
return 0;
}
std::initializer_list的介绍文档:
链接文档
std::initializer_list是什么类型:
int main()
{
// the type of il is an initializer_list
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
return 0;
}
这里的{}被识别成了一个类,这个类叫std::initializer_list。
std::initializer_list使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。
int main()
{
vector<int> v = {1, 2, 3, 4};
list<int> lt = {1, 2};
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = {{"sort", "排序"}, {"insert", "插入"}};
// 使用大括号对容器赋值
v = {10, 20, 30};
return 0;
}
回顾一下当初模拟实现的vector,由于我们并没有设置std::initializer_list的构造,因此采用大括号进行初始化是错误的,所以我们可以增加一个由std::initializer_list参数的构造函数
vector(initializer_list<T> il)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(il.size());
typename initializer_list<T>::iterator it = il.begin();
while (it != il.end())
{
push_back(*it);
++it;
}
}
c++11提供了多种简化声明的方式,尤其是在使用模板时。
在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;
return 0;
}
关键字decltype将变量的类型声明为表达式指定的类型。
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; //ret的类型是double
decltype(&x) p; //p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
//vector存储类型跟x*y表达式返回值类型一致
//decltype推导表达式类型,用这个类型实例化模板参数或者定义对象
vector<decltype(x* y)> v(10);
cout << typeid(v).name() << endl;
return 0;
}
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
在学习右值与右值引用之前,我们首先要明确什么是左值?什么是左值引用?
可以获取它的地址+可以对它赋值
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main()
{
// 以下的p、b、c、*p都是左值
int *p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int *&rp = p;
int &rb = b;
const int &rc = c;
int &pvalue = *p;
return 0;
}
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
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;
}
左值引用总结:
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;
}
右值引用总结:
int main()
{
// 右值引用只能右值,不能引用左值。
int &&r1 = 10;
// error C2440: “初始化”: 无法从"int"转换为"int &&"
// message : 无法将左值绑定到右值引用
int a = 10;
int &&r2 = a;
// 右值引用可以引用move以后的左值
int &&r3 = std::move(a);
return 0;
}
左值引用做参数和做返回值都可以提高效率。
事实上,对于左值引用,最有意义的就是在函数传参以及传返回值时,通过左值引用可以减少拷贝,因为普通的变量实际上都是将传入的参数拷贝到函数参数上或者将返回值拷贝到一个临时变量中。
template<class T>
const T& func1(const T& x)
{
// ...
return x;
}
对于返回值来说,如果是传入的参数作为返回值当然没有问题,因为出了函数栈帧之后该参数还在,静态变量也是如此;但如果在函数内部创建的变量作为返回值,即函数的局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。
template<class T>
const T& func2(const T& x)
{
T ret;
// ...
return ret;
}
C++11中,将右值又详细的进行了划分:
对于右值中的将亡值,直接深拷贝代价大,需要做一种资源的转移,C++11提供了移动构造函数和移动赋值运算符重载来解决右值的问题。
下面以模拟实现过的string来讲解
namespace bit
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
string operator+(char ch)
{
string tmp(*this); //这里还有一层深拷贝
tmp += ch;
return tmp;
}
~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;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
bit::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
bit::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()
{
bit::string s1("hello world");
bit::string ret1 = s1; //深拷贝
bit::string ret2 = (s1+'!');
}
就按照我们之前的写法,s1是左值, s1+’ ! '是右值,观察运行结果:
发现3个都是走的深拷贝,对于内置类型来说区别不大,对于自定义类型深拷贝的代价还是很大的,既然s1+’ ! '是右值中的将亡值,C++11提出的方式是直接进行资源的转移,我们给出string的移动拷贝函数。
//移动构造 --- 区分左右值
string(string&& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 移动拷贝" << endl;
swap(s);
}
发现将亡值s1+’ ! '这里走的是移动拷贝。
将s1变成右值即 bit::string ret3 = move(s1)
这里走的也是移动拷贝。
我们在以模拟实现的to_string来演示
int main()
{
bit::string valStr = bit::to_string(1234);
return 0;
}
如果不给移动拷贝函数,走的是深拷贝
给移动拷贝函数,走移动拷贝
大家可能会疑惑,这里返回str是一层深拷贝,直接拷贝构造对象也是一层拷贝,那为什么没有多走一层深拷贝呢,因为编译器做了优化:
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。
move作用:将左值变成右值。 move后的值也就变成了将亡值。不要轻易使用move。
对比以下两种写法,
写法1: 这里我们把s1 move处理以后会被当成右值,调用移动构造但是这里要注意,一般是不要这样用的,因为我们会发现s1的资源被转移给了s3,s1直接被置空了。
写法2:使用写法2,是使move(s1)表达式变成右值,正常调用拷贝构造函数,s1不会被置空
不仅仅有移动构造,还有移动赋值:
以下代码,在上面的基础上提供移动拷贝函数,不提供移动赋值,那么会调用string的赋值重载,还是走深拷贝
对于bit::to_string(1234)
是右值中的将亡值,直接深拷贝代价大,还是按照C++11提出的方式直接进行资源的转移,我们给出string的移动赋值函数。
//s1 = 将亡值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动赋值" << endl;
swap(s);
return *this;
}
继续运行刚才的代码,发现这里走的是移动赋值。
根据上面to_string实现对比C++98和C++11, C++11提供了移动构造和移动赋值
为了补齐左值引用的短板,C++11增加了右值引用和移动语义,同时配合这一特性:
C++11以后,STL中所有的容器都增加了移动构造和移动赋值;
C++11以后,STL中所有的容器插入数据接口都增加了右值引用版本;
我们以list为例,用模拟实现过的string来演示这个现象
int main()
{
list<bit::string> lt;
bit::string s1("hello world");
lt.push_back(s1);
//string("hello world") 构造的匿名对象, 是右值
lt.push_back(bit::string("hello world"));
lt.push_back("hello world");
return 0;
}
上面的代码,如果注释掉上面我们给的移动拷贝运行结果:
发现即使是string("hello world")
构造的匿名对象, 属于右值还是走的深拷贝;
如果提供移动拷贝函数,那么左值正常走深拷贝,右值走移动拷贝
我们知道,左值引用可以修改,const修饰的左值引用不能修改。但是对于右值引用来说,右值本身就不能修改,为什么还要加上const呢?按照正常的理解,这没有任何意义啊!有了这样的疑惑,我们继续来看const修饰的右值引用
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
因此,在给右值引用时,rr1就有了左值的属性,因为其具备了地址并可以进行访问修改,但需要注意的是,修改的并不是字面量10,10是右值,而是被存储到特定位置的变量。因此,const右值的引用的作用和const左值的作用是一样的,因此也可以看出我们之前进行的移动拷贝,其中的swap实际上就相当于将右值引用的变量进行了交换,这就是因为右值引用的变量具有左值的属性。这样才能实现转移资源。
namespace bit
{
template<class T>
struct list_node
{
list_node<T>* _next;
list_node<T>* _prev;
T _data;
list_node(const T& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
};
// 1、迭代器要么就是原生指针
// 2、迭代器要么就是自定义类型对原生指针的封装,模拟指针的行为
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> node;
typedef __list_iterator<T, Ref, Ptr> self;
node* _node;
__list_iterator(node* n)
:_node(n)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
self& operator++()
{
_node = _node->_next;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
template<class T>
class list
{
typedef list_node<T> node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end() const
{
return const_iterator(_head);
}
void empty_init()
{
_head = new node(T());
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
template <class Iterator>
list(Iterator first, Iterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& tmp)
{
std::swap(_head, tmp._head);
}
list(const list<T>& lt)
{
empty_init();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
// lt1 = lt3
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
//it = erase(it);
erase(it++);
}
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(),x);
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
void insert(iterator pos, const T& x)
{
node* cur = pos._node;
node* prev = cur->_prev;
node* new_node = new node(x);
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
void insert(iterator pos, T&& x)
{
node* cur = pos._node;
node* prev = cur->_prev;
node* new_node = new node(x);
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
iterator erase(iterator pos)
{
assert(pos != end());
node* prev = pos._node->_prev;
node* next = pos._node->_next;
prev->_next = next;
next->_prev = prev;
delete pos._node;
return iterator(next);
}
private:
node* _head;
};
}
这样调用的还是深拷贝,我们明明已经加上移动构造和移动拷贝了,为什么仍然调用深拷贝?这就是我们在标题说到的,在传参数的过程中,右值引用的变量接收了右值,但它会继续传到另一个函数,此时就会因这个值具有左值属性而被当成左值,所以在匹配函数的时候调用的仍然是深拷贝,因此,我们可以在这些移动构造的函数还有要将参数通过move再次转化成右值(不推荐)来暂时解决这个问题,真正问题的解决请看5.4完美转发部分。
调用同一个函数,函数参数是左值、右值要分别提供一种,否则就会报错。
为了不用区分左值和右值,在模板中对于左值和右值都可以进行匹配,C++11提出了万能引用(引用折叠)。
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值, 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。
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;
}
运行结果:
我们发现左值正常调用左值引用,无论是右值,还是move左值后变成的右值,都没有右值引用,反而调用了左值引用,这就是上面提到的引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。要想真正解决这个问题,我们来学习下面的完美转发。
完美转发在传参的过程中保留对象原生类型属性
下面我们使用forward对上面的代码进行修改
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
//std::forward(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
运行后发现使用forward后的确保留了对象原生类属性
还是我们5.2.4演示中模拟实现list的代码,发现按照原来的代码即使是右值,但它在传参中因这个值具有左值属性而被当成左值,所以在匹配函数的时候调用的仍然是深拷贝。上面提出的方法是使用move,下面使用完美转发来解决这个问题。
凡是需要往下一层传参的,需要保持右值属性的都需要使用完美转发 。
namespace bit
{
template<class T>
struct list_node
{
list_node<T>* _next;
list_node<T>* _prev;
T _data;
list_node(const T& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
list_node(T&& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(forward<T>(x))
{}
};
// 1、迭代器要么就是原生指针
// 2、迭代器要么就是自定义类型对原生指针的封装,模拟指针的行为
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> node;
typedef __list_iterator<T, Ref, Ptr> self;
node* _node;
__list_iterator(node* n)
:_node(n)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
self& operator++()
{
_node = _node->_next;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
template<class T>
class list
{
typedef list_node<T> node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end() const
{
return const_iterator(_head);
}
void empty_init()
{
_head = new node(T());
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
template <class Iterator>
list(Iterator first, Iterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& tmp)
{
std::swap(_head, tmp._head);
}
list(const list<T>& lt)
{
empty_init();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
// lt1 = lt3
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
//it = erase(it);
erase(it++);
}
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), forward<T>(x));
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
void insert(iterator pos, const T& x)
{
node* cur = pos._node;
node* prev = cur->_prev;
node* new_node = new node(x);
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
void insert(iterator pos, T&& x)
{
node* cur = pos._node;
node* prev = cur->_prev;
node* new_node = new node(forward<T>(x));
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
iterator erase(iterator pos)
{
assert(pos != end());
node* prev = pos._node->_prev;
node* next = pos._node->_next;
prev->_next = next;
next->_prev = prev;
delete pos._node;
return iterator(next);
}
private:
node* _head;
};
}
使用上面5.2.4给出的示例运行后,结果符合我们的预期。
默认成员函数
原来C++类中,有6个默认成员函数:
演示:
下面的类,按照上面规则所说没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,编译器自动生成了移动拷贝和移动赋值。
一旦实现了析构函数 、拷贝构造、拷贝赋值重载中的任意一个,会直接走深拷贝。
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
delete的使用,在C++IO流部分就有体现,具体内容我们放在C++IO流章节博客去讲
在C语言的学习中我们已经接触过可变的参数数量的函数,即我们耳熟能详的scanf和printf,因为、其可以传任意数量的参数,这一特性又在C++11中得到了更好的体现。
C++11的新特性可变参数模板能够创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止。
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl; //用sizeof看有几个参数
}
int main() //根据模板去实例化
{
ShowList();
ShowList('x');
ShowList('x', 'y');
ShowList('x',1);
return 0;
}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
void ShowList()
{
cout << endl;
}
template <class T, class ...Args>
void ShowList(const T&val, Args... args)
{
//解析可变参数包 --- 递归推导思维
cout << val << " ";
ShowList(args...);
}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
当然我们也可以使用递归的方式查看参数包及其调用个数
void ShowList()
{
cout << endl;
}
template <class T, class ...Args>
void ShowList(const T&val, Args... args)
{
cout << __FUNCTION__ << "(" << sizeof...(args) << ")" << endl; //看参数包及其调用个数
ShowList(args...);
}
int main()
{
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
但是我们发现STL容器中提供的emplace_back中的模板参数只有一个class ...Args
并没有class T
,所以我们可以在上面代码原有的基础上在套一层来写
void _ShowList()
{
cout << endl;
}
template <class T, class ...Args>
void _ShowList(const T&val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
template <class ...Args>
void ShowList(Args... args)
{
_ShowList(args...);
}
int main()
{
ShowList(1, 'A', std::string("sort"));
return 0;
}
通过逗号表达式的方式,可变参数列表能够推演数组的大小并将参数进行实例化从而调用PrintArg(T t)
,需要注意的是,这种方式不能传0个参数,即上面注释的ShowList()
,因为不能分配常量大小为 0 的数组。
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
我们可以将上面的代码优化一下:
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;
}
将逗号表达式换成PrintArg带有返回值的方式,因为数组里面初始化必须有值,除了逗号表达式就是这种方式,相比前者,这种更直观。
ShowList(1, 'A', std::string("sort"));
写法,编译器编译推演生成了以下代码
void ShowList(int a1, char a2, std::string a3)
{
int arr[] = { PrintArg(a1),PrintArg(a2),PrintArg(a3) };
cout << endl;
}
C++11提供了可变参数模板后,对于STL中容器又给出了emplace、emplace_back方法,参数无论是右值还是左值,都存在一个可变参数列表为函数的重载函数,其功能与push、push_back是一样的。emplace系列的接口,支持模板的可变参数,并且万能引用。
template <class... Args>
void emplace_back (Args&&... args);
还是以我们上面模拟实现过的string为例,如果只是以下的写法,那么emplace_back和push_back没有区别
int main()
{
// 深拷贝的类
std::list<bit::string> mylist;
// 没区别
bit::string s1("1111");
mylist.push_back(s1);
mylist.emplace_back(s1);
cout << endl;
bit::string s2("2222");
mylist.push_back(move(s2));
mylist.emplace_back(move(s2));
}
运行后发现,是左值就走深拷贝;是右值就走移动拷贝。emplace_back和push_back没有区别。
下面的写法开始有区别,在模拟实现过的string添加一句cout << "string(char* str)" << endl;
来观察
int main()
{
//开始有区别
mylist.push_back("3333"); //构造匿名对象 + 移动构造
mylist.emplace_back("3333"); //直接构造
}
运行后发现push_back走的是构造+移动构造;emplace_back走的是直接构造,其实对于深拷贝的类emplace系列和传统的插入效率差别并不大。
所以从效率上来说,emplace系列会高效一点.如果一个深拷贝的类没有实现移动构造,这个时候传统插入的效率将远不如emplace系列。
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法,如果待排序元素为自定义类型,需要用户定义排序时的比较规则,C语言中可以使用函数指针,C++中可以使用仿函数。下面的例子就是利用仿函数的方式来定义比较规则。
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());
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了lambda表达式。
int main()
{
auto add1 = [](int x, int y)->int {return x + y; };
cout << [](int x, int y)->int {return x + y; }(1, 2) << endl;
cout << add1(1, 2) << endl;
//展开写 返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
auto add2 = [](int x, int y)
{
return x + y;
};
cout << add2(1, 2) << endl;
}
上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是匿名仿函数的类。
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
[var] | 表示值传递方式捕捉变量var |
---|---|
[=] | 表示值传递方式捕获所有父作用域中的变量(包括this) |
[&var] | 表示引用传递捕捉变量var |
[&] | 表示引用传递捕捉所有父作用域中的变量(包括this) |
[this] | 表示值传递方式捕捉当前的this指针 |
注意:
比如用lambda表达式实现两个变量的值交换
int main()
{
int x = 0, y = 1;
auto swap1 = [](int x, int y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(x, y);
cout << x << " " << y;
}
上面的写法无法实现x, y值交换,就像函数传值传参无法实现值交换一样;x和y虽然和原有的x, y同名但并不是原有的x和y,它只是原有x, y的临时拷贝,所以交换它们的值无法实现真正x,y的交换。
真正的交换需要实现成以下的形式:可以像swap2一样实现成引用传参的方式,也可以像swap4一样在捕捉列表对x,y进行引用捕捉,捕捉后不用传参,也能实现x, y值交换。
int main()
{
int x = 0, y = 1;
auto swap2 = [](int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
};
swap2(x, y);
cout << x << " " << y<<endl;
//引用捕捉
auto swap4 = [&x, &y]()
{
int tmp = x;
x = y;
y = tmp;
};
swap4();
cout << x << " " << y<<endl;
}
抛开交换两值问题,我们也可以对x,y在捕捉列表中进行传值捕捉。
捕捉时要使用mutable,因为lambda函数总是一个const函数,要修改捕捉列表中参数的值,就要用mutable取消其常量性,此时参数列表不可省略(即使参数为空)
int main()
{
int x = 0, y = 1;
//传值捕捉
auto swap3 = [x, y]()mutable //mutable可以取消其常量性
{
int tmp = x;
x = y;
y = tmp;
};
swap3();
cout << x << " " << y;
cout << endl;
}
int main()
{
int x = 0, y = 1;
//混合捕捉
auto func1 = [&x, y]()
{
};
//全部引用捕捉
auto func2 = [&]()
{
};
//全部传值捕捉
auto func3 = [=]()
{
};
//x传值捕捉, 其余引用捕捉
auto func4 = [&, x]()
{
};
return 0;
}
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
我们分别用仿函数和lambda表达式两种方式来实现同样的功能。
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
//lambda
auto r2 = [=](double monty, int year)->double {return monty * rate * year;
};
r2(10000, 2);
return 0;
}
调试模式下转到反汇编
仿函数的汇编代码和 lambda表达式进行比较(图中圈起来的地方),比较发现,仿函数和lambda表达式的调用函数的汇编代码几乎一致,除了类名不相同
从图中可以看出,lambda表达式的底层被转换成了仿函数,所以 lambda表达式的本质上就是仿函数
该类的名字是编译器随机生成的,该类名只有编译器可以认识,这也是我们为什么要借助 auto 将其赋值给一个变量的原因。
解释上面 lambda表达式之间不能相互赋值的原因
int main()
{
auto f1 = [] {cout << "hello world" << endl; };
auto f2 = [] {cout << "hello world" << endl; };
cout << typeid(f1).name() << endl;
cout << typeid(f2).name() << endl;
return 0;
}
运行结果,可以看出,即使是两个一模一样的lambda表达式,它们的uuid是不同的。
在linux多线程章节我们已经学习了POSIX线程库,但是因为Windows下不支持POSIX线程库,Windows有自己的多线程方案,windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
C++11的线程库在windows下能用,在Linux环境下也能用,是因为其内部的条件编译。
函数名功能 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1, args2,…) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程。 |
jion() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
注意:
#include
int main()
{
std::thread t1;
cout << t1.get_id() << endl;
return 0;
}
get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:
// vs下查看
typedef struct
{
/* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
}_Thrd_imp_t;
同时C++11还提供了this_thread的类,我们可以直接使用这个类中get_id接口静态地查看线程id,而不使用对象调用的方式查看
int main()
{
cout << this_thread::get_id() << endl;
return 0;
}
当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。
线程函数一般情况下可按照以下三种方式提供:
在linux下,POSIX线程库中的pthread_create函数以C语言方式传参时,只能传函数指针,在学习C++11线程库和lambda表达式后,这里就有3种传参方式。
#include
using namespace std;
#include
void ThreadFunc(int a)
{
cout << "Thread1" << a << endl;
}
class TF
{
public:
void operator()()
{
cout << "Thread3" << endl;
}
};
int main()
{
// 线程函数为函数指针
thread t1(ThreadFunc, 10);
// 线程函数为lambda表达式
thread t2([] {cout << "Thread2" << endl; });
// 线程函数为函数对象
TF tf;
thread t3(tf);
t1.join();
t2.join();
t3.join();
cout << "Main thread!" << endl;
return 0;
}
可以看出thread类也结合了可变参数模板部分的知识
thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。
C++98中传统的解决方式:可以对共享修改的数据可以加锁保护。
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在访问临界资源时时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。
注意:需要使用以上原子操作变量时,必须添加头文件
#include
int main()
{
atomic<int> x = 0; //原子性操作库(atomic)
mutex mtx;
int n = 100000;
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();
cout << x << endl;
return 0;
}
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。
更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。
atmoic<T> t; // 声明一个类型为T的原子类型变量t
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了
#include
int main()
{
atomic<int> a1(0);
//atomic a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0;
}
在C++11中,Mutex总共包了四个互斥量的种类:
函数名 | 函数功能 |
---|---|
lock() | 上锁:锁住互斥量 |
unlock() | 解锁:释放对互斥量的所有权 |
try_lock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
注意,线程函数调用lock()时,可能会发生以下三种情况:
线程函数调用try_lock()时,可能会发生以下三种情况:
recursive_mutex mtx;
int x = 0;
void Func(int n)
{
if (n == 0)
return;
mtx.lock();
++x;
Func(n - 1);
mtx.unlock();
}
int main()
{
thread t1(Func, 100);
thread t2(Func, 200);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
原子操作的本质是CAS
CAS(Compare and Swap)操作是一种常见的并发编程技术,用于保证多个线程访问同一共享资源时的数据一致性。它可以在不使用锁的情况下实现对共享变量的原子更新操作。
CAS操作通常由三个参数组成:内存地址V、预期值A和新值B。当多个线程同时尝试更新V时,只有其中一个线程能够成功执行CAS操作,即当且仅当V的当前值等于A时,才会将其更新为B。如果V的当前值不等于A,则说明其他线程已经修改了V,那么当前线程会放弃更新操作,并重新尝试。
CAS操作通常用于实现无锁算法,在高并发场景中可以提高程序的并发性能。但是,CAS操作也存在一些问题,例如ABA问题和自旋次数过多等,需要开发者在实际应用中注意避免和解决。
在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。
锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。
std::lock_gurad 是 C++11 中定义的模板类。
lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。
与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。
与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lk)
:_lk(lk)
{
_lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
private:
Lock& _lk;
};
int x = 0;
mutex mtx;
void Func(int n)
{
for (int i = 0; i < n; ++i)
{
try
{
unique_lock<mutex> lock(mtx);
++x;
if (rand() % 3 == 0)
{
throw exception("抛异常");
}
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
}
int main()
{
thread t1(Func, 10);
thread t2(Func, 10);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
下面是一段有问题的代码,两个线程同时对全局变量++,不是原子操作,也没有加锁,但是打印结果大都是300,看似没有问题,是由于cpu跑太快了
我现在想要解释的是即使t1和t2线程执行的是同一个Func函数,但是我们在linux部分学过,每个线程的: 栈和一组寄存器都是独立的。在调用函数时,两个线程会各自创建自己的栈空间,所以回调函数的栈区变量从属于各个线程,线程间互不影响。
int x = 0;
void Func(int n)
{
cout << &n << endl;
cout << &x << endl; //静态区共享
for (int i = 0; i < n; ++i)
{
++x;
}
}
int main()
{
//执行同一个函数: 每个线程都有独立的栈
thread t1(Func, 100);
thread t2(Func, 200);
t1.join();
t2.join();
//cout << x << endl; //几乎不会小于300, cpu跑的太快了
return 0;
}
我们修改一下代码中数值,打印一下n和x的地址观察
发现n的地址不同,在每个线程栈空间上都有一个独立的n,而全局变量x是共享的。
以下分别是并行运行和串行运行的代码
并行:
int x = 0;
mutex mtx;
void Func(int n)
{
//并行
for (int i = 0; i < n; ++i)
{
mtx.lock();
++x;
mtx.unlock();
}
}
int main()
{
int n = 10000000;
size_t begin = clock();
thread t1(Func, n);
thread t2(Func, n);
t1.join();
t2.join();
size_t end = clock();
cout << x << endl;
cout << end - begin << endl;
return 0;
}
串行:
int x = 0;
mutex mtx;
void Func(int n)
{
//串行
mtx.lock();
for (int i = 0; i < n; ++i)
{
++x;
}
mtx.unlock();
}
int main()
{
int n = 10000000;
size_t begin = clock();
thread t1(Func, n);
thread t2(Func, n);
t1.join();
t2.join();
size_t end = clock();
cout << x << endl;
cout << end - begin << endl;
return 0;
}
分别运行对比时间效率:
发现串行运行的时间效率更高
加锁时总是想着把锁放到临界区的极限边界,这是不对的。这里我们要保护的代码是++x,像并行把锁就放在++x的前后,最小化临界区,卡在了极限的距离。但是t1, t2线程每进行一次++操作,就要判断一次锁,会产生高频次的线程切换,影响效率。但是串行这种加锁方式,一个线程跑完循环再放另一个线程进去,极大地减少了加锁带来的线程切换。
当然需要结合实际代码分析,如果这里是其他执行时间较长的代码,串行的写法就不合适了,因为会导致另一个线程饥饿。比如以下场景:
在临界区中还需要把这个元素尾插到链表中,此时并行和串行执行效率差不多。
这里要使用condition_variable,condition_variable我们linux多线程部分已经讲过了,他们用来进行线程之间的互相通知。
条件变量本身不是线程安全的,所以使用的时候需要传锁。
这里的大思路是: 一个线程打印奇数,另一个线程等待,此线程运行完后通知另一个线程,同样另一个线程打印偶数,还是相同的操作。因此我们还要用到wait和notify_one接口。
假设t1打印奇数,t2打印偶数。这里主要解决2个问题:
int main()
{
mutex mtx;
condition_variable cv;
int x = 1;
int n = 100;
thread t1([&, n] {
while( x < n )
{
unique_lock<mutex> lock(mtx);
if (x % 2 == 0) //偶数就阻塞
{
cv.wait(lock); //wait的一瞬间就解锁, 让其他线程运行
}
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();
return 0;
}
上述的代码存在一个这样的小漏洞:
假如x为100时,t1刚判断完时间片到了,然后t2判断也进入了while循环,然后打印并++,后退出循环,释放锁;此时t1又来竞争锁,发现t2退出循环,没有抢锁,然后打印101。
我们使用wait提供的predicate版来优化一下
int main()
{
mutex mtx;
condition_variable cv;
int x = 1;
int n = 100;
thread t1([&, n] {
while (1)
{
unique_lock<mutex> lock(mtx);
if (x >= 100)
break;
//false就wait阻塞, true就不wait阻塞
//这种适用于多生产者多消费者模型
cv.wait(lock, [&x]()->bool {return x % 2 != 0; });
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
thread t2([&, n] {
while (1)
{
unique_lock<mutex> lock(mtx);
if (x > 100)
break;
cv.wait(lock, [&x]()->bool {return x % 2 == 0; });
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。(实际上是类模板)
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
#include
using namespace std;
ret = func(x);
// 上面func可能是什么呢?那么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 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;
// lambda表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
count作为static类型,每一次的地址都不同,所以可以看出,实例化了三次。为了防止这种方式造成的效率低下,使其只实例化一份,让这个地方统一一下,这就利用到了function
function包装器的作用: 对各种可调用对象进行类型统一
实例化三次的代码我们也可以用包装器让其置为同一类型:
#include
int main()
{
// 函数名
cout << useF(function<int(double)>(f), 11.11) << endl;
// 函数对象
Functor ft;
cout << useF(function<int(double)>(ft), 11.11) << endl;
// lamber表达式
cout << useF(function<int(double)>([](double d)->double { return d / 4; }), 11.11) << endl;
return 0;
}
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;
}
};
class Plus
{
public:
Plus(int rate = 2)
:_rate(rate)
{
}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b)*_rate;
}
private:
int _rate = 2;
};
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) {return a + b;}" << endl;
return a + b;
};
cout << f1(1, 2) << endl;
cout << f2(10, 20) << endl;
cout << f3(100, 200) << endl;
}
int main()
{
//调用静态函数
function<int(int, int)> f1 = Plus::plusi;
//function f1 = &Plus::plusi; //这样写也可以
cout << f1(1, 2) << endl;
//调用类的成员函数
function<int(Plus, double, double)> f2 = &Plus::plusd; //推荐写法
cout << f2(Plus(), 1, 2) << endl;
//这种写法下面也没法调用
//function f2 = &Plus::plusd;
//cout << f2(&Plus(), 1, 2) << endl;
Plus pl(3);
cout << f2(pl, 1, 2) << endl;
}
比如leetcode150.逆波兰表达式求值,就可以通过c++11的方式进行编写:
150. 逆波兰表达式求值 - 力扣(LeetCode)
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
map<string, function<int(int,int)>> opFunMap{
{"+", [](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(opFunMap[str])
{
int right=st.top();
st.pop();
int left=st.top();
st.pop();
st.push(opFunMap[str](left,right));
}
else //操作数入栈(要把字符串转为整数)
{
st.push(stoi(str));
}
}
return st.top();
}
};
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置: _ 1 为newCallable的第一个参数,_2为第二个参数,以此类推。
void Print(int a, int b)
{
cout << a << " " << b << endl;
}
int main()
{
Print(10, 20);
//调整参数顺序
auto RPrint = bind(Print, placeholders::_2, placeholders::_1);
RPrint(10, 20);
return 0;
}
运行结果:
class Sub
{
public:
Sub(int rate)
:_rate(rate)
{}
int func(int a, int b)
{
return (a - b) * _rate;
}
private:
int _rate;
};
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
map<string, function<int(int, int)>> opFuncMap =
{
{ "+", [](int i, int j) {return i + j; } },
{ "-", [](int i, int j) {return i - j; } },
{ "*", [](int i, int j) {return i * j; } },
{ "&", bind(&Sub::func,Sub(3), placeholders::_1,placeholders::_2)}
};
for (auto& str : tokens)
{
if (opFuncMap.find(str) != opFuncMap.end())
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(opFuncMap[str](left, right));
}
else
{
// 1、atoi itoa
// 2、sprintf scanf
// 3、stoi to_string C++11
st.push(stoi(str));
}
}
return st.top();
}
};
int main()
{
//调整参数个数
function<int(Sub, int, int)> fSub = &Sub::func;
cout << fSub(Sub(3), 10, 20) << endl;
//绑死第一个参数
function<int(int, int)> fSub1 = bind(&Sub::func, Sub(3), placeholders::_1, placeholders::_2);
cout << fSub1(10, 20) << endl;
//绑死第二个参数
function<int(Sub, int)> fSub2 = bind(&Sub::func, placeholders::_1, 100, placeholders::_2);
cout << fSub2(Sub(3), 20) << endl;
return 0;
}
运行结果: