文章目录
- 1. 列表初始化
- 1.1 列表初始化的使用格式
- 1.1.1 内置类型
- 1.1.2 自定义类型的列表初始化
- 1.2 列表初始化的本质
- 2. 变量类型的推导
- 2.1 auto 关键字
- 2.2 decltype类型推导
- 3. 范围for
- 4. final与override
- 5. 智能指针
- 6. 新增容器
- 6.1 静态数组array
- 6.2 单向链表 forward_list
- 6.3 unordered系列
- 7. 默认成员函数控制
- 7.1 显示缺省函数
- 7.2 删除默认成员函数
- 8. 右值引用
- 8.1 区分左值和右值
- 8.2 左值引用和右值引用
- 8.3 交叉引用
- 8.4 右值引用的应用
- 8.4.1 实现移动构造,移动赋值
- 8.4.2 给中间临时变量起别名
- 8.4.3 实现完美转发
- 9. lambda表达式
- 9.1 lambda表达式的格式
- 9.2 lambda表达式的底层原理
- 10. 线程库
- 10.1 线程库的认识
- 10.1.1 < atomic > 原子性操作。
- 10.1.2 < condition_variable> 条件变量
- 10.1.3 < mutex > 锁
- 10.1.4 < thread > 线程
- 10.2 线程的创建和使用
- 10.2.1 创建一个线程对一个数进行 ++ 操作
- 10.2.1.1 简单实现
- 10.2.1.2 函数传参的一些细节(局部变量)
- 10.2.2 多线程对一个数进行 累加的操作
- 10.2.2.1 简单实现
- 10.2.2.2 锁的引入
- 10.2.2.3 原子性操作库 < atomic >的引入
- 10.2.2.4 lambda表达式进行捕捉
- 10.2.3 锁的考验
- 10.2.3.1 锁的使用常见问题
- 10.2.3.2 lock_guard与unique_lock
- 10.2.4 两个线程交替打印,一个打印奇数 一个打印偶数(100以内)
- 10.2.4.1 简易实现(失败版本)
- 10.2.4.2 条件变量
- 11. 可变参数列表
- 12. 包装器
- 12.1 可调用对象
- 12.2 function包装器(一般包装)
- 12.3 function包装器(bind包装)
- 12.3.1 调整参数顺序
- 12.3.2 固定默认的参数
- 12.3.3 调整参数个数
前言: C++11 较C++98 更新许多有用的库函数,以及一些新的特性,使得C++能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。本文,主要讲解C++ 11 相较C++ 98 做出的一些更新。
1. 列表初始化
为什么要有列表初始化呢?它的出现 使得 初始化 自定义对象时,更加的方便高效。
- C++98 中 什么是支持列表初始化的?数组。
- C++11 中 什么是支持列表初始化的 ?所有的内置类型,以及用户自定义的类型。
举个例子:
在C++98下,在vector容器中插入值,需要一个一个的push_back()。
// C++ 98
int a[] = { 1,2,3,4,5 };
vector<int> aa;
for (int i =1 ;i<=5;i++)
{
aa.push_back(i);
}
但C++11下,支持了 列表初始化:
vector<int> a1 = { 1,2,3,4,5 };
这样初始化高效了许多,当然还支持很多容器 去利用 列表初始化:
vector<int> a1 = { 1,2,3,4,5 };
list<int> l1 = { 1,2,3,4,5 };
map<int, string> m1 = { {1,"hh"},{2,"ww"},{3,"ll"} };
string s1 = { "wwwww" };
自定义对象,也是可以利用列表初始化的:
class A
{
private:
int _a;
int _b;
public:
A(int a,int b)
:_a(a),
_b(b)
{}
};
int main()
{
A _a = {1,2};
}
1.1 列表初始化的使用格式
上面 只是 展示一下 列表初始化的使用,接下来 我们 来具体的说明一下。
上面 都是 用了 加 =
的格式,其实也可以不用加 =
。
1.1.1 内置类型
// 内置类型
int a1{ 3 };
char s1{ 'w' };
// 数组
int a[]{ 1,2,3,4,5 };
char s[]{ "ssssss" };
// 动态数组
int* p1 = new int[]{1, 2, 3, 4, 5};
char* p2 = new char[] {"ssssss"};
// 标准容器
vector<int> v{ 1,2,3,4,5 };
map<int, string> m{ {1,"hh"},{2,"ww"} };
1.1.2 自定义类型的列表初始化
class A
{
private:
int _a;
int _b;
public:
A(int a,int b)
:_a(a),
_b(b)
{
}
};
int main()
{
A _a{1,2};
}
1.2 列表初始化的本质
支持列表初始化用的是 initialzer_list
:
上面也标注了,它是C++11 才开始有的。
它的成员函数:
也就是说:
{1,2,3,4,5} 这种列表,它是一个 initialzer_list
对象。
我举个例子:
initializer_list<int> il;
il = { 1,2,3,4,5 };
cout << il.size() << endl;
cout << il.begin() << endl;
cout << *il.begin() << endl;
cout << il.end() << endl;
cout << *(il.end()-1) << endl;
size() 是 列表的大小,begin() 指向第一个元素;end() 指向 最后一个元素的下一个位置。
运行结果如下:
所以说:列表初始化本质是 将列表对象initialzer_list
中的值 赋值到 指定对象 里。
它不是传统意义上的 拷贝构造,咱们熟知的拷贝构造是 拷贝同类型的对象,这个比较特殊,拷贝的是initialzer_list里的值。
我们去官方文档中查看一些:
vector重载的构造函数
string:
list:
还有很多,不一 一 展示了。
我们来画图理解一下:
vector<int> v = { 1,2,3,4,5 };
先是形成了 {1,2,3,4,5}的initialzer_list的对象,然后 再去 调用vector重载的拷贝构造:
但是 有个疑问 :自定义对象中,并没有重载A (initializer_list< value_type > il) ,是怎么实现的 列表拷贝?
发生了隐式转换,即便你没有主动写 列表拷贝构造,类里会有默认生成的 供你使用,默认的构造函数,这大家应该懂。
比如:我使用 关键字 explicit
,修饰构造函数,使得不能发生隐式转换,看看会有什么效果
explicit A(int a,int b)
:_a(a),
_b(b)
{}
A a_ = {1,2};
可以看到 直接 报错了,复制列表初始化 …… 不能 ……:
这就反向的证明了 发生隐式转换 。
2. 变量类型的推导
变量类型推导怎么说呢? 它 是比较方便的,比如 遇到比较复杂的类型 ,自己写起来很不方便,直接利用 类型推导 就可以了。
2.1 auto 关键字
auto 可以 根据后面的值,进行类型推导 :
当然上面的例子用的很少,关键 是推导那些复杂的类型:
map<int, string> m;
std::map<int, string> ::iterator i =m.begin();
这样的迭代器 比较 复杂吧,看 如果是用 auto呢?
auto i = m.begin();
2.2 decltype类型推导
为什么要有 decltype 推导呢?按理说 有auto 推导就比较方便。
因为auto使用的前提:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型,也就是 说 某些在 编译过程中才初始化的类型,auto 无法进行推导,所以 就有了 decltype类型推导。
不能使用auto的例子:
(1)这个函数调用就明显不可以:
int add(auto x, auto y)
{
return x + y;
}
(2)auto当 模板
vector<auto>s;
(3) auto数组的初始化
auto i[] = { 1,2,3,4 };
等等例子,终归到底,使用auto推导,auto声明的类型已经初始化。
然后看 decltype推导的例子:
int a = 1;
int b = 2;
decltype(a+b) c;
cout << typeid(c).name() << endl;
decltype(a+b) 相当于 推导出 a+b的类型,然后用推导出的类型,定义了一个变量 c。然后 利用 typeid (c).name() ,知晓它的类型:
运行结果:
对吧,其实 变量类型推导 是容易理解的。
3. 范围for
范围for 又被称为 语法糖,因为用起来比较的甜(好用)。
它的底层其实 就是 迭代器的使用:
比如我要 遍历vector ,那么我可以使用下标遍历,也可以用迭代器,也能用 范围for。
vector<int>vc{ 1,2,3,4,5,6,7,8,9,10 };
vector<int>::iterator i = vc.begin();
while (i != vc.end())
{
cout << *i << endl;
i++;
}
上面使用迭代器版本的,现在我们来使用范围for:
for (auto& e : vc)
{
cout << e << endl;
}
明显下面的比较简单,看看运行结果:
- 范围for广泛用于 遍历容器的操作,它使得遍历 变得简单。
- 它的本质是利用的迭代器,所以 要求迭代器支持begin(),end(),!=,++等操作。
4. final与override
- final 放在类后,表示该类不能被继承;放在虚函数后,表示该虚函数不能被重写
- override 用于检查 派生类虚函数 是否重写了基类的被override修饰的虚函数,如果没有重写就会报错。
举个例子:
class person
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是全价" << endl;
}
};
class student : public person
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是半价" << endl;
}
};
这是一个简单的单继承,而且还实现了多态。
- 先来验证final :
class person final
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是全价" << endl;
}
};
class student : public person
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是半价" << endl;
}
};
class person
{
public:
virtual void buy_ticekt() final
{
cout << "买的票,是全价" << endl;
}
};
class student : public person
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是半价" << endl;
}
};
- 再来验证 override:
class person
{
public:
virtual void buy_ticekt()
{
cout << "买的票,是全价" << endl;
}
};
class student : public person
{
public:
virtual void buy_ticekt(int a =1) override
{
cout << "买的票,是半价" << endl;
}
};
综上理解一下:
- final 相当于限制了 某个类不能被继承,或者类中的某个虚函数不能被重写,用于父类中
- override 相当于 提个醒,提醒子类要对父类的某个虚函数进行重写,没重写会报错,它用于子类中。
5. 智能指针
至于,智能指针,后续会给出文章链接,还没肝完。
6. 新增容器
新增的容器 有array ,forward_list 以及unordered系列。
6.1 静态数组array
模板参数 T是定义的静态数组的元素类型,N是 元素个数。
比如: array<int, 10> a;
就是定义了一个定长的数组,它的元素类型是int,包含10个元素。
有点奇怪,明明我定义 一个静态的数组,其是方法是有的:
#define N 10
int main()
{
int arr[N];
}
定义动态数组可以使用vector。结果 又搞出来一个array。
这个确实被人吐槽过,但它存在必然还是有点价值的,比如它提供了些接口函数:
有迭代器,容量,还支持随机访问等,对吧,其实用的也不多。
简单评价:食之无味,弃之可惜。
6.2 单向链表 forward_list
有来个单向链表forward_list ,本来是用的双向链表 list,为什么又要整出来个单向链表呢?
它怎么说呢?有些时候,它是要比list高效的,
比如:
存储相同个数的同类型元素,单链表耗用的内存空间更少,空间利用率更高,并且对于实现某些操作单链表的执行效率也更高。
但是单向链表 只支持 从前往后 遍历 ,因为单向嘛。它支持头插,头删,也支持任意位置的插入,只不过 插入也有点奇怪。
(1) 构造函数
int main ()
{
// constructors used in the same order as described above:
std::forward_list<int> first; // default: empty
std::forward_list<int> second (3,77); // fill: 3 seventy-sevens
std::forward_list<int> third (second.begin(), second.end()); // range initialization
std::forward_list<int> fourth (third); // copy constructor
std::forward_list<int> fifth (std::move(fourth)); // move ctor. (fourth wasted)
std::forward_list<int> sixth = {3, 52, 25, 90}; // initializer_list constructor
std::cout << "first:" ; for (int& x: first) std::cout << " " << x; std::cout << '\n';
std::cout << "second:"; for (int& x: second) std::cout << " " << x; std::cout << '\n';
std::cout << "third:"; for (int& x: third) std::cout << " " << x; std::cout << '\n';
std::cout << "fourth:"; for (int& x: fourth) std::cout << " " << x; std::cout << '\n';
std::cout << "fifth:"; for (int& x: fifth) std::cout << " " << x; std::cout << '\n';
std::cout << "sixth:"; for (int& x: sixth) std::cout << " " << x; std::cout << '\n';
return 0;
}
运行结果:
(2) 迭代器
可以看到没有 那种 cbegin(),cend() 之类的迭代器,因为单向链表嘛,所以不支持反向迭代器。
(3) 容量
(4) 访问
每次只能访问头节点,然后 通过头节点,一个一个往后找。
(5) 操作
- assign,用新元素替换容器中原有内容。
- emplace_front ,在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。
- push_front ,pop_front 是头插,头删
- emplace_after,在指定位置之后插入一个新元素,并返回一个指向新元素的迭代器。和 insert_after() 的功能相同,但效率更高
- insert_after() ,注意这个是 在指定位置 之后 插入 元素。
- erase_after(),删除容器中某个指定位置或区域内的所有元素。
6.3 unordered系列
大家可以参考我这篇博客unordered系列。
7. 默认成员函数控制
默认的成员函数,大家应该知道,我们定义一个类,类中会生成默认的成员函数。
C++98 是有六个默认的成员函数:
- 构造函数
- 析构函数
- 拷贝构造
- 赋值重载
- 取地址重载
- const取地址重载
c++11 多加了俩个:
- 移动拷贝构造函数
- 移动赋值运算符重载
C++98的六个默认成员函数生成的原则是:只要我们不显示的定义成员函数,那么 就会 生成类内 默认的成员函数。
C++11的移动拷贝构造函数默认生成的条件很复杂:
- 如果没有实现移动构造函数,且没有实现析构函数,拷贝构造,拷贝重载中的任意一个,那么编译器会默认生成一个移动构造。
- 如果没有实现移动赋值重载函数,且没有实现析构函数,拷贝构造,拷贝重载中的任意一个,那么编译器会默认生成一个移动赋值重载。
- 如果实现移动构造或是移动拷贝重载的任意一个,那么编译器不会自动提供拷贝构造和拷贝赋值。
听上去就感觉很复杂,所以 C++11 允许程序去 控制 是否 生成默认的成员函数,而不是只依据以上的规则,可以人为控制。
7.1 显示缺省函数
在C++11中,可以在默认函数定义或者声明时加上=default
,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
比如:
class A
{
private:
int _a;
int _b;
public:
A(int a,int b)
:_a(a),
_b(b)
{}
};
int main()
{
A();
return 0;
}
这种情况下,因为我们显示的实现了 构造函数,所以默认的构造函数就不生成了。
现在运行就会报错:
我们可以怎么解决以上问题呢?
(1) 可以重载一个无参数的构造函数
A()
{
_a =0;
_b =0;
}
这种方式在C++98中常见,但是不够安全。
(2) 使用关键字default
A() = default;
这就默认生成了构造函数。
7.2 删除默认成员函数
上面是指定生成默认成员函数,这个就是要 删除默认成员函数,也可以说是 禁止生成默认的成员函数。
-
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。这样确实是没生成默认的构造函数,但是有些复杂。
-
在C++11中更简单,只需在该函数声明加上
=delete
即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
比如:
这是一个类A,它的拷贝构造,赋值重载都用默认生成的。
class A
{
private:
int _a;
int _b;
public:
A(int a,int b)
:_a(a),
_b(b)
{}
A() = default;
};
int main()
{
A a = {1,2};
A b(a);
A c = a;
return 0;
}
先试试 C++98 时的做法:
class A
{
private:
int _a;
int _b;
public:
A(int a,int b)
:_a(a),
_b(b)
{}
A() = default;
private:
A(const A& tem);
A& operator = (const A tem);
};
int main()
{
A a = {1,2};
A b(a);
A c = a;
return 0;
}
很明显会报错:
再试试c++11的做法:
A(const A& tem) = delete;
A& operator = (const A tem)= delete;
报错信息:
8. 右值引用
可以说 C++11 中 右值引用的实现,是很成功的,它提高了 c++的效率,哎呀,这说的有点笼统,但想表达就是 c++11的右值引用 很重要。
8.1 区分左值和右值
- 左值可以出现在 符号的左边,也可以出现在符号的右边 并且可以取地址
- 右值可以出现在 符号的右边,不能出现再符号的左边 并且不可以取地址
比如:
int a;
const int b = 10;
int* p = &a;
int* p1 = new int(2);
以上都是 左值,最直接的就是 可以对它们取地址。
20;
a + b;
add(a + b);
以上都是 右值,不能对它们取地址。
8.2 左值引用和右值引用
C++ 98提出引用,只能对左值引用,就相当于对 左值起别名,它的底层实现是指针。
C++ 11 支持的右值引用。
比如:
int a;
const int b = 10;
int* p = &a;
int* p1 = new int(2);
int& s = a;
const int& s1 = b;
int*& m = p;
int*& m1 = p1;
以上都是左值引用,就是对左值起别名。
比如:
20;
a + b;
int&& n = 20;
int&& n1 = a + b;
这就是 右值引用,用的是&&
这个符号,上面 &
是左值引用用的符号。
8.3 交叉引用
就有个问题,左值引用可以引用 右值吗?还有就是 右值引用可以引用 左值吗?
其实 我们在 C++98中,就用过 左值引用 来引用 右值,但不是直接引用:
比如 我们使用的容器string ,我们是不是也用过这样的方式去构造string ,string("hhhhhh")
这样的方式,传参 传的就是 一个右值,但是我们用的是左值引用来接收的:
所以得出第一个答案:
左值引用可以引用 右值,但是需要是 const 左值引用来接收 右值、
比如:
const int& n3 = a + b;
const char& n4 = 'w';
右值引用可以引用 左值吗?说实话,理论上不可以,但是 有一个骚操作可以帮助我们把左值变成右值。怎么说呢?其实还是 右值引用 去引用右值,但是这个右值 是左值变的。那么右值到底可不可以 引用左值?这个答案,我想说 :能,但不完全能。
这个将左值变为右值的函数就是 move()
。
int&& n5 = move(a);
8.4 右值引用的应用
上面的右值引用的到底有什么作用?直观来说 ,可以支持 给右值 取别名。
- 实现移动构造,移动赋值
- 给中间临时变量起别名
- 实现完美转发
8.4.1 实现移动构造,移动赋值
什么是移动构造,移动赋值?为什么要移动构造,移动赋值?怎么使用移动构造,移动赋值?
移动构造和移动赋值:是c++11 中新增的俩个默认成员函数。
它们的出现,减少了类中的深拷贝,提高了效率。
比如 类的临时变量返回值问题,注意不是引用返回,会用到 移动构造,移动赋值、
先给出一个简易的string类,来帮助我们学习这块知识:
namespace ly
{
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)
, _size(0)
, _capacity(0)
{
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()
{
//cout << "~string()" << endl;
delete[] _str;
_str = nullptr;
}
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);
push_back(ch);
return tmp;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
很明显,上面并没有 实现 移动构造和移动赋值,也没有默认生成的。
我们来看一下:如果是简单的函数 返回 一个string临时对象,会发生 什么?
ly::string func3()
{
ly::string str("hello world");
//cin >> str;
return str;
}
int main()
{
ly::string ret = func3();
return 0;
}
发生了一次深拷贝,其实是发生两次深拷贝,编译器优化后是一次深拷贝。
图解:
但是 编译器做了优化,变成了一次深拷贝:
但是一次深拷贝的代价,也不小。
移动拷贝构造登场:
首先,fun3()函数的返回值,是一个右值。右值 分为纯右值,将亡值。一些表达式 一般都是纯右值,但是对于函数来讲,出来函数作用域的临时变量就会被销魂,如果函数的返回值是函数域内的临时变量,那么 这个临时变量就是一个将亡值,也是一个右值。
返回这个右值 ,需要在内存空间开辟一个空间,拷贝它的值,这是一次深拷贝;然后返回给接收方,又是一次深拷贝。这讲的是没优化的哈。
有没有一种可能?我不做深拷贝,这个将亡值在 出作用域的时候,把值给交换走,只是简单的交换,不做深拷贝?
是可以实现的,那就需要移动拷贝构造
,我的参数 需要是一个右值引用来识别右值,简单得来说就是 实现一个新的拷贝构造版本,这个拷贝构造完成的是 值交换,它适用于右值的拷贝构造。
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
// 仅仅是交换,比深拷贝好多了
this->swap(s);
}
再来运行一下程序:
但是还有一个问题:
如果main函数中这样写,没有编译器的优化:
ly::string ret;
ret = func3();
运行结果:
怎么回事?又出现了深拷贝。
-
因为有 赋值拷贝,这也是深拷贝。
-
func3() 返回的是一个右值,通过移动构造使得返回时不需要做深拷贝,而是资源转移,但是这次没有编译器的优化,它的资源是转移到临时空间,然后 再从临时空间 通过深拷贝 赋值给 ret。
怎么解决这个问题呢?
移动赋值登场:
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 转移资源" << endl;
this->swap(s);
return *this;
}
先看运行结果:
图解: 来图解一下,以上全部过程
学到这里,可能有人还是不太懂右值拷贝,右值赋值的意义,我问个问题:如果是一个左值,咱们去拷贝,赋值,敢不敢直接 就是 交换一下值?
肯定是不敢的,因为左值,人家只是给你拷贝一下,赋值一下,人家还存在呢,你直接把人家的值也给交换了,肯定不行,况且 左值的拷贝,赋值,压根就不能改变左值的值,因为人家传参带着 const 。
就是因为,是右值,将亡值,它马上就不存在了,所以交换一下值,没什么毛病,而且不用深拷贝了,很香。
8.4.2 给中间临时变量起别名
其实这个咱们在上面已经用过了,就是给右值起别名。
string s1;
string s = s1 + 'w';
string&& ss = s1 + 's';
string s 是 用 s1 + ‘w’ 构造的新对象,string &ss 是 s1+‘s’ 的别名。
那有个问题:ss是右值的别名,那么 ss的属性是右值还是左值? 验证这个问题,我们可以取一下ss的地址,看看 可不可以取到地址,如果能取到地址,说明 ss是左值,反之为右值。
答案是可以取到地址,说明 右值引用后,退化为 一个左值。
那么从这里我们也可以看出右值引用的本质,原本右值是不可以取地址的,右值引用其实就是将右值存到一个新建的同类型变量中,变为一个左值。我看书时,有的将 右值引用,使得右值的生命周期变长了,可以这么理解,但是 不过就是把它的值 报存到一个左值变量中罢了。
8.4.3 实现完美转发
有了上面的认识,我们来看看 什么叫做完美转发?听上去还蛮高大上的。
再讲完美转发前,我们先认识一个概念:万能模板
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
这是函数模板,它的模板参数是T&& t
。不要认为 在模板中 这代表 只能匹配右值。这是万能模板,它既可以匹配左值,也可以匹配右值。
那么我们来验证一下,万能模板:
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;
}
运行结果:
结果有些出乎意料,全都匹配到左值上去了。
提问: 万能模板失效了?传参是右值,为什么会匹配到左值上?
- 因为,上面也说过,右值引用的本质,是把右值的值保存到一个左值中,再往下传参,传的就不是右值了,而是一个左值。
怎么解决这个问题?那就是用完美转发
。
所谓完美转发,就是 为了保存右值的属性。用的函数是forward()
。有人可能会想:既然它退化为一个左值,那么我用move() 也可以将这个左值再转换为右值。但是其实 很欠缺考虑,因为人家 万能模板 ,你传右值是右值引用,你传左值是左值引用。如果人家 传来就是左值,一个左值引用,你再给人家转为右值。是不是就不符合我们预期了。
所以 要用 forward()。这个函数不会影响左值引用的属性,只是将 右值 的属性 保持下去。
那么我们来改一下代码:
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
来看运行结果:
对吧,都匹配正确了。
完美转发的应用,还比较广泛,比如 容器插入 右值,我们用右值引用接收,那么需要保证右值的属性就需要 用forward() 把右值 属性保持下去。
9. lambda表达式
为什么要有lambda表达式?必然是了为了更加便捷的写代码。
仿函数大家应该都知道,它是一个提供了operator () 重载的类,比如 std::sort()要自定义比较就会用到仿函数,还有 优先级队列 std::priority_queue 等等,要自己控制的时候比较的时候都需要用到仿函数。
但是 会不会有点繁琐?假如 排序,我要根据多个方面排序,那我就得实现多个仿函数去实现。
举个例子:
struct product
{
int _price;
int _size;
string _name;
};
struct compre_price
{
bool operator()(const product& s1,const product& s2)
{
return s1._price > s2._price;
}
};
int main()
{
product s[] = { {10,20,"手套"},{230,43,"鞋子"},{3,12,"笔"} };
sort(s,s+sizeof(s)/sizeof(s[0]),compre_price());
return 0;
}
上面是根据价格去排序,我们看看效果怎么样:
排序前:
排序后:
很明显根据价格,排成了降序。
那么我现在要求根据 名字 来排序,好嘛,还得实现一个仿函数:
struct compre_name
{
bool operator()(const product& s1, const product& s2)
{
return s1._name > s2._name;
}
};
然后再传参给 sort() 进行排序,这显然是 繁琐的,有没有办法 不去实现仿函数 ,就能完成上述功能呢?
lambda表达式登场:
我们先来写代码,后面 会讲其使用规则已经底层原理。
int main()
{
product s[] = { {10,20,"手套"},{230,43,"鞋子"},{3,12,"笔"} };
sort(s, s + sizeof(s) / sizeof(s[0]), [](const product& s1, const product& s2)
->bool
{
return s1._price > s2._price;
});
return 0;
}
就是这样的,这是以价格做比较完成的,现在我们实现以名字为比较的版本:
int main()
{
product s[] = { {10,20,"手套"},{230,43,"鞋子"},{3,12,"笔"} };
sort(s, s + sizeof(s) / sizeof(s[0]), [](const product& s1, const product& s2)
->bool
{
return s1._name > s2._name;
});
return 0;
}
对吧,只是对代码稍作改动就可以了。
9.1 lambda表达式的格式
[capture-list] (parameters) mutable -> return-type { statement }
- [capture-list] 是捕捉列表,它用于捕捉上下文变量,供lambda表达式使用。
-
[] ,空,表示不进行变量捕捉,但是不可以省略。
-
[val] ,表示 以值传递的方式,捕捉某个具体的变量
-
[=] ,表示值传递方式捕获所有父作用域中的变量(包括this)
-
[&val],表示引用传递的方式捕获某个变量
-
[&],表示引用传递的方式捕获所有变量
-
以上可以组合使用,但是不允许重复使用。
比如:[a,&b] 意思是 值传递捕获 a,引用捕获 b;[=,&a] 意思是值传递捕获其他变量,引用捕获a;但是 [=,a] 或是 [&,&a] 都是不可以的,因为这是重复值捕获,或是 重复引用捕获。
- (parameters) 是参数列表 ,可以理解成普通的函数参数,如果需要传参 那么就需要声明;如果 不需要传参,那么就可以给空,或者直接连
()
都省略掉。 - mutable ,它是一个修饰;默认情况下 lambda函数是一个const属性的函数,如果加上mutable可以改变其 常量性。如果用 mutable修饰,那么参数列表必须存在。
int m = 0;
int n = 0;
[&, n](int a) {m = ++n + a; } (4);
cout << m << endl << n << endl;
比如上述代码,n是值传递的,默认是const类型,那么{}中 ++n ,是不可以的。
但是加上 mutable 后,就可以了:
[&, n](int a)mutable{m = ++n + a; } (4);
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。注意是返回值类型,返回值类型确定,这种情况下,也可以省略。
- { statement }: 这是lambda表达式中的函数体,函数体中可以使用函数参数,也可以使用捕捉的变量。注意函数体为空可以,但是
{}
不可以省略。
综上给出lambda表达式 的几种省略形式:
[]{} // 最简单的lambda表达式,但没意义哈
[=]{cout<<a+b<<endl;} // 省略参数列表,和返回类型
[=](int a) {cout<<a<<endl;} // 省略返回类型,因为没有返回值嘛
/// 以上都默认省略 mutable
注意: lambda表达式 不可以相互赋值,即便类型相同,但是可以赋值给,类型相同的指针
9.2 lambda表达式的底层原理
其实底层原理,我们来想一想:std::sort()的,第三个参数 是传仿函数,为什么传lambda表达式也可以完成传参?有没有可能 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);
// lamber
auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
r2(10000, 2);
}
这是函数对象的反汇编:
它是构造了一个函数对象,然后调用函数对象的 operator() 重载。
这是lambda表达式的反汇编:
它是构造了一个lambda表达式对象,它是一个仿函数类,然后调用lambda表达式中的 operator()重载。
嗯,这就是lambda表达式的底层原理,它其实也是仿函数,只不过是封装到了lambda表达式类中。
10. 线程库
一个编程语言,它的标准库中的函数,可以说是它的宝贵资源之一。C++11 封装了线程库,也就是说 线程也可以面向对象操作了。这是方便程序员操作的,封装成一个类,是比我们自己去调用函数舒服的。我之前一直在Linux环境下 ,进行线程,多线程的学习。windows的线程实现和Linux还不一样,它有自己的线程库函数。通过这一小章,我们来在windows下,进行线程操作。
10.1 线程库的认识
10.1.1 < atomic > 原子性操作。
构造一个原子性的数据,这个数据可以很多类型,但是不能是浮点数类型。
想维持数据的原子性,一般需要 加锁,但是 如果只是一个数据要保持原子性,那么就需要用到 < atomic >。
atomic<int> b(0);
b++;
int a = 0;
a++;
比如 多线程对 b和a 进行++,操作,那么b肯定是保持原子性的,a的原子无法保证。
可以看反汇编:
b的话是去调用 atomic 类中的 operator++。a是三句汇编进行++操作。
如果原子性不懂的话,这里就理解一下吧。因为a++的汇编要执行三步,那么多线程执行时,就有可能被打断 ,从而导致 ++ 进行到一半,被别的线程去执行,等到这个线程再开始执行时发现数据已经变了。这就是 原子性没有保持。
为什么 atomic类中的 operator++是 原子性的呢?这个我没查阅,毕竟人家这个类 就是为了保持原子性的操作的,所以大家只要知道,用atomic构造出的对象,它的操作是原子性的就行了。至于应用后面,会用到的。
10.1.2 < condition_variable> 条件变量
学过多线程的老铁,对这个肯定不陌生。条件变量配合着 互斥锁,就能完成多线程的同步和互斥。
我们来看看 这些接口:
它的构造函数:
所以说无参构造就可以了。
它的wait(),也就是在某个条件下开始等待:
wait()重载了两个 版本,但是都得传参进一个锁,这是为什么呢?
- 我们来看一下官方文档:
条件变量一般是 在锁得保护下,条件变量等待时,还需要占用锁资源嘛?答案是不需要占用。所以 条件变量下 进行等待时,需要把锁资源释放掉,看上面也写着。
它的唤醒函数:
10.1.3 < mutex > 锁
这些 是 锁 ,自旋锁,等……
10.1.4 < thread > 线程
线程创建,线程等待……,这些接口 会用就OK了。
10.2 线程的创建和使用
线程创建允许构建无参的,也可以传右值进行构造。
比如:
thread t1;
thread t2("可调用对象","参数");
可调用对象包括:函数指针(函数名),函数对象(仿函数),匿名函数( lambda表达式)。
线程的创建后,需要主线程去等待,等待的方式有两种:
- join(),线程等待,主线程回收线程的退出信息
- detach(),线程分离,主线程不需要回收其退出信息,线程运行结束后,直接溜就行
举个例子吧:
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;
}
运行结果:
10.2.1 创建一个线程对一个数进行 ++ 操作
10.2.1.1 简单实现
int number = 0;
void run()
{
number++;
}
int main()
{
thread t1(run);
t1.join();
cout << number << endl;
}
我们来看看结果:
确实是完成了 ++ 操作。
10.2.1.2 函数传参的一些细节(局部变量)
但是这里有个问题就是,用到了全局变量,全局变量是不希望用到工程中的。所以改成对变量++操作,看看效果如何:
void run(int number)
{
number++;
}
int main()
{
int x = 0;
thread t(run, x);
t.join();
cout << x << endl;
}
看看结果:
发现并没有完成++操作,这是什么原因?因为是传值调用,所以不会对局部变量产生影响,C语言基础好些,应该能反应出来,说:应该传址调用,也就是传指针。但是都到C++了,咱们多给几种方案:
(1) 传地址
void run1(int* number)
{
(*number)++;
}
int main()
{
int x = 0;
thread t1(run1,&x);
t1.join();
}
(2) 传引用
void run2(int& number)
{
number++;
}
int main()
{
int x;
thread t2(run2, std::ref(x));
t2.join();
}
注意 : 传引用这里用到了一个 函数ref(),它就是传x的引用;这里不能直接传x的引用,只能通过这个函数进行转换。
(3) 利用lambda表达式进行捕捉
int main()
{
int x =0;
thread t3([&x]() {x++; });
t3.join();
}
对吧,这里一个引用捕捉就完成任务了。
10.2.2 多线程对一个数进行 累加的操作
10.2.2.1 简单实现
上面是一个线程对一个数进行 ++ 操作,现在创建 五个线程对一个数 进行 累加,这样是不是存在线程安全问题呀,用什么解决?利用锁,来维护。
我们先来演示不加锁的情况:
void run(int &x,int n)
{
int i = 0;
while (i < n)
{
x++;
i++;
}
}
int main()
{
int x = 0;
vector<thread> vt;
vt.resize(5);
for (int i = 0; i < 5; i++)
{
vt[i] = thread(run,ref(x),1);
}
for (int i = 0; i < 5; i++)
{
vt[i].join();
}
cout << x << endl;
return 0;
}
运行结果:
因为是累加到1,然后总共五个线程所以累加最终结果是 5;现在我让每个线程累加这个数字多些,让它出现 问题:
就改一行代码vt[i] = thread(run,ref(x),100000);
看结果:
10.2.2.2 锁的引入
很奇怪吧,按理说应该是 500000,结果是这样的。非常不人性,那么解决方案是上锁。
mutex mx;
void run(int &x,int n)
{
int i = 0;
while (i < n)
{
mx.lock();
x++;
i++;
mx.unlock();
}
}
把锁上在循环里面,或者上到循环外面都可以,但是效率有差别,这个一会分析,我们先来看看 是否解决了上面问题:
可以,上锁就可以解决这块的问题。
其实把锁上到外面或者是里面,对于这个程序来说本质上就是串行和并行的区别:
10.2.2.3 原子性操作库 < atomic >的引入
但是可不可以不用锁来管这件事,毕竟我只是对 一个数据 进行 累加操作,昂,可以,那就是用原子性操作库 < atomic > :
void run(atomic<int>& x,int n)
{
int i = 0;
while (i < n)
{
x++;
i++;
}
}
int main()
{
atomic<int>x = 0;
vector<thread> vt;
vt.resize(5);
for (int i = 0; i < 5; i++)
{
vt[i] = thread(run,ref(x),100000);
}
for (int i = 0; i < 5; i++)
{
vt[i].join();
}
cout << x << endl;
return 0;
}
对吧,就是这样,对一个数据进行原子保护,我建议用< atomic >。
10.2.2.4 lambda表达式进行捕捉
上面对锁的使用,依旧是用到全局变量,不太好对吧,所以改成局部变量的,这就需要用到
lambda表达式了:
int main()
{
int x = 0;
int n = 100000;
int j = 0;
vector<thread> vt;
vt.resize(5);
mutex mx;
for (int i = 0; i < 5; i++)
{
vt[i] = thread([&mx, &x,n,j]()mutable {
mx.lock();
while (j < n)
{
x++;
j++;
}
mx.unlock(); });
}
}
10.2.3 锁的考验
10.2.3.1 锁的使用常见问题
其实对锁的使用,挺考验人的,你得考虑死锁的问题,或者你得记得释放锁。尤其是这个释放锁,很可能就没释放掉,为啥没释放掉锁,可能还很纳闷,毕竟我已经写了unlock()了。
我列举俩种可能释放锁失败的例子:
-
代码在释放锁前返回:
-
抛异常,导致锁未释放:
void func(vector<int>& v, int n, int base, mutex& mtx)
{
try
{
// 死锁
for (int i = 0; i < n; ++i)
{
mtx.lock();
cout << this_thread::get_id() << ":" << base + i << endl;
// 失败了 抛异常 -- 异常安全的问题
v.push_back(base+i);
// 模拟push_back失败抛异常
if (base == 1000 && i == 888)
throw bad_alloc();
mtx.unlock();
}
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
int main()
{
thread t1, t2;
vector<int> vec;
mutex mtx;
try
{
t1 = thread(func, std::ref(vec), 1000, 1000, std::ref(mtx));
t2 = thread(func, std::ref(vec), 1000, 2000, std::ref(mtx));
}
catch (const exception& e)
{
cout << e.what() << endl;
}
t1.join();
t2.join();
return 0;
}
比如以上代码,就是 一个线程,出现异常,但是没有释放锁,导致死锁问题,为了模拟这个问题,代码里面主动让它抛了一个异常:
报错了:
怎么解决,很简单,在捕获到异常后,释放掉锁:
catch (const exception& e)
{
cout << e.what() << endl;
mtx.unlock();
}
10.2.3.2 lock_guard与unique_lock
通过上面的了解,发现了,锁比较难控制,有么有办法,让锁这东西自动去释放呢?就像类一样,不需要的时候,它会去调用它的析构函数。
其实是有解决方法的,那就是:lock_guard与unique_lock。
它俩是C++11采用RAII的方式对锁进行了的封装,也就是 锁的释放 靠它们自己决定,不需要我们手动的去释放。
比如上面那个抛异常的代码我们可以这样写:
void func(vector<int>& v, int n, int base, mutex& mtx)
{
try
{
// 死锁
for (int i = 0; i < n; ++i)
{
lock_guard<mutex>tx(mtx);
// unique_lock<mutex>tx(mtx);
cout << this_thread::get_id() << ":" << base + i << endl;
// 失败了 抛异常 -- 异常安全的问题
v.push_back(base+i);
// 模拟push_back失败抛异常
if (base == 1000 && i == 888)
throw bad_alloc();
}
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
我们模拟实现一下lock_guard,它其实就是利用类对象,在释放资源时会自动调用析构函数这一个特性:
template<class T>
class lockguard
{
public:
lockguard(T& mtx)
:_mtx(mtx)
{
_mtx.lock();
}
~lockguard()
{
_mtx.unlock();
}
private:
T& _mtx;
};
注意
这里模拟实现的细节还挺多:
- 私有成员是 一个 引用,它是为了到时候可以析构传来的锁,所以需要是引用
- 构造函数的参数我们一般都是 const T& ,但是这里需要是 T& ,不加const因为我们要释放锁,不能设置为const属性。
- 析构函数,将锁释放掉。
图解:
10.2.4 两个线程交替打印,一个打印奇数 一个打印偶数(100以内)
讲这个主要是想 带大家认识 条件变量,一起加油!!!
10.2.4.1 简易实现(失败版本)
int main()
{
int n = 100;
int i = 0;
mutex mtx;
// 偶数-先打印
thread t1([n, &i, &mtx]{
while (i < n)
{
unique_lock<mutex> lock(mtx);
cout <<this_thread::get_id()<<":"<<i << endl;
++i;
}
});
// 奇数-后打印
thread t2([n, &i, &mtx]{
while (i < n)
{
unique_lock<mutex> lock(mtx);
cout << this_thread::get_id() << ":" << i << endl;
++i;
}
});
// 交替走
t1.join();
t2.join();
return 0;
}
看看结果:
很明显不是交替打印,所以需要使用条件变量,来控制这块。
10.2.4.2 条件变量
我们先来学习下,它的接口:
wait():
第一个参数 是 一个unique_lock< mutex >&lck 锁,对吧,这好理解,必须得传入锁,当进程进入wait()状态,它会把锁资源释放掉,等它被唤醒,又会立马获得锁。
第二个参数 是 一个可调用对象,它得返回一个bool值,这个bool值就是我们用来判断是否要被唤醒的条件,而且 wait()底层中,对这个可调用对象是一个while循环判断,防止被伪唤醒。while (!pred()) wait(lck);
这俩个唤醒函数,一个是唤醒在此条件变量下等待的一个线程,另一个是唤醒在此条件变量下等待的所有线程,使用起来比较简单。
好,有了以上基础,我们就来模拟实现:
int main()
{
int i = 0;
int n = 100;
mutex mtx;
condition_variable mtc;
bool flage = false;
thread t1([n,&i,&mtx,&mtc,&flage]()
{
while (i < n)
{
unique_lock<mutex> tx(mtx);
mtc.wait(tx, [&flage]{return flage;});
cout << this_thread::get_id() << ":" << i << endl;
i++;
flage = false;
mtc.notify_one();
}});
thread t2([n, &i, &mtx, &mtc, &flage]()
{
while (i < n)
{
unique_lock<mutex> tx(mtx);
mtc.wait(tx, [&flage] {return !flage; });
cout << this_thread::get_id() << ":" << i << endl;
i++;
flage = true;
mtc.notify_one();
}});
t1.join();
t2.join();
return 0;
}
这对lambda表达式的应用需要懂哈,不熟悉的话,会写的很难受。
然后难点就是 wait()中 第二个参数的编写了,也是lambda表达式哈,条件变量先设置为 false:
(1) 线程t1 wait()返回判断为false,然后线程t1 就会陷入等待状态,被阻塞
mtc.wait(tx, [&flage]{return flage;});
注意是flage
(2)线程2 wait()返回判断为true,然后线程t2不被阻塞
mtc.wait(tx, [&flage] {return !flage; });
注意是 !flage
(3)线程2 执行一次后,将flage 设为true,并唤醒线程1,因为flage为true,所以线程1不被阻塞,线程2被阻塞。
(4) 线程1 执行一次后,将flage设为false,并唤醒线程2,因为flage为flase,所以线程2不被阻塞,线程1被阻塞。
(5) 就是这样完成的交替打印。
11. 可变参数列表
可变参数列表是如何实现的呢?其实是通过模板来实现的。
我们最早接触的可变参数列表无非就是 printf(),
我们通过代码来学习去块内容:
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(Args) << endl;
cout << sizeof...(args) << endl << endl;
for (size_t i = 0; i < sizeof...(Args); ++i)
{
// 无法编译,编译器无法解析
cout << args[i] << "-";
}
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
先讲点基础知识:
template <class ...Args>
这就是模板参数包。void ShowList(Args... args)
这就是函数形参的参数包sizeof...(args)
这是求参数的个数
我现在的要求就是 我传过去参数,要求 函数可以把它们打印出来。很简单的要求哈。但是其实涉及 如何解参数包这一任务,本文给出几种 解包的方式。
先看一下:上面的代码可以完成任务嘛?
结果是不能,说明不能够 通过下标这种方式来解包。
- 通过写递归函数来解包
//递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl << endl;
}
解析并打印参数包中每个参数的类型及值
template <class T, class ...Args>
void ShowList(T val, Args... args)
{
cout << typeid(val).name() << ":" << val << endl;
ShowList(args...);
}
//
int main()
{
ShowList(1, 'A', std::string("sort"));
return 0;
}
它是一步一步的来解包,知道剩下一个参数时,去调用递归结束函数,然后开始返回。
通过调试窗口来看看,递归的过程:
一直递归到 终止函数,然后开始返回:
通过画图来理解:
- 利用数组解包
template <class T>
void PrintArg(T val)
{
cout << typeid(T).name() << ":" << val << endl;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
这个数组利用的 逗号表达式,逗号表达式是以最后一个值作为返回值的。
所以(PrintArg(args), 0)
的返回值 是 0,那么 (PrintArg(args), 0)...
,它会被展开成 (PrintArg(args1), 0),(PrintArg(args2), 0) ……(PrintArg(argsn), 0)。为什么要这样做呢?其实是因为 c++的数组只能保持一种类型的数据,所以利用逗号表达式,使得数组 既可以执行函数 又能最终以 0 被保存。
- 其实还是利用数组解包,但是换个方式
template <class T>
int PrintArg(T val)
{
T copy(val);
cout << typeid(T).name() << ":" << val << endl;
return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... };
cout << endl;
}
上面数组是利用的是 逗号表达式 ,目的是让数组中的元素一致,但是利用函数的返回值,也可以作到这一点。上面的逗号表达式,函数返回值,使得数组中的元素都是 int整型,当然这个类型是根据数组定的,我们当然还可以定义成其他类型的数组。
比如:
char arr[] ={(printArg(args),'a')...};
template <class T>
char PrintArg(T val)
{
T copy(val);
cout << typeid(T).name() << ":" << val << endl;
return 'a';
}
char arr1[] = {printArg(args)...};
总结:可变参数列表的实现,难点不在于定义一个多参数模板,而是在于如何拿出参数,也就是 解包。给出的方案总的来说有两个,也就是 递归(注意写终止函数),数组(注意数组的元素类型一致)。
12. 包装器
包装器,它是将可调用对象包装成容器,方便程序员去操作。为什么要封装成容器呢?因为在某些情况下,需要对函数 进行一些特殊的操作,但是 重载函数比较 费劲,比如想要操作函数的参数等。还是得看代码,才能 理解包装器 的妙处。
12.1 可调用对象
先得搞清楚什么是可调用对象:
- 函数指针,普通函数
- lambda表达式,匿名函数
- 仿函数,函数对象
函数指针用起来比较晦涩,难用,所以用的较少。 lambda表达式 用起来挺挺方便。仿函数是多用于模板参数,也很方便。
12.2 function包装器(一般包装)
function包装器,它是常用于将可调用对象,封装成一个容器。它方便在可以 使得 可调用对象变为统一的类型 function< >,还有就是 它还能方便我们去简化代码。
先来讲讲它的用法:
function 它的本质是一个类模板。
原型:
template<class Ret,class... Args>
class function<Ret(Args)>
Ret 是函数返回类型,Args 是函数的参数列表。
我们来看一段代码:
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);
}
这是个函数模板,里面有一个静态变量 count 它可以帮助我们看到,实例化出多少份函数。
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;
}
};
class A
{
public:
A() = default;
static double func(double a)
{
return a / 4;
}
double func_(double a)
{
return a / 5;
}
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double{ return d / 4; }, 11.11) << endl
return 0;
}
上面的代码,应该会实例化出三份函数,因为传的可调用对象都不一致。我们来看看结果:
可以看到 count 的地址都不一样,所以明显是 实例化出来三份,但是 我有个问题: 需要实例化出三份嘛?细心点可以发现,函数指针,函数对象,匿名函数 它们三个的 返回值,函数参数列表的类型都是完全一样的。我可以用function 进行包装,使得它们三个类型都是 function类型,从而使得useF() 函数模板,实例出一份函数。
function<double(double)> f1 = f;
cout << useF(f1, 11.11) << endl;
function<double(double)> f2 = Functor();
cout << useF(f2, 11.11) << endl;
function<double(double)> f3 = [](double d) ->double { return d / 4; };
cout <<useF(f3,11.11) << endl;
看运行结果:
很明显是实例化成了一份usef() 函数。
function包装器可以包装函数,当然也可以包装类内的函数,这里有些注意事项:
这是一个类A,它有静态成员函数func() 和 成员函数func_();
class A
{
public:
A() = default;
static double func(double a)
{
return a / 4;
}
double func_(double a)
{
return a / 5;
}
};
使用function 进行包装:
function<double(double)> f4 = A::func;
cout << f4(11.11) << endl;
function<double(A,double)> f5 = &A::func_;
cout << f5(A(), 11.11) << endl;
类中静态成员函数的包装,只需要指定类域就完事了;但是成员函数的包装,需要将类名作为函数参数列表的第一个参数,因为 成员函数的参数里有this指针。并且 类域前需要加上符号&
。使用时,还得传一个类的匿名对象。
12.3 function包装器(bind包装)
function包装器就是对函数的包装,包装后的对象的功能,用法和以前保持一致,这不是包装后的类型变为了 function<>;但是bind包装, 它对函数进行包装后形成的新对象,可能用法和之前的函数不一样了,对,它可能会对参数做出一些调整,比如 加一个默认参数,改变参数顺序等等。所以 bind包装后,它可以 对原有函数的用法 做出一些调整。
12.3.1 调整参数顺序
int SubFunc(int a, int b)
{
return a - b;
}
int main()
{
function<int(int, int)> ff1 = bind(SubFunc, placeholders::_1, placeholders::_2);
function<int(int, int)> ff2 = bind(SubFunc, placeholders::_2, placeholders::_1);
cout << ff1(1, 2) << endl;
cout << ff2(1, 2) << endl;
}
ff1和ff2都是bind的同一个函数,但是我对参数的顺序做出了调整。
我们来看结果:
12.3.2 固定默认的参数
int SubFunc(int a, int b)
{
return a - b;
}
int main()
{
function<int(int)> ff3 = bind(SubFunc,placeholders::_1,10);
cout << ff3(2) << endl;
}
这就相当于每次传进来的数据都 减去 10。
注意事项:
也就是说,你绑定的参数列表中 只有一个参数,那么后面placeholders也只能操作_1,表示第一位参数。
假如这样搞:
function<int(int)> ff3 = bind(SubFunc,placeholders::_2,10);
毫无疑问会报错:
out of bounds,也就是超出范围了。
上述我们在稍微操作一下,要求 是 10 - 传参,也就是换一下顺序,那也简单了吧:
function<int(int)> ff3 = bind(SubFunc,10,placeholders::_1);
所以说,对参数都调整是可以组合使用的。
12.3.3 调整参数个数
其实上面也是调整参数的个数,但下面讲的例子还不太一样,我们这次是要调整类的中函数的参数个数。我们之前用function普通包装,那么还得传参一个默认对象对吧,感觉有点小麻烦。来用bind包装操作一些
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
function<int(Sub, int, int)> f4 = &Sub::sub;
cout << f4(Sub(), 10, 3) << endl;
function<int(int, int)> f5 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << f5(10, 3) << endl;
}
就是在bind中默认绑定一个类的匿名对象。操作很简单。
但是 我想出点难题,我要求在此基础上,继续调整函数参数:函数的调用 默认是一个参数,要求每次都是参数数据 减去 5。
答案:
function<int(int)> f6 = bind(&Sub::sub, Sub(), placeholders::_1, 5);
对吧。