第十三章 拷贝控制
一个类通过定义五种特殊的成员函数来控制这些操作:拷贝构造函数 、拷贝赋值运算符 、移动构造函数 、移动赋值运算符 、析构函数 。 拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么,析构函数定义了当此类型对象销毁时做什么,这些操称为拷贝控制操作。
13.1 拷贝、赋值与销毁
从拷贝构造函数、拷贝赋值运算符和析构函数作为开始。
13.1.1 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。 拷贝构造函数通常不应该是explicit,因为几种情况下都会被隐式地使用。
class Foo
{
Foo ( ) ;
Foo ( const Foo& ) ;
} ;
如果没有为一个类定义拷贝构造函数,编译器会定义一个。 对某些类来说,编译器的合成拷贝构造函数 用来阻止我们拷贝该类类型的对象。 关于与合成的拷贝构造函数等价的拷贝构造函数的声明。
class Sales_data
{
public :
Sales_data ( const Sales_data & ) ;
private :
std:: string bookNo;
int units_sold = 0 ;
double revenue = 0.0 ;
}
Sales_data :: Sales_data ( const Sales_data & orig) : bookNo ( orig. bookNo) ,
units_sold ( orig. units_sold) ,
revenue ( orig. revenue)
{
}
直接初始化和拷贝初始化之间的差异。
当直接初始化 时,实际上是要求编译器使用普通的函数匹配来选择与提供的参数最匹配的构造函数。 当拷贝初始化 时,要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
string dots ( 10 , '.' ) ;
string s ( dots) ;
string s2 = dots;
string null_book = "9-999-99999-9" ;
string nines = string ( 100 , '9' ) ;
拷贝初始化不仅在我们用=定义变量时会发生,在下列情况下也会发生。
将一个对象作为实参传递给一个非引用类型的形参。 从一个返回类型为非引用类型的函数返回一个对象。 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。 拷贝构造函数被用来初始化非引用类类型参数,如果其参数不是引用类型,则调用永远也不会成功,会形成循环拷贝。 使用的初始化值要求通过一个explicit的构造函数来进行类型转换,那么使用拷贝初始化还是直接初始化就不是无关紧要的了。 当传递一个实参或函数返回一个值,不能隐式使用一个explicit构造函数。
vector< int > v1 ( 10 ) ;
vector< int > v2 = 10 ;
void f ( vector< int > ) ;
f ( 10 ) ;
f ( vector < int > ( 10 ) ) ;
在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象。
string null_book = "9-999-99999-9" ;
string null_book ( "9-999-99999-9" ) ;
即使编译器略过了拷贝/移动构造函数,但在这个程序点上,拷贝/移动构造函数必须是存在且可访问的(不能是private)。
13.1.2 拷贝赋值运算符
与类控制其对象如何初始化一样,类也可以控制其对象如何赋值。
Sales_data trans, accum;
trans = accum;
首先关于重载运算符 ,本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。
class Foo
{
public :
Foo & operator = ( const Foo & ) ;
}
如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个合成拷贝赋值运算符 。
Sales_data &
Sales_data:: operator = ( const Sales_data & rhs)
{
bookNo = rhs. bookNo;
units_sold = rhs. units_sold;
revenue = rhs. revenue;
return * this ;
}
13.1.3 析构函数
析构函数释放对象使用的资源,并销毁对象的非static数据成员。
class Foo
{
~ Foo ( ) ;
} ;
由析构函数不接受参数,因此它不能被重载,对一个给定类,只会有唯一一个析构函数。 构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。 构造函数中,成员的初始化是在函数体执行之前完成的,按照它们在类中出现的顺序进行初始化。 析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。 通常,析构函数释放对象在生存期分配的所有资源。 析构函数中不存在类似构造函数中初始化列表的东西来控制成员如何销毁,是隐式的。 隐式销毁一个内置指针类型的成员不会delete它所指向的对象。 什么时候会调用析构函数:
变量在离开其作用域时被销毁。 当一个对象被销毁时,其成员被销毁。 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。 对于临时对象,当创建它的完整表达式结束时被销毁。
{
Sales_data * p = new Sales_data;
auto p2 = make_shared < Sales_data> ( ) ;
Sales_data item ( * p) ;
vector< Sales_data> vec;
vec. push_back ( * p2) ;
delete p;
}
当指向一个对象的的引用或指针离开作用域时,析构函数不会执行。 合成析构函数
class Sales_data
{
public :
~ Sales data ( ) { }
} ;
13.1.4 三/五法则
有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数。 在新标准下,一个类还可以定义一个移动构造函数和一个移动赋值运算符。 C++语言并不要求我们定义所有这些操作:可以只定义其中一个或两个,而不必定义所有。 但是通常被看作一个整体,因为需要析构函数的类也需要拷贝和赋值操作。 下例子中,类使用合成的拷贝构造函数和拷贝赋值运算符,在两个对象上都会调用HasPtr的析构函数,此代码会导致此指针被delete两次。
class HasPtr
{
public :
HasPtr ( const std:: string & s = std:: string ( ) ) : ps ( new std:: string ( s) ) , i ( 0 ) { }
~ HasPtr ( ) { delete ps; }
}
HasPtr f ( HasPtr hp)
{
HasPtr ret = hp;
return ret;
}
HasPtr p ( "some values" ) ;
f ( p) ;
HasPtr q ( p) ;
如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。 无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。
13.1.5 使用=default
可以通过将拷贝控制成员定义=default来显式地要求编译器生成合成的版本。
class Sales_data
{
public :
Sales_ data ( ) = default ;
Sales_data ( const Sales_data & ) = default ;
Sales_data & operator = ( const Sales_data & ) ;
~ Sales_data ( ) = default ;
} ;
Sales_data & Sales_data:: operator = ( const Sales_data & ) = default ;
当我们在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的。 如果不希望合成的成员是内联函数,应该类外定义使用=default。 我们只能对具有合成版本的成员函数使用=default。
13.1.6 阻止拷贝
对某些类来说,需要阻止拷贝,不定义拷贝控制成员,这种策略是无效的,编译器为它生成合成的版本。 在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数 来阻止拷贝。 删除的函数 :我们虽然声明了它们,但不能以任何方式使用它们,使用方法是在函数的参数列表后面加上=delete。
struct NoCopy
{
NoCopy ( ) = default ;
NoCopy ( const NoCopy & ) = delete ;
NoCopy & operator = ( const NoCopy & ) = delete ;
~ NoCopy ( ) = default ;
}
析构函数不能是删除的成员,对于删除了析构函数的类型,虽然不能定义这种类型的变量或成员,却可以动态分配这种类型的对象,但是不能释放这些对象。
struct NoCopy
{
NoDtor ( ) = default ;
~ NoDtor ( ) = delete ;
} ;
NoDtor nd;
NoDtor * p = new NoDtor ( ) ;
delete p;
对某些类来说,编译器将这些合成的成员定义为删除的函数。
如果类的某个成员的析构函数是删除的或不可访问的(例如是private的),则类的合成析构函数被定义为删除的。 如果类的某个成员的拷贝构造函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的,如果类的某个成员的析构函数是删除的或不可访问的,则类合成的拷贝构造函数也被定义为删除的。 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或是类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的。 如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,它没有类内初始化器,或是类有一个const成员,它没有类内初始化器且其类型未显式定义默认构造函数,则该类的默认构造函数被定义为删除的。 本质上规则的含义是如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的,原因是,如果没有这条规则,我们可能会创建出无 法销毁的对象。 在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝。
13.2 拷贝控制和资源管理
通常,管理类外资源的类必须定义拷贝控制成员。 拷贝语义一般来说有两种:拷贝值或指针。
13.2.1 行为像值的类
为了提供类值的行为,对于类管理的资源,每个对象都应该拥有份自己的拷贝。
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 = ( constHasPtr & rhs)
{
auto newp = new string ( * rhs. ps) ;
delete ps;
ps = newp;
i = rhs. i;
return * this ;
}
对于一个赋值运算符来说,在销毁左侧运算对象资源之前拷贝右侧运算对象。
HasPtr & HasPtr:: operator = ( constHasPtr & rhs)
{
delete ps;
ps = new string ( * ( rhs. ps) ) ;
i = rhs. i;
return * this ;
}
13.2.2 定义行为像指针的类
对于行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身。 定义一个使用引用计数的类。
classHasPtr
{
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;
}
析构函数不能无条件地delete ps,可能还有其他对象指向这块内存。
HasPtr :: ~ HasPtr ( )
{
if ( -- * use == 0 )
{
delete ps;
delete use;
}
}
赋值运算符必须处理自赋值,我们通过先递增rhs中的计数然后再递减左侧运算对象中的计数来实现这一点
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 ;
}
13.3 交换操作
除了定义拷贝控制成员,管理资源的类通常还定义一个swap的函数。 交换两个类值可能是这样。
HasPtr temp = v1;
v1 = v2;
v2 = temp;
理论上,这些内存分配都是不必要的,希望交换指针,而不是分配新副本。
string * temp = v1. ps;
v1. ps = v2. ps;
v2. ps = temp;
class HasPtr
{
friend void swap ( HasPtr & , 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;
}
inline void swap ( HasPtr & lhs, HasPtr & rhs)
{
using std:: swap;
swap ( lhs. ps, rhs. ps) ;
swap ( lhs. i, rhs. i) ;
}
如果一个类的成员有自己类型特定的swap函数,调用std::swap就是错误的。 假定有另一个命名为Foo的类,它有一个类型为HasPtr的成员h。
void swap ( Foo & lhs, Foo & rhs)
{
std:: swap ( lhs. h, rhs. h) ;
}
void swap ( Foo & lhs, Foo & rhs)
{
using std:: swap;
swap ( lhs. h, rhs. h) ;
}
定义swap的类通常用swap来定义它们的赋值运算符,这些运算符使用了一种名为拷贝井交换 的技术。
HasPtr & HasPtr:: operator = ( HasPtr rhs)
{
swap ( * this , rhs) ;
return * this ;
}
使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。
13.4 拷贝控制示例
两个类命名为Message和Folder,分别表示电子邮件和消息目录。 每个Message对象可以出现在多个Folder中,但是,任意给定的Message的内容只有一个副本。 如果一条Message的内容被改变,则从它所在的任何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_Folders ( const Message & m)
{
for ( auto f : m. folders)
{
f-> addMsg ( this ) ;
}
}
Message :: Message ( const Message & m) : contents ( m. contents) , folders ( 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_Folders ( ) ;
contents = 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. contents) ;
for ( auto f : lhs. folders)
{
f-> addMsg ( & lhs) ;
}
for ( auto f : rhs. folders)
{
f-> addMsg ( & rhs) ;
}
}
13.5 动态内存管理类
某些类需要在运行时分配可变大小的内存空间,这种类通常可以使用标准库容器来保存它们的数据。 如果实现标准库vector类的一个简化版本。StrVec。 每个StrVec有三个指针成员指向其元素所使用的内存。
elements,指向分配的内存中的首元素。 first_free,指向最后一个实际元素之后的位置。 cap,指向分配的内存末尾之后的位置。 除了这些指针之外,StrVec还有一个名为alloc的静态成员,其类型为allocator。 alloc成员会分配StrVec使用的内存,我们的类还有4个工具函数。
alloc_n_copy会分配内存,并拷贝一个给定范围中的元素。 free会销毁构造的元素并释放内存。 chk_n_alloc保证StrVec至少有容纳一个新元素的空间。如果没有空间添加新元素,chk_n_alloc会调用reallocate来分配更多内存。 reallocate在内存用完时为StrVec分配新内存。
class StrVec
{
public :
StrVec ( ) : elements ( nullptr ) , first_free ( nullptr ) , cap ( nullptr ) { }
StrVec ( const StrVec & ) ;
StrVec & operator = ( const StrVec & ) ;
~ StrVec ( ) ;
void push_back ( const std:: string & ) ;
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, canst string * e)
{
auto data = alloc. allocate ( e - b) ;
return { data, uninitialized_copy ( 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 ;
}
在编写reallocate成员函数之前,我们稍微思考一下此函数应该做什么。
为一个新的、更大的string数组分配内存。 在内存空间的前一部分构造对象,保存现有元素。 销毁原内存空间中的元素,并释放这块内存。
void StrVec :: reallocate ( )
{
auto newcapacity = size ( ) ? 2 * size ( ) : 1 ;
auto newdata = alloc. allocate ( newcapacity) ;
auto dest = newdata;
auto elem = elements;
for ( size_ti = 0 ; i != size ( ) ; ++ i)
{
alloc. construct ( dest++ , std:: move ( * elem++ ) ) ;
}
free ( ) ;
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
13.6 对象移动
新标准的一个最主要的特性是可以移动而非拷贝对象的能力。 很多情况下都会发生对象拷贝,在其中某些情况下,对象拷贝后就立即被销毁了,在这些情况下,移动而非拷贝对象会大幅度提升性能。 标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
13.6.1 右值引用
为了支持移动操作,新标准引入了一种新的引用类型,右值引用。 通过&&而不是&来获得右值引用。
int i = 42 ;
int & r = i;
int && rr = i;
int & r2 = i * 42 ;
const int & r3 = i * 42 ;
int && rr2 = i * 42 ;
int && rr1 = 42 ;
int && rr2 = rr1;
左值持久,右值短暂。 右值引用指向将要被销毁的对象,因此可以从绑定到右值引用的对象“窃取”状态。 变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。 虽然不能将一个右值引用直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型。 可以通过调用一个名为move 的新标准库函数来获得绑定到左值上的右值引用,定义在头文件utility中。
int && rr3 = std:: move ( rr1) ;
调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。
13.6.2 移动构造函数和移动赋值运算符
类似string类(及其他标准库类),如果自己创建的类也同时支持移动和拷贝,那么也能从中受益。 如为StrVec类定义移动构造函数。
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。 如果移动到一般出异常了,就会产生问题,移动源元素已经被改变了,而新空间中未构造的元素可能尚不存在。 移动赋值运算符执行与析构函数和移动构造函数相同的工作。
StrVec & StrVec:: operator = ( StrVec && rhs) noexcept
{
if ( this != & rhs)
{
free ( ) ;
elements = rhs. elements;
first_free = rhs. first_free;
cap = rhs. cap;
rhs. elements = rhs. first_free = rhs. cap = nullptr ;
}
}
在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。 与处理拷贝构造函数和拷贝赋值运算符一样,编译器也会合成移动构造函数和移动赋值运算符,但条件大不相同。 只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
struct X
{
int i;
std:: string s;
}
struct hasX
{
X mem;
} ;
X x, x2 = std:: move ( x) ;
hasX hx, hx2 = std:: move ( hx) ;
与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。 将合成的移动操作定义为删除的函数遵循与定义删除的合成拷贝操作类似的原则
有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。 类似拷贝赋值运算符,如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的。
struct hasY
{
hasY ( ) = default ;
hasY ( hasY && ) = default ;
Y mem;
} ;
hasY hy, hy2 = std:: move ( hy) ;
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认地被定义为删除的。 如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则使用哪个构造函数。
StrVec v1, v2;
v1 = v2;
StrVec getVec ( istream & ) ;
v2 = getVec ( cin) ;
class Foo
{
public :
Foo ( ) = default ;
Foo ( const Foo & ) ;
} ;
Foo x;
Foo y ( x) ;
Foo z ( std:: move ( x) ) ;
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 ;
}
} ;
hp = hp2;
hp = std:: move ( hp2) ;
所有五个拷贝控制成员应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作,如前所述,某些类必须定义拷贝构造函数、 拷贝赋值运算符和析构函数才能正确工作,这些类通常拥有一个资源,而拷贝成员必须拷贝此资源,一般来说,拷贝一个资源会导致一些额外开销,在这种拷贝并非必要的情况下,定义了移动构造函数和移动赋值运算符的类就可以避免此问题。 新标准库中还定义了一种移动迭代器 适配器,make_move_iterator
函数。 由于一个移后源对象具有不确定的状态,对其调用std::move是危险的,当调用move时,必须绝对确认移后源对象没有其他用户。
13.6.3 右值引用和成员函数
除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动版本,也能从中受益。 如push_back的标准库容器提供两个版本。
void push_back ( const X & ) ;
void push_back ( X && ) ;
通常在一个对象上调用成员函数,而不管该对象是一个左值还是右值。
string s1 = "a value" , s2 = "another" ;
auto n = ( s1 + s2) . find ( 'a' ) ;
s1 + s2 = "wow'" ;
新标准库类仍然允许向右值赋值,但是,我们可能希望在自己的类中阻止这种用法。 在参数列表后放置一个引用限定符 强制左侧运算对象是个左值。
class Foo
{
public :
Foo & operator = ( const Foo & ) & ;
} ;
Foo & Foo:: operator = ( const Foo & rhs) &
{
return * this ;
}
Foo & retFoo ( ) ;
Foo retVal ( ) ;
Foo i, j;
i = j;
retFoo ( ) = j;
retVal ( ) = j;
i = retVal ( ) ;
一个函数可以同时用const和引用限定,在此情况下,引用限定符必须跟随在const限定符之后。
class Foo
{
public :
Foo someMem ( ) & const ;
Foo anotherMem ( ) const & ;
}
class Foo
{
public :
Foo sorted ( ) && ;
Foo sorted ( ) const & ;
private :
vector< int > data;
}
Foo
Foo :: sorted ( ) &&
{
sort ( data. begin ( ) , data. end ( ) ) ;
return * this ;
}
Foo Foo :: sorted ( ) const &
{
Foo ret ( * this ) ;
sort ( ret. data. begin ( ) , ret. data. end ( ) ) ;
return ret;
}
retVal ( ) . sorted ( ) ;
retFoo ( ) . sorted ( ) ;
如果定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加。