C++——C++11(1)
今天我们来了解一下,C++11的一些新的功能和特性(包括新增加了一些库,语法,容器):
C++11的历史路程
不过,在这之前,还是得简单介绍一下C++11的发展经历:
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多
C++11的更新,直接深升华了C++的价值,使得C++更加强大,那么C++11主要增加了哪些内容呢?
C++11主要更新了以下内容:
语法改进:
Lambda表达式:允许定义匿名函数对象,可以捕获局部变量,并用于各种需要函数对象的场合,如算法库中的排序、查找等。
自动类型推断:通过auto关键字,编译器可以自动推断变量的类型,提高了代码的可读性和编写效率。
统一的初始化语法:使用花括号{}进行初始化,适用于各种类型,包括数组、结构体等。
成员变量默认初始化:未显式初始化的成员变量将自动进行默认初始化。
基于范围的for循环:简化了对容器或数组的遍历操作。
空指针nullptr:替代了原来的NULL宏定义,提高了类型安全性。
标准库扩充:
新的容器类:如std::forward_list(单向链表)、std::unordered_set(基于哈希表的集合)、std::unordered_map(基于哈希表的映射)等,提供了更多灵活性和性能选择。
正则表达式库:支持正则表达式的匹配和操作。
线程库:提供了多线程编程的支持,包括互斥锁、条件变量等同步原语。
算法库增强:增加了更多算法,提高了算法库的功能性和性能。
C++11在性能提升方面进行了多项重要改进。以下是一些主要的性能提升点:
右值引用与移动语义:C++11引入了右值引用和移动语义的概念,使得程序员能够更高效地处理临时对象。传统的拷贝构造函数在对象传递时可能涉及深拷贝操作,这在处理大型对象时可能导致显著的性能开销。通过右值引用和移动语义,C++11允许程序员在对象传递时避免不必要的深拷贝,而是进行资源的转移,从而显著提高性能。
智能指针:C++11新增了如std::shared_ptr、std::unique_ptr和std::weak_ptr等智能指针,它们能够自动管理动态分配的内存,减少了手动管理内存的需求,降低了内存泄漏的风险,从而提高了程序的稳定性。同时,智能指针的使用也有助于简化代码,提高开发效率。
并发支持库:C++11引入了线程库,包括线程、锁、条件变量等并发编程相关的标准库组件,使得多线程编程更加便捷和高效。多线程编程能够充分利用现代计算机的多核处理器资源,提高程序的执行效率。通过线程库,程序员可以更加容易地编写出高性能的并发程序。
标准库性能优化:C++11对标准库进行了大量的性能优化。通过改进算法和数据结构,以及利用新的语言特性,标准库的性能得到了显著提升。这使得在使用标准库进行编程时,能够获得更好的性能表现。
这些我们都会涉及,我们一个一个来:
{} 初始化
在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;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
int main()
{
int j = { 0 };
int k{ 0 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
return 0;
}
结构体和自定义对象也是可以的:
struct Point
{
int _x;
int _y;
};
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{
cout << "year: " << _year << "month: " << _month
<< "day: " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//结构体是可以的
Point p = { 2,3 };
Point u{ 1,2 };
//一般写法
Date d1(1234, 5, 7); //圆括号
//C++11
Date d2 = { 1235, 23 ,4 }; //花括号
//并且可以省略=
Date d3{ 1321,5,7 };
}
这里大家可以理解成为类型转换:
先用右边的构造一个临时对象,然后再拷贝构造给左边的,因为连续的构造和拷贝构造编译器会优化,所以只有一次构造。
那该怎么证明呢?
我们知道临时对象具有常属性:
我们加一个const就好啦:
有了这个特性之后,我们在用new的时候,就可以这么用:
//一般写法
Date* p1 = new Date[3]{ d1,d2,d3 };
//C++11写法
Date* p2 = new Date[3]{ {1321,5,7},{1234,5,6},{1234, 5, 7} };
std::initializer_list
我们来看这样一段代码:
int main()
{
auto li = { 1,2,3,4,5,6,7,8 };
cout << typeid(li).name() << endl;
}
这段代码会打印出来li的类型:
这是一个全新的类型:std::initializer_list。
std::initializer_list 是 C++11 标准库中引入的一个类模板,用于表示初始化列表。初始化列表是一组由花括号 {} 包围的值,可以用于初始化数组、容器、结构体等对象。
std::initializer_list 具有以下特性和用途:
初始化列表的包装器: std::initializer_list 可以包装一组值,使得这组值可以被作为单个参数传递给函数或对象的构造函数。
不可变性: std::initializer_list 中的元素是不可变的,即不能修改其中的值。
轻量级: std::initializer_list 对象本身是轻量级的,可以高效地传递和复制。
用于构造函数: std::initializer_list 经常用于构造函数的参数列表,以便接受初始化列表作为参数。
我们也可以找到它的官方文档:
说白了initializer_list允许用花括号(列表)初始化,我们常见的容器都会封装initializer_list,用来构造:
比如说:vector:
list:
所以,我们初始化vector和list的时候,可以这样来:
int main()
{
//auto li = { 1,2,3,4,5,6,7,8 };
//cout << typeid(li).name() << endl;
vector<int> d1 = { 1,2,3,4,5,6,7 };
list<int> d2 = { 1,2,3,4,5,6,7 };
}
我们自己写的容器也可以实现花括号初始化,我们要用上initializer_list:
namespace Myspace
{
template <class T>
class MyClass
{
public:
MyClass(initializer_list<T> list)
{
for (const auto e : list)
{
vec.push_back(e);
}
cout << "initializer_list<T>构造" << endl;
}
private:
vector<T> vec;
};
}
int main()
{
//auto li = { 1,2,3,4,5,6,7,8 };
//cout << typeid(li).name() << endl;
Myspace::MyClass<int> d1 = { 1,2,3,4,5,6 };
}
如果我们把这个initializer_list的构造函数去掉,就会报错:
右边自动识别为了initializer_list类型,但是没有相应的构造函数,所以就会报错。
声明
auto
在C++11及其后续版本中,auto关键字用于自动类型推断。当编译器看到auto时,它会根据初始化表达式自动为变量选择正确的类型。这提供了编写代码时的灵活性,特别是当变量的类型可能比较复杂或者难以直接从上下文中判断时。使用auto有几个优点:
代码简洁性:不需要显式写出变量的类型,可以使代码更简洁。
模板编程中的便利性:在模板编程中,类型经常是复杂的,使用auto可以避免写出冗长的类型名。
与范围基于的for循环结合使用:当遍历容器或数组时,auto可以自动推断出迭代器的类型,使得代码更加易读。
auto我们之前用的很多,这里就不再赘述了。
decltype类型推导
decltype用于当我们不知道类型时的类型推导:
int main()
{
auto e = 1;
auto j = 89.34;
auto k = e * j; //假设我不知道e和j相乘之后的类型
vector<decltype(k)> d1; //这里decltype可以推导k的类型
d1.push_back(e);
d1.push_back(j);
d1.push_back(k);
for (auto i : d1)
{
cout << i << endl;
}
}
decltype和typeid.name的区别
typeid.name 和 decltype 是 C++ 中用于获取类型信息的两种不同机制,它们有一些相似之处,但也有一些重要的区别。
typeid.name:
typeid.name 是运行时的特性,用于获取对象或表达式的实际类型的名称。它返回一个表示类型的字符串,通常是编译器特定的名称,不一定是人类可读的。返回的字符串可能因为编译器的不同而有所不同,且不是标准化的。通常在调试时使用,用于了解对象的实际类型。
#include <iostream>
#include <typeinfo>
int main() {
int x = 5;
std::cout << typeid(x).name() << std::endl; // 打印出具体类型的名称
return 0;
}
decltype:
decltype 是一个编译时的特性,用于推导表达式的类型,而不需要实际执行该表达式。它返回表达式的类型,可以用于声明变量、定义函数返回类型等返回的类型与表达式的结果相关,是一种编译器推断的结果。通常用于模板编程、泛型编程等场景,以便在编译时获取类型信息。
示例:
#include <iostream>
int main() {
int x = 5;
decltype(x) y; // 定义变量 y 的类型与 x 相同
y = 10;
std::cout << y << std::endl;
return 0;
}
总的来说,typeid.name 用于获取对象或表达式的实际类型的字符串表示,而 decltype 用于推导表达式的类型并在编译时获取类型信息。它们各自适用于不同的场景,并且提供了在不同阶段获取类型信息的方式。
decltype和auto的区别
decltype 和 auto 是 C++ 中两种用于类型推导的关键字,它们在某些方面有相似之处,但也存在一些关键的区别。
decltype:
decltype 用于从表达式中推导出类型,并在编译时获取类型信息。
返回的类型与表达式的类型完全一致,包括 const 修饰符和引用。
通常用于模板编程、泛型编程,以及在复杂表达式中精确获取类型信息的场景。
int x = 5;
decltype(x) y = 10; // y 的类型与 x 完全相同,包括 const 和引用
auto:
auto 用于让编译器自动推导变量的类型,通常用于简化代码,提高可读性。
auto 忽略顶层 const 修饰符,但会保留引用。
在声明时,必须初始化变量,以便编译器能够正确推导类型。
int x = 5;
auto y = x; // y 的类型是 int,忽略了顶层 const
auto& z = x; // z 是 x 的引用
区别总结:
decltype 返回表达式的准确类型,包括 const 和引用。
auto 简化变量声明,忽略顶层 const,但保留引用。
decltype 通常用于需要准确类型信息的场景,而 auto 用于简化代码、提高可读性的场景。
总体而言,选择使用 decltype 还是 auto 取决于具体的需求和编码风格。在需要准确类型信息时,使用 decltype 更为合适;而在简化代码和提高可读性时,使用 auto 更为方便。
新容器
用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和
unordered_set。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。
右值引用(&&)
在将右值引用之前,我们的先讲一下左值和右值:
左值
左值(lvalue,left value)是编程中的一个重要概念,尤其在C++等语言中。左值指的是那些可以出现在赋值运算符左边的表达式,它代表一个可被标识的存储位置。换句话说,左值可以被赋值或修改。左值通常是表达式(不一定是赋值表达式)后依然存在的持久对象,可以被看作是一个关联了名称的内存位置,允许程序的其他部分来访问它。
右值
右值(rvalue,right value)是编程中的一个重要概念,尤其在C/C++等语言中。右值指的是只能出现在赋值操作符右侧的值。换句话说,它们代表的是临时的、不可取地址的数据。
右值可以总结为以下几个特点:
临时性:右值通常是在表达式求值过程中生成的临时值,如常量、表达式的结果,或者被转换为右值引用的对象。这些值在表达式求值之后立即销毁,没有持久的生命周期。
无法取地址:由于右值是临时的,其地址无法获取,无法使用取地址操作符&获取其指针。
左右值的概念特别像旅客住酒店房间的特征,酒店的房号只要酒店存在,我就可以查到这个酒店的房号,像左值。而住在房里的旅客,可能住了了几天,就不住了,之后,你永远不知道他去哪了,像右值。
如果还不清楚,记住一点,左值指的空间,空间可以被取地址查到,右值是临时变量,不可以取地址。
了解上面的基础知识之后我们来看看右值引用:
右值引用是C++11中引入的一个新特性,它允许程序员以引用传递(而非值传递)的方式使用C++右值。右值通常指的是临时对象或字面量,这些对象在表达式结束后就不再存在,因此无法取地址。
右值引用使用“&&”符号进行声明,例如“int&&”。它的主要特点是可以绑定到右值上,使右值在表达式结束后依然保持其有效性,从而实现资源的有效转移,避免不必要的拷贝操作,提高程序的效率和性能。
此外,右值引用还可以配合std::move函数使用,将左值转换为右值引用,从而实现左值的移动语义。这使得程序员能够更加灵活地管理内存和资源,特别是在处理大型对象或资源密集型操作时,能够显著提升性能。
总的来说,右值引用是C++语言的一个强大工具,它使得程序员能够更精确地控制资源的生命周期和转移方式,从而优化程序的性能和内存使用。
左值引用
那就有一个问题,我们之前使用的是不是一直都是左值引用呢?是的:
左值引用是C++语言中的一个重要概念,它实际上是一种隐式的指针,用于为对象建立别名。通过操作符“&”实现,左值引用的语法为“type &引用名 = 左值表达式”。左值引用允许程序员通过引用而不是实际拷贝来操作对象,从而在某些情况下提高代码的效率。
左值引用有以下几个关键特点:
绑定左值:左值引用只能绑定到左值上。左值是有名字的变量(对象),它们可以被赋值,可以在多条语句中使用,具有明确的内存地址。
初始化后不可更改:一个左值引用被初始化后,不能再重新绑定到另一个对象上。
操作等同于原对象:对左值引用的操作实际上等同于对原对象的操作。这是因为引用只是原对象的一个别名,它们共享同一块内存地址。
左值引用在编程中有广泛的应用,特别是在函数参数传递和返回值方面。通过传递引用而非实际对象,可以避免不必要的拷贝操作,提高代码效率。同时,左值引用也常用于实现操作符重载、构建复杂的数据结构等场景。
左右值引用的区别
左值引用和右值引用是C++中两种不同的引用类型,它们在语法、语义和用途上有显著的区别。
绑定对象:左值引用主要用于绑定左值,即那些有持久状态、可以取地址的表达式,通常包括变量、数组元素等。而右值引用则用于绑定右值,这些通常是临时的、即将被销毁的对象,如字面常量、算术运算的结果、函数返回的临时对象,或者通过std::move()转换后的对象。简单来说,左值引用绑定的是具有持久性的对象,而右值引用绑定的是临时的或即将被销毁的对象。
语义与用途:左值引用的主要目的是为对象提供别名,使得通过引用操作对象可以像直接操作对象一样。这在函数参数传递、返回值和修改对象状态等场景中非常有用。而右值引用的主要目的是实现移动语义,通过转移资源的所有权来提高性能。它常用于避免不必要的拷贝操作,特别是在处理大型对象或资源密集型操作时,能够显著提升性能。
与std::move的关系:左值引用与std::move没有直接的关系。而右值引用常与std::move一起使用,以将左值转换为右值引用,从而实现移动语义。通过std::move,我们可以将左值“转换为”右值,从而允许使用右值引用进行资源转移。
操作限制:左值可以被赋值,而右值则不能。左值具有持久的状态,因此可以被多次赋值和修改。而右值则是临时的,一旦使用完毕就会被销毁,因此不能对其进行赋值操作。
总结来说,左值引用和右值引用在C++中各自扮演着不同的角色。左值引用主要用于为对象提供别名,方便操作;而右值引用则主要用于实现移动语义,提高性能。
左值引用给右值取别名
左值引用一般不能给右值:
但是加const的可以:
右值引用给左值取别名
右值引用一般也不能给左值取别名:
但是加move的可以:
std::move
std::move是C++标准库中的一个函数模板,用于将对象转换为右值引用,以便支持移动语义。它位于头文件中,是移动语义的关键工具之一。
std::move的作用并不是移动任何东西,而是改变一个对象的值分类,将其从左值转换为右值引用。这样做的目的是为了告诉编译器,我们希望对该对象使用移动语义,而非复制语义,从而优化资源的使用和提高代码的性能。
需要注意的是,std::move本身并不执行任何数据的移动或复制操作,它只是将对象标记为可移动状态。实际的数据移动操作是由对象的移动构造函数或移动赋值运算符来完成的。
另外,std::move不会创建新的对象或分配新的内存空间,它只是将对象的引用类型改变为右值引用。被std::move“移动”后的对象通常不应该再被使用,除非它们经过了重新构造或赋值操作。
简单一点来说,move会改变返回值数据的属性,数据的属性会变为将亡值,这个时候,右值引用的目的就是为了接管将亡值的数据内容:
将亡值和纯右值
将亡值和纯右值是C++11中引入的两个概念,它们都是右值表达式的子类,但有一些区别。
纯右值主要指的是字面值(如整数、字符、字符串)、不具名的临时对象,或者是返回右值引用的函数调用表达式。这些表达式没有持久的身份或地址,无法被修改或访问。纯右值的特点在于它们没有关联的对象,一旦表达式求值结束,它们就会被销毁。
而将亡值则是C++11新引入的一个概念,它指的是即将被销毁或离开作用域的对象。这些对象可以被移动,但不能被复制。将亡值的特点在于它们的资源可以被移动到其他对象,从而实现资源的优化管理,如移动语义。将亡值是一种特殊的纯右值,其右值引用可以被获取,使得我们可以实现如移动构造函数或移动赋值运算符这样的操作,来移动资源而不是复制资源,从而提高了程序的效率。
总的来说,纯右值和将亡值都是右值表达式的子类,但它们在性质和用途上有所不同。纯右值主要关注于不可访问或不可修改的值,而将亡值则关注于资源的管理和优化,特别是在移动语义中的应用。
在C++11中,纯右值和将亡值的概念在优化资源管理和提高代码效率方面发挥了重要作用。以下是它们的主要应用场景:
纯右值的应用场景:
字面量和临时对象:纯右值主要包括字面量(如整数、字符、字符串字面量)和临时对象。这些值在表达式中直接使用,无需持久化存储,一旦使用完毕就会被销毁。因此,它们非常适合作为函数的参数或返回值,以避免不必要的复制操作。
返回值优化(RVO):当函数返回一个对象时,如果返回的是一个纯右值(如临时对象),编译器可能会应用返回值优化(RVO),直接构造返回值在调用者的上下文中,避免不必要的拷贝或移动操作。
将亡值的应用场景:
移动语义:将亡值的主要应用场景是实现移动语义。当一个对象即将被销毁或离开作用域时,它的资源(如动态分配的内存)可以被安全地“移动”到另一个对象中,而不是进行昂贵的复制操作。这通过移动构造函数和移动赋值运算符实现,它们接受一个右值引用作为参数,并将资源从源对象“移动”到目标对象。
资源转移:在某些场景下,我们可能希望将一个对象的状态或资源转移到另一个对象,而不是复制它们。例如,在处理大型数据结构或动态分配的内存时,复制这些资源可能非常昂贵。通过使用将亡值和移动语义,我们可以避免这些不必要的复制操作,提高程序的性能和效率。
函数返回临时对象:当函数返回一个临时对象时,这个临时对象在函数返回后就变成了将亡值。通过使用移动语义,我们可以避免在返回过程中复制这个对象,而是将其资源直接移动到调用者提供的存储位置。
总的来说,纯右值和将亡值的应用场景主要关注于优化资源管理和提高代码效率。通过利用这些特性,我们可以减少不必要的复制操作,提高程序的性能,特别是在处理大型对象或资源密集型操作时效果尤为显著。
右值引用的场景
我们这里模拟string库的场景:
namespace MyString
{
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)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
/*string tmp(s);
swap(tmp);*/
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
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 = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
MyString::string to_string(int x)
{
MyString::string ret;
while (x)
{
int val = x % 10;
x /= 10;
ret += ('0' + val);
}
reverse(ret.begin(), ret.end());
return ret;
}
}
int main()
{
MyString::string s;
s = MyString::to_string(1234);
return 0;
}
我们自己写了一个to_string的函数,里面我们有一个临时变量ret(将亡值),这个ret是一个临时变量,如果我们直接返回一个对象,那么,我们深拷贝的次数就会增加:
这个对性能有大大的损耗,我们该怎么办呢?这个时候,可以用上右值引用,但是在这之前,我们的介绍两个东西移动赋值和移动构造。
移动赋值和移动构造
移动赋值的移动构造是右值引用的衍生物,简单点来说就是专门针对右值引用的构造函数和赋值函数:
移动赋值和移动构造是C++11引入的两个重要概念,它们允许我们以更高效的方式处理对象的资源。在理解这两者之前,我们先明确一个概念:右值引用。右值引用只能绑定到一个即将销毁的对象上,比如临时对象,这样我们就可以自由地接管这个对象的资源。
移动赋值:
移动赋值运算符是用来将一个对象的资源“移动”到另一个已经存在的对象中的操作。它接受一个右值引用作为参数,并使用这个右值引用的资源来初始化或更新当前对象,然后使右值引用的对象处于有效但未定义的状态。移动赋值操作避免了不必要的复制,提高了性能。
移动构造:
移动构造函数是用来创建一个新对象,并使用另一个即将销毁的对象的资源来初始化这个新对象的操作。它同样接受一个右值引用作为参数,并使用这个右值引用的资源来初始化新对象,然后使右值引用的对象处于有效但未定义的状态。
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// 移动构造函数实现
}
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
// 释放当前对象的资源
// 使用other的资源
// 将other的资源置为空或处于有效但未定义状态
}
return *this;
}
// ... 其他成员函数 ...
};
根据上面的理解我们可以写出,string的移动构造和移动赋值:
//移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)-- 移动赋值" << endl;
swap(s);
return *this;
}
我们来看看,没有移动赋值和移动构造,我们的开销有多大:
这里我们开了三次空间,性能非常不好。
但是如果我们是移动赋值和移动构造:
一步到位:
右值引用注意点
这里注意一下,我们这里有了右值引用,移动赋值和移动构造,但不要随便乱用,不要随便move,要不然会出一点问题:
我们这里的代码完成了移动赋值和移动构造,我们调试一下:
到最后,s1会消失,因为我们用了move让s1成为将亡值,s3会继承s1,继承之后,s1就会消失。如果这个不是我们的本意,那就糟糕了。