C++ Primer读书笔记梳理系列(三)
在我看来这部分的内容,比较底层,也是比较难的,但是非常重要,面试过程中也经常遇到
第13章 拷贝控制
拷贝、赋值与销毁
-
拷贝控制操作
特殊的成员函数 控制类的行为 拷贝和移动构造函数 用同类型的另一个对象初始化本对象时做什么(class a(b)) 拷贝和移动赋值运算符 将一个对象赋予同类型的另一个对象时做什么(class a = b) 析构函数 此类型对象销毁时做什么 如果没有,编译器会生成这些函数,但是不一定是我们想要的
-
拷贝构造函数
-
合成的拷贝构造函数,简单的拷贝复制
-
直接初始化与拷贝初始化区别:一个简单的普通函数匹配(构造函数或者拷贝构造函数),另一个是拷贝构造函数
string dots(10, 's'); //直接初始化 string s(dots); //直接初始化 string s2 = dots; //拷贝初始化
拷贝初始化会在定义变量时会用=会发生外,还有以下情况会使用拷贝初始化(值传递)
-
实参传递给非引用形参
-
返回类型为非引用
-
用花括号列表初始化数组和聚合类:这里就有初始化列表会少调用一次赋值运算符操作
对于此部分仍然还有疑问,与effective C++第四款解释有所区别
当然需要注意的是,为什么拷贝构造函数要使用引用传递,防止无线递归(值传递需要使用拷贝构造函数)
-
-
-
拷贝赋值运算符
注意 :拷贝构造函数是在定义时用 = ,拷贝赋值是在赋值时
int a = 0; //拷贝构造函数 int b; b = a; //拷贝赋值运算符
- 赋值运算符通常返回左侧对象的引用,为了可以连续赋值
-
析构函数
构造函数中,成员的初始化是在函数体执行之前完成的,顺序是类中出现的顺序初始化
析构函数,首先执行函数体,然后销毁成员,按初始化顺序来逆序销毁
- 隐式销毁一个内置指针类型的成员不会delete它所指向的对象(普通指针没有析构函数),但智能指针是类类型,是有析构函数的,所以智能指针在析构阶段会被自动销毁。
- 什么时候会调用析构函数
- 变量在离开作用域时被销毁
- 当一个对象被销毁时成员被销毁
- 容器被销毁时,其元素被销毁
- 动态分配的对象,当指向它的指针被delete时被销毁
- 对于临时对象,当创建它的完成表达式结束时
三/五法则
一般而言,析构函数,拷贝构造函数、拷贝赋值运算符,一旦一个定义了,最好其他三个都全部自定义了
为什么?
-
需要析构函数的时候也需要拷贝赋值操作
一般而言,如果成员变量含有指针变量时 ,一般会定义析构函数,如果不定义拷贝构造函数与构造函数与赋值运算符,这在函数中拷贝与赋值操作时,由于是浅拷贝,产生的副本只是简单的对指针进行赋值(但指针指向的对象是同一个),于是在释放过程中该指针指向的对象释放了两次就会出现未定义错误。
class HasPtr { piblic: HasPtr(const string &s = string()) : ps(new string(s)), i(0){} //看看这个构造函数 ~HasPtr(){delete p;} //自定义析构函数 }; //调用这个类,你能看出哪里错了吗? HasPtr f(HasPtr hp) { HasPtr ret = hp; return ret; }
f函数返回时, hp和ret都会被销毁,都会调用HasPtr的析构函数,都会delete rer和hp中的指针成员,两个指针指向同一个对象,释放两次出现错误,如果是深拷贝就不会出现这种问题
-
使用 =default
如果定义拷贝构造函数,如果定义了,编译器就不会生成,但是可以显示通过将拷贝控制成员函数显式要求编译器生成合成的版本(且是内联的),
-
阻止拷贝
如果一个类不想让我们的类对象被拷贝与赋值,如unique_ptr不允许拷贝与赋值,只需要在后面添加**=delete** ,delete可以用在任何函数,而default只能在合成的默认构造函数与拷贝控制成员。特别注意的是析构函数不能被定义为删除的
struct NoDtor { ~NoDtor() = delete; //删除析构函数 }; NoDtor nd; //错误:无法定义该类对象 NoDtor *p = new NoDtor(); //正确:但是我们无法delete p delete p; //错误:NoDtor的析构函数是删除的
合成的拷贝控制成员可能是删除的
-
如果一个类有数据成员不能默认构造、拷贝、复制或销毁,那对应生成的合成成员函数将被定义为删除的
-
一个成员有删除的或不可访问的析构函数会导致合成的默认和拷贝构造函数被定义为删除的,如果不定义为删除的话,我们就会创建出无法被销毁的对象
-
对于具有引用成员或无法默认构造的const成员类,编译器不会为其合成默认的构造函数,因为引用和const都必须在创建的时候初始化,所以编译器无法去赋值它们,就是说你在类成员定义的时候就必须要去初始化它们,即在构造之前去初始化他,初始化列表
于是我们得出了一个结论,如果一个成员变量没有默认的构造函数,则需要使用初始化类别将其直接初始化
当然effective C++条例06 , 提到了如果不需要编译器合成的拷贝控制成员,需要将其设置为私有,并且继承这个类,这在boost库中也使用了这种方法。
-
拷贝控制和资源管理
这部分主要介绍两个内容,通过定义不同的拷贝操作,使自定义的类的行为看起来像一个值或者指针(难道就是传说中的浅拷贝与深拷贝)
-
类的行为像一个值,有自己的状态,当我们拷贝一个像值得对象时,副本和原对象是完全独立的,改变副本不会对原对象有任何影响反之亦然。所谓的深拷贝
-
行为像指针的类的共享状态当我们拷贝一个这样的类的对象时,副本和原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然。所谓的浅拷贝
标准库容器 和string类的行为像一个值; shareed_ptr提供一个类似指针类型的行为,当然两个都不像,IO类和unique_ptr不允许拷贝和赋值;
接下里实现一个Hasptr让它的行为像一个值
-
行为像值的类
为了像值,每个string指针指向那个string对象,都得有自己的一份拷贝,为了实现这个目的,我们需要以下三个小工作:
- 定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针
- 定义析构函数来释放string
- 定义一个赋值运算符来释放当前的string,并从右侧运算对象拷贝string
class HasPtr { public: HasPtr(const string &s = string()) : ps(new string(s)), i(0){} //默认实参,列表初始化 HasPtr(const HasPtr &p) : ps(new string(*p.ps)), i(p.i){} //拷贝构造函数 HasPtr& operator=(const HasPtr &); //赋值运算符声明 ~HasPtr(){delete ps;} //析构函数 private: string *ps; int i; };
类值拷贝运算符
当 a = b;
HasPtr对象出现这样的赋值
-
销毁左侧运算对象的资源,类似于析构函数
-
从右侧运算对象拷贝数据,类似拷贝构造函数,为了保证一个对象能为它本身赋值,我们先拷贝右边的对象,在释放左侧资源,并更新指向新分配的string;
HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newp = new string(*rhs.ps); //拷贝底层string delete ps; //释放旧内存 ps = newp; //从右侧对象拷贝数据到本对象 i = rhs.i; return *this; //返回本对象 }
-
行为像指针的类
你们可能觉得拷贝指针就行了,但是还需要释放内存,当最后一个指向string的HasPtr销毁时才能释放内存,所以使用share_ptr,这里不用智能指针,看看底层如何实现引用计数
class HasPtr { public: HasPtr(const string &s = string()) : ps(new string(s)), i(0), use(size_t(1)){} //默认实参,列表初始化 HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use){++(*use)} //拷贝构造函数,要递增计数器 HasPtr& operator=(const HasPtr &); //赋值运算符声明 ~HasPtr() //析构函数 { if(--(*use) == 0) //没人引用了才释放 { delete ps; delete use; } } private: string *ps; int i; size_t *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; //返回本对象 }
交换操作swap
这个在Effective C++里面也有提及了
除了定义拷贝控制成员外,管理资源的类通常还会定义一个swap函数交换两个元素,对于需要重排元素的算法很重要
如果对象很大,借助中间变量来交换对象会多次使用拷贝构造函数与赋值运算符,比较消耗时间,因此可以通过交换指针更划算
string *temp = v1.ps;
v1.ps = v2.ps;
v2.ps = temp;
为自己的类编写swap函数
class HasPtr
{
firend void swap(HasPtr&, HasPtr&); //友元函数,为了能访问private的数据成员
};
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);
return *this;
}
动态内存管理类
使用allocator类将内存分配和对象构造分离