面向对象程序设计(OPP)的三个基本概念:数据抽象、继承和动态绑定。
继承和动态绑定对程序编写的两个影响:
- 更容易定义与其他类相似但不完全相同的新类
- 使用这些彼此相似的类编写程序时,可以在一定程度上忽略掉它们的区别
15.1 OPP:概述
数据抽象可以将类的接口和实现分离;继承可以定义相似的类型并对其相似关系建模;动态绑定可以忽略区别,统一的方式区使用它们的对象
继承
构成层次关系。在根部有一个基类,其它继承过来的称为派生类。基类负责定义共同拥有的成员,派生类定义各自特有的成员
在C++中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数希望派生类给自定义合适自身的版本,就声明为虚函数
class Quote{
public:
std::string isbn() const; //返回编号
virtual double net_price(std::size_t n) const; //实际价格,打折和不打折的有区别
}
派生类使用类派生列表指出从哪个类继承过来
class Bulk_quote : public Quote {
public:
double net_price(std::size_t) const override;
}
使用了pubilc,可以把Bulk_quote对象当成Quote来使用
派生类必须对其内部所有重新定义的虚函数进行声明。派生类可以在这样的函数前面加上virtual也可以不加,C++11允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数形参列表后加一个override关键字
动态绑定
//计算并打印销售给定数量的某种书籍所得费用
double print_total(ostream &os, const Quote &item, size_t n)
{
//根据item形参调用Quote::net_price还是Bulk_quote::net_price
double ret = item.net_price(n);
os << "ISBN:" << item.isbn()
<< " # sold: " << n << " total due: " << ret << endl;
return ret;
}
根据实际传入的对象决定调用哪个
print_total(cout, basic, 20);
print_total(cout, bulk, 20); //bulk是Bulk_quote,就调用派生类的
函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定也成为运行时绑定
15.2 定义基类和派生类
首先完成Quote类的定义
class Quote {
public:
Quote() = default;
Quote(const std::string &book, double sales_price):
bookNo(book), pricen(sales_price) { }
std::string isbn() const { return bookNo; }
virtual double net_price(std::size_t n) const
{ return n * price;}
virtual ~Quote() = default; //对析构函数动态绑定
private:
std::string bookNo;
protected:
double price = 0.0;
}
作为继承关系中根节点的类通常会定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
成员函数和继承
基类有两种函数,一个是希望派生类进行覆盖的函数,定义为虚函数,另一种希望直接继承。但使用指针或者引用调用虚函数时,该调用将被动态绑定。根据所绑定的对象类型不同,调用不同版本。
可以用关键字virtual使得函数执行动态绑定。任何构造函数之外的非静态函数都可以是虚函数。virtual只能出现在类内部的声明语句而不用用于外部的函数定义,如果基类声明成虚函数,则派生类中隐式地也是虚函数
成员函数如果没有被声明为虚函数,则其解析过程发生在编译时而非运行时
访问控制与继承
派生类的成员函数不一定有权访问从基类继承而来的成员。派生类能访问公有,不能访问私有。还有一个protected,派生类有权访问,其它用户无法访问
例:Quote希望它的派生类重新定义net_price,需要访问price,定义成protected;而isbn函数是不重新定义,不需要访问bookNo,因此bookNo是private的
15.2.2 定义派生类
必须使用类派生列表指出从哪里继承,由冒号和基类列表组成,基类前可以加访问说明符,派生类将需要覆盖的重新声明
class Bulk_quote : public Quote{
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double);
//覆盖
double net_price(std::size_t) const override;
private:
//新增的成员
std::size_t min_qty = 0;
double discount = 0.0;
}
访问说明符的作用是控制派生类从基类继承而来的成员是否对派生类的用户可见。如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。此外,我们能将公有派生类型的对象绑定到基类的引用或指针上。因为在派生类用了public,所以Bulk_quote的接口隐式地包含了isbn函数,同时在任何需要Quote地引用或指针地地方都能用Bulk_quote对象
派生类的虚函数
如果派生类没有覆盖基类中的虚函数,则行为直接继承基类版本。派生类显式注明覆盖可以用关键字override
派生类对象及派生类向基类的类型转换
一个派生类含有自己定义的(非静态)成员的子对象,以及继承基类对应的子对象。如果由多个基类子对象就也有多个。
因为派生类含有与基类对应的组成部分,所以可把派生类对象当成基类来使用,也能将基类的指针或引用绑定到派生类对象中的基类部分上
Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk; //p指向bulk的Quote部分
Quote &r = bulk; //r绑定到bulk的Quote部分
通常称为派生类到基类的类型转换,由编译器隐式执行
派生类构造函数
派生类不能直接初始化继承而来的成员,也必须使用基类的构造函数来初始化
每个类控制它自己的成员初始化过程
派生类构造函数也通过构造初始化列表来把实参传递给基类构造函数,如
Bulk_quote(const std::string& book, double p,
std::size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) {}
前两个参数传递给Quote的构造函数来初始化。
除非特别指出,否则派生类对象的基类部分像数据成员一样执行默认初始化。
首先初始化基类的部分,然后按照声明顺序依次初始化派生类的成员
派生类使用基类的成员
派生类可以访问基类的公有成员和受保护成员
double Bulk_quote::net_price(size_t cnt) const
{
if (cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
如果数量超过min_qty则打折
继承与静态成员
如果基类使用静态成员,则整个继承体系中只存在该成员的唯一定义,不论基类从中派生多个类,每个静态成员都只存在唯一实例
class Base{
public:
static void statmem();
}
class Derived : public Base{
void f(const Derived&);
};
静态成员遵循访问控制规则
void Derived::f(const Derived &derived_obj)
{
Base::statmem(); //Base定义了
Derived::statmem(); //Derived继承了statmem
//派生类对象能访问基类的静态成员
derived_obj.statmen(); //通过Derived对象访问
statmen(); //通过this访问
}
派生类的声明
派生类的声明和其他类一样,不包含派生列表
class Bulk_quote; //正确
class Bulk_quote : public Quote; //错误
声明语句的目的是令程序知晓某个名字的存在以及名字表示什么样的实体
被用作基类的类
如果想把某个类作为基类,则必须定义而非声明
class Quote; //声明未定义
class Bulk_Quote : public Quote {...}; //错误,Quote必须定义
原因是派生类包含并且可以使用基类继承的成员,并且一个类不能派生它本身
一个基类也可以是派生类
class Base {};
class D1 : public Base {};
class D2 : public D1 {};
Base是D1直接基类,是D2间接基类。最终派生类包含它的直接基类的子对象和每个间接基类的子对象
防止继承的发生
用final关键字,不希望其它类继承:
class Base { /* */};
class Last Final : Base {}; //Last不能作为基类
15.2.3 类型转换与继承
继承关系可以使得我们将基类的指针或引用绑定到派生类对象上,有一层重要含义:当使用基类的引用或指针时,实际上我们并不清楚引用所绑定对象的真实类型。该对象可能时基类也可能是派生类
智能指针也支持派生类的类型转换
静态类型与动态类型
使用继承必须区分静态类型和动态类型。静态类型编译时可知,动态内存则存在内存中,运行时才可知
//静态类是Quote&,动态类依赖于item绑定的实参
double ret = item.net_price(n);
如果表达式既不是引用也不是指针,则动态类型和静态类型一致
不存在从基类向派生类的隐式类型转换
因为一个基类对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换
Quote base;
Bulk_quote* bulkP = &base; //错误
Bulk_quote& bulkRef = base; //错误
即使一个基类指针或引用绑定在一个派生类对象,也不能执行转换
Bulk_quote bulk;
Quote *itemP = &bulk; //正确
Bulk_quote *bulkP = itemP; //错误
如果基类含有虚函数,可以用dynamic_cast请求一个类型转换。如果已知某个基类向派生类转换是安全的,可以使用static_cast来强制覆盖掉编译器的检查工作
······在对象之间不存在类型转换
自动类型转换只对指针或引用有效。派生类向基类转换用基类的拷贝/移动操作传递一个派生类对象
Bulk_quote bulk;
Quote item(bulk); //使用Quote::Quote(const Quote&)构造函数
item = bulk; //是呀Quote::operator=(const Quote&)
不过上述过程会忽略Bulk_quote部分,可以说bulk的Bulk_quote部分被切掉了
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略
15.3 虚函数
当使用基类的引用或指针调用一个虚函数时会执行动态绑定。不使用某个函数可以不定义,但虚函数必须定义
对虚函数的调用可能在运行时才被解析
Quote base("0-201-82470-1", 50);
print_total(count, base, 10); //调用Quote::net_price
Bulk_quote derived("0-201-82470-1", 50, 5, .19);
print_total(count, derived, 10); //调用Bulk_quote::net_price
动态绑定只有指针或引用调用虚函数时才发生
base = derived;
base.net_price(20); //调用的是Quote::net_price
OOP的核心思想是多态。把具有继承关系的多个类型称为多态,能使用这些类型的多种形式而无须在意差异。
当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道函数真正作用的对象是什么类型,可能是基类可能是派生类,指定运行时才决定到底执行哪个版本,判断的依据是引用或指针绑定对象的真实类型。
而非虚函数的调用在编译时绑定,通过对象调用的函数也是编译时绑定,绑定到该对象所属类中的函数版本。
派生类中的虚函数
基类中的虚函数在派生类隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中地形参必须与派生类中地形参严格匹配
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; //错误,形参不对
void f3() override; //错误,f3不是虚函数
void f4() override; //错误,B没有f4
};
把函数指定为final,任何尝试覆盖的操作都会错误
struct D2 : B {
void f1(int) const final;
};
struct D3 : D2 {
void f2(); //正确
void f1(int) const; //错误
}
虚函数与默认实参
通过基类的引用或指针调用函数,使用基类中定义的实参,即使实际运行的是派生类的版本。如果派生类依赖不同实参,则程序结果会和预期不符
回避虚函数的机制
某些情况下希望虚函数的调用不要动态绑定,而强迫执行某一个版本,则用作用域运算符来实现
double undiscounted = baseP->Quote::net_price(42);
通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制。
如果一个派生虚函数需要调用它的基类版本,但是没有用作用域运算符,则在运行时该调用会被解析为对派生类版本自身的调用,从而导致无限递归
15.4抽象基类
纯虚函数
通过在函数体的位置书写=0,将一个虚函数说明为纯虚函数,其中=0只能出现在类内部的虚函数声明语句处。来告诉用户当前这个函数没有意义。
//用于保存折扣值和购买量的类,派生类可以使用这个实现不同的价格策略
class Disc_quote : public Quote {
public:
Disc_quote() = default;
Disc_quote(const std::string& book, double price,
std::size_t qty, double disc):
Quote(book, price),
quantity(qty), discount(disc) {}
double net_price(std::size_t) const = 0;
protect:
std::size_t quantity = 0; //折扣适用的购买了
double discount = 0.0; //表示折扣的小数值
}
不能之间定义这个类的对象,但是Disc_quote的派生类构造函数将会使用Disc——quote构造函数来构造各个派生类对象的Disc_quote部分。
可以在函数体外部定义这个纯虚函数
含有纯虚函数的类是抽象基类
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类,抽象基类负责定义接口,后续的类可以覆盖。我们不能直接创建一个抽象基类的对象,如果派生类覆盖了纯虚函数,可以定义这个派生类
Disc_quote discounted; //错误,不能定义含有纯虚函数的对象
Bulk_quote bulk; //正确,不过必须给出自己的net_price定义,否则仍是抽象基类
派生类构造函数只初始化它的直接基类
例:让Bulk_quote继承Disc_quote
//当同一书籍销售量超过某个值启用折扣
class Bulk_quote : public Disc_quote{
public:
Bulk_quote() = default;
Bulk_quote(const std::string& book, double price,
std::size_t qty, double disc) :
Disc_quote(book, price, qty, disc) {}
//覆盖基类中的函数版本来实现一种新的折扣策略
double net_price(std::size_t) const override;
}
该构造函数将实参传递给Disc_quote的构造函数,随后继续调用Quote的构造函数。
重构:在Quote的继承体系中增加Disc_quote类是重构的一个示例。重构负责重新设计类的体系以便将操作和数据从一个类移动到另一个类中。
不过即使我们改变了整个继承体系,那些使用了Bulk_quote或Quote的代码也无须进行任何改动。
15.5 访问控制和继承
每个类分别控制自己成员初始化过程,还分别控制这对其成员对于派生来来说是否可访问
受保护的成员
一个类使用protect来声明那些希望与派生类分享但不想被其它公共访问使用的成员
- 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
- 和公有成员类似,对于派生类的成员和友元来说是可访问的
- 派生类的成员或友元只能通过派生类对象访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权
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; //j默认是private
}
公有、私有和受保护继承
类对继承而来的成员访问权限有两个因素
- 基类中该成员的访问说明符
- 派生类的派生列表中的访问说明符
class Base{
public:
void pub_mem();
protect:
int prot_mem;
private:
char priv_mem;
};
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 d1;
Priv_Derv d2; //继承自Base成员是private的
d1.pub_mem(); //正确
d2.pub_mem(); //错误,pub_mem在派生类是private的
派生访问说明符还可以控制继承自派生类的新类的访问权限:
struct Derived_from_Public : public Pub_Derv{
//正确,Base::prot_mem在Pub_Derv仍是protected
int use_base(){return prot_mem;}
}
struct Derived_from_Private : public Priv_Derv{
//错误,Base::prot_mem在Priv_Derv是private的
int use_base(){return prot_mem;}
}
派生类向基类转换的可访问性
由使用该转换的代码决定,同时派生类的派生访问说明符也会由影响。假定D继承自B:
- 只有当D公有继承B时,用户代码才能使用派生类向基类的转换,如果是protected或者private则不行
- 不论D什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;
- 如果D继承B是公有或保护的,则D的派生类的成员和友元可以使用D向B的类型转换
对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反正则不行
关键概念:类的设计与受保护成员
类有两个不同用户:普通用户和类的实现者。普通用户只能访问公有成员,实现者负责编写类的成员和友元,既能访问公有也能访问私有。
如果考虑派生类,基类把它希望派生类能够使用的部分声明成受保护的。普通用户无法访问受保护的,派生类和友元仍旧不能访问私有。
基类应该将接口声明为公有,实现部分分两组,一组给派生类访问为受保护的,一组只能由基类和基类友元访问,为私有。
友元和继承
友元关系不能传递不能继承
class Base{
friend class Pal;
}
class Pal {
public:
int f(Base b) {return b.prot_mem;} //正确,Pal是Base友元
int f2(Sneaky s) { return s.j;} //错误,Pal不是Sneaky友元
int f3(Sneaky s) { return s.prot_mem; } //正确,Pal是Base友元
}
Pal能访问Base对象的成员,包括了Base对象内嵌在其派生类对象中的情况
当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类,其友元的基类或者派生类不具有特殊的访问能力:
class D2 :public Pal{
public:
int mem(Base b)
{ return b.prot.mem; } //错误,友元不能继承
}
每个类负责控制各自成员的访问权限
改变个别成员的可访问性
有时需要改变派生类继承的某个名字访问权限,可以使用using
class Base{
public:
std::size_t size() const {return n;}
protected:
std::size_t n;
};
class Derived :private Base {
public:
//保持对象尺寸相关的成员的访问级别
using Base::size;
protected:
using Base::n;
}
私有继承而来的size和n是Derived私有成员,但使用using后,用户可以使用size成员,派生类能使用n
派生类只能为那些它可以访问的名字提供using声明
默认的继承保护机制
使用class关键字定义的派生类是私有继承,而struct是公有继承.
一个私有派生类最好显式地声明而不仅仅依赖默认设置
15.6 继承中的类作用域
每个类定义自己的作用域,在作用域定义类的成员。当存在继承关系时,派生类的作用域嵌套在基类内,如果一个名字在派生类作用域无法解析则去外层的基类中寻找定义
例:
Bulk_quote bulk;
cout << bulk.isbn();
解析过程如下:
- 在Bulk_quote中查找
- 在基类Disc_quote中查找
- 在Quote中查找,此时找到了isbn,最终解析为Quote中的isbn
在编译时进行名字查找
即使静态类型和动态类型可能不一致,但我们能使用哪些成员由静态类型决定
class Disc_Quote : public Quote{
public:
std::pair<size_t, double> discount_policy() const
{ return {quantity, discount}; }
}
只能通过Disc_quote及派生类来访问discount_policy()
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; //一致
Quote *itemP = %bulk; //静态和动态不一致
bulkP->discount_policy(); //正确
itemP->discount_policy(); //错误
名字冲突和继承
派生类也能重新定义在其直接基类或间接基类中的名字,此时定义在内层的名字隐藏外层的名字
struct Base{
Base() : mem(0) {}
protected:
int mem;
};
struct Derived : Base{
Derived(int i): mem(i) { }
int get_mem() {return mem;}
protected:
int mem;
};
get_mem中的mem返回的是定义在Derived中的名字
通过作用域运算符来使用隐藏的成员
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); //调用Berived::memfcn
d.memfcn(); //错误。参数列表为空的memfcn被隐藏
d.Base::memfcn(); //正确
虚函数与作用域
所以基类与派生类中的虚函数必须有相同的形参列表,如果实参不同就无法通过基类来调用派生类的虚函数了
class Base{
public:
virtual int fcn();
};
class D1 : public Base{
public:
int fcn(int); //没有覆盖,而是隐藏了
virtual void f2();
}
class D2 : public D1 {
public:
int fcn(int); //非虚函数隐藏了D1::fcn(int)
int fcn(); //覆盖了Base的虚函数
void f2(); //覆盖了D1的虚函数f2
}
通过基类调用隐藏的虚函数
Base bobj; D1 d1boj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fun(); //虚调用Base::fun
bp2->fun(); //虚调用Base::fun
bp3->fun(); //虚调用D2::fun
D1 *d1p = &d1obj; D2 *d2p = &d2obj;
bp2->f2(); //错误,Base没有f2
d1p->f2(); //调用D1::f2();
d2p->f2(); //调用D2::f2();
//非虚函数
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)
覆盖重载的函数
成员函数无论是否是虚函数都能重载,派生类可以覆盖重载函数的0个或多个实例。如果派生类希望所有的重载版本都可见,就要覆盖所有的版本或者一个也不覆盖。
为重载成员提供一条using声明语句,可以无须覆盖基类中的每一个重载版本。using指定一个名字而不指定形参列表,所以一条基类成员函数的using声明可以把该函数的所有重载实例添加到派生类作用域中。此时派生类只需定义特有的函数,无须为继承而来的其他函数重新定义
类内using声明规则也适用于重载函数的名字;基类函数在每个实例的派生类中都必须是可访问的。对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问。
15.7 构造函数与拷贝控制
15.7.1 虚析构函数
继承关系对基类拷贝控制最直接的影响就是定义一个虚析构函数,来动态分配继承体系中的对象
当静态类型和动态类型不符时,通过在基类中将析构函数定义成虚函数来确保执行正确的析构函数版本:
class Quote{
public:
//如果我们删除的是一个执行派生类对象的基类指针,则需要虚析构函数
virtual ~Quote() = default; //动态绑定析构函数
}
Quote *itemP = new Quote;
delete itemP; //调用Quote的析构
itemP = new Bulk_quote;
delete itemP; //调用Bulk_quote的析构
当析构定为虚函数,无法推断该基类是否还需要赋值运算符和拷贝构造
虚析构函数将阻止合成移动操作
一个类定义了析构函数,即使通过=deafult的形式使用了合成的版本,编译器也不会为这个类合成移动操作
15.7.2 合成拷贝控制与继承
合成的拷贝控制成员除了对本身成员初始化赋值或销毁,还负责使用直接基类中对于的操作对一个对象的直接基类部分进行初始化操作:如
- 合成的Bulk_quote默认构造函数运行Disc_quote的默认构造函数,后者又运行Quote的默认构造函数
- Quote的默认构造函数将bookNo默认初始化为0,同时使用类内初始值将price初始化为0
- Quote构造函数完成后,执行Disc_quote的,使用类内初始值初始化qty和discount
- Disc_quote执行完后执行Bulk_quote的构造函数,但什么工作也不做
无论基类成员是合成还是自定义都无影响,只需要相应的成员应该是可访问并且不是一个被删除的函数
对于派生类析构时销毁自己的成员,还负责销毁直接基类,以此类推到继承链的顶端。
派生类中删除的拷贝控制与基类的关系
和其他类一样,基类和派生类也可以出于同样的原因将其合成的默认构造函数或者拷贝控制成员定义为删除的,此外某些定义基类的方式也会导致有的派生类成员成为被删除的函数:
- 如果基类中的拷贝控制成员时被删除或不可访问的,则派生类中的也是被删除的。因为编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作
- 如果基类中有不可访问或删除的析构,则派生类中的合成的默认和拷贝构造函数是被删除的。因为编译器无法销毁派生类对象的基类部分。
- 编译器不会合成一个删除掉的移动操作。当用=default请求一个移动操作时,如果基类中的操作时删除或不可访问,则派生类中的该函数是被删除的,原因是派生类对象的基类部分不可以的
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被删除地拷贝构造函数
移动操作与继承
大多数基类定义一个虚析构函数,通常就不含有合成的移动操作,而且在派生类也没有合成的移动操作
因此如果需要执行移动操作,首先要在基类中定义,Quote可以使用合成的版本,不过必须显式定义。一旦Quote定义了自己的移动操作,它必须同时显式地定义拷贝操作
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地派生类中含有排斥移动地成员,否则它将自动获得合成地移动操作
15.7.3 派生类地拷贝控制成员
派生类构造,拷贝和移动构造函数在初始化*成员时,也要初始化或拷贝移动基类地成员。派生类赋值运算符也必须为基类部分地成员赋值
不过析构函数只负责销毁派生类自己分配地*,派生类对象地基类部分是自动销毁的
定义派生类的拷贝或移动构造函数
通常使用对应的基类构造函数初始化对象的基类部分
class Base { /* ... */ };
class D: public Base {
public:
//默认情况下,基类的默认构造函数初始化对象的基类部分
//要想使用拷贝或移动构造函数,必须在构造函数初始值列表中显式地调用该构造函数
D(const D& d): Base(d) //拷贝基类成员
{ /*D的成员的初始值 */ }
D(D&& d): Base(std::move(d)) //移动基类成员
{ /* 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.4 继承的构造函数
C++11,派生类能够重用其直接基类定义的构造函数。一个类只继承其直接基类的构造函数,类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。
派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句:
class Bulk_quote : public Disc_quote{
public:
using Disc_quote::Disc_quote; //继承Disc_quote的构造函数
double net_price(std::size_t) const;
}
如果派生类还有自己的数据成员,则这些成员默认初始化
继承的构造函数的特点
一个构造函数的using不会改变该构造函数的访问级别
而且一个using声明语句不能指定explicit或constexpr。如果基类的构造函数是explicit或constexpr,则继承的也有相同属性
当一个基类构造函数含有默认实参,这些实参不会继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别会省略掉一个含有默认实参的形参。
如果基类由几个构造函数,除了两个例外派生类会继承所有这些构造函数:第一个例外是派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本;第二个例外是默认、拷贝和移动构造函数不会被继承。
15.8 容器与继承
使用容器存放对象时,通常采用间接存储方式,因为不允许在容器中保存不同类型的元素。
例:用vector保存Quote,可以把Bulk_quote对象放置在容器中,但这些对象也不再是Bulk_quote对象了,它的派生类部分会被忽略
vector<Quote> basket;
basket.push_back(Quote("0-201-82470-1", 50));
//正确,但只能把对象的Quote部分拷贝给basket
basket.push_back(Bulk_quote("0-201-8230-1", 50, 10, .25));
cout << basket.back().net_price(15) << endl; //调用Quote定义的版本
容易和存在继承关系的类型无法兼容
在容器中放置指针而非对象
在容器中存放具有继承关系的对象时,通常是基类的指针,这些指针所指对象的动态类型可能是基类类型也可能是派生类类型
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("1-23-4", 50));
basket.push_back(make_shared<Bulk_quote>("1-223-4", 50, 10, .25));
cout << basket.back()->net_price(15) << endl; //实际调用版本依赖于指针所指对象
15.8.1 编写Basket类
定义一个购物篮类
class Basket{
public:
//Basket使用合成的默认构造函数和拷贝控制成员
void add_item(const std::shared_ptr<Quote> &sale)
{ items.insert(sale); }
//打印每本书的总价和购物篮中所有书的总价
double total_receipt(std::ostream&) const;
private:
//该函数用于比较shared_ptr, multiset成员会用到它
static bool compare(const std::shared_ptr<Quote> &lhs,
const std::shared_ptr<Quote> &rhs)
{
return lhs->isbn() < rhs->isbn();
}
//multiset保存多个报价,按照compare成员排序
std::multiset<std::shared_ptr<Quote>, decltype(compare)*>
items{compare};
}
使用multiset保存交易信息,元素是shared_ptr。这个multiset使用一个与compare成员类型相同的函数对其中的元素进行排序,成员名字是items.我们初始化items并令其使用compare函数
定义Basket成员
double Basket::total_receipt(ostream &os) const
{
double sum = 0.0; //保存实时计算出的总价格
//iter指向ISBN相同的一批元素中的第一个
//upper_bound返回一个迭代器,指向同一批元素的尾后位置
for (auto iter = items.cbegin();
iter != items.cend();
iter = items.upper_bound(*iter))
{
//打印每本书的细节
sum += print_total(os, **iter, items.count(*iter));
}
os << "Total Sale: " << sum << endl;
return sum;
}
调用upper_bound可以跳过与当前关键字相同的所有元素,返回与iter关键字相同的最后一个元素的尾后位置。
*iter是一个Quote对象或派生类,print_total调用了虚函数net_price,最后计算结果依赖于*iter的动态类型
隐藏指针
Basket必须处理动态内存,add_item需要接受一个shared_ptr参数,因此不得的按照如下形式编写代码:
Basket bsk;
bsk.add_item(make_shared<Quote>("123", 45));
bsk.add_item(make_shared<Bulk_quote>("12323", 45, 3 , .15));
重新定义add_item,使得它接受有个Quote对象而非是shared_ptr
//一个拷贝它,一个采取移动操作
void add_item(const Quote& sale);
void add_item(Quote&& sale);
唯一的问题是add_item不知道要分配的类型。当add_item进行内存分配时,它将拷贝或移动它的sale参数。某处可能会有:
new Quote(sale)
这是错误的,sale可能指向的是Bulk_quote对象,会*切掉一部分
模拟虚拷贝
为了解决这个问题,给Quote类添加一个虚函数,该函数申请一份当前对象的拷贝
class Quote{
public:
//该虚函数返回当前对象的一份动态分配的拷贝
//这些成员使用引用限定符
virtual Quote* clone() const & { return new Quote(*this);}
virtual Quote* clone() &&
{ return new Quote(std::move(*this));}
//其他成员保持一致
};
class Bulk_quote : public Quote{
Bulk_quote* clone() const & {return new Bulk_quote(*this);}
Bulk_quote* clone() &&
{ return new Bulk_quote(std::move(*this));}
//其他成员保持一直
};
分别定义clone的左值和右值版本,每个clone函数分配当前类型的一个新对象,其中const左值引用成员将它自己拷贝给新分配的对象;右值引用成员则将自己移动到新数据中。
使用clone写新版本add_item
class Basket{
public:
//拷贝给定的对象
void add_item(const Quote& sale)
{ items.insert(std::shared_ptr<Quote>(sale.clone()))); }
//移动给定的对象
void add_item(Quote&& sale)
{ items.insert(std::shared_ptr<Quote>(std::move(sale).clone()))); }
}
sale的动态类型决定了clone运行Quote还是Bulk_quote的版本。clone返回一个新分配对象的指针,把一个shared_ptr绑定过去,调用insert把新分配对象添加到items中。
15.9 文本查询程序再探
扩展12.3的文本查询程序,支持单词查询,逻辑与或非查询
应该将不同的查询建模成相互独立的类,共享一个公共基类:
WordQuery
NotQuery
OrQuery
AndQuery
这些类只包含两个操作
- eval,接受一个TextQuery对象并返回一个QueryResult,eval函数使用给定的TextQuery对象查找与之匹配的行
- rep,返回基础查询的string表示形式,eval函数使用rep创建一个表示匹配结果的QueryResult,输出运算符使用rep打印查询表达式
继承组合的设计准则一个是IsA,还有一个是HasA
抽象基类
抽象基类命名为Query_base,把eval和rep定义为纯虚函数,直接派生出WordQuert和NotQuery。AndQuery和OrQuery有一个特殊属性:包含两个运算对象,再抽象出一个BinaryQuery的抽象基类。
将层次关系隐藏于接口类中
首先创建查询命令
Query q = Query("firend") & Query("bird") | Query("wind");
用户层代码不会直接使用继承的类,而定义一个Query的接口类,负责隐藏整个继承体系。Query类将保存一个Query_base指针,绑定到Query_base的派生类对象上。Query类提供的操作和Query_base是相同的:eval和rep,同时Query也会定义一个重载的输出运算符用于显示查询
定义Query对象的三个重载运算符以及一个接受string参数的Query构造函数,动态分配一个新的Query_base派生类的对象:
- &运算符生成一个绑定到新的AndQuery对象上的Query对象
- |生成一个绑定到新的OrQuery对象上的Query对象
- ~生成一个绑定到新的NotQuery对象上的Query对象
- 接受string参数的Query构造函数生成一个新的WordQuery对象
可以见图15.3
15.9.2 Query_base类和Query类
首先定义Query_base类
class Query_base{
firend class Query;
protected:
using line_no = TextQuery::line_no //用于eval
virtual ~Query_base() = default;
private:
//eval返回与当前Query匹配的QueryResult
virtual QueryResult eval(const TextQuery&) const = 0;
//rep表示查询的是一个string
virtual std::string rep() const = 0;
};
eval和rep都是纯虚函数,Query_base是一个抽象基类。因为不希望直接使用,所以没有public成员。
所有对Query_base的使用都需要通过Query对象,因为Query需要调用Query_base的虚函数,所以把Query声明为Query_base的友元。
受保护成员line_no将在eval函数内部使用,类似的析构函数也是受保护的,因为它将在派生类析构函数中使用
Query类
Query对外提供接口并隐藏Query_base的继承体系
//这是一个管理Query_base继承体系的接口类
clasee Query{
//这些运算符需要访问接受shared_ptr的构造函数,而该函数是私有的
firend Query operator~(const Query &);
firend Query operator|(const Query&, const Query&);
firend Query operator&(const Query&, const Query&);
public:
Query(const std::string&); //构造一个新的WordQuery
//接口函数:调用对应的Query_base操作
QueryResult eval(const TextQuery &t) const
{ return q->eval(t); }
std::string rep() const { return q->rep(); }
private:
Query(std::shared_ptr<Query_base> query): q(query) { }
std::shared_ptr<Query_base> q;
}
Query的输出运算符
std::ostream &
operator<<(std::ostream &os, const Query &query)
{
//Query::rep通过它的Query_base指针对rep()进行了需调用
return os << query.rep();
}
输出运算符通过指针成员需调用当前Query所指对象的rep成员
15.9.3 派生类
WordQuery类
一个WordQuery查找一个给定的string,它是在给定的TextQuery对象上实际执行查询的唯一一个操作:
class WordQuery: public Query_base{
friend class Query; //使用WordQuery构造函数
WordQuery(const std::string &s): query_word(s) { }
//具体的类:WordQuery将定义所以继承而来的纯虚函数
QueryResult eval(const TextQuery &t) const
{ return t.query(query_word); }
std::string rep() const { return query_word; }
std::string query_word;
};
//定义接受string的Query构造函数
inline
Query::Query(const std::string &s) : q(new WordQuery(s)) { }
NotQuery类及~运算符
class NotQuery: public Query_base{
friend Query operator~(const Query &);
NotQuery(const Query &q): query(q) { }
std::string rep() const { return "~(" + query.rep() + "+";}
QueryResult eval(const TextQuery&) const;
Query query;
};
inline Query operator~(const Query &operand)
{
return std::shared_ptr<Query_base>(new NotQuery(operand));
}
BinaryQuery类
//保存两个运算对象的查询类型所需的数据
class BinaryQuery: public Query_base{
protected:
BinaryQuery(const Query &l, const Query &r, std::string s):
lhs(l), rhs(r), opSym(s) { }
std::string rep() const { return "(" + lhs.rep() + " " + opSym
+ " " + rhs.rep() + ")"; }
Query lhs, rhs;
std::string opSym;
}
BinaryQuery不定义eval,而是继承了该纯虚函数,因此BinaryQuery也是一个抽象基类,不能创建BinaryQuery类型的对象
AndQuery类、OrQuery类及相应的运算符
class AndQuery: public BinaryQuery{
friend Query operator&(const Query&, const Query&);
AndQuery(const Query &left, const Query &right):
BinaryQuery(left, right, "&") { }
//具体的类:AndQuery继承了rep并且定义了其他纯虚函数
QueryResult eval(const TextQuery&) const;
};
inline Query operator&(const Query &lhs, const Query &rhs)
{
return std::shared_ptr<Query_base>(new AndQuery(lhs, rhs));
}
class OrQuery: public BinaryQuery{
friend Query operator|(const Query&, const Query&);
OrQuery(const Query &left, const Query &right):
BinaryQuery(left, right, "|") { }
//具体的类:AndQuery继承了rep并且定义了其他纯虚函数
QueryResult eval(const TextQuery&) const;
};
inline Query operator|(const Query &lhs, const Query &rhs)
{
return std::shared_ptr<Query_base>(new OrQuery(lhs, rhs));
}
15.9.4 eval函数
OrQuery::eval
返回并集
QueryResult OrQuery::eval(const TxetQuery& text) const
{
//通过Query成员lhs和rhs进行的虚调用
//调用eval返回每个运算对象的QueryResult
auto right = rhs.eval(text), left = lhs.eval(text);
//将左侧运算对象的行号拷贝到结果set中
auto ret_lines = make_shared<set<line_no>>(left.begin(), left.end());
//插入右侧运算对象所得的行号
ret_lines->insert(right.begin(), right.end());
//返回一个新的QueryResult,它表示lhs和rhs的并集
return QueryResult(rep(), ret_lines, left.get_file());
}
AndQuery::eval
//返回运算对象查询结果set的交集
QueryResult AndQuery::eval(const TextQuery& text) const
{
//通过Query运算对象进行的虚调用,以获得运算对象的查询结果set
auto left = lhs.eval(text), right = rhs.eval(text);
//保存left和right交集的set
auto ret_lines = make_shared<set<line_no>>();
//将两个范围的交集写入一个目的迭代器中
//本次调用的目的迭代器向ret添加元素
set_intersection(left.begin(), left.end(),
right.begin(), right.end(),
inserter(*ret_lines, ret_lines->begin()));
return QueryResult(rep(), ret_lines, left.get_file());
}
NotQuery::eval
//返回运算对象的结果set中不存在的行
QueryResult NotQuery::eval(const TextQuery& text) const
{
//通过Query运算对象对eval进行虚调用
auto result = query.eval(text);
//开始时结果set为空
auto ret_lines = make_shared<set<line_no>>();
//我们必须在运算对象出现的所有行中进行迭代
auto beg = result.begin(), end = result.end();
//对于输入文件的每一行,如果该行不在result当中,则将其添加到ret_lines
auto sz = result.get_file()->size();
for (size_t n = 0; n != sz; ++n){
//如果我们还没有处理完result的所有行
//检查当前行是否存在
if (beg == end || *beg != n)
ret_lines->insert(n);
else if(beg != end)
++beg;
}
return QueryResult(rep(), ret_lines, result.get_file());
}