C++ Pirmer第十五章④
C++ Primer
面向对象程序设计
构造函数与拷贝控制
和其他类一样,位于继承体系中的类也需要控制当其对象执行一系列操作时发生什么样的行为,这些操作还是包括创建、拷贝、移动、赋值和销毁,当然如果你没有定义的,编译器还是会帮你合成一个版本的(你也可以把它定义为删除的,不让调用)
虚析构函数
为什么我们要把继承体系的析构函数定义为虚函数?
如果我们delete一个Quote*类型的指针,则该指针有可能实际指向了一个Bulk_quote类型的对象,这样的话,编译器就必须要执行Bulk_quote的析构函数,所以我们要把整个继承体系的析构函数定义为虚函数:
class Quote
{
public:
virtual ~Quote() = default; //这样一来,后面继承它的类的析构函数都是虚的了
};
以前我们说过,定义了析构函数就要定义拷贝和赋值,这里基类的析构函数是个例外
虚析构函数会阻止合成移动操作
如果一个类定义了析构函数,即使是=default合成的,编译器也不会为这个类合成移动操作了
合成拷贝控制与继承
与之前的没什么差别,值得一提的就是派生类调用直接基类的构造函数去初始化它含有的基类部分成员
派生类中定义为删除的拷贝控制与基类的关系
有几个准则,虽然有点繁琐,不过还是很讲道理的:
- 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是delete或private的,则派生类中对应的成员将是被删除的,因为编译器不能调用基类的那些函数了啊,那还玩屁啊
- 如果基类有一个private或delete的析构函数,那么派生类中合成的默认和拷贝构造函数将是delete的,因为如果创建了派生类对象,我们无法去销毁里面的基类部分,所以就不让调用了
- 如果基类的移动操作时delete或private的,那么派生类中对应的也是delete的,因为派生类对象中的基类部分无法移动;同样,如果基类的析构函数是delete或private的,那么派生类的移动构造函数也将是被删除的
talk is cheap, show me the code:
//类的知识忘掉的自己去补前面的,面壁思过
class B
{
public:
B(); //默认构造函数声明
B(const B&) = delete; //定义为删除的拷贝构造函数
//既然定义了拷贝构造函数,就不会合成移动操作了
};
class D : public B
{
//没有声明任何东西,只是单纯继承了B
};
D d; //正确:D的合成默认构造函数调用B的默认构造函数,自己反正也没成员
D d2(d); //错误:因为B的拷贝构造函数是delete的,所以D的也是delete,无法被调用
移动操作与继承
这个东西需要点逻辑和之前的知识,注意看了啊:
刚刚讲到,大多数基类都会定义一个虚析构函数,既然定义了析构函数,那么基类就不会有合成的移动操作,在其派生类中也没有合成的移动操作(因为基类都没有,你派生类想移动,你怎么移动基类部分?)
我们现在想实现这个移动,怎么搞呢?首先,我们肯定得在基类中定义移动操作,而且我们想定义移动操作的话,就必须定义拷贝操作(如果不定义自己的拷贝构造函数,编译器又不能为其合成移动操作,那移动操作就会被定义为delete的,这个准侧在十三章第六节的最后):
class Quote
{
public:
Quote() = default; //强行合成默认构造函数
Quote(const Quote&) = default; //拷贝构造函数
Quote(Quote&&) = default; //移动构造函数
Quote& operator=(const Quote&) = default; //拷贝赋值
Quote& operator=(Quote&&) = default; //移动赋值
virtual ~Quote() = default; //虚析构函数
};
Quote的派生类也会自动获得合成的移动操作(只要派生类自己不出幺蛾子)
派生类的拷贝控制成员
前面说过,派生类构造函数在其初始化阶段不但要初始化派生类自己的成员,还负责初始化其基类部分(通过调用基类构造函数)
同样的,派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分,赋值运算符也类似
不同的是派生类析构函数,它的基类部分是自动销毁的,其实我们本来也没怎么管过它,不知道它是怎么销毁的
定义派生类的拷贝或移动构造函数
class Base
{
};
class D : public Base
{
//我们要拷贝或移动基类部分,就必须在派生类的构造函数初始值列表中显式调用
public:
D(const D& d) : Base(d) {} //调用基类的拷贝构造函数来拷贝基类部分
//这儿比较特殊的是Base(d)会去匹配Base的拷贝构造函数,虽然人家其实接受B类型
D(D&& d) : Base(std::move(d)){}
};
派生类赋值运算符(拷贝赋值和移动赋值一样,放一起讲了)
一样的:
D &D::operator=(consty D &rhs)
{
Base::operator=(rhs); //显式地为基类部分赋值
//接下来为派生类自己的部分赋值
return *this;
}
派生类析构函数
特简单,不用管,跟普通的以前的一样:
class D : public Base
{
public:
~D() //Base::~Base()会自动被调用执行
};
注意一点:对象销毁的顺序与创建顺序相反:派生类析构函数先执行,然后是基类的析构函数
在构造函数和析构函数中调用虚函数是不允许的
假设我们在基类的构造函数中调用派生类的某个函数去访问其成员,这会派生类对象还没构建完成,怎么能访问呢?
于是,C++禁止了这种行为
继承的构造函数
在C++11新标准中,派生类能够重用其直接基类定义的构造函数,相当于是派生类偷懒调用直接基类的构造函数了(只有构造函数能这么干,其他的拷贝移动什么的不能继承,默认构造函数也不行),基类部分这么初始化,派生类自己的成员则被默认初始化:
class Bulk_quote : public Disc_quote
{
public:
using Disc_quote::Disc_quote; //使用using继承Disc_quote的构造函数
};
以前我们用using都是作用域那个,而把using放到构造函数上时,using声明语句将令编译器产生代码,而且,对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数,针对上面代码的例子生成的派生类构造函数如下:
Bulk_quote(const string& book, double price, size_t qty, double disc) :
Disc_quote(book, price, qty, disc) {}
继承的构造函数的特点
要记住几点:
- 构造函数的using声明不会改变该构造函数的访问级别
- 如果基类的构造函数是explicit或constexpr的,则继承的构造函数也是这样的
- 当一个基类构造函数含有默认实参,派生类将获得多个继承的构造函数,其中每个构造函数分别忽略掉一个含有默认实参的形参
对于派生类会继承所有构造函数的结论,还有两个例外要注意的:
- 允许重载:如果派生类自己又定义了一个和基类构造函数具有相同参数列表的构造函数,那么基类的这个构造函数不会被继承,就用派生类自己定义的就好了
- 再强调一遍,默认构造函数、拷贝构造函数、移动构造函数不能被继承