拷贝构造函数的第一个参数必须是引用类型,此参数几乎总是const的引用。拷贝构造函数在几种情况下会隐式地使用。因此,拷贝构造函数不应该是explicit的
即使我们定义了其他构造函数,在没有拷贝构造函数时,编辑器也会为我们合成的。编辑器从给定对象中依次将每个非static成员拷贝到创建的对象中。每个成员决定了它使用何种方式进行拷贝。类调用拷贝构造函数,数组逐个拷贝,内置类型直接拷贝
string dots(10,‘.‘) //直接初始化
string noll_book="999999"; //拷贝初始化
当我们使用拷贝初始化时,我们要去编译器将右侧运算对象拷贝到正在创建的对象中,如果需要可能进行类型转化
拷贝初始化通常使用的是拷贝构造函数。但是,如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数。
拷贝初始化不仅在我们用=定义变量时发生,在以下情况也会发生:
将一个对象作为实参传递给一个非引用类型的形参
从一个返回类型为非引用类型的函数返回一个对象
用花括号列表初始化一个数组中的元素或一个聚合类中的成员
某些类类型还会对他们所分配的对象使用拷贝初始化。如,当我们初始化标准库容器或是调用insert或push成员时,容器会对其元素进行拷贝初始化。与之相对的,emplace用的是直接初始化。
如果拷贝构造函数的参数不是引用,那么我们将会再次调用拷贝构造函数。如此循环
如果拷贝构造函数声明为explicit那么,不能进行隐式转换
vector<int> v1(10);
vector<int> v2 = 10 ; //// 错误,
void f(vector<int>);// 声明一个函数
f(10); //错误:
f(vector<int>(10));///正确,从int直接构造一个临时vector
编辑器可以绕过拷贝构造函数,但不是必须。而且拷贝/移动构造函数必须是可访问的
string null_book = "9999";
编辑器可改写为:
string null_book("9999");
某些运算符,包括赋值运算符,必须定义为成员函数。如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。对于二元运算符,例如赋值运算符,其右侧运算对象作为显示参数传递。
为了与内置类型保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。另外值得注意的是,标准库通常要求保存在容器中的类型要具有赋值运算符,且返回值是左侧对象的引用。
如果未定义自己的拷贝赋值运算符,编译器会为它自动生成一个合成赋值运算符,会将右侧对象的每个非static成员赋予左侧。
//等价于合成拷贝赋值运算符
Sales_data& Sales_data::operator = (const Sales_data &rhs)
{
bookNo = rhs.bookNo;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return *this;
}
析构函数释放对象使用的资源,并销毁对象的非static数据成员。由于析构函数不接有参数也没有返回值,对一个给定的类,只有唯一一个。
在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。通常,会释放对象在生存期分配的所有资源
析构部分是隐式的。成员销毁时依赖成员的类型。销毁类会调用自己的析构函数。内置类型没有,什么也不做。
隐式销毁一个内置指针类型的成员不会delete掉它所指的对象。但是智能指针会调用自己的析构函数,在析构阶段会自动销毁
无论何时一个对象被销毁,就会自动调用其析构函数:
变量离开其作用域时被销毁
当一个对象被销毁时,其成员被销毁
容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
对于动态分配的对象,当对指向它的指针引用delete运算符时被销毁
对于临时对象,当创建它的完整表达式结束时别销毁。
析构函数时自动运行的,我们无须担心资源何时被释放。
当指向一个对象的引用或指针离开作用域时,析构函数不会被执行。
当类未定义自己的析构函数时,编辑器会自己定义一个合成析构函数。
类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来阻止该类型的对象被销毁。如果不是这种情况,课程析构函数的函数体就为空。在(空)析构函数执行完毕后,成员会被自动销毁。
析构函数自身并不直接销毁成员。成员实在析构函数体之后隐含的析构阶段中被销毁的。析构函数体是作为成语销毁之外的另一部分而进行的
我们可以从析构函数开始判断:如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个 拷贝赋值运算符。
如果一个雷需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然,如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是拷贝赋值运算符都不必然意味着也需要析构函数。
~Sales_data() = default;//显示的要求编辑器生成合成的版本;
当我们在雷内使用= default时,合成的函数将隐式的声明为内联函数,如果我们不需要内联应该只在定义是使用。
大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是显示的还是隐式的。
有时候类必须采取某种机制阻止拷贝和赋值,如,iostream类就阻止了拷贝,以避免多个对象写入或读取相同IO缓冲。
在新标准下,我们可以通过将拷贝构造函数和拷贝运算符定义成删除的函数来阻止拷贝。
删除函数是这样的,我们虽然声明了它们,但不能以任何方式使用它们。
NoCopy(const NoCapy& ) = delete;//阻止拷贝
与=default不同,=delete必须是在函数第一次声明出现的时候。我们可以对任何函数指定delete,我们只能对编辑器可以合成的默认构造函数或拷贝控制成员使用=default。虽然删除函数主要是阻止拷贝,担当我们希望引导函数匹配过程时,删除函数有时也是很有用的。
值得注意的是我们不能定义析构函数时删除函数。
如果将析构函数定义成删除函数,我们不能定义该类的变量或临时对象。可以定义动态分配这种类型的对象。但是,不能释放这些对象。
对某些类来说,编辑器将合成的成员定义为删除的函数:
如果类的某个成员的析构函数时删除的或不可访问的(如,private),则类的合成析构函数被定义为删除的。
如果类的某个成员的拷贝构造函数时删除的或不可访问的,则合成的拷贝构造函数被定义为删除的。如果类的某个成员的析构函数是删除的或不可访问的,则类合成的拷贝构造函数也被定义为删除的。
如果类的某个成员的拷贝赋值运算是删除的或不可访问的,或类有一个const或引用成员,则类的合成拷贝赋值运算符被定义为删除的
如果类的某个成员的析构函数是删除的或不可访问的,或一个类有一个引用成员,它么有类内初始化器,或是类有一个const成员,它没有类内初始化器且其未显示定义默认构造函数,则该类的默认构造函数被定义为删除的。
这些规则的含义是:如果一个类有数据成员不能默认构造,拷贝,复制或销毁,则对应的成员函数被定义为删除的。
一个成员有删除的或不可访问的析构函数会导致合成默认和拷贝构造函数被定义为删除的。
虽然我们可以将一个新值赋予一个引用成员,但这样做改变的是引用指向的对象的值,而不是引用本身。
在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝的、为了阻止友元和成员函数进行拷贝,我们将这些拷贝控制成员声明为private的,但不定义他们。如果访问就会在编译阶段或链接阶段报错。
声明但不定义一个成员函数是合法的,对此只有一个例外。
应使用新版的=delete。
拷贝控制和资源管理有两种方式,令类的行为像一个值,令类的行为像一个指针。
//行为像值得类
class HasPtr{
public:
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& operator = (const HasPtr &);
~HasPtr() { delete ps;}
private:
std::string *ps;
int i;
};
赋值运算符通常组合了析构函数和构造函数的操作。要保证正确的顺序执行,即使将一个对象赋予它自身,也保证正确。而且,如果有可能,我们编写赋值运算还应该是异常安全的,当异常发生时能将左侧对象置于一个有意义的状态。
//类值拷贝赋值运算符
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newp = new string(*rhs.ps); //在销毁左侧运算对象资源之前拷贝右侧运算对象
delete ps;
ps=newp;
i=rhs.i;
return *this;
}
如果写成delete ps; ps = new string(*(rhs.ps)); 当是同一个对象时将报错。
引用计数的工作方式:
除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象正在创建的对象共享状态。当我们创建一个对象时,只有一个对象共享状态,因此将技术器初始化为1.
拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新的用户所共享。
析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态。
拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。
引用计数应该保存在哪里,一种方法是保存在动态内存中。
//定义行为像指针的类,我们也可以用shared_ptr实现数据的共享。
class HasPtr{
public:
HasPtr(const std::string& s = 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&);
~HasPtr();
private:
std::string *ps;
int i;
std::size_t *use;
};
HasPtr::~HasPtr()
{
if(--*use == 0)
{
delete ps;
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。我们的版本应该是通过改变指针来实现数据交换。
class HasPtr{
friend void swap(HasPtr& , HasPtr& );
};
inline swap(HsaPtr &lhs , HasPtr &rhs)
{
using std::swap;
swap(lhs.ps , rhs.ps);
swap(lhs.i , rhs.i);
}
与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种重要的优化手段。
对于swap,如果存在类型中特定的swap版本,swap调用会与之匹配。如果不存在,则会调用std中的版本(假定作用域中有using声明)
在定义了swap的类中,通常使用swap实现赋值运算符。使用的是一种名为拷贝交换的技术。
HasPtr& HasPtr::operator = (HasPtr rhs)
{
swap(*this , rhs);
return *this; //rhs被销毁
}
使用拷贝和交换的赋值运算符自动就是异常安全的。且能正确处理自赋值。它保证异常安全的方法与原来的赋值运算符实现一样。代码中唯一可能抛出异常的是拷贝构造函数中的new表达式。如果真的发生了异常,它也会在我们改变左侧运算对象之前发生。
每个message对象可以出现在多个Folder中。析构函数和拷贝运算都必须包含一条Message的所有Folder 中删除它。
拷贝赋值运算符通常执行拷贝构造函数和析构函数中也要只有的工作。这种情况下,公共的工作应该放在private的工具函数中完成。
我们假定Folder类包含名为addMsg和remMsg,分别完成在给定Folder对象的结合中添加和删除Message的工作。
//拷贝控制示例
class Message{
friend class Folder;
public:
explicit Message(const std::string &str = " ") : contents(str) { }
Message(const Message&);
Message& operator = (const Message& );
~Message();
void save(Folder&);
void remove(Folder&);
private:
std::string contents;
std::set<Folder*> folders;
void add_to_Folders(const Message&);
void remove_from_Folders();
};
void Message::save(Folder &f)
{
folders.insert(&f);
f.addMsg(this);
}
void Message::remove(Folder &f)
{
folders.erase(&f);
f.remMsg(this);
}
void Message::add_to_Floders(const Message &m)
{
for(auto f : m.folders)
f->addMsg(this);
}
Message::Message(const Message &m) : contents(m.constents) , foders(m.folders)
{
add_to_Folders(m);
}
void Message::remove_from_Folders()
{
for (auto f : folders)
f->remMsg(this);
}
Message::~Message()
{
remove_from_Folders();
}
Message& Message::operator = (const Message &rhs)
{
//通过先删除指针在插入它们来处理自赋值情况
remove_from_Folder();
constents = rhs.contents;
folders = rhs.folders;
add_to_Folders(rhs);
return *this;
}
void swap(Message &lhs, Message &rhs)
{
using std::swap;
for(auto f : lhs.folders)
f->remMsg(&lhs);
for(auto f : rhs.folders)
f->remMsg(&rhs);
swap(lhs.folders,rhs.folders);
swap(lhs.contents,rhs.constents);
for( auto f : lhs.folders)
f->addMsg(&lhs);
for(auto f : rhs.folders)
f->addMsg(&rhs);
}
//动态内存管理类
class StrVec{
public:
StrVec() : elements(nullptr) , first_free(nullptr) , cap(nullptr) { }
StrVec(const StrVec&);
StrVec &operator = (const StrVec&);
~Strvec();
void push_back(const std::stirng& );
size_t size () const { return first_free-elements; }
size_t capacity() const { return cap - elements; }
std::string *begin() const { return elements; }
std::string *end() const {return first_free; }
private:
static std::allocator<std::string> alloc;
void chk_n_alloc()
{ if(size() == capacity() ) reallocate();}
std::pair<std:: string*,std::string*> alloc_n_copy (const std::string* , const std::string*);
void free();
void reallocate();
std::string *elements;
std::string *first_free;
std::string *cap;
};
void StrVec::push_back(const string& s)
{
chk_n_alloc();
alloc.construct(first_free++,s);
}
pair<string*,string*> StrVec::alloc_n_copy(const string *b, const string *e)
{
auto data = alloc.allocate(e-b);
return {data,uninitialized_coyp(b,e,data)};
}
void StrVec::free()
{
if(elements)
{
for(auto p = first_free ; p!=elements ;)
alloc.destroy(--p);
alloc.deallocate(elements, cap-elements);
}
}
StrVec::StrVec(const StrVec &s)
{
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::~StrVec()
{
free();
}
StrVec &StrVec:: operator = (const StrVec &rhs)
{
auto data = alloc_n_copy(rhs.begin(),rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
void StrVec::reallocate()
{
auto newcapacity = size() ? 2 *size() :1;
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;
auto elem = elements;
for(size_t i=0 ; i !=size() ; ++i)
alloc.construct(dest++ , std::move(*elem++));
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
上述使用了避免拷贝的两种机制,使用移动构造函数,通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象。而且标准库保证“移后源”string仍然保持一个有效的、可析构的状态。另一种机制,使用move,如果遗漏将使用拷贝构造函数,其次,我们通常不为move提供一个using声明,而是直接调用std::move;
io类或unique_ptr。这些类都包含不能被共享的资源。因此,这些类型的对象不能拷贝但可移动
右值引用:必须绑定到右值的引用。通过&&来获得右值的引用。而且只能绑定到一个将要销毁的对象。因此,我们可以 自由地将一个右值引用的资源“移动”到另一个对象中。
一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
对常规引用(左值引用)我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不恩能够将一个右值引用直接绑定到一个左值上:
int i = 42;
int &r = i; //正确
int &&rr = i ; //错误
int &r2 = i * 42 ; //错误
const int &r3 = i*42; //正确,我们可以讲一个const的引用绑定到一个右值上
int &&rr2 = i * 42 //正确,
返回左值引用的函数, 连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的列子。我们可以讲一个左值引用绑定到这类表达式的结果上。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,我们知道,所引用的对象将要被销毁,该对象没有其他用户。这说明,使用右值引用的代码可以自由地接管所引用的对象的资源。
右值引用指向将被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态。
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变是右值引用类型也不行。
我们可以显式的将一个左值转换为对应的右值引用类型,调用move,在头文件utility中
int &&rr3 = std::move(rr1); ///ok,把一个左值当成右值来处理。
在调用move之后,我们不能对移后源对象的值做任何假设。
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
使用move应该使用的是std::move而不是move。可以避免潜在的名字冲突。
移动构造函数和移动赋值运算符从给定的对象“窃取”资源而不是拷贝资源。
移动构造函数的第一个参数是该类类型的以个引用。与拷贝构造函数一样,任何额外的参数都必须有默认实参。
除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态,销毁它是无害的。特别,一旦资源完成移动,源对象必须不在指向被移动的资源,这些资源的所有权已经归属新创建的对象了
StrVec:StrVec(StrVec &&s)noexcept :: elements(s.elements) , first_free(s.first_free) , cap(s.cap)
{
s.elements = s.first_free = s.cap =nullptr;
}
noexcept,它通知标准库我们的构造函数不抛出任何异常。如果我们忘记了改变s.first_free,则销毁移后源对象就会释放我们刚刚移动的内存
由于移动资源,通常是不分配任何资源的。因此通常不会抛出任何异常。我们应该告诉标准库,来阻止标准库为了处理抛出异常的可能性而做的一些额外的工作。一种通知方法就是使用noexcept,我们必须在声明和定义(如果定义在类外)都指定noexcept。
虽然移动操作通常不抛出异常,但抛出异常也是允许的。其次,标准库容器能对异常发生时,其自身的行为的到保障。如,vector保证,如果我们调用push_back时发生异常,vector自身不会发生改变。
为了避免对象移动到一半发生异常导致的问题。除非vector知道元素在移动构造函数不会抛出异常,否则在重新分配内存过程中他就必须使用拷贝构造函数,如果希望使用,那么就显示的告诉标准库我们的移动构造函数可以安全的使用。通过标记为noexcept来做到。
移动赋值运算符执行与析构函数和移动构造函数相同的工作。
StrVec &Strvec::operator = (StrVec &&rhs) noexcept
{
if (this !=&rhs)
{
free();
elements = rhs.elements;
frist_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设;
如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编辑器才会为他们合成移动构造函数或移动赋值运算符。编辑器可以移动内置类型成员。还能移动定义了对应移动操作的类。
与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。但是,如果我们呢显示的要求编译器正常=default的移动操作,且编辑器不能移动所有成员,则编辑器会将移动操作定义为删除的函数。除了一个重要的例外,遵循与合成拷贝操作类似的原则:
与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或是有成员为定义自己的拷贝构造函数且编辑器不能为其合成移动构造函数。移动赋值运算符的情况类似
如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的
类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
类似拷贝赋值运算符,如果有类成员是const或是引用,则类的移动赋值运算符被定义为删除的。
一个类是否定义了自己的移动操作对拷贝操作如何合成有影响。如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符被定义为删除的。
换句话,定义了一个移动构造函数,或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认为删除的。
移动右值,拷贝左值,但是如果没有移动构造函数,右值也被拷贝。
值得注意的是,用拷贝构造函数代替移动构造函数几乎肯定是安全的,拷贝构造函数甚至都不改变源对象的值。
class HasPtr{
public:
HasPtr(HasPtr &&p)noexcept : ps(p.ps),i(p.i) { p.ps = 0;}
HasPtr& operator = (HasPtr rhs) { swap (*this , rhs); return *this; }
//其他成员
};
不管使用的是拷贝构造函数还是移动构造函数,赋值运算符的函数体都swap两个运算对象的状态。
移动操作的例子:
void Message::move_Folder(Message *m)
{
folders = std::move(m->folders);
for (auto f : folders)
{
f->remMsg(m);
f->addMsg(this);
}
m->folders.clear();
}
通过move,我们使用了set的移动赋值运算符。值得注意的是,向set插入元素可能会抛出一个异常,bad_alloc。因此我们没有将他们标记成noexcept。
Message :: Message(Message &&m)
{
move_Folders(&m);
}
Message& Message::operator = (Message &&rhs)
{
if(this != &rhs)
{
remove_from_Folders();
contents = std::move(rhs.contents);
move_Foder(&rhs);
}
return *this;
}
一般来说一个迭代器解引用返回的是一个指向元素的左值。与其他迭代器不同,移动迭代器的解引用生成的是一个右值引用。我们通过标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。
void StrVec::reallocate()
{
auto newcapacity = size( )? 2*size() :1 ;
auto first = alloc.allocate(newcapacity);
auto last = uninitialized_copy(make_move_iterator(begin()),make_move_iterator(end()),first);
free( );
elements = first;
first_free = last;
cap = elements + newcapacity;
}
uninitialized_copy对输入序列中的每个元素调用construct来将元素“拷贝”到目的位置。由于我们传递的是移动迭代器,因此解引用运算符生成的是一个右值引用,这意味着construct将使用移动构造函数来构造元素。
值得注意的是,标准库不保证那些算法使用移动迭代器,哪些不适用,由于移动一个对象可能销毁掉原对象,因此你只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法。
不要随意适用move,由于一个移后源对象具有不确定的状态,对其调用std::move是危险的,当我们调用move时,必须确定移后源对象么有其他用户。
在移动构造函数和移动赋值运算符这些类实现代码之外的地方,只有当你确信需要进行移动操作且移动操作是安全的,才可以使用std::move
对于一般的成员函数也可以只用于构造函数赋值函数相同的移动版,有两个版本,一个版本接受一个指向const的左值引用,第二个版本接受一个指向非const的右值引用。通常我们不会定义const X&& 和一个X&参数的版本。
class StrVec{
public:
void push_back(const std::string& );
void push_back(std::string&& );
};
void StrVec::push_back(const string& s)
{
chk_n_alloc();
alloc.construct(first_free++, s);
}
void StrVec::push_back(string &&s)
{
chk_n_alloc();
alloc.construct(first_free++, std::move(s));//使用的是string的移动构造函数来构造元素
}
在旧版本,可以向右值赋值:s2+s1 = "wow!"; 无法阻止
为了向后兼容新标准仍然支持,但是我们可能洗完再自己的类中阻止这种用法。
我们通过在参数列表后放置一个引用限定符,来指出this的左值/右值属性的方式。声明和定义中都要指出
引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。
在有const的情况下,引用限定符必须在const后。
Foo &operator = (const Foo&) &;//只能向可修改的左值赋值。
引用限定符也可以重在。而且,我们可以综合引用限定符合const来区分一个成员函数的重载版本。
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符
class Foo{
public:
Foo sorted() &&;
Foo sorted() const ; //错误
using Comp = bool(const int& , const int &);
Foo sorted(Comp*);
Foo sorted(Comp*)const ;
}