通过前面的学习,我们知道emplace_back和push_back的区别其实不是很大,真正的区别就是:
emplace_back可以传参数包进行构造,而push_back不能传参数包,要么是传插入的对象,或者是进行隐式类型转换



ok,当我们了解了这些,我们就来实现一个emplace_back:
template<class ...Args>
void emplace_back(Args... args)
{
emplace(end(), forward<Args>(args)...);
}
template<class ...Args>
iterator emplace(iterator pos,Args&&... args)
{
Node* cur = pos._node;
Node* newnode = new Node(forward<Args>(args)...);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}ok,当我们实现了emplace_back和emplace接口后,我们就要实现相应的可变模板参数版本的构造节点的代码:
template <class... Args>
ListNode(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(std::forward<Args>(args)...)
{}ok,这样改完之后,我们就可以使用emplace_back进行尾插操作。
但是,当我们加上emplace_back后,push_back就不能使用万能引用版本的尾插,为什么?

总结:有了emplace_back,就不需要这个万能引用版的push_back,直接使用左值版本和右值版本的push_back

emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列


原来的C++类中,有6个默认成员函数:构造、析构、拷贝构造、赋值重载、取地址重载、const取地址重载,最重要的是前4个,后2个用处不大
所谓默认成员函数,就是我们不显示写,编译器会默认生成一个。
C++11中新增了两个默认成员函数——
通过前面的学习,我们知道,对于默认成员函数的学习,我们通常进行一下几点:
ok,那我们先来看移动构造函数——
如果你没有自己实现移动构造函数,并且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,(换句话来说:就是没有实现移动构造、析构函数、拷贝构造、拷贝赋值重载),那么编译器会自动生成一个默认移动构造(若实现了其中一个,编译器都不会生成默认移动构造)
默认生成的移动构造函数:
这就说明:自身需要深拷贝处理的类型,需要自己实现移动构造
通过上面,我们也能看出,移动构造函数试用于复合类型——
就比如,我们之前的MyQueue——
class MyQueue
{
std::stack _pushSt;
std::stack _popSt;
};移动构造函数对于MyQueue就很有意义,MyQueue中没有写析构函数、拷贝构造、赋值重载以及移动构造,MyQueue中就会默认生成一个移动构造函数。
这个默认生成的移动构造函数:
移动构造和拷贝构造的逻辑相似!!!
namespace carrot
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
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);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
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];
if (_str)
{
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)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
class Person
{
public:
Person(const char* name="张三",int age=18)
:_name(name)
,_age(age)
{}
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;//构造
Person s2 = s1;//拷贝构造
Person s3 = std::move(s2);//右值,移动构造
return 0;
}
ok,前两个还是比较容易理解的,我们直接看第三个,第三个就比较有意思——

运行结果——

那我们现在来验证一下,当自定义类型成员中没有移动构造函数,会不会调用自定义类型中的拷贝构造函数——

// 拷贝构造
string(const string& s)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}注意:通过上面的代码,我们也能看出,不要轻易的对一个值进行move操作!!!
默认移动赋值和上面的移动构造完全类似
如果你没有自己实现移动构造函数,并且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,(换句话来说:就是没有实现移动构造、析构函数、拷贝构造、拷贝赋值重载),那么编译器会自动生成一个默认移动赋值(若实现了其中一个,编译器都不会生成默认移动赋值)
默认生成的移动赋值函数:
ok,我们通过代码来看一下——
class Person
{
public:
Person(const char* name="张三",int age=18)
:_name(name)
,_age(age)
{}
private:
carrot::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s4("李四", 20);
s4 = std:: move(s2);
return 0;
}运行一下——


那我们现在来验证一下,当自定义类型成员中没有移动赋值函数,会不会调用自定义类型中的拷贝构造函数——

ok,移动赋值函数这里还有一个特殊的地方——

我们通过调试来看一下——

移动赋值函数的写法和赋值重载的写法相似——
//赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}如果你自己提供了移动构造或者移动赋值,编译器就不会自动提供默认的移动构造或者移动赋值。
如果显示提供析构、拷贝构造、赋值重载中的任意一个,编译器就默认生不成移动构造和移动赋值了,就会去调用拷贝构造或者拷贝赋值(我们不写拷贝构造或者拷贝赋值,编译器会生成默认的拷贝构造),左值可以使用,右值也可以使用
成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表中用这个缺省值进行初始化
比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
如果我不想让别人去调用这个函数,就加上delete,加上之后,别人用了就会报错!!!
代码演示:
class Person
{
public:
Person(const char* name="张三",int age=18)
:_name(name)
,_age(age)
{}
//C++11
//不想让别人去调用这个拷贝构造函数
Person(const Person& p) = delete;
//强制生成默认移动构造函数
Person(Person&& p) = default;
private:
//C++98 中不想让别人去调用这个拷贝构造函数
//在private中只定义这个函数,但不实现
Person(const Person& p);
carrot::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s4("李四", 20);
s4 = std:: move(s2);
return 0;
}

