🎉作者简介:👓 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢 c + + , g o , p y t h o n , 目前熟悉 c + + , g o 语言,数据库,网络编程,了解分布式等相关内容 \textcolor{orange}{博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容} 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容
📃 个人主页: \textcolor{gray}{个人主页:} 个人主页: 小呆鸟_coding
🔎 支持 : \textcolor{gray}{支持:} 支持: 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦 \textcolor{green}{如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦} 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦👍 就是给予我最大的支持! \textcolor{green}{就是给予我最大的支持!} 就是给予我最大的支持!🎁
💛本文摘要💛
本专栏主要是对c++ primer这本圣经的总结,以及每章的相关笔记。目前正在复习这本书。同时希望能够帮助大家一起,学完这本书。 本文主要讲解第13章 拷贝控制
c++ primer 第五版 系列文章:可面试可复习
第2章 变量和基本类型
第3章 字符串、向量和数组
第4章 表达式
第5章 语句
第6章 函数
第8章 IO库
第9章 顺序容器
第10章 泛型算法
第11章 关联容器
第12章 动态内存
第13章 拷贝控制
第 14章 重载运算符
第15章 面向对象程序设计
第 16章 模板与泛型编程
定义了当用同类型的对象初始化另一个对象时做什么
定义了当将同类型的一个对象赋予另一个对象时做什么
拷贝控制成员是类的必要部分,如果没有显式定义,编译器会自动为其隐式地定义。
难点:认识到什么时候需要定义这些操作
第一个参数是自身类类型的引用,且其他参数都有默认值
,则此构造函数为拷贝构造函数。
必须是引用,且一般是 const 引用
。(如果不使用引用会导致无限循环,因为传递实参本身就是拷贝)拷贝构造函数通常不是 explicit 的。
class B{
public:
B(){cout << "默认构造"<< endl;};
B(const B & b) {cout << "拷贝构造"<< endl;};
};
B f(B a) {return a;};
int main()
{
B b; //默认构造
B a(b); //显式调用拷贝
B c = b; //赋值运算是拷贝
f(b); //传递参数与return 都是拷贝,所以调用2次拷贝
return 0;
}
合成拷贝构造函数
如果没有为一个类定义拷贝构造函数,则编译器会定义一个合成拷贝构造函数。
对于某些类,合成拷贝构造函数用来禁止该类型对象的拷贝(通过 =delete)
一般合成拷贝构造函数会逐个拷贝类的每个成员。
直接初始化和拷贝初始化区别
string dots(10,'.'); //直接初始化
string s1(dots); // 直接初始化,选择匹配的构造函数来初始化 s
string s2 = dots; // 拷贝初始化,使用拷贝构造函数或移动构造函数来完成。
理解
拷贝构造函数一般要求编译器将右侧运算对象拷贝到正在创建的对象中。拷贝初始化发生于那些没有显式调用构造函数却生成了类的对象的场合,比如使用 = 初始化一个对象。
拷贝初始化发生情况
以 = 定义变量
将一个对象作为实参传递给非引用形参
从一个返回类型为非引用类型的函数返回一个对象
用花括号列表初始化一个数组中的元素或一个聚合类中的成员
某些类类型会为它们所分配的对象进行拷贝初始化。
比如标准库容器初始化或调用 insert 和 push 成员时,会对其元素进行拷贝初始化(emplace 则是直接初始化)。重载赋值运算符
重载运算符本质上也是函数
。重载赋值运算符必须定义为成员函数
。如果一个运算符是成员函数,其左侧运算对象自动绑定到隐式的 this 参数。
拷贝赋值运算符接受一个与其所在类同类型的参数
class Foo{
public:
Foo& operator=(const Foo &); // 重载的赋值运算符通常返回一个指向其左侧运算对象的引用
}
重载的赋值运算符通常返回一个指向其左侧运算对象(也就是自身)的引用,赋值操作会在函数体内完成。
理解: while(a=2) 的含义:a=2 返回了 a 的引用,值为 2,条件为真.
合成拷贝赋值运算符
对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值(通过=delete)
构造函数和析构函数的区别:
析构函数包括一个函数体和一个隐式的析构部分。先执行函数体,然后执行析构部分销毁成员。成员按初始化的顺序逆序销毁。
如果是类类型的成员,需要执行成员自己的析构函数。
注意析构函数体自身并不直接销毁成员,成员是在析构函数体之后的隐含的析构阶段中被销毁的。
销毁指针
隐式地销毁一个内置指针类型的成员不会 delete 它所指向的对象。
智能指针是类类型,具有析构函数。智能指针的析构函数会递减对象的引用计数,如果计数变为 0,则销毁对象并释放内存。
调用时机:
合成析构函数
析构函数体自身并不直接销毁成员,成员是在析构函数体之后的隐含的析构阶段中被销毁的。
对于非静态的成员函数回收是自动的,所以函数体中是空的,不需要写任何东西,需要写的是在执行过程中创建的动态的内存
一个例子
如何定义一个类,这个类可以为每个对象生成一个唯一的序号?
方法:使用一个 static 成员,然后在构造函数中对它递增并基于递增后的 static 成员构造序号。
注意:要在所有的构造函数及拷贝赋值运算符中都对它进行递增(下面的例子中仅列出了默认构造函数)。
class numbered {
public:
numbered() { mysn = unique++; }
int mysn;
static int unique;
};
int numbered::unique = 10;
如果定义了其中一个操作,一般也需要定义其他操作。
确定类是否需要定义自己的拷贝控制函数,有俩条原则
判断它是否需要一个析构函数。如果它需要自定义一个析构函数,几乎可以肯定它也需要一个拷贝构造函数和一个拷贝复制运算符。
如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符,反之亦然。但是需要拷贝构造函数不意味着一定需要析构函数。
需要析构函数的类也需要拷贝和赋值操作,反之亦然
需要拷贝操作的类也需要赋值操作,反之亦然,无论需要拷贝构造还是需要拷贝赋值的运算符不一定需要析构函数
当需要定义析构函数,一般意味着在类内有指向动态内存的指针成员。因为合成析构函数只会销毁指针成员而不会 delete,所以需要定义析构函数。
这种情况下,如果使用合成的拷贝和赋值操作,它们会直接复制该指针,这就导致可能有多个指针指向相同的一块动态内存,当有一个类对象执行了析构函数,该内存就会被释放,其他指针就变成了悬空指针。所以需要定义拷贝和复制操作。
只能对默认构造函数或拷贝构造成员这些具有合成版本的函数使用 =default。
class Student{
public:
Student(const Student&) = default; // 不用加函数体。在参数列表后加一个 =default 即可
Student& operator(const Student &); //内联
~Student() = default;
};
Student& Student::operator=(const Student&) = default //非内联
虽然声明了它们,但是不能以任何方式使用它们。
定义删除的函数:=delete。
有一些例外需要阻止类进行拷贝或赋值。如 iostream 类、unique_ptr 等。
阻止拷贝的方式是将其定义为删除的函数
析构函数不能是删除的成员。
default与delete区别
析构函数不能定义为删除的成员
对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针,如果析构函数被删除,就无法销毁此类型的对象了。
合成拷贝控制成员可能是删除的
private拷贝控制
因此将拷贝控制成员声明为private但是不定义他们只声明
class PrivateCopy {
//无访问说明符,接下来的成员默认Wieprivate的
PrivateCopy(const PrivateCopy&); //拷贝控制成员是 private 的,因此普通用户代码无法访问
PrivateCopy &operator=(const PrivateCopy&);
public:
PrivateCopy() = default; //使用合成默认构造函数
~PrivateCopy(); //用户可以定义此类型对象,但无法拷贝他们
}
将拷贝控制成员定义为 private 可以阻止普通用户拷贝对象,但是无法阻止友元和成员函数拷贝对象,为此还要注意:只能声明不能定义这些拷贝控制成员。
理解:在此情况下,普通用户调用拷贝控制成员将引发编译时错误,友元和成员函数调用拷贝控制成员将引发链接时错误。
通常管理类外资源的类都需要定义拷贝控制成员,因为它们需要定义析构函数来释放对象所分配的资源,一个类一旦需要析构函数,那么它几乎肯定需要一个拷贝构造函数和一个拷贝赋值函数
行为像值
:对象有自己的状态,副本和原对象是完全独立的。如 strnig 看起来像值行为像指针
:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。如shared_ptr类提供类似指针的行为还有一些其他的类,如 IO 类型和 unique_ptr 不允许拷贝和赋值,所以它们的行为既不像值也不像指针。
行为像值的类中,对于类管理的资源,每个对于都应该有一份自己的拷贝。
定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针
定义一个析构函数来释放string
定义一个拷贝赋值运算符来释放对象当前的string(也就是销毁左侧运算对象),并从右侧对象拷贝string
注意:这些操作要以正确的顺序执行,即使将一个对象赋予它自身,也保证正确。
对于拷贝构造函数来说,传值,而不是传指针
HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0) {} // 构造函数
'使类的行为像值一样,传递解引用的值'
HasPtr(const HasPtr& p) : ps(new std::string(*p.ps)), i(p.i) {} // 拷贝构造函数
对于拷贝赋值运算符来说,一般将右侧运算对象拷贝到一个局部临时对象中,以保证将对象赋予自身也能正确工作
理解
:拷贝赋值运算符,本来左侧对象就是存在的指向一个内存地址,首先需要断开,然后使得它指向新的内存地址(顺序很重要)'正确顺序'
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newp = new string(*rhs.ps); //拷贝底层string
delete ps; //释放旧内存
ps = newp; //从右侧运算对象拷贝数据到本对象
i = rhs.i;
return *this //返回本对象
}
'错误顺序'
'如果是a = a这种情况,那么内存先被释放了,就无法拷贝了'
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
delete ps; //释放旧内存
auto newp = new string(*rhs.ps); //拷贝底层string
ps = newp; //从右侧运算对象拷贝数据到本对象
i = rhs.i;
return *this //返回本对象
}
综上
还需要析构函数来释放分配的内存,但是注意不能简单地直接释放关联的内存,应确保最后一个指向该内存的指针也销毁掉后才释放内存。
引用计数
但是有时候我们想直接管理资源,不使用shared_ptr
,于是使用引用计数
引用计数工作方式
除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当创建一个对象时,计数器初始化为 1。
而是拷贝对象的计数器并递增它。
难点:在哪里存放引用计数。计数器不能直接作为HasPtr对象的成员
计数器应该保存在动态内存中,当拷贝或赋值对象时,拷贝指向计数器的指针。这样使得副本和原对象都会指向相同的计数器
例子
class HasPtr {
public:
HasPtr(const std::string& s = new std::string()): ps(new std::string(s)), i(0), use(new std::size_t(1)) {}
HasPtr(const HasPtr& p): ps(p.ps), i(p.i), use(p.use) { ++*use; }
HasPtr& operator=(const HasPtr& rhs) {
++*rhs.use; // 递增右侧运算对象的引用计数
if(--*use == 0) { // 递减本对象的引用计数
delete ps; // 如果没有其他用户,释放本对象的资源。
delete use;
}
ps = rhs.ps; // 将数据从 rhs 拷贝到本对象
i = rhs.i;
use = rhs.use;
return *this; // 返回本对象
}
~HasPtr() { if(--*use == 0) { delete ps; delete use; } }
private:
std::string* ps;
int i;
std::size_t* use; // 引用计数器
}
HasPtr::~HasPtr()
{
if(--*use == 0) //如果引用计数变为0
{
delete ps; //释放string内存
delete use; //释放计数器内存
}
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++*rhs.use; //递增右侧运算对象的引用计数
if (--*use ==0) //然后递减本对象的引用计数
{
delete ps;
delete use;
}
ps =rhs.ps;
i = rhs.i;
use = rhs.use;
return *this
}
除了定义拷贝控制成员,管理资源的类通常还定义一个 swap 函数。
需要使用 swap 来交换元素的位置。因此被重排的元素类型是类时,为类定义自己的 swap 函数非常重要。
错误写法
HasPtr temp = v1; //创建v1的值的一个临时副本
v1 = v2; //将v2的值赋予v1
v2 = temp; //将保存的v1的值赋予v2
将原来的v1中的string拷贝俩次:第一次HasPtr的拷贝构造函数将v1拷贝给temp,第二次是赋值运算符将temp赋予v2,在把v2赋予v1,还拷贝了原来v2的sting,多次拷贝,实属不必
正确写法(交换指针)
与拷贝控制成员不同,swap 不是必要的,但是对于分配了资源的类,定义 swap 有时是一种很重要的优化手段。
'类似交换指针'
string *temp = v1.ps;
v1.ps = v2.ps;
v2.ps = temp;
class HasPtr {
friend void swap(HasPtr& HasPtr);
// 其他成员定义与 13.2.1 中定义的 HasPtr 一样。
}
inline void swap(HasPtr& lhs, HasPtr& rhs)
{
using std::swap; // 使用此声明而非直接通过 std::swap 调用。因为这样的话,如果某个类成员定义了自己版本的 swap,对其进行 swap 时会优先使用自定义版本。
swap(lhs.ps, rhs.ps); // 交换指针,而不是 string 数据
swap(lhs.i, rhs, i);
}
swap 函数应该调用 swap,而不是 std::swap
而类的成员可能是另一个类类型,这时为了保证调用的是该类自定义的 swap 版本,应该使用 swap,而不是 std::swap。
std::swap
是正确的,但是性能低。所以应该先调用自己写的swap
,然后如果自己没有写,在调用标准库中的swap
理解:使用 using std::swap 的目的是保证当某个成员没有自定义的 swap 版本时,能够执行标准库版本。
在赋值运算符中使用swap
定义了 swap 的类通常用 swap 来定义赋值运算符,注意这时参数要使用值传递而非引用传递。
理解:所谓薄记工作的应用场景是有两个或两个以上的类,且当创建、复制或销毁其中某个类的对象时,需要更新另一个类的对象的值。
例子
Folder需要删除和保存Message,使用函数addMsg和reMsg(save 和 remove)
每个Folder包含很多Message,每个Message里面维护一个Folders(是一个Folder列表)
Message类
add_to_folders
add_to_folders
也需要remove_from_folders
(对于拷贝赋值左边和右边不一样)remove_from_floders
void swap(Message &lhs, Message &rhs);
class Message {
friend class Folder;
friend void swap(Message &lhs, Message &rhs); // 要将 swap 定义为友元
public:
//folder被隐式的初始化为空集合
explicit Message(const std::string &str = "") : contents_(str) {}
//拷贝控制成员,用来管理指向本Message的指针
Message(const Message &msg) : contents_(msg.contents_), folders_(msg.folders_) //拷贝构造函数
{
add_to_Folders(msg);
}
Message &operator=(const Message &rhs) //拷贝赋值运算符
{
//通过先删除指针在插入它们来处理自赋值的情况(a = a与上面将的拷贝一样,先加在在删除就没了),对于a = b,a本身存在,先把a维护的folders拿出来,把a的记录清楚掉,然后更新成b的folders,最后把a的folders加到b维护的folders中
remove_from_Folders(); //更新已有的folder
contents_ = rhs.contents_; // 从rhs拷贝消息内容
folders_ = rhs.folders_; //从rhs拷贝folder指针
add_to_Folders(*this); //将本Message添加到那些folder中
return *this;
}
~Message() //析构函数
{
remove_from_Folders();
}
//从给定Folder集合中添加/删除本Message
void save(Folder &folder)
{ //将message放到某一个folder中,它维护的folders需要增加一条记录,同样相应的folder也需要加入一条记录
folders_.insert(&folder); //将给定folder添加到我们的floder列表中
folder.messages_.insert(this); //将本message添加到f的message集合中
}
//需要移除某个消息,则需要移动俩个地方,1.从folders集合中移除。2.从folder当中移除
void remove(Folder &folder)
{
folders_.erase(&folder); //将给定folder从我们的folder列表中删除
folder.messages_.erase(this); //将本message从f的message集合中删除
}
private:
std::string contents_; //实际消息文本
std::set<Folder *> folders_; //包含本Message的Folder(message包含在多个folder中)
//Folders是一个folder列表,当添加一个message,则会在多个目录下添加相应消息
//在拷贝构造函数、拷贝赋值运算符和析构函数所使用的工具函数
void add_to_Folders(const Message &msg)
{
for (auto folder : msg.folders_)
folder->addMsg(this);
}
//从folders中的每个folder中删除message
void remove_from_Folders()
{
//需要删除message时,需要将folders列表的中的所有folder拿出来,一个一个的清理folder中所包含的message
for (auto folder : folders_)
folder->remMsg(this);
}
};
add_to_folders
是在message中添加folders
remove_from_folders
是在Message 中删除folders
save
是在folder中加入message,需要添加俩个地方
1. folder中添加message
2. folders列表中添加folder
remove
是在folder中移除message,需要添加俩个地方
1. folder中移除Message
2. folders列表中移除folder
对于拷贝赋值,只能先删在加,不能先加在删。而且删除时floders并没有删除,只是删除消息
Message类的swap函数
Folder类的定义
class Folder {
friend void swap(Folder &, Folder &);
friend class Message;
public:
Folder() = default;
Folder(const Folder &);
Folder& operator=(const Folder &);
~Folder();
private:
std::set<Message*> msgs;
void add_to_Message(const Folder&);
void remove_from_Message();
void addMsg(Message *m) { msgs.insert(m); }
void remMsg(Message *m) { msgs.erase(m); }
};
void swap(Folder &, Folder &);
但某些类需要自己进行内存分配,这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存。
理解:区分动态内存管理类与分配资源的类。动态内存管理类主要特点是其所占用内存大小是动态变化的,而分配资源的类其特点是使用了堆内存。
StrVec 类的设计
StrVec的内存管理模仿vector
String 类的设计和实现
String.h
#include
//类vector类内存分配策略的简化实现
class String {
public:
//allocator成员进行默认初始化
String() : String("") {}
String(const char *);
String(const String &);
String &operator=(const String &);
~String();
const char *c_str() const { return elements; }
size_t size() const { return end - elements; }
size_t length() const { return end - elements - 1; }
private:
std::pair<char *, char *> alloc_n_copy(const char *, const char *);
void range_initializer(const char *, const char *);
void free();
private:
char *elements; // elements 指向字符串的首部
char *end; // end 指向字符串的尾后
std::allocator<char> alloc; // 定义了一个分配器成员
};
String.cpp
#include "String.h"
#include
#include
std::pair<char*, char*>
String::alloc_n_copy(const char *b, const char *e) {
auto str = alloc.allocate(e - b);
return{ str, std::uninitialized_copy(b, e, str) };
}
void String::range_initializer(const char *first, const char *last) {
auto newstr = alloc_n_copy(first, last);
elements = newstr.first;
end = newstr.second;
}
String::String(const char *s) {
char *sl = const_cast<char*>(s);
while (*sl)
++sl;
range_initializer(s, ++sl);
}
String::String(const String& rhs) {
range_initializer(rhs.elements, rhs.end);
std::cout << "copy constructor" << std::endl;
}
void String::free() {
if (elements) {
std::for_each(elements, end, [this](char &c){ alloc.destroy(&c); });
alloc.deallocate(elements, end - elements);
}
}
String::~String() {
free();
}
String& String::operator = (const String &rhs) {
auto newstr = alloc_n_copy(rhs.elements, rhs.end);
free();
elements = newstr.first;
end = newstr.second;
std::cout << "copy-assignment" << std::endl;
return *this;
}
移动对象
IO 类或 unique_ptr 这样的类不能拷贝但可以移动。
新标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝
右值引用性质:
区分左值/右值/左值引用/右值引用
左值
:左值和右值是表达式的属性,左值表达式表示的是一个对象的身份,左值持久状态右值
:右值表达式表示的是一个对象的值,右值要么是字面值常量,要么是表达式求值过程中创建的临时对象左值引用
:就是前面普通引用(只是起了一个别名,而且不能将其绑定到要求准换的表达式、字面值常量或是返回右值表达式右值引用
:右值引用和左值引用完全相反的绑定特性,可以将右值引用绑定到这类表达式上,但不能将一个右值引用绑定到一个左值上。注意:const的左值也可以绑定到右值上
int i = 42; // i 是一个左值
int&& r = i; // 错误,不能将右值引用绑定到左值上
int &r2 = i * 2; // 错误,i * 2 是一个右值,不能将左值引用绑定到一个右值上。
const int& r3 = i * 2; // 正确,可以将一个 const 引用绑定到一个右值上
int&& r4 = i * 2; // 正确
函数返回的左/右值
返回非引用类型的函数返回的也是右值。
变量是左值
一个变量表达式是一个左值。
但该变量本身也是一个左值
。因此不能将一个右值引用直接绑定到一个右值引用变量上。
int&& rr1 = 42; // 正确
int&& rr2 = rr1; // 错误,表达式 rr1 是左值。
标准库move函数
但我们可以显示地将一个左值准换为对应的右值引用类型
,但是可以通过标准库move函数实现
#include
int &&rr3 = srd::move(rr1); //正确
解释
调用move后,可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值
std::move
而非直接用 move,这可以避免名字冲突。实例
int f();
vector<int> vi(100);
int&& r1 = f();
int& r2 = vi[0];
int& r3 = r1;
int&& r4 = vi[0] * f();
与拷贝构造不同的是,这个引用参数在引用构造函数中是一个右值引用。与拷贝构造函数一样,额外的参数都必须是默认实参
只是移动,并不发生拷贝,所以不需要分配任何新内存
noexcept
移动赋值运算符
合成移动操作
具体看书
)假定Y是一个类,它定义了自己的拷贝构造函数但未定义自己的移动构造函数
struct hasY{
hasY() = default;
hasY(hasY&&) = default;
Y mem; //hasY将有一个删除的移动构造函数
};
hasy hy,hy2 = stdf::move(hy); //错误:移动构造函数是删除的
定义了一个移动构造函数或者移动赋值运算符的类必须定义自己的拷贝操作。否则,这些成员默认地被定义为删除的
。因为如果一个类定义了一个移动构造函数或者一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会定义为删除的
移动右值,拷贝左值
但如果没有移动构造函数,右值也会被拷贝
拷贝并交换赋值运算符和移动操作
Message类的移动操作
步骤
Message类可以使用string和set的移动操作来拷贝contents和folders成员的额外开销
除了移动folders成员外,还必须更新每一个指向原message的folder我们必须删除指向旧message的指针,并添加一个指向新message指针。
一个版本接受指向const的左值引用,第二个版本接受一个指向非const的右值引用
class StrVex{
public:
void push_back(const std::string&); //拷贝
void push_bac(std::string &&); //移动
//其他成员定义,如前
};
void StrVec::push_back(const string&s)
{
chk_n_alloc(); //确保有空间容纳新元素
//在first_free指向的元素中构造s的一个副本
alloc.construct(forst_free, s);
}
void StrVec::push_back(string&&s)
{
chk_n_alloc(); //如果需要的话为StrVec宠幸分配内存
alloc.aonstruct(first_free, std::move(s));
}
StrVec vec; //空Strvec
string s = "some string";
vec.push_back(s); //拷贝,因为s是左值
vec.push_back("done") //移动,done是右值
右值和左值引用成员函数
string s1 = "hello", s2 = "world";
auto n = (s1 + s2).find('a');
s1 + s2 = "world";