C++ Primer第十三章②
C++ Primer
拷贝控制
拷贝、赋值与销毁
三/五法则
这一部分就是要告诉你,前面介绍的三个函数,如果你要自定义的话最好三个全都自定义了。
我们一共要学五个特殊的成员函数,现在已经学了三个了:
- 拷贝构造函数
- 拷贝赋值运算符
- 析构函数
C++允许我们定义任意个数的这些函数,我们的建议是这三个作为整体一起定义或者不定义。接下来就会解释为啥
需要析构函数的类也需要拷贝和赋值操作
当我们决定一个类是否需要自定义它的拷贝控制成员时,一个基本原则是确定它要不要析构函数,因为对析构函数的需求更明显(教材这么说的)。如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和拷贝赋值运算符。
我们来看个只定义析构函数的类,分析一下会发生什么问题:
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 ret和hp中的指针成员,但这两个指针指向的是同一个对象,于是它被释放了两次,就错了。
需要拷贝操作的类也需要赋值操作,反之亦然
拷贝构造函数与赋值运算符互为充要条件(这是个好的建议)
使用=default
我们知道拷贝构造函数的话,无论如何编译器会为我们生成,其他的话,如果你自定义了,编译器就不生成了,但是,我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本(这个之前就学过的,反正C++允许你做任何事情):
class Sales_data
{
public:
Sales_data() = default; //生成构造函数
Sales_data(const Sales_data&) = default; //生成拷贝构造函数(不写也行,但是有区别,内联)
Sales_data& operator=(const Sales_data &); //赋值运算符
~Sales_data() = default; //生成析构函数
};
类内用=default的合成函数,被隐式地声明为内联;如果不希望是内联,可以在类外用=default(声明在类内,定义在类外)
阻止拷贝
你看,C++是不是搞事情,本来蛮好的拷贝构造函数,赋值运算符的,现在又来个阻止拷贝。。。不过还是有道理的,道理如下:
我不想让我的类对象被拷贝(这个需求是比较合理的),于是我不定义拷贝构造函数,但是编译器又会自动生成,我能怎么办,只好定义一个拷贝构造函数来阻止拷贝。
定义删除的函数
在函数的参数列表后面加上=delete就是删除的,意思是:我们虽然声明了它们,但是不以任何方式使用它们:
struct NoCopy
{
NoCopy(const NoCopy&) = delete; //阻止拷贝
NoCopy &operator=(const NoCopy&) = delete; //阻止赋值
};
=delete必须出现在第一次声明函数的时候(不像default一样),因为default直到编译器生成代码时才需要知道,而delete是需要编译器在声明时就知道,以便禁止试图使用它的操作。
delete和default还有一个差别:delete可以用在任何函数(这里介绍的主要是拷贝控制方面而已);而default只能用在合成的默认构造函数或拷贝控制成员。
析构函数不应该是删除的成员
语法上可能允许,但如果析构函数被删除,我们就无法销毁该类型的对象了。所以啊,如果你定义了一个删除的析构函数,那编译器就不许你定义这个类的对象,临时对象也不行。更严格的是,如果你这个类里面的某个成员的类型删除了析构函数,那么,不好意思,你这个类被株连了,也不能定义该类的对象。
下面来个定义了删除析构函数的类,我们还是可以以一定的方式去使用它的(只不过我们不建议这么做):
struct NoDtor
{
~NoDtor() = delete; //删除析构函数
};
NoDtor nd; //错误:无法定义该类对象
NoDtor *p = new NoDtor(); //正确:但是我们无法delete p
delete p; //错误:NoDtor的析构函数是删除的
合成的拷贝控制成员可能是删除的
-
如果一个类有数据成员不能默认构造、拷贝、复制或销毁,那对应生成的合成成员函数将被定义为删除的。
-
很好理解:一个成员有删除的或不可访问的析构函数会导致合成的默认和拷贝构造函数被定义为删除的,因为如果不是删除的话,我们就会创建出无法被销毁的对象。
-
对于具有引用成员或无法默认构造的const成员的类,编译器不会为其合成默认构造函数,因为引用和const都必须在创建的时候就初始化,所以编译器无法去赋值它们,就是说你在类成员定义的时候就必须去初始化它们,那你要默认构造函数干嘛,索性就不合成了。
对于最后一个再解释一下:如果有const,那在定义的时候就初始化了,你在默认构造函数里面再去赋值给const就是错的。
对于引用,虽然我们可以把一个新值给这个引用,但我们改变的是引用绑定的那个对象,而不是引用本身,这个行为往往不是我们想要的。
所以,索性把它俩都不要了。
以前通过访问控制权限来阻止拷贝的我就不介绍了,大家也不用去了解了。(通过将析构函数声明为public(允许定义该类的对象),且将拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝)
#C++工程师#