老朋友,这个final和override我们在继承和多态那个章节已经进行了详细讲过了,uu们如果忘了就看一下往期的博客——
下面这张图中圈起来的就是 C++11的STL中的新增容器,但是实际中最有用的是**unordered_map和unordered_set**。
C++11新增容器:array、forward_list(单链表)、unordered_map和unordered_ed(真正有用的就这俩)——

STL中容器的新接口也不少,最重要的就是右值引用和移动语义相关的push / insert / emplace系列接口(插入数据系列的接口);移动构造和移动赋值(雪中送炭),还有initializer_list版本的构造(锦上添花的作用)等,这些前面都讲过了,还有一些无关痛痒的cbegin / cend等需要时查查文档即可。
容器的范围for遍历,这个在容器部分也讲过了
lambda表达式的格式——
[capture-list] (parameters) -> return type { function boby }匿名函数,定义一个没有名字的函数,但是定义出来的又是一个对象!!!
注意:返回值类型写出来更清晰!!!,不写可以自动推导
下面就是一个简单的lambda表达式——
int main()
{
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(2, 3) << endl;
return 0;
}
不知道会不会有uu会有这个疑问:为什么这里可以这么写?
其实lambda表达式的底层是仿函数!!!

lambda表达式中有些部分是可以省略的,有些地方是不可以被省略的:

ok,我们再来看一个lambda表达式的例子——
int main()
{
int x = 1;
int y = 2;
cout << x << ":" << y << endl;
//返回值类型为void,可以省略
auto swap = [](int& x, int& y)
{
//若函数体较长,不必写在同一行
int tmp = x;
x = y;
y = tmp;
};
swap(x, y);
cout << x << ":" << y << endl;
return 0;
}通过上面的示例,我们看到返回值类型是可以省略的,并且如果函数体较长,可以像普通函数一样分行书写

在学习lambda表达式之前,我们使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对来说比较麻烦——
#include<vector>
struct Goods
{
string _name;//名字
double _price;//价格
int _evaluate;//评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3}, { "菠萝", 1.5, 4 } };
return 0;
}假设现在我们想对这些商品进行排序,我们可以根据价格排序,也可以根据评价进行排序,那现在我们是不是要写出相应的比较的仿函数——
#include<vector>
#include<algorithm>
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& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}
};
struct CompareEvaluateLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._evaluate < g2._evaluate;
}
};
struct CompareEvaluateGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._evaluate > g2._evaluate;
}
};
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());
sort(v.begin(), v.end(), CompareEvaluateLess());
sort(v.begin(), v.end(), CompareEvaluateGreater());
return 0;
}这样写还是比较麻烦的,sort的第三个参数不是只可以接收仿函数,还可以接收lambda表达式——

其实我们还可以更简便点,因为lambda表达式本身就是一个匿名对象,我们可以直接将lambda作为第三个参数传过去,lambda表达式作为模板参数


这么来看,lambda表达式很明显比仿函数要简单点!!!
ok,接下来,我们来看一下这个比较陌生的内容——捕捉列表
所谓捕捉列表:就是在lambda函数体中我既可以使用参数传过来的,也可以使用我直接捕捉的
lambda表达式可以写到全局,但很少,因为全局变量不需要捕捉就可以直接使用,没有捕捉的变量,所以如果lambda表达式写在全局,捕捉列表必须为空

所以,通常情况下,lambda表达式写在局部!!!
第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量之间用逗号隔开。[x,y,&z],表示x和y为值捕捉,z为引用捕捉
显示捕捉,用谁就捕捉谁!!!
int main()
{
int a = 0, b = 1, c = 2, d = 3;
//显示捕捉,用谁就捕捉谁
auto func1 = [a, &b]
{
//a++; //值捕捉的变量不能修改
++b;//引用捕捉的对象可以修改
int ret = a + b;
return ret;
};
cout << func1() << endl;
return 0;
}
int main()
{
int a = 0, b = 1, c = 2, d = 3;
//显示捕捉,用谁就捕捉谁
auto func1 = [a, &b]()mutable
{
a++;//值捕捉的变量可以修改,但不影响外面的那个
++b;//引用捕捉的对象可以修改,影响外面的那个
int ret = a + b;
return ret;
};
cout << func1() << endl;
return 0;
}第二种捕捉方式是在捕捉列表中隐式捕捉:
这样我们lambda表达式中用了哪些变量,编译器就会自动捕捉哪些变量
也就是说,参数比较多,但是我都想用,可以隐式捕捉,我们在捕捉列表中写一个 = 表示隐式值捕捉;在捕捉列表中写一个 & 表示隐式引用捕捉,当前域的全捕捉(局部和全局)
int x = 10;
int main()
{
int a = 0, b = 1, c = 2, d = 3;
//捕捉列表中为 = 表示当前域的全部值捕捉
//全局域的值不需要捕捉,也不能捕捉,可以直接用
auto func2 = [=]
{
return a + b + c+x;
};
cout << func2() << endl;
//捕捉列表中为 & 表示当前域的全部引用捕捉
auto func3 = [&]
{
++a;
++b;
++c;
return a + b + c + x;
};
cout << func3() << endl;
return 0;
}
所谓混合捕捉,就是在捕捉列表中混合使用隐式捕捉和显示捕捉。
例如:
当使用混合捕捉时,第一个元素必须是&或者=,并且:
也就是说不能自相矛盾!!!
int x = 10;
int main()
{
int a = 0, b = 1, c = 2, d = 3;
//混合捕捉1
//a和b是值捕捉,其余元素为引用捕捉
auto func4 = [&, a, b]
{
//a++;//值捕捉,不能修改
//b++;//值捕捉,不能修改
c++;//引用捕捉,可以修改
d++;//引用捕捉,可以修改
return a + b + c + d;
};
cout << func4() << endl;
//混合捕捉2
//a和b引用捕捉,其余元素值捕捉
auto func5 = [=, &a, &b]
{
a++;//引用捕捉,可以修改
b++;//引用捕捉,可以修改
//d++;//值捕捉,不能修改
//c++;//值捕捉,不能修改
return a + b + c + d;
};
cout << func5() << endl;
return 0;
}注意:在混合捕捉中,&或者=必须在第一个!!!
int x = 10;
int main()
{
int a = 0, b = 1, c = 2, d = 3;
static int y = 20;
auto func6 = [a, b, c, d]
{
return a + b + c + d + x+y;//局部的静态变量和全局变量可以直接使用
};
cout << func6() << endl;
return 0;
}
但是加上mutable就相当于去掉了const属性,就可以修改了,但是这种修改不会影响外面被捕捉的对象,只能影响里面的对象,因为值捕捉是一种拷贝——

