【C++进阶】C++11的新特性 | 列表初始化 | 可变模板参数 | 新的类功能
👓️博主简介:
文章目录
- 前言
- 一、C++11 的发展历史
- 二、列表初始化
- 2.1、C++98 传统的 { }
- 2.2、C++11 中的 { }
- 2.3、C++11 中的 std::initializer_list
- 三、可变模板参数
- 3.1、基本语法及原理
- 3.2、包扩展
- 3.3、empalce 系列接口
- 四、新的类功能
- 4.1、默认的移动构造和移动赋值
- 4.2、成员变量声明时给缺省值
- 4.3、default 和 delete
- 4.4、final 与 override
- 总结
前言
我们 C++11 实现了非常多的新特性,使我们的很多操作都方便了很多的同时,我们的 C++ 的运行效率也得到了提升,我们一起来了解了解这些新特性吧。
一、C++11 的发展历史
C++11 是 C++ 的第二个主要版本,并且是从 C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月 12 日采纳前,人们曾使⽤名称 “C++0x”,因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故而这是迄今为止最长的版本间隔。从那时起,C++ 有规律地每 3 年更新一次。
二、列表初始化
2.1、C++98 传统的 { }
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;
}
2.2、C++11 中的 { }
- C++11 以后想统一初始化方式,试图实现一切对象皆可用 { } 初始化,{ } 初始化也叫做列表初始化。
- 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
- { } 初始化的过程中,可以省略掉 =
- C++11 列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset 多参数构造的对象时,{ } 初始化会很方便
class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}
private:int _year;int _month;int _day;
};// ⼀切皆可用列表初始化,且可以不加 =int main()
{// C++11⽀持的// 内置类型⽀持int x1 = { 2 };// ⾃定义类型⽀持// 这⾥本质是用{ 2025, 1, 1}构造⼀个 Date 临时对象// 临时对象再去拷贝构造 d1,编译器优化后合二为一变成 { 2025, 1, 1} 直接构造初始化 d1// 运行一下,我们可以验证上面的理论,发现是没调⽤拷贝构造的Date d1 = { 2025, 1, 1 };// 这⾥ d2 引用的是 { 2024, 7, 25 } 构造的临时对象const Date& d2 = { 2024, 7, 25 };// 需要注意的是 C++98 支持单参数时类型转换,也可以不⽤ {}Date d3 = { 2025 };Date d4 = 2025;// 可以省略掉=int x2{ 2 };Date d6{ 2024, 7, 25 };const Date& d7{ 2024, 7, 25 };// 不⽀持,只有{}初始化,才能省略=// Date d8 2025;vector<Date> v;v.push_back(d1);v.push_back(Date(2025, 1, 1));// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐v.push_back({ 2025, 1, 1 });return 0;
}
2.3、C++11 中的 std::initializer_list
-
上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个 vector 对象,我想用 N 个值去构造初始化,那么我们得实现很多个构造函数才能支持, vector v1 = {1,2,3}; vector v2 = {1,2,3,4,5};
-
C++11 库中提出了一个 std::initializer_list 的类, auto il = { 10, 20, 30 }; // the type of il is an initializer_list ,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束
我们之前的各种容器中都是支持我们的 initializer_list 的构造的。
// STL 中的容器都增加了一个 initializer_list 的构造
vector(initializer_list<value_type> il, const allocator_type& alloc = allocator_type());
list(initializer_list<value_type> il, const allocator_type& alloc = allocator_type());
map(initializer_list<value_type> il, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type());
它的原理就是有一个遍历,然后我们直接把遍历的每一个元素都尾插即可实现我们的列表插入。就像下面 vector 实现的那样。
template <class T>
class vector
{
public:typedef T* iterator;vector(initializer_list<T> l){for (auto e : l)push_back(e)}
private:iterator _start = nullptr;iterator _finish = nullptr;iterator _endofstorage = nullptr;
};// 另外,容器的赋值也支持 initializer_list 的版本
vector& operator= (initializer_list<value_type> il);
map& operator= (initializer_list<value_type> il);
- 容器支持一个 std::initializer_list 的构造函数,也就支持任意多个值构成的 {x1, x2, x3…} 进行初始化。STL 中的容器支持任意多个值构成的 {x1, x2, x3…} 进行初始化,就是通过 std::initializer_list 的构造函数支持的
std::initializer_list<int> mylist;
mylist = { 10, 20, 30 };
cout << sizeof(mylist) << endl;// 这⾥ begin 和 end 返回的值 initializer_list 对象中存的两个指针
// 这两个指针的值跟 i 的地址跟接近,说明数组存在栈上
int i = 0;
cout << mylist.begin() << endl;
cout << mylist.end() << endl;
cout << &i << endl;// {} 列表中可以有任意多个值
// 这两个写法语义上还是有差别的,第一个 v1 是直接构造,
// 第⼆个 v2 是构造临时对象 + 临时对象拷贝 v2 + 优化为直接构造
vector<int> v1({ 1,2,3,4,5 });
vector<int> v2 = { 1,2,3,4,5 };
const vector<int>& v3 = { 1,2,3,4,5 };// 这⾥是 pair 对象的 {} 初始化和 map 的 initializer_list 构造结合到一起用了
map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };// initializer_list 版本的赋值⽀持
v1 = { 10,20,30,40,50 };
三、可变模板参数
3.1、基本语法及原理
可变模板参数其实是我们第一次学习 C 语言就碰到了的,那就是 printf,大家不知道那个时候会不会疑惑,printf 明明也是一个函数,但是为什么我们想传几个参数就传几个参数,我们函数的参数个数不是规定死的吗,难道它函数重载了非常多个吗?其实不然,它只是用到了我们现在学的可变模板参数,我们来看看基本原理吧。
-
C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包(…Args),表示零或多个模板参数;函数参数包(…args),表示零或多个函数参数。
-
template <class …Args> void Func(Args… args) { }
-
template <class …Args> void Func(Args&… args) { }
-
template <class …Args> void Func(Args&&… args) { }
-
我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class… 或 typename… 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟 … 指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
-
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
-
这里我们可以使用 sizeof… 运算符去计算参数包中参数的个数。
// 代表 0 - N 个参数
template <class ...Args>
void Print(Args&&... args)
{cout << sizeof...(args) << endl;
}
我们通过尝试传不同数量和类型的参数来看看我们的可变模板的基本用法。
double x = 2.2;
Print(); // 包⾥有0个参数
Print(1); // 包⾥有1个参数
Print(1, string("xxxxx")); // 包⾥有2个参数
Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
如果我们不支持这个语法,那我们想要实现上面的操作就得实现 4 个函数进行函数重载,我们可变模板参数相当于在原来的基础上创建出一个更加灵活的模板出来,也就是模板的模板。
// 原理1:编译本质这里会结合引用折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能支持
// 这里的功能,有了可变参数模板,我们进一步被解放,他是类型泛化基础
// 上叠加数量变化,让我们泛型编程更灵活。
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
可变模板参数的存在会让我们的编译器变得更加麻烦,由于它要推演,所以它的编译速度会变慢,但是由于编译器处理问题的速度非常快,所以速度相差不大。
3.2、包扩展
看完上面有的同学就非常高兴了,原来可变模板参数这么简单,也不过如此嘛。其实不然,它难的地方不在基本语法,而是在调用,它的调用方式设计的十分复杂,我们可以来看一下。
- 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(…)来触发扩展操作。
- C++ 还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
我们上面只是把我们的右多少个参数包打印出来了而已,如果我们想要把这些参数的内容打印出来该这么做呢?有的同学可能就开动脑筋了,想说 C++11 不是已经提供了能够得到有多少个参数的方式了嘛,既然这样,我们直接按数组的方式打印不久行了。
// 打印参数包内容
template <class ...Args>
void Print(Args... args)
{// 可变参数模板编译时解析// 下⾯是运⾏获取和解析,所以不⽀持这样⽤cout << sizeof...(args) << endl;for (size_t i = 0; i < sizeof...(args); i++){cout << args[i] << " ";}cout << endl;
此时我们打印试试。
Print(1, string("xxxxx"), 2.2);
按道理其实没有错,这样的设计方法按道理来说也是极佳的。但是 C++ 委员会却并没有这么设计,可能是因为这样设计对编译器来说成本高,也有可能是有别的什么深意。它的真正调用方法其实是这样的。
void ShowList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数cout << endl;
}template <class T, class ...Args>
void ShowList(T x, Args... args)
{cout << x << " ";// args是N个参数的参数包// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包ShowList(args...);
}// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{ShowList(args...);
}
想要解析参数包,必须像上面那样使用包扩展才行。
Print();
Print(1);
Print(1, string("xxxxx"));
Print(1, string("xxxxx"), 2.2);
此时我们的代码才能运行成功,并把参数包解析出来。
它的推演流程是这样的。
我们就像俄罗斯套娃一样去层层解析可变模板参数,每次都拨开一层,就像这样一层一层的拨,直到里面的内容没了,我们也就拨完了,此时我们在重载一个拨完的函数。这样就实现了参数包的解析了。
有的人会想既然我们能够知道它的参数包中有几个参数,为什么我们不能让它的参数为 0 当终止条件,而是要这样操作呢?
if (sizeof...(args) == 0)return;
那是因为这个是运行时的递归终止条件,而我们刚才说的那是编译时递归终止条件。我们的编译器在编译时就会来判断我们的包扩展是否有终止条件,但是我们上面的那个条件是在运行时才会起效的,所以我们编译器在编译时判断没有,然后就不会让程序通过了。
我们直接做我们编译器做的工作也是可以解析我们的参数包的。
void ShowList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数cout << endl;
}//Print(1, string("xxxxx"), 2.2);调⽤时
//本质编译器将可变参数模板通过模式的包扩展,编译器推导的以下三个重载函数函数
void ShowList(double x)
{cout << x << " ";ShowList();
}void ShowList(string x, double z)
{cout << x << " ";ShowList(z);
}void ShowList(int x, string y, double z)
{cout << x << " ";ShowList(y, z);
}// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{ShowList(args...);
}
此时我们想要解析这样的可变模板也是可以的
Print(1, string("xxxxx"), 2.2);
此举本质就是把上面的迭代器方式展开来,使我们的每一层都有一个对应的函数重载去输出。换句话说,就是把丢给编译器的活自己来干,所以是十分不推荐的。
我们还有一种解析方式。
template <class T>
const T& GetArg(const T& x)
{cout << x << " ";return x;
}
template <class ...Args>
void Arguments(Args... args)
{
}template <class ...Args>
void Print(Args... args)
{// 注意GetArg必须返回获得到的对象,这样才能组成参数包给ArgumentsArguments(GetArg(args)...);
}
此时我们输出是反向的。
Print(1, string("xxxxx"), 2.2);
我们想要解析我们的参数包,最重要的是让我们的包能够展开。第一种方式我们是用递归去把我们的参数包展开,这种方式我们展开参数包的方式比较奇特。它相当于是通过我们 Arguments 函数去重复调用我们的 GetArg 三次来实现我们的参数包展开的。是靠编译的时候,由于我们的 Arguments 是可变模板参数,所以编译器就得在编译时推演出它的类型,此时就会把 GetArg 函数的参数都识别一遍,它对应的函数也就会运行了。
当然,我们的 Arguments 函数只是为了我们的参数展开的用处,所以它的参数没有用处,我们 GetArg 函数只要有返回值就可以了。
template <class T>
int& GetArg(const T& x)
{cout << x << " ";return 0;
}
3.3、empalce 系列接口
我们通过之前学习实现的 list 来看看 emplace 系列接口和我们的之前的接口有什么区别吧
- C++11 以后 STL 容器新增了 empalce 系列的接口,empalce 系列的接口均为模板可变参数,功能上兼容 push 和 insert 系列,但是empalce 还⽀持新玩法,假设容器为 container < T >,empalce 还支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。
- emplace_back 总体而言是更高效,推荐以后使用 emplace 系列替代 insert 和 push 系列
- 第⼆个程序中我们模拟实现了 list 的 emplace 和 emplace_back 接口,这里把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型 T 的构造,所以达到了前面说的 empalce 支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。
- 传递参数包过程中,如果是 Args&&… args 的参数包,要用完美转发参数包,方式如下 std::forward(args)… ,否则编译时包扩展后右值引用变量表达式就变成了左值。
namespace zxl
{template<class T>struct ListNode{ListNode<T>* _next;ListNode<T>* _prev;T _data;ListNode(T&& data):_next(nullptr), _prev(nullptr), _data(move(data)){}template <class... Args>ListNode(Args&&... args): _next(nullptr), _prev(nullptr), _data(std::forward<Args>(args)...){}};template<class T, class Ref, class Ptr>struct ListIterator{typedef ListNode<T> Node;typedef ListIterator<T, Ref, Ptr> Self;Node* _node;ListIterator(Node* node):_node(node){}// ++it;Self& operator++(){_node = _node->_next;return *this;}Self& operator--(){_node = _node->_prev;return *this;}Ref operator*(){return _node->_data;}bool operator!=(const Self& it){return _node != it._node;}};template<class T>class list{typedef ListNode<T> Node;public:typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T, const T&, const T*> const_iterator;iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}void empty_init(){_head = new Node();_head->_next = _head;_head->_prev = _head;}list(){empty_init();}void push_back(const T& x){insert(end(), x);}void push_back(T&& x){insert(end(), move(x));}iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* newnode = new Node(x);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}iterator insert(iterator pos, T&& x){Node* cur = pos._node;Node* newnode = new Node(move(x));Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}template <class... Args>void emplace_back(Args&&... args){insert(end(), std::forward<Args>(args)...);}template <class... Args>iterator insert(iterator pos, Args&&... args){Node* cur = pos._node;Node* newnode = new Node(std::forward<Args>(args)...);Node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}private:Node* _head;};
}
我们给list容器传输一个左值可以看见,它们调用的都是拷贝构造。
zxl::list<zxl::string> lt;
// 传左值,跟push_back⼀样,⾛拷⻉构造
zxl::string s1("111111111111");
lt.emplace_back(s1);
cout << "*********************************" << endl;
lt.push_back(s1);
cout << "*********************************" << endl;
我们的 emplace 是可变模板参数,此时只有一个参数,这个 s1 是一个左值,所以编译器就会推出是左值引用,此时传给函数参数时由于引用折叠,所以还是左值。所以它们此时没有任何区别。
如果传的是右值,我们来看看有什么区别。
zxl::list<zxl::string> lt;
zxl::string s1("111111111111");
// 右值,跟push_back⼀样,⾛移动构造
lt.emplace_back(move(s1));
cout << "*********************************" << endl;
lt.push_back(move(s1));
cout << "*********************************" << endl;
也是一样的,都是调用移动构造。
以上我们的 push_back 和 emplace_back 的效果都是一样的,我们现在来看看它们之间的真正区别。
// 直接把构造string参数包往下传,直接用string参数包构造string
// 这⾥达到的效果是push_back做不到的
lt.emplace_back("111111111111");
cout << "*********************************" << endl;
lt.push_back("111111111111");
cout << "*********************************" << endl;
此时我们发现我们的 push_back 走的是隐式类型转换,是构造 + 移动构造。而我们的 emplace_back 却是直接构造。我们的 emplace_back 由于是我们的可变模板参数,所以它没有确定是什么类型,这个地方在编译的时候就会直接推成 const char*,此时就把这个参数往下传,从而没有移动构造了,而是直接构造。虽然这个地方 push_back 和 emplace_back 不同,但是它们效率相差不大,因为移动构造的效率消耗极低。当然,如果是浅拷贝的类型,那就没有移动构造了,此时 push_back 和 emplace_back 效率相差的可能就会大一点。
我们再来看看如果是多个参数的插入,push_back 和 emplace_back 的区别。
zxl::list<pair<zxl::string, int>> lt1;
// 跟push_back⼀样
// 构造pair + 拷⻉/移动构造pair到list的节点中data上
pair<zxl::string, int> kv("苹果", 1);
lt1.emplace_back(kv);
cout << "*********************************" << endl;
lt1.push_back(kv);
cout << "*********************************" << endl;
zxl::list<pair<zxl::string, int>> lt1;
pair<zxl::string, int> kv("苹果", 1);
lt1.emplace_back(move(kv));
cout << "*********************************" << endl;
lt1.push_back(move(kv));
cout << "*********************************" << endl;
左值和右值插入都和我们的上面的是一样的。我们再来看看直接构造。
zxl::list<pair<zxl::string, int>> lt1;
lt1.emplace_back("苹果", 1);
cout << "*********************************" << endl;
lt1.push_back("苹果", 1);
cout << "*********************************" << endl;
我们发现我们的 push_back 是无法做的这样子传参的
我们这里不是一个 pair 对象,而是两个参数,所以我们得用 { } 进行初始化才行。
zxl::list<pair<zxl::string, int>> lt1;
lt1.emplace_back("苹果", 1);
cout << "*********************************" << endl;
lt1.push_back({ "苹果", 1 });
cout << "*********************************" << endl;
我们 emplace_back 反而不能够加 { } 进行隐式类型转换。我们 emplace_back 不支持隐式类型转换,而是支持传输多个参数。emplace_back 在这里相当于是把这两个参数的参数包不断的往下传,然后传到结点 data 的位置后就可以直接构造我们的 pair 了。
由于我们的 emplace_back 是直接构造的,所以相比 push_back 少一个移动构造,和上面的直接传参是一样的。
从上面的操作就可以看出来,我们 emplace 系列综合而言更加强大好用,以后推荐用 emplace 系列。
四、新的类功能
4.1、默认的移动构造和移动赋值
-
原来 C++ 类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
-
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
-
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
-
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
class Person
{
public:Person(const char* name = "张三", int age = 0):_name(name), _age(age){}private:zxl::string _name;int _age;
};int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}
我们的代码没有实现任何构造,我们的编译器会默认生成拷贝构造和移动构造,我们来看看它们的行为是什么。
同时我们的编译器也会实现移动赋值。
Person s1;
Person s2 = s1;
Person s4;
s4 = std::move(s2);
如果我们实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那我们的移动构造和赋值就不会默认生成了。
class Person
{
public:Person(const char* name = "张三", int age = 0):_name(name), _age(age){}~Person(){}
private:zxl::string _name;int _age;
};int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}
4.2、成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个缺省值初始化,这个我们在类和对象部分讲过了。
4.3、default 和 delete
- C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认⽣成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。
- 如果能想要限制某些默认函数的生成,在 C++98 中,将该函数设置成 private,并且声明补丁,这样只要其他⼈想要调用就会报错。在C++11 中更简单,只需在该函数声明加上 =delete 即可,该语法指示编译器不生成对应函数的默认版本,称 =delete 修饰的函数为删除函数。
class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p):_name(p._name), _age(p._age){}Person(Person&& p) = default;
private:zxl::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}
我们这里虽然写了析构,按道理不会默认生成移动构造和移动赋值,但是由于这里写了 = default,也就是说不让系统默认生成,所以这里是可以生成默认的移动构造移动赋值的。
4.4、final 与 override
这个我们在继承和多态章节已经详细讲过了,如果不记得就去复习吧。
总结
从这一部分的内容我们就可以知道我们 C++11 的各种新用法的难度和我们设计者的天才构思,我们一定要仔细学习了解吗,下一章节我们接着来继续了解我们的新特性,大家下一章节再见。
🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど