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