21:知识点:判断一个类是否需要拷贝控制函数成员,首先判断其是否需要自定义版本的析构函数,如果需要,则拷贝控制成员函数都需要。由于这两个类中的指针为智能指针,可以自动控制内存的释放,所以使用类的合成析构函数即可。另外类默认的拷贝控制成员对于智能指针的拷贝也不需要自定义版本来修改,所以全部定义为 =default 即可
22:知识点1:管理类外资源的类必须定义拷贝控制成员
知识点2:为了定义拷贝控制成员,我们可以定义拷贝操作,使得类的行为看起来像是一个值或者一个指针
知识点3:类的行为像一个值,拷贝发生时,副本和原对象是完全独立的,改变副本不会对原对象产生影响
知识点4:类的行为像一个指针,拷贝发生时,副本和原对象共用底层数据,改变副本也会改变原对象
知识点5:标准库容器和string类就是像值的类,shared_ptr类就是像指针的类,IO类和unique_ptr不允许拷贝和赋值,所以都不是
class Hasptr{public: Hasptr();//默认构造函数 //拷贝构造函数,完成string 指针指向内容的拷贝和i值的拷贝 Hasptr(const Hasptr& p):ps(new string(*p.ps)),i(p.i){} //拷贝赋值运算符 Hasptr& Operator= (const Hasptr& p) { auto new_ps = new string(p.ps); delete ps; ps = new_ps; return *this; } //析构函数 ~Hasptr(){delete ps;}PRivate: string *ps; int i;};23:知识点1:类值版本,类的构造函数需要可能需要动态分配其成员的副本
知识点2:类值版本,类的拷贝赋值运算符相当于结合了构造函数和析构函数的操作,首先销毁左侧运算对象的资源,再从右侧运算符对象拷贝资源,注意顺序
知识点3:由于有上述的顺序存在,所以我们必须保证这样的拷贝赋值运算符是正确的:首先将右侧运算对象拷贝到一个临时的对象中,再销毁左侧的运算对象的现有成员,之后将临时对象中的数据成员拷贝至左侧对象中(防范自赋值的情况发生—首先就销毁了自身的成员,再进行拷贝自身则会访问到已经释放的内存中)
见22题,编写时忘了一个析构函数,ps在构造函数中是动态分配的内存,所以需要进行delete
24:未定义析构函数,ps在使用结束后不会被合成版本的析构函数释放,造成内存泄漏。未定义拷贝构造函数,使用自定义版本的拷贝构造函数,对于ps的拷贝就会是指针本身的拷贝。
25:动态分配的内存由shared_ptr管理,析构函数之后会自动判断进行释放,所以不需要自定义版本的析构函数。
26:知识点1:定义行为像指针的类,在不想使用shared_ptr的情况下我们可以使用引用计数来确定是否释放内存
知识点2:每个构造函数(拷贝构造函数除外)都创建一个引用计数,记录对象的共享状态,第一次被新建时,计数为1
知识点3:析构函数递减引用计数,拷贝赋值运算符递增右侧对象的引用计数,递减左侧的,当左侧的引用计数为0时,拷贝赋值运算符就必须销毁状态
知识点4:计数器不能直接作为类对象的成员,否则在拷贝中,会出现歧义,我们可以将计数器保存在动态内存中,只定义一个指向计数器的指针,这样拷贝或者赋值时,我们拷贝该指针,副本和原对象指向同样的计数器
class Hasptr1{public: //构造函数,初始化相关成员 Hasptr1(const string& s = string()):ps(new string(s)),i(0),use(new size_t(1)){} //拷贝构造函数,将引用计数也拷贝过来,并且递增引用计数 Hasptr1(const Hasptr1& p):ps(p.ps),i(p.i),use(p.use){++*use;} //拷贝赋值运算符 Hasptr1& operator= (const Hasptr1& p1) { ++*p1.use;//首先递增右侧运算符对象的引用计数 if (--*use == 0)//递减本对象的引用计数,若没有其他用户,则释放本对象的成员 { delete ps; delete use; } ps = p1.ps;//进行拷贝 use = p1.use; i = p1.i; return *this; } //析构函数 ~Hasptr1() { if (*use == 0)//引用计数变为0,说明已经没有对象再需要这块内存,进行释放内存操作 { delete ps; delete use; } }private: //定义为指针,是我们想将该string对象保存在动态内存中 string *ps; size_t *use;//将计数器的引用保存 int i;};28:(a)类似于27题 (b)只有一个指针成员,参照27题
29:知识点1:如果一个类定义了自己的swap,那么算法将利用类自己的版本(重排顺序等算法)
知识点2:自定义版本的swap存在的必要性:我们不希望进行新的内存分配,只希望将其指针进行拷贝赋值(交换的本质),省去不必要的内存分配,将函数定义为friend,以便访问private成员
知识点3:相对于拷贝控制成员,swap并不是不要的,但是对于那些分配了资源的类,定义swap可能是一种很重要的优化手段
知识点4:swap函数自定义版本与std中版本的重合问题:对于swap函数,其调用应该都是不加限定的,若加std::swap则调用的是标准库的版本,而标准库的版本在一定程度上是为了那些内置类型没有自定义版本的swap而准备的,若一个类有其自定义版本的swap函数,则我们就不应该使用std版本的。所以我们只要在前加上using std::swap声明,即可,在使用中,若有类特定的swap,其匹配程度则会优于std中的版本(616页有详解)
知识点5:在赋值运算符中使用swap,以传值的方式传入新对象,再进行拷贝赋值,在一定程度上会比较安全
见知识点4,因为其调用到最后使用的是std中的swap,不存在循环
30:
class Hasptr{ friend void swap(Hasptr&,Hasptr&);public: Hasptr();//默认构造函数 //拷贝构造函数,完成string 指针指向内容的拷贝和i值的拷贝 Hasptr(const Hasptr& p):ps(new string(*p.ps)),i(p.i){} //拷贝赋值运算符 Hasptr& operator= (const Hasptr& p) { auto new_ps = new string(*p.ps); delete ps; ps = new_ps; return *this; } //析构函数 ~Hasptr(){delete ps;}private: string *ps; int i;};inline void swap(Hasptr& a,Hasptr& b){ using std::swap; swap(a.ps,b.ps); std::swap(a.i,b.i); cout<<"123";}
新闻热点
疑难解答
图片精选