15.7构造函数与拷贝控制
位于继承体系中的类也需要控制当其对象执行一系列操作时发生什么样的行为(创建,拷贝,移动,赋值和销毁)
如果一个类没有定义拷贝控制操作,则编译器会给出一个合成的版本,可以被定义为被删除的函数
15.7.1虚析构函数
为什么需要:当我们要动态分配继承体系中的对象时,就该用到虚析构函数,
当我们delete一个动态分配的对象的指针时将执行析构函数,可有时候出现指针的静态类型与被删除对象的动态类型不符的情况时,我们就一个在基类定义一个虚析构函数,以确保执行正确的析构函数版本。
class Quote
{
public:
//如果我们删除的是一个指向派生类对象的基类指针,则需要虚析构函数
virtual ~Quote() = default; //动态绑定析构函数
};
class Bulk_quote :public Quote
{
public:
~Bulk_quote()
{
cout << "动态绑定时执行这个Bulk_quote析构函数" << endl;
}
};
void test01()
{
//析构函数的虚属性也会被继承,也就是说Quote派生类使用合成的析构函数还是自己定义的析构函数,都将是虚析构函数
//只有基类的析构函数是虚函数,delete基类指针将执行正确的析构函数版本
Quote* itemp = new Quote;//静态类型与动态类型一致
delete itemp; //调用Quote的析构函数
itemp = new Bulk_quote; //静态类型与动态类型不一致
delete itemp; //调用Bulk_quote的析构函数
}
int main()
{
std::cout << "Hello World!\n";
test01();
}
注意:基类的析构函数不是虚析构函数,delete一个指向派生类对象的基类的指针会发生未定义的行为,
无法确定基类需要赋值运算符或拷贝构造函数,一个基类总是需要析构函数,而且它能将析构函数设定为虚函数,但析构函数为了成为
虚函数而令内容为空。
虚析构函数将阻止合成移动操作
一个类定义了析构函数,编译器不会为这个类合成移动操作
15.7.1节练习
当基类有继承的派生类时,需要虚析构函数,保证在删除指向动态分配对象的指针时,删除的动态类型的析构函数版本一致
虚析构函数可以为空,不执行任何操作,析构函数的主要作用是清除本类定义的数据成员,
如果该类没有定义指针类成员,用合成的版本就行,如果有,那么需要自定义析构函数以及对指针成员进行适当的清除
虚析构函数必须执行的操作就是清除本类中定义的数据成员的操作
15.7.2合成拷贝控制与继承
Quote---------->Disc_quote---------->Bulk_quote
合成Bulk_quote的默认构造函数将执行Dis_quote的默认构造函数,而Dis_quote又执行Quote的默认构造函数
从上往下,Quote的默认构造函数先将boono默认初始化为空字符串,同时将类内初始值将price初始化为0,
在往下,就是执行Disc_quote的构造函数,它使类内初始值初始化qty和discount,
最后,将执行Bulk_quote的构造函数,可它什么具体工作都不做
注意:无论基类成员是合成的版本还是自定义的版本都没有太大影响,要求的是成员应该可访问,而不是一个被删除的函数
Quote继承体系用的是合成的析构函数
派生类隐式地使用而基类通过将虚析构定义成=default而显示地使用
合成的析构函数是空的,其隐式的结构部分负责销毁类的成员。
对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;该直接基类又销毁它自己的直接基类,
直到继承链的顶端
派生类中删除的拷贝控制与基类的关系
当基类或派生类出于同样的原因将其合成的默认构造函数或者任何一个拷贝成员定义成被删除的函数,
某些定义基类的方式也可能导致有的派生类成为被删除的函数:
1:基类的默认构造,拷贝构造,拷贝赋值运算符或析构函数是被删除的或不可访问
,派生类对应的成员将是被删除的
因为编译器不能使用基类成员来执行派生类对象基类部分的构造,赋值或销毁操作
2:基类中有一个不可访问或删除掉的析构函数
派生类合成的默认和拷贝构造函数将是被删除的因为编译器无法销毁派生类对象的基类部分
3:编译器不会合成一个删除掉的移动操作
当我们使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的
因为派生类对象的基类部分不可移动。如果基类的析构函数是删除的或不可访问则派生类的移动构造函数也将是被删除的
class B {
public:
B();//一个可以访问的默认构造函数
B(const B&) = delete; //显示删除的拷贝构造函数,
//因为B定义拷贝构造函数,编译器不会为B合成一个移动构造函数,而且不能移动也不能拷贝B的对象
};
class D: public B
{
};
void test02()
{
D d1;//D继承了B,D的默认构造函数使用B是默认构造函数
// D d2(d1);//错误:D的合成拷贝构造函数是被删除的
// D d3(std::move(d1));//错误隐式地使用D的被删除的拷贝构造函数
}
移动操作与继承
当基类有虚析构函数时,基类通常没有合成的移动,派生类也没有,
如果需要就要在基类中自定义,而且是显示的,可一旦显示定义了移动版本,则必须同时定义拷贝操作
class Quote
{
public:
Quote() = default; //对成员依次进行默认初始化
Quote(const Quote&) = default; //对成员依次拷贝
Quote(Quote&&) = default; //对成员依次拷贝
Quote& operator=(const Quote&) = default;//拷贝赋值
Quote& operator=(Quote&&) = default; //移动赋值
//如果我们删除的是一个指向派生类对象的基类指针,则需要虚析构函数
virtual ~Quote() = default; //动态绑定析构函数
};
15.7.2节练习
因为Bulk_quote是它的派生类,而且它自己也有自己的数据成员需要初始化,
如果删除了,那么Bulk_quote的默认构造函数也是删除的,
编译器不能使用Disc_quote成员来执行Bulk_quote对象基类部分的构造,赋值或销毁操作
我的笔记啊,全没了
#include<iostream>
using namespace std;
class Base
{
};
class D :public Base
{
public:
//默认情况下,基类的默认构造函数初始化对象的基类部分
//要想使用拷贝或移动构造函数,我们必须在构造函数初始值列表中
//显示地调用该构造函数
D(const D& d) :Base(d)//拷贝基类成员
{
//D的成员的初始值自己赋值
}
D(D&& d):Base(std::move(d))//移动基类成员
{
}
//总之,拷贝构造函数就是初始化*this,可*this包含两个部分,一部分是D的,一部分是Base的
D& operator=(const D& rhs);
//Base::~Base被自动调用执行
~D()
{
//该处由用户自定义清除派生类成员的操作
}
};
D& D::operator=(const D& rhs)
{
Base::operator=(rhs); //为基类部分赋值
//按照过去的方式为派生类的成员赋值
//酌情处理自赋值及释放已有资源等情况
return *this;
}