文章目录
一、15.1 OOP:概述
面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
通过继承联系在一起的类构成了一种层次关系,通常在层次关系的根部有一个基类,其他类则直接或间接从基类继承而来,这些继承得到的类称为派生类。
在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待,对于某些函数,基类希望派生类各自定义适合自己的版本,此时基类就将这些函数声明成虚函数。
class Quote {
public:
string isbn() const;
virtual double net_price(size_t n) const;
};
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来。类派生列表访问形式:
class Bulk_Quote :public Quote { //继承了Quote类
public:
double net_price(size_t n) const override;
};
派生类必须对其内部所有重定义的虚函数进行声明。派生类可以在这样的函数之前加上virtual关键字,但是并不是非得这么做,C++11允许派生类显式注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后加一个override关键字。
动态绑定
通过使用动态绑定,我们可以用同一段代码处理Quote和Bulk_quote的对象。
double print_total(ostream &os, const Quote &item, size_t n){
double ret = item.net_price(n);
os << "ISBN:" << item.isbn() //调用Quote::isbn()
<< "# sold: " << n << "total due:" << ret <<endl;
return ret;
}
我们既能使用基类Quote的对象调用该函数,也能使用Bulk_quote派生类的对象调用它,又因为print_total是使用引用类型调用net_price函数,实际传入的对象类型将决定执行哪个版本的函数。
print_total(cout, basic, 20); //调用Quote::net_price
print_total(cout, bulk, 20); //调用Bulk_quote::net_price
`
二、15.2 定义基类和派生类
15.2.1 定义基类
class Quote {
private:
string bookNo; //书籍的isbn编号
protected:
double price = 0; //普通状态下不打折的价格
public:
Quote() = default;
Quote(const string &book, double sales_price) :
bookNo(book), price(sales_price) {}
string isbn() const { return bookNo; }
virtual double net_price(size_t n) const
{
return n * price;
}
virtual ~Quote() = default; //对析构函数进行动态绑定
};
注意:基类都应该定义一个虚析构函数,即使该函数不执行任何操作也是如此。
成员函数与继承
在C++语言中,基类必须将它的两种函数区分开来:一种是基类希望派生类进行覆盖的函数,另一种是基类希望派生类直接继承不需要改变的函数。对于前者,基类通常将其定义为虚函数。当我们使用指针或者引用调用虚函数时,该调用将被动态绑定。
关键字virtual只能出现在类内部进行声明,不能用于类外部的函数定。
如果基类把一个函数声明为虚函数,则该函数在派生类中隐式的也是虚函数。
访问控制与继承
派生类可以继承基类的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员,派生类可以访问公有成员,而不能访问私有成员。不过有些时候,基类希望它的派生类有权访问该成员而拒绝其他用户访问,我们用受保护的(protected)运算访问符说明这样的成员。
15.2.2 定义派生类
class Bulk_quote :public Quote { //继承了Quote类
public:
Bulk_quote() = default;
Bulk_quote(const string&, double, size_t, double);
//覆盖基类函数版本以实现基于大量购买的折扣政策
double net_price(size_t n) const override;
private:
size_t min_qty = 0; //适用于折扣政策的最低购买量
double discount = 0.0; //以小数表示折扣额
};
如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分,此外,我们能将公有派生类的对象绑定到基类的引用或指针上。
派生类中的虚函数
派生类经常(但不总是)覆盖他继承的虚函数,如果派生类没有覆盖基类中的某个虚函数,则该函数的行为类似于其他普通成员,派生类会直接继承其在基类中的版本。
派生类对象及派生类向基类的类型转换
Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p = &item; //p指向基类对象
p = &bulk; //p指向bulk的Quote部分
Quote &r = bulk; //r绑定到bulk的Quote部分
这种转换通常是派生类到基类的类型转换,与其他类型一样,编译器会隐式地执行从派生类到基类的转换。
在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
派生类构造函数
每个类控制他自己的成员初始化过程。
Bulk_quote::Bulk_quote(const string& book, double p,
size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) {};
首先初始化基类部分,然后按照声明的顺序依次初始化派生类的成员。
派生类使用基类成员
派生类可以访问基类公有成员和受保护成员
double Bulk_quote::net_price(size_t cnt) const {
if (cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
注意:派生类对象不能直接初始化基类成员,尽管从与发生可以在派生类构造函数体内给它的公有或受保护的基类成员赋值,但最好不这么做。和使用基类的其他场合一样,派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员。
继承与静态成员
class base {
public:
static void statmem();
};
class Derived :public base{
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj) {
base::statmem(); //正确 Base中定义了statmem
Derived::statmem(); //正确 Derived中继承了statmem
//派生类对象能访问基类静态成员
derived_obj.statmem(); //通过派生类对象访问
statmem(); //通过this对象访问
}
派生类声明
class Bulk_quote:public Quote; //错误 派生列表不能出现在这里
class Bulk_quote; //正确 生命派生类的正确方式
被作用的基类
如果某个类用作基类,则该类必须定义而非仅仅声明:
class Quote; //生命但没有定义
class Bulk_quote:public Quote {...}; //错误 Quote未定义
class base{/*...*/};
class D1:public base{/*...*/};
class D2:public D1{/*...*/};
在上述这个继承关系中,base是D1的直接基类,同时是D2的间接基类。
防止继承的发生
有时候我们并不希望类被继承,C++11新标准提供了一种防止继承发生的方法,即在类后面跟一个关键字final
class NoDerived final{/*...*/}; //NoDerived不能作为基类
class Base{/*...*/};
//Last是final的 我们不能继承last
class Last final:Base{ /*...*/ }; //Last不能作为基类
class Bad:NoDerived{ /* */ }; //错误:NoDerived是final的
class Bad2:Last{ /* */ }; //错误:Last是final的
15.2.3 类型转换与继承
通常情况下,如果我们把引用或指针绑定到一个对象上,则引用或指针的类型应与对象的类型一致,或者对象的类型含有一个可接受的const类型转换规则,存在继承关系的类是一个重要例外,我们可以把基类指针或者引用绑定到派生类对象上。
静态类型与动态类型
当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开。
基类的指针或引用的静态类型可能与动态类型不一致。
不存在从基类向派生类的隐式类型转换
Quote base;
Bulk_quote* bulkp = &base; //错误:不能将基类转换成派生类
Bulk_quote& bulkRef = base; //错误:不能将基类转换成派生类
Bulk_quote bulk;
Quote *itemP = &bulk; //正确:动态类型是Bulk_quote
Bulk_quote *bulkP = itemP; //错误:不能将基类转换成派生类
…在对象之间不存在类别转换
派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和积累类型之间不存在这样的转换,很多时候,我们希望派生类对象转换为基类对象,但这种转换过程往往与我们预计有所差别。
Bulk_quote bulk; //派生类对象
Quote item(bulk); //使用Quote::Quote(const Quote&)构造函数
item = bulk; //调用Quote::operator=(const Quote&)
上述过程中会忽略Bulk_quote部分,所以我们可以说Bulk_quote部分被切掉了。
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有派生类对象中的基类部分被拷贝、移动或赋值,它的派生类部分被忽略掉。
关键概念:存在继承关系的类型之间的转换规则
1.从派生类向基类类型转换只对指针或者引用类型有效
2.基类向派生类不存在隐式类型转换
3.和任何其他成员一样,派生类向基类的类型转换可能也会由于访问受限而变得不可行。
尽管自动类型转换只对指针或引用有效,但是继承体系中的大多数仍然定义了拷贝控制成员,因此,我们通常能够将一个派生类对象拷贝、移动或赋值给一个基类对象,不过需要注意的是,这种操作只处理派生类对象的基类部分。
三、15.3 虚函数
在C++语言中,当我们使用基类的引用或指针调用调用一个虚成员函数会执行动态绑定,因为知道运行的时候我们才知道调用了哪个版本的虚函数。
对虚函数的调用可能在运行时才被解析
Quote base("0-201-82470-1", 50);
print_total(cout, base, 10); //调用Quote::net_price
Bulk_quote derived("0-201-82470-1", 50, 5, .19);
print_total(cout, derived, 10); //调用Bulk_quote::net_price
动态绑定只有当我们通过指针或引用调用虚函数时才会发生
base = derived; //啊derived的Quote部分拷贝给base
base.net_price(20); //调用Quote::net_price
当我们通过一个具有普通类型(非引用类型)的表达式调用虚函数时,在编译时调用版本就被确定下来。
派生类中的虚函数
当我们在派生类覆盖了某个虚函数,可以在一次用virtual关键字指出函数性质,然而并非必须这么做,因为一旦某个函数被声明为需哈桑农户,则所有派生类中它都是虚函数。同样,派生类中虚函数的返回类型也必须与基类函数匹配。该规则存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。
final和override说明符
派生类如果定义了一个函数与基类中需函数的名字相同但是形参列表不同,这仍然是一个合法的行为。编译器认为这个函数与原有函数相互独立,这是,派生类的函数并没有覆盖基类中的版本。就实际编程习惯来言,这种生命发生了错误,但找出这个错误比较困难,C++11中可以使用override关键字来说明派生类的虚函数,这么做的好处是更加清晰,编译器可以为我们发现一些错误,如果我们使用override标记了某个函数,但该函数并没有覆盖已经存在的虚函数,此时编译器报错。
struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1:B{
void f1(int) const override;
void f2(int) override; //错误 B中没有形如f2(int)的参数
void f3() override; //错误 f3不是虚函数
void f4() override; //错误 B中没有名为f4的函数
};
struct D2:B{
//从B继承f2()、f3(),覆盖f1(int)
void f1(int)const final; //不允许后续的其他类覆盖f1(int)
};
struct D3:D2{
void f2(); //正确 覆盖从间接基类B继承来的f2
void f1(int) const; //错误 D2已经将f2声明成了final
};
虚函数与默认实参
和其他函数一样,需要函数也可以有默认实参,函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
回避虚函数的机制
在某些情况下,我们希望对虚函数调用不能进行动态绑定,而是强迫其执行虚函数的某个特定版本,使用作用域运算符可以实现
double undiscounted = baseP->Quote::net_price(42);
通常情况下,只有成员函数(友元函数)中的代码才需要使用作用域运算符来回避虚函数机制。
四、15.4 抽象基类
纯虚函数
定义成纯虚函数,这么做可以清晰明了地告诉用户该函数没有实际意义,和普通的虚函数不一样,纯虚函数不需要定义。我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数,其中,=0只能出现在类内部的虚函数声明语句之处。
//用于保存折扣值和购买量的类,派生类可以使用这些数据实现不同的价格策略
class Disc_quote :public Quote {
public:
Disc_quote() = default;
Disc_quote(const string& book, double price,
size_t qty, double disc) :
Quote(book, price),
quantity(qty), discount(disc) {};
double net_price(size_t) const = 0;
protected:
size_t quantity; //折扣适用购买量
double discount = 0; //表示折扣的小数值
};
值得注意的是,我们可以为纯虚函数提供定义,不过函数体必须定义在类的外部,也就是说,我们不能在类的内部为一个=0的函数提供函数体
含有纯虚函数的类是抽象基类
含有纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其它类可以覆盖该接口,我们不能(直接)创建一个抽象基类的对象。
//Disc_quote声明了纯虚函数,而Bulk_quote将覆盖该函数
Disc_quote discounted; //错误 不能定义抽象基类的对象
Bulk_quote bulk; //正确 Bulk_quote中没有纯虚函数
我们不能创建抽象基类的对象
派生类构造函数只初始化它的直接基类
class Bulk_quote :public Disc_quote {
public:
Bulk_quote() = default;
Bulk_quote(const string& book, double price,
size_t qty, double disc) :
Disc_quote(book, price, qty, disc) {};
//覆盖基类中的函数版本以实现一种新的折扣策略
double net_price(size_t n) const override;
};
这个版本的Bulk_quote直接基类是Disc_quote,间接基类是Quote。每个Bulk_price对象包含三个子对象:一个(空的)Bulk_quote部分、一个Disc_quote部分的子对象和一个Quote子对象。
如前所述,每个类各自控制其对象的初始化过程。
关键概念:重构
在Quote的继承体系中增加Disc_quote类是重构的一个典型例子。重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类当中,值得注意的是,即使我们改变了整个继承体系,那些使用了Bulk_quote或Quote的代码也无需进行任何改动。不过一旦类被重构,那就意味着我们必须重新编译含有这些类的代码。
五、15.5 访问控制与继承
每个类分别控制着自己成员的初始化过程,每个类还分别控制着成员对于派生类是否访问。
受保护的成员
protected说明符可以看作是public和private中和后的产物
1.和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
2.和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。此外,protected还有一条重要性质。
3.派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。
class Base {
protected:
int prot_mem;
};
class Sneaky :public Base {
friend void clobber(Sneaky&); //可以访问Sneaky::prot_mem
friend void clobber(Base&); //不能访问Base::prot_mem
int j; //默认private
};
//正确 可以访问
void clobber(Sneaky &s) {
s.j = s.prot_mem = 0;
}
//错误 不能访问
void clobber(Base &b) {
b.prot_mem = 0;
}
公有、私有和受保护继承
class Base {
public:
void pub_mem(); //public成员
protected:
int prot_mem; //protected成员
private:
char priv_mem; //private成员
};
struct Pub_Derv :public Base {
//正确 派生类可以访问受保护成员
int f() { return prot_mem; }
//错误 派生类不可以访问私有成员
char g() { return priv_mem; }
};
struct Priv_Derv :private Base {
//正确 private不影响派生类的访问权限
int f1() const { return prot_mem; }
};
派生类访问说明符对于派生类的成员(友元)能否访问基类成员没有什么影响。对基类成员的访问权限只与基类中的访问说明符有关。Pub_Derv与Priv_Derv都可以访问受保护成员prot_mem,同时都不能访问Priv_mem.
派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限:
Pub_Derv d1; //继承的Base成员是public的
Priv_Derv d2; //继承自Base成员是private的
di.pub_mem; //正确 pub_mem在派生类中是public的
d2.pub_mem; //错误 pub_mem在派生类中是private的
派生访问说明符还可以控制继承自派生类的新类的访问权限:
struct Derrived_from_public :public Pub_Derv {
//正确 Base::prot_mem在Pub_Derv中仍然是protected
int use_base() { return prot_mem; }
};
struct Derived_from_public :public Priv_Derv {
//错误 Base::prot_mem在Priv_Derv中是private的
int use_base() { return prot_mem; }
};
派生类向基类转换的的可访问性
关键概念:类的设计与受保护的成员
不考虑继承的话,我们可以认为一个类有两种不同的用户:普通用户和类的实现者。其中,普通用户编写的代码使用类的对象,这部分代码只能访问类的公有(接口)成员,实现者则负责编写类的成员和友元的代码,成员和友元既能访问类的公有部分,又能访问类的私有部分。
如果进一步考虑继承的话会出现第三种用户,即派生类。基类把它希望派生类能够使用的部分声明为受保护的,普通用户不能访问受保护的成员,而派生类及其友元仍然不能访问私有成员。
和其它类一样,基类应该将其接口成员声明为公有的,同时将属于其实现的部分分成两组:一组供派生类访问,另一组只能由基类和基类的友元访问,对于前者应该声明为受保护的,对于后者应该声明为私有的。
友元与继承
就像友元不能传递那样,友元关系也不能继承。基类的友元在访问派生类成员时不具有特殊性,同样,派生类的友元也不能随意访问基类成员:
class Base {
friend class pal;
public:
void pub_mem(); //public成员
protected:
int prot_mem; //protected成员
private:
char priv_mem; //private成员
};
class Sneaky :public Base {
int j; //默认private
};
class pal {
public:
int f(Base b) { return b.prot_mem; } //正确
int f(Sneaky s) { return s.j; } //错误 pal不是Sneaky的友元
//对基类的访问权限由基类本身控制,即使对于派生类的基类也是这样
int f3(Sneaky s) { return s.prot_mem; } //正确 Pal是Base的友元
f3是正确的,Pal是Base的友元,所以Pal可以访问Base对象所有成员,这种可访问性包括了Base对象内嵌在其派生类对象中的情况。
class D2 :public pal {
public:
int mem(Base b) {
return b.prot_mem;//错误 友元关系不能继承
}
};
不能继承有缘关系;每个类控制自己成员的访问权限
改变个别成员的可访问性
class Base {
public:
size_t size() const { return n; }
protected:
size_t n;
};
class Derived :private Base { //注意,private继承
public:
using Base::size;
protected:
using Base::n;
};
上述例子,默认情况下使用了私有继承,继承而来的size和n是私有成员,使用using声明语句可以变该其可访问性。改变之后,派生类用户可以使用size成员,而派生类的派生类将能使用n。
默认的继承保护级别
class Base{ /*...*/ };
struct D1: Base{ /*...*/ }; //默认为public继承
class D2: Base{ /*...*/ }; //默认为private继承
事实上,struct与class唯一的差别就是默认成员访问说明符以及默认派生访问说明符,除此之外,没有其他差距。
六、15.6 继承中的类作用域
每个类都有自己的作用域,在这个作用域内我们定义类的成员。
在编译时按名字查找
class Disc_quote :public Quote {
public:
pair <size_t, double> discount_policy() const
{ return { quantity,discount }; }
//其他成员
};
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; //静态类型与动态类型一致
Quote *itemP = &bulk; //动态类型与静态类型不一致
bulkP->discount_policy(); //正确:类型为Bulk_Quote
itemP->discount_policy(); //错误:Quote无这个成员
名字冲突与继承
struct Base{
Base():mem(0){}
protected:
int mem;
};
struct Derived : Base{
Derived(int i): mem(i){ }
int get_mem() { return mem; }
protected:
int mem; //将隐藏基类的mem
};
Derived d(42);
cout << d.get_mem() <<endl; //打印42
派生类的成员将隐藏同名的基类成员
通过作用域运算符来使用隐藏的成员
struct Derived:Base{
int get_Base_mem{ return Base::mem; }
//...
};
除了覆盖继承而来的虚函数之外,派生类最好不要用其他定义在基类的名字。
一如往常,名字查找优先于类型检查
声明在内层作用域的函数并不会重载声明在外层作用域的函数,因此,定义在派生类的函数也不会重载基类中的成员。
struct Base {
int memfcn();
};
struct Derived:Base{
int memfcn(int);
};
Derived d;
Base b;
b.memfcn(); //调用Base::memfcn
d.memfcn(10); //调用Derived::memfcn
d.memfcn(); //错误:参数列表为空的memfcn被隐藏了
d.Base::memfcn(); //正确 调用Base::memfcn
注意:一旦编译器找到了名字,就不再继续查找。
虚函数与作用域
class Base {
public:
virtual int fcn();
};
class D1:public Base{
public:
int fcn(int); //形参列表与Base中的不一致
virtual void f2(); //是一个新的虚函数
};
class D2:public D1{
public:
int fcn(int); //非虚函数,隐藏了D1中的fcn
int fcn(); //覆盖了Base中的虚函数
void f2(); //覆盖了D1中的虚函数f2
};
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); //虚调用,运行时调用Base::fcn
bp2->fcn(); //虚调用,运行时调用Base::fcn
bp3->fcn(); //虚调用,运行时调用D2::fcn
D1 *d1p = &d1obj; D2 *d2p = &d2obj;
bp2->f2(); //错误 Base没有f2成员
d1p->f2(); //虚调用,将在运行时调用D1::f2()
d2p->f2(); //虚调用,将在运行时调用D2::f2()
再观察对于非虚函数fcn(int)的调用语句
Base *p1 = &d2obj;
D1 *p2 = &d2obj;
D2 *p3 = &d2obj;
p1->fcn(42); //错误:Base中没有接受一个int的fcn
p2->fcn(42); //调用D1::fcn(int)
p3->fcn(42); //调用D2::fcn(int)
上面的调用语句,指针都指向了D2类型对象,但是由于我们调用的是非虚函数,所以不会发生动态绑定,实际调用的函数版本由指针的静态类型决定。
覆盖重载的函数
和其他函数一样,成员函数无论是否为虚函数都能被重载。派生类可以覆盖重载函数的0个或者多个实例,如果派生类希望所偶有的重载版本对于它来说都是可见的,那它就需要从覆盖所有版本,或者一个也不覆盖。
七、15.7 构造函数与拷贝控制
15.7.1 虚析构函数
继承关系对基类拷贝控制最直接的影响是基类通常定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
例如:如果我们delete一个Quote*类型的指针,则该指针有可能实际指向了一个Bulk_quote类型的对象,那么这样的话,编译器必须清楚他执行的是Bulk_quote的虚构函数,和其他函数一样,我们在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本:
class Quote{
//如果我们删除的是一个指向派生类对象的基类指针,则需要析构函数
virtual ~Quote() = default; //对析构函数进行动态绑定
};
Quote *itemP = new Quote; //动态类型与静态类型一致
delete itemP; //调用Quote析构函数
itemP = new Bulk_quote; //静态类型与动态类型不一致
delete itemP; //调用Bulk_quote析构函数
如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义
的行为。
虚析构函数将阻止合成移动操作
如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
15.7.2 合成拷贝控制与继承
例如:
1.合成的Bulk_quote默认构造函数运行Disc_quote的默认构造函数,后者又运行Quote的默认构造函数。
2.Quote的默认构造函数将bookNo成员默认初始化为空字符串,同时使用类内初始值price初始化为0.
3.Quote构造函数完成后,继续执行Disc_quote的构造函数,它使用类内初始值初始化qty和discount
4.Disc_quote的构造函数完成后,继续执行Bulk_quote的构造函数,但是它什么工作都不做。
值得注意的是,无论基类成员还是合成版本、还是自定义版本,都没有太大影响,唯一的要求是相应成员应该可访问并且不是一个被删除的函数。
派生类中删除的拷贝控制和基类的关系
class B {
public:
B();
B(const B&) = delete;
//其他成员,不含有移动构造函数
};
class D :public B {
//没有声明任何函数
};
D d; //正确,D的合成默认构造函数使用B的默认构造函数
D d2(d); //错误,D的合成拷贝构造函数是被删除的
D d3(std::move(d)); //错误,隐式使用D的被删除的拷贝构造函数
基类B中有一个可访问的默认构造和一个显式删除的拷贝构造函数,因为我们定义了拷贝构造,所以编译器不会合成一个移动构造函数。因此,我们既不能移动也不能拷贝B的对象,如果B的派生类希望它自己的对象能被移动和拷贝,则需要自定义相应版本的构造函数。
移动操作与继承
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.3 派生类的拷贝控制成员
当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。
定义派生类的拷贝或移动构造函数
class Base{ /* */ };
class D:public Base{
public:
//默认情况下,积累的默认构造函数初始化对象的基类部分
//要想使用拷贝或移动构造函数,我们在构造函数初始值列表中显式调用该构造函数
D(const D& d): Base(d) //拷贝基类成员
{ }
D(D &&d): Base(d) //移动基类成员
{ }
};
派生类赋值运算符
与拷贝和移动构造函数一样,埃圣雷的赋值运算符也必须显示地为基类部分赋值
//Base::operator=(const Base&)不会被自动调用
D &D::operator=(const D& rhs){
Base::operator=(rhs);//为基类部分赋值
//按照原来的方式为派生类的成员赋值
return *this;
}
派生类析构函数
class D:public Base{
public:
//Base::~Base被自动调用执行
~D(){ };
};
对象销毁的顺序正好与其创建的顺序相反,派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直至最后。
15.7.3 继承的构造函数
在C++11标准中,派生类可以重用其直接基类定义的构造函数。
class Bulk_quote:public Disc_quote{
public:
//继承Disc_quote的构造函数
using Disc_quote::Disc_quote;
//其他成员
};
编译器会生成构造函数形如:derived(parms) :base(args){ }
等价于`
Bulk_quote(const string& book, double price,
size_t qty, double disc):
Disc_quote(book, price, qty, disc){}
//如果派生类有自己的数据成员,那么这些成员将被默认初始化
继承的构造函数的特点
和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。
而且,一个using声明语句不能指定explicit或constexpr。如果基类的构造函数时explicit或者constexpr,则继承的构造函数也有相同的属性。
当一个基类构造函数含有默认实参,这些实参并不会被继承