拷贝构造函数
如果一个拷贝构造函数的第一个参数是自身类型的引用,且其他参数都有默认值,则此构造函数是拷贝构造函数
class Foo
{
public :
Foo ( ) ;
Foo ( const Foo& ) ;
} ;
也可以定义接受非const引用的拷贝构造函数,但大多数情况下都是一个const引用 拷贝构造函数不应该是explict的 如果没有给类定义拷贝构造函数,编译器会自动定义一个 合成拷贝构造函数会从给定对象中依次将每个非static成员拷贝到正在创建的对象中,等价于下面这样:
class Sales_data
{
public :
Sales_data ( const Sales_data& ) ;
private :
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 nullBook = "9-999-99999-9" ;
将对象作为实参传递给一个非引用类型的实参时、从一个返回类型为非引用类型的函数返回一个对象时、用花括号列表初始化一个数组中的元素或者一个聚合类中的成员时都会发生拷贝初始化 可以绕过拷贝构造函数,但是就算绕过没用到它,拷贝构造函数还是必须存在且可访问的(不能是private)
string nullBook = "9-999-99999-9" ;
string nullBook ( "9-999-99999-9" ) ;
拷贝赋值运算符
拷贝构造函数是使用已有的对象创建一个新的对象,而拷贝赋值运算符是将一个对象的值赋给另一个已存的对象。区别就是有没有新的对象产生
Sales_data trans, accum;
trans = accum;
重载运算符的参数表示运算符的运算对象。如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数
class Foo
{
public :
Foo& operator = ( const Foo& ) ;
} ;
如果没定义自己的拷贝赋值运算符,编译器会自动生成一个合成拷贝赋值运算符。 如果类有const成员,则它不能使用合成的拷贝赋值运算符,因为运算符试图给所有成员赋值。 拷贝赋值运算符会从右侧运算对象中的非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它指向的对象 当指向一个对象的引用或指针离开作用域时,析构函数不会执行(离开作用域,指针自己被销毁,但是指向的内容没有释放。因为指针也是个内置数据类型,就和int float一样,离开作用域自然被销毁;假设离开作用域指向的内容会被释放,那如果出现多个指针指向同一个对象,离开作用域发生什么情况?) 如果没有定义自己的析构函数,编译器会自动定义一个合成析构函数。对于某些类,合成析构函数用来阻止该类型的对象被销毁,如果不是这种情况,合成析构函数的函数体就为空 析构函数本身并不直接销毁成员。执行完(空)析构函数体之后,成员生命周期结束而被自动销毁
三五法则
如果一个类需要析构函数,则可以肯定它也需要一个拷贝构造函数和拷贝赋值运算符。如下代码所示:
class HasPtr
{
public :
HasPtr ( const string & s = string ( ) ) : ps ( new string ( s) ) , i ( 0 ) { }
HasPtr f ( HasPtr hp)
{
HasPtr ret = hp;
return ret;
}
~ HasPtr ( ) { delete ps; }
private :
string * ps;
int i;
} ;
如果一个类需要拷贝构造函数,则几乎可以肯定它也需要一个拷贝赋值运算符。
=default
使用-default能显式地要求编译器生成合成版本
{
Sales_data ( ) = default
Sales_data& operator = ( const Sales_data & ) ;
Sales_data ( const Sales_data& ) = default ;
~ Sales_data ( ) = default ;
}
Sales_data& Sales_data:: operator = ( const Sales_data & ) = default ;
在类内使用=default时,合成的函数是隐式声明为内联的。如果不想声明成内联的,可以只对成员的类外定义使用=default,比如上面的拷贝赋值运算符 只能对具有合成版本的成员函数使用=default
阻止拷贝
有些类比如iostream必须阻止拷贝以避免多个对象写入或读取相同的IO缓冲 可以将拷贝构造函数和拷贝赋值运算符定义为删除函数来阻止拷贝。在函数参数列表后面加上delete指出将它定义为删除的
NoCopy ( const NoCopy) = delete ;
NoCopy operator = ( const Nocopy & ) = delete ;
可以对任意函数指定=delete,但析构函数不能是删除的成员。对于一个删除了析构函数的类型就不能定义这种类型的遍历或释放该类型动态分配对象的指针 如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的 新标准出来之前是将拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝的
class PrivateCoPy
{
PrivateCopy ( const PrivateCopy& ) ;
PrivateCopy & operator ( const PrivateCopy& ) ;
public :
PrivateCopy ( ) = default ;
~ PrivateCopy ( ) ;
} ;
定义像值的类
行为像值的类,意味着副本和原对象是独立的。改变副本不会对原对象有任何影响(比如string) 行为像指针的类,意味着副本和原对象使用相同的底层数据。改变副本也会改变原对象(比如shared_ptr)
class HasPtr
{
public :
HasPtr ( const string & s = string ( ) ) : ps ( new string ( s) ) , i ( 0 ) { }
HasPtr ( const HasPtr & ph) : ps ( new string ( * ph. ps) ) , i ( ph. i) { }
HasPtr& operator = ( const HasPtr& rhs)
{
string * newp = new string ( * rhs. ps) ;
delete ps;
ps = newp;
i = rhs. i;
return * this ;
}
~ HasPtr ( ) { delete ps; }
private :
string * ps;
int i;
} ;
这里拷贝赋值运算符考虑到了自赋值的情况。如果写成这样就会发生错误:
HasPtr& operator = ( const HasPtr& rhs)
{
delete ps;
ps = new string ( * ( rhs. ps) ) ;
i = rhs. i;
return * this ;
}
写赋值运算符的注意点
如果将一个对象赋予自身,赋值运算符必须能正常工作 大多数赋值运算符组合了析构函数和拷贝构造函数的工作
定义像指针的类
定义行为像指针的类,我们要拷贝指针成员本身而不是它指向的string 析构函数不能单方面释放关联的string。只有当最后一个指向string的HasPtr销毁时才能释放string 让类表现的像指针的最好放法是使用shared_ptr管理类中的资源。如果我们想直接管理资源,就可以使用引用计数
除了初始化对象,构造函数还要创建引用计数 拷贝构造函数递增共享的计数器 析构函数递减计数器,递减为0则释放状态 拷贝赋值运算符递增右侧对象的计数器,递减左侧对象的计数器。递减为0则销毁
class HasPtr
{
public :
HasPtr ( const string & s = string ( ) ) :
ps ( new string ( s) ) , i ( 0 ) , use ( new size_t ( 1 ) ) { }
HasPtr ( const HasPtr & p) :
ps ( p. ps) , i ( ph. i) , use ( p. use) { ++ * use; }
HasPtr& operator = ( const HasPtr& rhs) ;
~ HasPtr ( ) ;
private :
string * ps;
int i;
size_t * use;
} ;
HasPtr :: ~ HasPtr ( )
{
if ( -- * use == 0 )
{
delete ps;
delete use;
}
}
HasPtr& HasPtr:: operator = ( const HasPtr & rhs)
{
++ * rhs;
if ( -- * use == 0 )
{
delete ps;
delete use;
}
ps = rhs. ps;
i = rhs. i;
use = rhs. use;
return * this ;
}
交换
如果一个类没定义自己的swap,将使用标准库定义的swap。在需要交换两个元素时会调用swap 但是标准库的swap会比较浪费,比如下面很多动态内存分配都是不必要的。
HasPtr temp = v1;
v1 = v2;
v2 = temp;
string * temp = v1. ps;
v1. ps = v2. ps;
v2. ps = temp
swap不是必要的,但是对于分配了资源的类来说swap可能会非常重要。我们可以定义自己的swap来重载默认的版本
class HasPtr
{
friend void swap ( HasPtr& , HasPtr& ) ;
} ;
inline void swap ( HasPtr & lhs, HasPtr & rhs)
{
using std:: swap;
swap ( lhs. ps, rhs. ps) ;
swap ( lhs. i, rhs. i) ;
}
HasPtr& HasPtr:: operator = ( HasPtr rhs)
{
swap ( * this , rhs) ;
return * this ;
}
右值引用
右值引用就是对右值的引用(在第四章-表达式中讲过左值、右值)。平时的引用可以理解为对左值的引用。使用&&来获得右值引用
int i = 42 ;
int & r1 = i;
int && rr1 = i;
int & r2 = i * 42 ;
const int & r3 = i * 42 ;
int && rr2 = i * 42 ;
虽然不能将右值引用绑定到左值上,但可以通过move函数获得绑定到左值上的右值引用
int && rr3 = std:: move ( rr1) ;
移动构造函数和移动赋值运算符
移动构造函数不分配新内存,而是接管原来内存的控制权 移动构造函数的第一个参数也是该类类型的引用,但是是右值引用 移动构造函数要确保移后源对象被销毁是无害的。且移动完成后源对象不再指向移动后的资源
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 ;
}
return * this ;
}
当一个类没有定义任何自己版本的控制成员,且类的每个非static数据成员都能移动时,编译器才会合成移动构造函数或移动赋值运算符 如果我们为难编译器,比如将移动操作声明为default的,但是有不能移动的数据成员,这时候会将移动操作定义为删除函数 如果一个类有拷贝构造函数但是没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的。
class Foo
{
public :
Foo ( ) = default ;
Foo ( const Foo& ) ;
} ;
Foo 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 ; }
}
三五/法则更新:如果一个类定义了任何一个拷贝操作,它就该把所有五个操作都定义了 由于一个移后源对象具有不确定的状态,对其调用std::move会比较危险。当调用move时必须绝对确认移后源对象没有其他用户
右值引用和成员函数
假设StrVec有两个版本的push_back,一个是const左值引用,一个是右值引用。可以将能转换为类型string的任何对象传递给第一个版本。但是第二个版本只能给它传递非const的右值
void push_back ( const string & s)
{
chk_n_alloc ( ) ;
alloc. construct ( first_free++ , s) ;
}
void push_back ( string && s)
{
chk_n_alloc ( ) ;
alloc. construct ( first_free++ , std:: move ( s) ) ;
}
StrVec vec;
string s = "abc" ;
vec. push_back ( s) ;
vec. push_back ( "abc" ) ;
string s1 = "abc" , s2 = "def" ;
auto n = ( s1 + s2) . find ( 'a' ) ;
s1 + s2 = "wow!" ;
新标准库允许这种行为,但是我们可以阻止这种用法——同时在函数声明和定义的参数列表后放置一个引用限定符&
class Foo
{
public :
Foo & operator = ( const Foo& ) & ;
} ;
Foo & Foo:: operator = ( const Foo & rhs) &
{
}
引用限定符可以是&或者&&,表示左侧只能是左值或者只能是右值 同时出现const和&时,引用限定符跟在const后面 可以利用引用限定符重载函数
class Foo
{
public :
Foo sorted ( ) && ;
Foo sorted ( ) const & ;
peivate:
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 ( ) ;
重载const函数可以一个有const一个没有。但是重载引用限定的不一样,如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符
class Foo
{
public :
Foo sorted ( Comp* ) ;
Foo sorted ( Comp* ) const ;
Foo sorted ( ) && ;
Foo sorted ( ) const ;