C++11简介
在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1),使得C03这个名字已经取代了C98称为C11之前的最新C标准名称。不过由于C03(TC1)主要是对C98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C98/03标准。从C0x到C11,C标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C98/03,C11则带来了数量可观的变化,其中包含了约140个新特性,以及对C03标准中约600个缺陷的修正,这使得C11更像是从C98/03中孕育出的一种新语言。相比较而言,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标准。所以最终定名为C11。
列表初始化{}
在C++98中,标准允许使用花括号对数组或者结构体元素进行统一的列表初始值设定。
struct A
{
int a;
}
int main()
{
int arr[] = {1, 2};
A a = {1};
return 0;
}
C++中定义了几种不同的初始化方式,现在我们要将一个int类型的变量a初始化为0.
int a = 0;
int a = {0};
int a{0};
int a(0);
int main()
{
int a = 0;
int a1 = { 0 };
int a2{ 0 };
int a3(0);
int arr[]{ 1,2,3 };
int arr1[5]{ 0 };
cout << a << endl;
cout << a1 << endl;
cout << a2 << endl;
cout << a3 << endl;
return 0;
}
输出结果 0 0 0 0
。
C++11扩大了花括号括起的列表的适用范围,现在可以用于所有的内置类型和用户自定义的类型,使用初始化列表时,可以添加等号,也可以不添加。
这种初始化方式称为列表初始化。
创建对象的时候也可以使用列表初始化的方式来调用构造函数进行初始化。
class Date
{
public:
Date(int y, int m, int d)
:_y(y)
,_m(m)
,_d(d)
{
cout << "Date(int y, int m, int d)" << endl;
}
private:
int _y;
int _m;
int _d;
};
int main()
{
Date date(2023, 10, 8);
Date time{2024, 10, 8};
return 0;
}
std::initializer_list
有时候,我们不知道应该提前向某个函数传递几个参数,为了能够编写出能处理不同类型数量实参的参数,C++11新标准提供了两种主要的方式:如果所有的参数类型相同,可以传递一个名为 initializer_list
的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板(暂时先不说)。
initializer_list
用于表示某种特定类型值的数组,它定义在同名的头文件当中。并且用法和vector类似。有size,begin,end等。。在定义 initializer_list
的时候,必须指定模板类型。
#include <initializer_list>
int main()
{
initializer_list<int> il = { 1, 2, 3 };
initializer_list<int> il1 = il;
initializer_list<int>il2(il1);
il.size();
auto it = il.begin();
auto ed = il.end();
return 0;
}
和vector不一样的是 initializer_list
对象中的元素永远是常量值,并且无法改变它对象中元素的值。

使用场景
initializer_list
一般是作为构造函数的参数,C++!1对STL中的不少容器增加 initializer_list
作为参数的构造函数,这样初始化容器对象就方便了,也可以作为 operator=的参数
,这样就可以用大括号赋值。

#include <list>
#include <vector>
#include <map>
int main()
{
vector<int> v = { 1,2, 3, 4 };
list<int> lt = { 1, 2 };
map<string, string> dict = { {"sort","排序"},{"insert","插入"} };
return 0;
}
auto
在C98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C11中废弃了auto原来的用法,可以让编译器自动分析表达式的类型。auto可以自动推导,这就很显然,auto定义的变量,一定要有初始值。
int i = 10;
auto a = &i;
cout << typeid(a).name() << endl;
输出:int *
。
decltype
从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值进行初始化,C++11中引入了 decltype
,它的作用是选择并返回操作数的数据类型,在这个过程中,编译器分析并得到它的类型,但是并不会实际计算表达式的值。
template<class T1, class T2>
void F(T1 a, T2 b)
{
decltype(a * b) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
int x = 1;
double y = 2.2;
decltype(x * y) ret;
decltype(&x) p;
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}

