C++primer第五版第13章笔记
拷贝控制
我们可以显示的写出一些函数,如:拷贝构造函数、移动构造函数:同类型另一个对象初始化本对象;拷贝和移动运算符:将一个对象赋予同类型的另一个对象;析构函数:对象销毁;这些统称为拷贝控制操作。
13.1拷贝、赋值与销毁
13.1.1拷贝构造函数
如果一个构造函数的第一个参数是自己类型的引用,且其他参数都有默认值,则为拷贝构造函数,并且通常都不是explicit的。即使我们定义了其他构造函数,编译器依然会合成一个默认构造函数,将参数依次拷贝到正在创建的对象中,将给定对象的非static成员依次拷贝到创建的对象中。
每个成员都有不同的拷贝方式:类类型采用拷贝构造函数拷贝,内置类型直接拷贝,数组会被逐元素依次拷贝。
使用直接初始化时编译器寻找提供参数最匹配的构造函数,拷贝初始化将右侧对象拷贝到正在创建的对象中,需要的话可以进行类类型转化。
拷贝初始化使用情况:
使用=时;从返回类型为非引用类型的函数返回对象;用花括号初始化数组或聚合类成员。使用emplace都是直接初始化。
由于拷贝构造函数用来初始化非引用类型的参数,所以拷贝构造函数第一个参数必须为引用,否则会调用想调用拷贝构造函数来初始化参数陷入循环。
当我们使用explicit时,直接初始化与拷贝初始化没有区别,我们可以绕过拷贝初始化,但是拷贝构造函数必须存在。
13.1.2拷贝赋值运算符
我们可以像控制类对象初始化一样控制对象的赋值。重置运算符的参数为它运算所需的对象,某些运算符必须为成员函数,运算符是成员函数,左侧运算对象就必须绑定到隐式的this参数。赋值运算符通常返回一个左侧对象的引用。若拷贝赋值运算符并非为了阻止某些类对象的拷贝,则会将右侧对象每个非static成员赋予左侧运算对象成员,并返回一个指向左侧运算对象的引用。
sales_data& sales_data::operator=(const sales_data &rhs){
bookno=rhs.bookno;
unites_sold=rhs.unites_sold;
return *this;
}
13.1.3析构函数
构造函数初始化对象的非static成员,析构函数释放对象资源,并销毁非static数据成员。析构函数是一个成员函数,前方有一个波浪号,由于没有返回类型和参数,所以析构函数不能重载,只有一个。析构函数中首先执行函数体,再隐式的按照数据成员逆序的销毁数据成员,隐式销毁一个内置指针类型的成员并不会delete其所指对象。
sales_data *data=new sales_data;(内置指针需要使用显示使用delete销毁)
调用析构函数的情况:
变量离开作用域时;一个对象被销毁时,其成员被销毁;容器(或者数组)销毁时,内部元素逐个销毁;动态分配的对象,对指向它的指针使用delete;临时对象,作用域结束时。
13.1.4三/五法则
我们可以通过三个操作来控制类的拷贝操作:拷贝构造函数、拷贝构造运算符和析构函数。一般需要析构函数我们也需要拷贝构造函数和拷贝构造运算符,因为如果所创建对象中有动态内存指针或者内置型指针,我们在析构函数中添加delete时,在某些情况下可能采用合成拷贝会造成同一块内存被delete两次的非法行为。
hasptr f(hasptr ph){
hasptr r=ph;
return r;
}//此函数因为实参会传递对象,会拷贝,并且内部赋值时也会拷贝,由于hasptr里有动态内存的指针,
//所以赋值时会指向同一内存区域,当块结束时,ph会调用析构delete一次,r也会delete一次,导致同一区域delete两次
hasptr ph;
f(ph);
hasptr o(ph);//这里由于合成拷贝将ph实参传递时动态内存指向同一区域,导致析构函数将ph的内存也delete了,所以ph无效
一般需要拷贝构造函数也需要拷贝操作运算符,反之亦然,但不一定需要析构函数。
13.1.5使用=default
我们可以使用=default显示的将能生成合成的成员函数默认初始化,如果在类内声明,则会自动转化成内联函数,如果在类外声明,则不会自动声明成内联函数。
13.1.6阻止拷贝
某些内的拷贝操作或者其他默认操作可能是不满足需要的,比如iostream希望避免拷贝,避免多个对象写入或读取相同的IO缓冲。我们可以定义删除的函数来表示不能使用该成员函数,在参数列表后加=delete即可。一般的,析构函数不能是删除函数,如果析构函数要定义成删除函数,我们不能有任何数据成员,因为无法释放,同时我们不能创建临时对象,只能动态分配对象,且不能delete;
class data;(data类中将析构函数定义成删除函数)
data p;(错误,不能建立临时对象)
data *q=new data();(正确)
delete q;(错误,不能delete释放对象)
如果类中某些成员的拷贝操作和析构操作是删除函数,则类的合成拷贝操作和合成析构也是删除的,具体看情况分析。早期,我们通过将函数在private中声明来防止访问,同时为了防止友元函数和成员函数访问,我们不给这些函数具体的定义。
13.2拷贝控制和资源管理
通常管理类外资源的类需要定义析构函数(可能是我们在构造函数中动态分配了内存,需要在析构中添加delete),通常我们还需定义拷贝操作,因此我们需要明确拷贝是基于哪种方式实现:
1、类行为像一个值,副本不会影响原对象,如vector和string
2、类行为像一个指针,共享底层的数据内存,当改变时都改变,如shared_ptr。
IO类型不允许拷贝和赋值,所以行为不像值或指针。
13.2.1行为像值的类
类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝
class hasptr{
public:
hasptr(const string &s=string()):
ps(new string(s)),i(0){}
hasptr(const hasptr &temp):
ps(new string(*temp.ps)),i(temp.i){};
hasptr& operator=(const hasptr &temp){
auto test=new string(*temp.ps);
delete ps;
ps=test;
i=temp.i;
return *this;
};
~hasptr(){delete ps;}
private:
string *ps;
int i;
};
如上所示,hasptr拷贝构造函数拷贝一个string对象不是拷贝指针;析构函数释放string动态内存;定义拷贝运算符释放当前对象,并拷贝右侧对象。
赋值拷贝操作通常组合了析构函数和构造函数的操作,因为我们需要对象自身使用拷贝赋值运算符时不出错,所以需要按照上述顺序来执行操作。
13.2.2定义行为像指针的类
对于行为类似指针的类,需要定义拷贝操作和析构函数,拷贝指针成员本身而不是指向的string,需要使用析构来释放构造函数分配的string内存,注意只有最后一个指向string的对象被销毁时才释放内存。
class hasptr{
public:
hasptr(const string &s=string()):
ps(new string(s)),i(0),use(new size_t(1)){}//构造函数创建一个动态内存存储string和use
hasptr(const string &rhs):
ps(rhs.ps),i(rhs.i),use(rhs.use){
++*use;
} //每当有拷贝时,use计数加1
hasptr& operator=(const string &rhs);
~hasptr();
private:
string *ps;
int i;
size_t *use;
};
hasptr& hasptr::operator=(const string &rhs){
++*rhs.use;
if(--*use==0){
delete ps;
delete use;
}
use=rhs.use;
i=rhs.i;
ps=rhs.ps
return *this;
} //首先增加左侧计数,再减小右侧对象计数,如果为
//0,则释放ps和use的动态内存
hasptr::~hasptr(){
if(--*use){
delete ps;
delete use;
}
}//减小计数,若为0,则释放ps和use的动态内存
13.3交换操作
管理资源的类通常还定义一个swap的函数,若未自定义,将使用标准库版本
hasptr temp=v1;
v1=v2;
v2=temp;
标准库版本先拷贝一个临时变量,再依次赋值,如果拷贝操作是值传递的,将额外开辟内存,我们更希望拷贝的是指针,所以我们需要自定义swap函数。
void swap(hasptr &lhs,hasptr &rhs){
using std::swap;
swap(lhs.ps,rhs.ps);
swap(lhs.i,rhs.i);
}//ps为一个string指针,在构造函数中我们为其开辟动态内存,因此我们直接交换指针更加快捷,同时也不需开辟新的内存
我们还可以在赋值运算符中使用swap提高效率,但要注意的是我们需要采用值传递的方式创建一个右侧对象的副本,否则将改变右侧对象的内容
hasptr& operator=(hasptr rhs){//注意采用值传递
sawp(*this,rhs);
return *this
}
13.4拷贝控制示例
#include <iostream>
#include<string>
#include<set>
using namespace std;
class folder;
class message{
friend class folder;
friend void swap(message &,message &);
public:
message(const string &s=""):content(s){}
message(const message &);
message& operator=(const message&);
~message(){
remove_from_folders();
};
void addfdr(folder*);
void remfdr(folder*);
void save(folder&);
void remove(folder&);
private:
string content;
set<folder*>fold;
void add_to_folders(const message&);
void remove_from_folders();
};
void message::addfdr(folder *f){
fold.insert(f);
}
void message::remfdr(folder *f){
fold.erase(f);
}
void message::save(folder &f){
fold.insert(&f);
f.addmsg(this);
}
void message::remove(folder &f){
fold.erase(&f);
f.remmsg(this);
}
message::message(const message &m):content(m.content),fold(m.fold){
add_to_folders(m);
}
message& message::operator=(const message &m){
remove_from_folders();
content=m.content;
fold=m.fold;
add_to_folders(m);
return *this;
}
void message::add_to_folders(const message &m){
for(auto f:fold){
f->addmsg(this);
}
}
void message::remove_from_folders(){
for(auto f:fold){
f->remmsg(this);
}
}
void swap(message &lhs,message &rhs){
using std::swap;
for(auto f:lhs.fold){
f->remmsg(&lhs);
}
for(auto f:rhs.fold){
f->remmsg(&rhs);
}
swap(lhs.fold,rhs.fold);
swap(lhs.content,rhs.content);
for(auto f:lhs.fold){
f->addmsg(&lhs);
}
for(auto f:rhs.fold){
f->addmsg(&rhs);
}
}
13.5动态内存管理类
#include <iostream>
#include<string>
#include<memory>
#include<algorithm>
#include<set>
using namespace std;
class strvec{
public:
strvec():elements(nullptr),first_free(nullptr),cap(nullptr){}
strvec(initializer_list<string>il);
strvec(const strvec&);
strvec& operator=(const strvec&);
~strvec();
void push_back(const string &);
size_t size(){
return first_free-elements;
}
size_t capacity(){
return cap-elements;
}
string *begin()const{
return elements;
}
string *end()const{
return first_free;
}
private:
allocator<string> alloc;
void chk_n_alloc(){
if(size()==capacity()) reallocate();
}
pair<string*,string*> alloc_n_copy(const string*,const string*);
void free();
void reallocate();
string *elements;
string *first_free;
string *cap;
};
strvec::strvec(initializer_list<string>il){
auto data=alloc_n_copy(il.begin(),il.end());
elements=data.first;
first_free=cap=data.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;
}
strvec::strvec(const strvec&s){
auto data=alloc_n_copy(s.begin(),s.end());
elements=data.first;
cap=first_free=data.second;
}
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_copy(b,e,data)};
}
void strvec::reallocate(){
auto newcapacity=size()?size()*2:1;
auto newdata=alloc.allocate(newcapacity);
auto data=newdata;
auto elm=elements;
for(size_t i=0;i<size();++i){
alloc.construct(data++,std::move(*elements++));
}
free();
elements=newdata;
first_free=data;
cap=newdata+newcapacity;
}
void strvec::free(){
if(elements){
for_each(elements,first_free,[this](string &p){
alloc.destroy(&p);
});
alloc.deallocate(elements,cap-elements);
}
}
13.6对象移动
新标准新增可以移动而非拷贝的特性,某些情况下对象拷贝后就立即被销毁了,如果采用移动而非拷贝将大幅度提高效率。某些情况下,如I/O类、unique_ptr类不能被拷贝只能被移动。
13.6.1右值引用
为了支持移动操作,引入了右值引用(右值引用必须绑定到右值上),通过&&来获得右值引用,需要绑定到将要销毁的对象上,因为我们一般对于这些对象采用拷贝,现在可以使用右值引用来移动提高效率。左值持久,右值短暂。我们使用右值引用绑定右值后,这个右值引用名就是左值,变量都是左值。我们可以使用move来将右值引用绑定到左值上,但是之后只能对这个左值进行赋值和销毁,不能使用这个左值。
13.6.2移动构造函数和移动赋值运算符
移动构造函数与拷贝构造函数结构类似,但传入参数为右值引用,我们必须保证移后原对象必须是可销毁的,由于移动操作不分配任何资源,所以应该不抛出异常,可以加上noexcept(在函数定义和声明都要加)。
移动赋值运算符执行析构函数和移动构造函数相同的操作,应该也不抛出异常。
通常编译器不会合成移动操作,只有对象的每个资源都可以移动操作且没有定义拷贝操作,才会定义合成移动操作,移动操作不会隐式的定义为删除函数,如果我们显示的定义为=defau且不能移动所有成员才会定义为删除函数,如果类既有拷贝函数也有移动函数,会根据右值和左值来进行判断采用何种函数,若没有移动函数,则为右值也使用拷贝函数,会将一个F&&转化为constF&。
strvec::strvec(strvec &&s)noexcept
:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
s.elements=s.first_free=s.cap=nullptr;//使移后原对象安全并可以析构
}
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;
}
如果我们定义了移动构造函数和拷贝构造函数,我们对于=操作符的参数不采取引用,将自动根据情况使用上述函数,如:
hasptr &operator=(hasptr rhs)//由于这里没有写左引用和右引用,所以进行拷贝rhs时会自行判断传入的是右值
[swap(*this,rhs); return *this;}//还是左值,并使用相应的构造函数
有时我希望对一些进行拷贝的算法使用移动,我们可以使用make_move_iterator来将迭代器变成右值引用,从而算法不采用拷贝而是采用移动。
13.6.3右值引用和成员函数
旧版本可能会使用右值来调用函数或者给右值来赋值,如:
string s1="a",s2="b";
auto n=(s1+s2).find('a');//采用右值来调用find函数
s1+s2="wow";//给右值赋值
foo &operator=(const &) &;//在函数后面加&表明左侧运算对象必须是左值,而且若要在函数后加const,&跟在后面
foo &operator=(const &) &&;//加两个&表明右值调用