加上mutable后,参数列表即使为空也不能省略!!!但是一般情况下,不会加上mutable!!!
使用引用捕捉,内部被修改也会影响外面的被捕捉的对象!!!
ok,在上面的这些情况中,我们的lambda表达式都不是写在成员函数中的,那如果我们在成员函数写这个lambda表达式,会是什么情况——
class A
{
public:
void func()
{
int x = 2, y = 3;
auto func1 = [=]//隐式值捕捉
{
++_b;//可以修改成员变量
return x + y + _a + _b;
};
cout << func1() << endl;
}
private:
int _a = 10;
int _b = 20;
};
int main()
{
A a;
a.func();
return 0;
}在上面所展示的代码中,我们在A这个类中的成员函数中写了一个lambda表达式,并在表达式中使用隐式值捕捉,能不能成功运行呢?

为什么这里可以运行成功呢?_a和_b不是成员变量吗?
其实这是因为隐式捕捉(=或者&,都可以),可以把成员变量给捕捉过来,相当于把this指针捕捉了
那如果我们使用显示捕捉,还能运行吗?
class A
{
public:
void func()
{
int x = 2, y = 3;
auto func1 = [x,y]//显示捕捉,值捕捉x和y
{
return x + y + _a + _b;
};
cout << func1() << endl;
}
private:
int _a = 10;
int _b = 20;
};
int main()
{
A a;
a.func();
return 0;
}
ok,此时我们就无法正常运行,这里我们显示捕捉,只捕捉了x和y,所以我们在lambda函数体中,我们只能使用x和y,不能使用成员变量。
但是现在我想使用成员变量,怎么办?可以显示捕捉this指针,捕捉了this指针,我们就可以访问成员变量
class A
{
public:
void func()
{
int x = 2, y = 3;
//显示捕捉this指针,就可以使用成员变量了
auto func1 = [x,y,this]
{
return x + y + _a + _b;
};
cout << func1() << endl;
}
private:
int _a = 10;
int _b = 20;
};
int main()
{
A a;
a.func();
return 0;
}并且当我们捕捉了this指针,还可以修改成员变量——
class A
{
public:
void func()
{
int x = 2, y = 3;
//显示捕捉this指针,就可以使用成员变量了
auto func1 = [x,y,this]
{
++_a;//可以修改成员变量
return x + y + _a + _b;
};
cout << func1() << endl;
cout << _a << endl;
}
private:
int _a = 10;
int _b = 20;
};
int main()
{
A a;
a.func();
return 0;
}
ok,除了这里可以修改成员变量,隐式捕捉也可以修改(因为捕捉的也是this)

lambda的原理和范围for很像,范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个lambda以后,编译器会生成一个对应的仿函数的类。
在语法层,我们拿不到lambda表达式的类型,编译器可以拿到,所以lambda表达式并不是没有类型,而是我们拿不到,因为不同的编译器生成的也不同,不同的编译器会生成对应的仿函数的类
仿函数的类名是编译器按一定规则生成的,保证不同的;ambda生成的类名不同,lambda表达式的参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质是生成的仿函数的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参 (很重要,是一个面试题!!!)
当然隐式捕捉,编译器要看使用哪些就传哪些对象

ok,在operator()中可以使用x很正常,x是形参,实参传给形参,那我怎么使用a和b呢?
ok,捕捉列表中的a和b就变成这个lambda类的成员变量——

😯,现在我们就能理解——lambda的捕捉列表本质是生成的仿函数的成员变量
那我们该怎么理解——捕捉列表的变量都是lambda类构造函数的实参

也就是说,在这个lambda中有一个构造函数——