typeid(var).name()
可以输出var的类型。如果var是引用类型则输出结果不会带 &
。
int i = 0;
int& a = i;
// decltype((i)) e;
cout << typeid(decltype((i))).name()<< endl;
cout << typeid(decltype(a)).name() << endl;
这个e会报错,因为 如果变量名上加上了一对括号,则得到的类型与不加括号时会有不同。如果使用的是不加括号的变量,则得到的结果就是表达式的类型,相反,加上括号得到的结果会是引用类型。

如果 decltype
中的内存是指针解引用,那么这个变量就是引用类型,必须初始化。

decltype((var))像这样得到的结果永远是引用。为单括号的时候,只有当var本身是一个引用的时候才是引用。
nullptr
在之前我们会用到名为NULL的预处理变量来给指针赋值,它的值是0,这样就可能会带来一些问题,因为0既能表示整形常量,也能表示指针常量。C++11中新增了 nullptr
,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
范围for循环
C++11中新增了一个范围for,这个东西非常的好用,如果你想对string中的每个字符做点什么事,范围for挺合适的。这种遍历语句会遍历给定序列中的每个元素。
for (declaration : expression)
{
statement
}
expression部分是一个对象,用于表示一个序列。
declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为下一个变量。
其实就是用迭代器实现的。
int main()
{
string str = "hao hao xue xue xi";
for (auto t : str)
{
cout << t << " " << endl;
}
return 0;
}
左值引用和右值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),可以获取它的地址,可以对他赋值,左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边。定义时const修饰符后的左值,不能给他赋值,但是可以对他取地址。左值引用就是给左值的引用,给左值取别名。
int main()
{
// 左值
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);
}
需要注意的是右值不能取地址的,但是给右值取别名之后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说,不可以对10取地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1.如果不能修改rr1,可以用const引用。
int main()
{
int&& rr1 = 10;
rr1 = 20;
cout << rr1 << endl;
} // 输出 20;
无论左值引用还是右值引用,都是给对象取别名。
左值
左值引用只能引用左值,不能引用右值,但是const左值既可以引用左值,也可以引用右值。
int a = 10;
int& ra = a;
int&& rra = 10;
const int& ra1 = rra;
右值
右值引用只能引用右值,不能引用左值,但是右值引用可以引用move以后的左值。
虽然不能将一个右值引用直接绑定到一个左值上,但是我们可以显示的将一个左值转换为对应的右值引用类型。通过调用一个move的新标准库函数来获得绑定到做之上的右值引用,此函数定义在头文件utility中。
move函数调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。应该使用std::move,避免潜在的名字冲突
int main()
{
int a = 10;
int&& r = std::move(a); // 右值引用可以引用move以后的值
cout << r << endl;
} // 输出 10
右值引用使用场景和意义
上面说了左值引用和右值引用,那么为什么C++11还要提出右值引用呢?
如果我们用前面自己实现的string做题,在很多情况下,会发生对象拷贝(接收函数返回值,拷贝等)在某些情况下,这些对象拷贝后就会立刻销毁。如下面代码的情况,虽然编译器会进行优化,但是如果b非常的大,在拷贝的时候,也会减少性能。
string func()
{
string b;
cin >> b;
return b;
}
int main()
{
string a = func();
return 0;
}
提出右值引用后,我们可以 移动对象。
移动构造函数
与拷贝构造函数类似,但移动构造函数是从对象中交换资源而不是拷贝资源,且第一个参数是该类类型的一个右值引用 。但是也不能随便的交换资源,要保证所交换的对象处于 即将被销毁的,销毁该对象对整个程序没有影响的,在完成交换之后,源对象指向的就是即将被销毁的对象。在此过程中,并不会分配任何空间。而是会接管上面代码中 b的空间。这样就完成了对象的移动操作,此对象继续存在,将要销毁的对象继续销毁。
自己先造一个轮子,用库里的看不出来。
using namespace std;
namespace haifan
{
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(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
HaiFan::string func()
{
HaiFan::string a = "aaaa";
return a;
}
int main()
{
HaiFan::string str = func();
return 0;
}
比如上面的代码,如果没有移动构造,程序运行的期间,执行的都是深拷贝,有了移动构造函数,就会大大的提高性能。

移动赋值运算符
如果说,我们要将一个非常大的对象(即将被销毁的)赋值给源对象,这会拉低性能,如果我们用右值引用,则可以直接将这两个对象的资源进行交换,这样就大大的提高了性能。
int main()
{
HaiFan::string str;
str = "aa";
return 0;
}

可变参数模板
一个可变参数模板就是一个接收可变参数的模板函数或模板类。刻板数目的参数被称为参数包。存在两种参数包:模板参数包,表示0个或多个模板参数。函数参数包,表示0个或多个函数参数。
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << sizeof...(args) << endl; // 插件args中的参数个数
}
int main()
{
ShowList(1); // 0
ShowList(1,2); // 1
ShowList(1,2,3); // 2
ShowList(1,2,3,4); // 3
return 0;
}
传递的参数和包里面的个数不一样是因为,value匹配的第一个参数。
如果args是一个可变参数,那我们可以对其直接进行一些操作吗?
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << sizeof...(args) << endl; // 插件args中的参数个数
for (int i = 0; i < sizeof...(args); i++)
{
cout << args[i] << endl;
} // 报错信息 必须在上下文中扩展参数包
}
展开参数包的两种方式
template <class T>
void ShowList(T value)
{
cout << value << " ";
cout << endl;
}
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1,2);
ShowList(1,2,3);
ShowList(1,2,3,4);
return 0;
}
要对参数包进行操作,可以通过递归展开参数包。
template< class T>
void PrintArg(T t)
{
cout << t << " ";
}
template<class ...Args>
void CppPrint(Args... args)
{
int a[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
CppPrint(1, 2, 3);
return 0;
}
这样也可以展开参数包,通过数组,编译器会把后面的自动推出来,逗号表达式取得是最右面的值,利用0进行初始化。
完美转发
模板中的 && 万能引用
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用 既可以接收左值,也可以接收右值
// 实参左值 他就是左值引用(引用折叠)
// 实参右值 他就是右值引用
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;
}

