C++ Primer笔记14---chapter13 拷贝控制1
Posted Ston.V
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Primer笔记14---chapter13 拷贝控制1相关的知识,希望对你有一定的参考价值。
1.当定义一个类时,会通过五种特殊的成员函数来控制此类型的对象的拷贝、移动、赋值和销毁:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数
2.拷贝构造函数
2.1 拷贝构造函数的第一个参数必须是自身类型的引用,此参数几乎总是一个const引用,且任何额外参数都有默认值
与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数
class Sales_data { public: Sales_data(); virtual ~Sales_data(); //与合成的拷贝构造函数等价的拷贝构造函数的声明 Sales_data(const Sales_data&); protected: private: std::string bookNo; int units_sold=0; double revenue=0.0; }; //与Sales_data的合成的拷贝构造函数等价 Sales_data::Sales_data(const Sales_data &orig): bookNo(orig.bookNo), units_sold(orig.units_sold), revenue(orig.revenue) { }
2.2 拷贝初始化
此时我们知道了拷贝初始化和赋值初始化的区别
//直接初始化,实际上是要求编译器使用函数匹配来选择对应的构造函数 string dots(10,'.'); //拷贝初始化,实际上是要求编译器将右侧对象拷贝到正在创建的对象中,如果需要还要进行类型转化 //拷贝初始化通常是由拷贝构造函数完成,但如果一个类有一个移动构造函数,则拷贝初始化时可能使用后者 string nines = string(100,'9');
拷贝初始化不仅在使用=定义变量时会发生,下列情况也会发生:将一个对象作为实参传递给一个非引用类型的形参、从一个返回类型为非引用类型的函数返回一个对象、从花括号列表初始化一个数组中的元素或一个聚合类的成员
2.3 返回值
拷贝构造函数用来初始化费引用类型参数,如果其参数不是引用类型,则调用永远不会成功:为了调用拷贝构造函数,我们必须拷贝他的实参,但为了拷贝实参又必须调用拷贝构造函数,如此会无限循环。
3. 拷贝赋值运算符
如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。
3.1 重载赋值运算符
重载运算符本质是函数,函数名由operator加上符号组成,因此赋值运算符就是名为operator=的函数
赋值运算符通常应该返回一个指向其左侧运算对象的引用
class Foo { public: Foo& operator=(const Foo&); //赋值运算符 };
类似拷贝构造函数,若自己未定义,编译器会定义拷贝赋值运算符;同样的,对于某些类,合成拷贝赋值运算符用来禁止该类型的对象赋值,如果拷贝赋值运算符并非此目的,则会将右侧对象的每个非static成员赋予给左侧对象的对应成员
//等价于合成赋值拷贝运算符 Sales_data& Sales_data::operator=(const Sales_data &rhs){ bookNo=rhs.bookNo; units_sold=rhs.units_sold; revenue=rhs.revenue; return *this; }
4.析构函数
析构函数释放对象使用资源,并销毁对象的非static数据成员
它没有返回值,也不接受参数,函数名前有~;由于没有参数因此不能被重载,一个类只会有唯一的析构函数
//等价于合成析构函数 class Sales_data{ public: //成员会自动销毁,除此之外不需要做其他的事 ~Sales_data() {} };
析构函数自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
5.三/五法则
有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数;在新标准下还可以定义一个移动构造函数和一个移动赋值运算符;并不要求定义所有,但是只需要其中一个操作,而不需要定义所有操作的情况很少见。
当我们决定一个类是否要定义他自己版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个析构函数
如果一个类需要自定义析构函数,几乎可以肯定他也需要自定义拷贝赋值运算和拷贝构造函数
class HasPtr{ public: HasPtr(const std::string &s = std::string()): ps(new std::string(s)),i(0){} //错误:HasPtr需要一个拷贝构造函数和一个拷贝赋值运算符 ~HasPtr(){delete ps} private: std::string *ps; int i; }; //这个版本类使用合成构造函数和合成赋值运算符,这些函数简单拷贝指针成员,这意味着对个HasPtr对象可能指向相同的内存 //错误case:连续delete两次 //当f返回时,hp和ret都会被销毁,但是二者包含相同指针值,最终导致此指针被delete两次 HasPtr f(HasPtr hp){ HasPtr ret=hp; return ret; } //另外 HasPtr p("hello"); f(p); //当f结束,p.ps指向内存会被释放 HasPtr q(p); //现在p和q都指向无效内存
如果一个类需要自定义拷贝赋值运算符,那么他一定需要拷贝构造函数,反之亦然;但是不意味着需要析构函数
6.使用default
可以将拷贝控制成员定义为=default来显示要求编译器生成合成的版本
class Sales_data{ public: //拷贝控制成员使用default Sales_data()=default; Sales_data(const Sales_data &)=default; Sales_data& operator=(const Sales_data&); ~Sales_data()=default; }; Sales_data& operator=(const Sales_data&) = default;
如果我们不希望合成成员是内联函数,应该只对成员的类外定义使用=default
只能对具有合成版本的成员函数使用=default(即默认构造函数或拷贝控制成员)
7.阻止拷贝
有时,定义类时必须采用某种机制组织拷贝或者赋值,例如iostream阻止了拷贝以避免多个对象写入或者读取相同的io缓冲。即使不定义拷贝控制成员,编译器也会生成合成的。
7.1 定义删除的函数
在新标准下,我们可以使用=delete来声明我们不希望以任何方式使用此函数,用法类似=default,但是与=default有两点不同:=delete必须出现在函数第一次声明的时候;我们可以对任何函数指定=delete
但是析构函数不能是删除的成员
7.2 private拷贝控制
在新标准前,类是通过将拷贝构造函数和拷贝赋值运算符生命为private来组织拷贝的。但是友元和成员函数仍然可以拷贝对象,为了阻止他们,我们讲这些拷贝控制成员声明为private的,但是不定义他。
8. 拷贝控制和资源管理
通常,管理类外资源的类必须定义拷贝控制成员,可以这种类可以看起来像值,也可以看起来像指针。
8.1 行为像值的类
定义行为像值的类需要拷贝其底层指针指向的对象
编写赋值运算符时,注意两点:如果将一个对象赋予了自身,赋值运算符必须正常工作;大多赋值运算符组合了析构函数和拷贝构造函数的工作。
注意如果使用赋值运算符赋予自身,不能先delete,这样的话会导致自身指针指向的对象被释放,从而无从拷贝,只剩一个空指针了。正常做法是先将右侧值拷贝到局部变量,然后delete自身指向的对象,最后将局部变量拷贝给本对象
class HasPtr { public : HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0){} //对ps指向的string,每个HasPtr对象都有自己的拷贝 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); //拷贝底层string delete ps; //释放旧内存 ps = newp; //从右侧运算对象拷贝数据到本对象 i = rhs.i; return *this; }
8.2 行为像指针的类
定义行为像指针多的类需要拷贝指针成员本身,此时析构函数不能单方面的释放关联的string,只有当最后一个指向string的HasPtr销毁时,他才可以释放string (类似于shared_ptr),有时我们希望直接管理资源(即不使用shared_ptr),就可以使用引用计数。
引用计数不能是HasPtr对象的成员,否则,p1先后拷贝给p2,p3,当p3的计数更新后,无法更新p2的计数。可以将计数器保存在动态内存中,当拷贝或者赋值对象时,我们拷贝指向计数器的指针,这样副本和原对象都会指向相同的计数器。
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() {delete ps;} private: std::string *ps; int i; std::size_t *use; //用来记录有多少个对象共享*ps的成员 }; 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; }
9. 交换操作
除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。
如果一个类定义了自己的swap,那就会使用自定义版本,否则使用标准库版本,我们希望swap交换指针,而非需要开辟临时对象来转存(开辟临时对象转存如:HasPtr tmp=v1;v1=v2;v2=tmp)。(swap并不是必须,但是可以通过定义swap进行代码优化),如果需要优化,此时swap函数应该调用自定义的swap,而非标准库的std::swap.
class HasPtr { public : //定义swap为friend,以便能访问到HasPtr的peivate数据成员 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); }
在赋值运算符中使用swap,这种称作是拷贝并交换的计数,将左侧运算对象与右侧对象的副本进行交换。注意,其中的参数并不是一个引用,这个技术有趣在于自动处理里了自赋值情况且天然就是异常安全的(在改变左侧运算对象之前,先拷贝右侧运算对象从而保证自赋值的安全,实际上和前面的方法是一致的)。
HasPtr& HasPtr::operator=(HasPtr rhs){ swap(*this,rhs); //rhs现在指向本对象曾经使用的内存 return *this; //rhs被销毁,从而delete了rhs中的指针 }
以上是关于C++ Primer笔记14---chapter13 拷贝控制1的主要内容,如果未能解决你的问题,请参考以下文章
C++ Primer笔记16---chapter13 代码实例
C++ Primer笔记15---chapter13 拷贝控制2