在运行代码之后,全是左值引用。
我们可以用库里面的一个函数 std::forward,当用于一个指向模板参数类型的右值引用函数参数T&&时,forward会保持实参类型的所有细节。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
完美转发,t是左值引用,保持左值属性,t是右值引用,保持右值属性。

STL中的一些变化

在C++11中新增了 <array>, <forward_list>, <unordered_map>, <unordered_set>
这几个新容器,但是实际最有用的是 unordered_map/set
这两个容器,都是用哈希来实现的,查找等效率都是O(1)。
如果我们在仔细的看,会发现基本每个容器中都增加了一些C++11的方法,但其实很多用的都比较少。比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义并不大,因为begin和end也是可以返回const迭代器的,这些都属于锦上添花的操作。
实际上C++11更新后,容器中增加的新方法最后用的接口插入接口函数的右值引用版本。

平常我们用的 尾插push_back,在 vector<pair<int,int>> v的情况下,我们尾插需要先用make_pair构造,然后再插入,emplace_back利用参数包,直接完成构造。
有了右值引用版本,在某些情况下,可以大大的提升性能。
lambda表达式
如果我们要对某种自定义类型进行排序,直接用sort函数是不可以的,我们可以用仿函数解决,也可以用lambda表达式解决。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
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(), [](const Goods& g1, const Goods& g2) {
return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate; });
}
像这种自定义类型,如果要排序,每次比较的逻辑都不一样,这给程序猿带来了一些不便。
lambda表达式语法
书写格式 [capture-list](paramenters)mutable->return-type{statement}。
- lambda表达式各部分说明
capture-list\] 捕捉列表, 该列表总是出现在lambda函数的开始位置,编译器根据\[\]来判断接下来的代 码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters) 参数列表, 与普通函数的参数列表一致,如果不需要传递参数,则可以连同()一起省略
mutable 默认情况下,与普通函数总是一个const函数,mutable可以取消其常量性。使用该修饰 符时,参数列表不可以省略(即参数为空)。
-\>returntype 返回值类型。 用追踪返回类型形式声明函数的返回值类型, 没有返回值时此部分可省 略,返回值类型明确情况下,也可以省略,由编译器对返回类型进行推导。
{statement} 函数体。 在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
**注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:\[\]{},该lambda函数不能做任何事情。**
**lambda函数中可以调用其他函数吗?**
void func()
{
cout << "123" << endl;
}
int main()
{
int a = 0;
int b = 2;
// 全局的函数可以使用
auto add = [](int x, int y)->int {func(); return x + y; };
auto swap1 = [](int& x, int& y) {
int c = x;
x = y;
y = c;
add(x, y);
}; // 局部的不可以使用
return 0;
}
1. 捕捉列表说明
捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式传值还是传引用。
\[var\] 表示值传递方式捕捉变量var
\[=\] 表示值传递方式捕获所有父作用域中的变量(包括this)
\[\&var\] 表示引用传递捕捉变量var
\[\&\] 表示引用传递捕捉所有父作用域的变量(包括this)
\[this\] 表示值传递方式捕捉当前的this指针
**auto add = \[\](int x, int y)-\>int {func(); return x + y; };**
**注意:**
**父作用域指包含lambda函数的语句块**
**语法上捕捉列表可由多个捕捉项组成,并以逗号分割**
**如 \[=,\&a\] 以引用传递的方式捕捉变量a和b,值传递的方式捕捉其他所有变量**
捕捉列表不允许变量重复传递,否则就会导致编译错误
\[=, a\] = 已经以值传递的方式捕捉了所有变量,捕捉a重复
在块作用域以外的lambda函数捕获列表必须为空
在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译错误
lambda表达式之间不能相互赋值,即使看起来类型相同
int main()
{
int a = 1;
int b = 2;
//auto swap1 = [a, b]() {
// int c = 0;
// a = b;
// b = c;
//}; // 这里会导致编译错误,这是因为捕获的变量是const属性
auto swap1 = [a, b]() mutable {
int c = 0;
a = b;
b = c;
};
cout << a << " " << b << endl;
return 0;
} // 输出结果 1, 2
为什么在调用玩swap1函数之后结果却没有改变,这是因为捕获变量之后,是临时变量。不会影响外面的a和b。
*** ** * ** ***
想要真正的改变a和b,我们可以用引用捕捉。
auto swap1 = [&a, &b]() mutable {
int c = 0;
a = b;
b = c;
};
这里的\&可不是把地址取出来,而是引用的意思。
*** ** * ** ***
auto swap1 = [&]() mutable {
cout << a << " " << b;
}; // 输出结果 1, 2
auto swap2 = [=]() mutable {
cout << a << " " << b;
}; // 输出结果 1, 2
*** ** * ** ***
cosnt对象能不能被捕捉。
int a = 1;
int b = 2;
const int c = 3;
auto swap1 = [&]() mutable {
cout << a << " " << b;
c++; // 报错,能被捕捉,但是不能修改
}; // 输出结果 1, 2
*** ** * ** ***
继上面说,要想在让函数调用局部变量中的函数,可以用捕捉列表捕捉局部函数,从而完成调用
auto add = [](int a, int b) { return a + b; };
auto swap2 = [add, a, b]() mutable {
cout << add(a, b) << endl;
};
## 新的类功能
原来C++类中,有6个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
~Person()
{}
private:
haifan::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
//Person s4;
//s4 = std::move(s2);
return 0;
} // 用上面造的轮子,输出结果是 深拷贝,如果把Person析构函数注释掉,输出的是移动构造
// Person(Person&& p) = default; 定义了default关键字之后,编译器就不会在生成=default的那个
// 默认成员函数
// Person(const Person& p) = default; 再定义这个之后,可以让编译器强制的执行string中的移动构造
// 和移动赋值
## 包装器
function包装器也叫做适配器。C++中的function本质是一个类模板,也是一个包装器。
定义在头文件functional中。
ret = func(x)
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
// 是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
// 为什么呢?我们继续往下看
template