第十五章(面向对象程序设计)
1).面向对象程序设计基于三个概念
- 数据抽象(接口和实现的分离)
- 继承(定义相似的类)
- 动态绑定(忽略类的差异,统一使用)
2).面向对象程序设计适用于,不同类有细微差别;但是使用时可以忽略这些差别的情况。
/1.OOP:概述
1).继承,通过继承联系在一起的类构成一种层次关系。
- 层次关系的根部,有一个基类
- 其他类间接或者直接地从基类继承而来。这些继承而来的类称为是派生类。
- 基类负责定义层次关系中所有类共同拥有的成员,派生类定义各自特有的成员。
- 大多数类只继承一个类,称为单继承。
2).对不同的定价进行建模。
- 类都包含,
isbn()
成员函数,net_price(size_t)
实际销售价格函数。但是对于实际销售价格,每一个类的不一样。
- **解决,引入虚函数。**区别于派生类不做改变直接继承的函数。基类中声明虚函数,加上关键字
virtual
。
- 定义基类,表示原价。
{
class Quote {
public:
string isbn() const;
virtual double net_price(size_t n) const;
};
}
3).派生类的定义。
- 通过类派生列表明确指出它是从哪一个,哪一些基类继承而来。
- 形式,
class Bulk_quote : public Quote {}
,用逗号分割基类的列表。每一个基类前 可以 有访问说明符。public,protected,private
中的一种,用来控制派生类从基类继承而来的成员是否对派生类的用户可见。如果是public
,那么基类的公有成员也是派生类接口的组成部分;并且,此时我们可以将这个派生类型的对象绑定到基类的引用或者指针上。 - 派生类在其内部对所有重新定义的虚函数进行声明。
- 派生类可以在这样的函数之前加上关键字
virtual
。但不是必须的 - 新标准,允许派生类显式地注明它将使用哪一个成员函数改写基类的虚函数,方式,在尾置返回类型以及形参列表(包括任何的修饰符
const
,引用等。)之后加上一个override
关键字。
{
class Bulk_quote : public Quote {
public:
double net_price(size_t n) const override;
};
}
4).动态绑定
- 可以使用同一段代码分别处理基类和派生类。
- 函数的形参为
const Quote &
是基类的常量引用。 - 因为传入的实参是被引用绑定的,所以,传入的实参将决定调用的是派生类的函数还是基类的,或者其他派生类的函数。
- 由于是在运行时选择函数的版本,动态绑定也叫做运行时绑定。
- 动态绑定的条件。
- 使用基类的引用或者指针
- 调用一个虚函数(而不是基类不希望派生类改变的成员函数。)
5).通过引入虚函数。基类中的成员函数大体上分为两个类,
- 虚函数,派生类必须进行覆盖
- 派生类不该改变的成员函数
6).虚函数的几点说明。
- 任何构造函数之外的非静态函数都可以是虚函数。
- 对于虚函数,派生类可以提供自己的定义覆盖从基类继承而来的旧定义。
- 如果没有覆盖这个虚函数,那么派生类就直接继承基类的版本。
- 关键字
virtual
只能出现在类内。 - 根节点的类(基类)通常都需要定义一个虚析构函数。
- 当我们覆盖虚函数时,可以再一次使用
virtual
关键字指出该函数的性质,但是不是必要的。 - 因为,如果一个基类把一个函数声明成虚函数,那么这个虚函数在所有的派生类中也隐式地是虚函数。
- 如果成员函数不是虚函数,那么它的解析发生在编译时,而不是运行时。
- 如果一个派生类的函数覆盖了某一个虚函数,它的形参类型必须和被它覆盖的基类型完全一致。同理,返回类型必须和基类虚函数**匹配。但是有一个例外是,当类的虚函数返回类型是类本身的指针或者引用时,上述规则无效。例如,D继承自B,B的虚函数返回B,那么D对应的函数可以返回D。
/2.定义基类和派生类
//1.定义基类
1).定义一个基类。
{
class Quote {
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; //动态绑定
private:
string bookNo;
protected:
double price = 0.0;//普通装填下不打折的价格
};
}
- 定义了一个虚析构函数。根节点的类(基类)通常都需要定义一个虚析构函数。
//2.定义派生类
1).定义派生类
{
class Bulk_quote : public Quote {
public:
Bulk_quote() = default;
Bulk_quote(const string &, double, size_t, double);
//覆盖基类的版本
double net_price(size_t) const override;
private:
size_t min_qty = 0;//使用折扣政策的最低购买量
double discount = 0.0;//用小鼠表示折扣额
};
}
- 这个派生类,从基类
Quote
中继承了isbn
函数,bookNo,price
数据成员。 - 由于使用了
public
,所以isbn
也是这个派生类的隐式接口。
2).派生类对象的组成。
- 自己定义的不含有静态成员的子对象。
- 基类继承而来的子对象。(
price
(protected
成员),bookNo
)两个数据成员。 - c++标准没有规定派生类对象在内存中的分布。但是我们可以认为
Bulk_quote
对象包含两个部分。并且,这两个部分在内存中不一定是连续存储的。
3).派生类和基类的指针,引用
- 由于派生类对象含有与其基类对应的组成部分。所以我们可以把派生类当成基类来是使用,并且可以将基类的指针或者引用绑定在派生类对象中的基类部分
- 在需要基类的地方可以用派生类类代替。
{
Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk;//p指向的是bulk的Quote部分
Quote &q = bulk;//r绑定到bulk中的Quote部分
}
- 这种转换通常称为**派生类到基类的类型转换。**编译器会隐式执行这种转换。
4).派生类的构造函数
- 虽然派生类中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。派生类必须使用基类的构造函数来初始化它的基类部分。每一个类控制它自己的成员初始化过程。
- 例如,以下在初始化列表中,调用了
Quote
的构造函数,有点像是委托构造函数一样。执行顺序和委托函数是一样的。 - 由
Quote
的构造函数类初始化Bulk_quote
类中的基类部分。 - 如果我们没有进行初始化,那么派生类对象中的基类部分的会执行默认初始化。
- 注意,首先初始化基类部分,然后才是按照声明的顺序初始化派生类的成员。
{
Bulk_quote(const string &book,double p,size_t qty,double disc) :
Quote(book,p),min_qty(qty),discount(disc) {}
}
5).派生类中使用基类的成员。
- 派生类可以访问基类的公有成员和受保护成员。
{
double Bulk_quote::net_price(size_t n) const {
if (n >= min_qty) {
return n * (1-discount) *price; //price是一个protected成员
}
else
return n * price;
}
}
- 派生类的作用域嵌套在基类的作用域之内。
6).虽然我们可以在派生类构造函数体内给它的公有或者受保护的基类成员赋值,但是最好遵循基类的接口。使用基类的构造函数来初始化从基类中继承而来的成员。
- 每一个类负责定义各自的接口,想要与类的对象交互必须使用该类的接口,即便是派生类中的基类部分。
const
函数成员不能修改对象的值,除非对象的值是mutable
。
7).基类和静态成员
- 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论基类中派生出多少个派生类,对于每一个静态成员来说都存在唯一的实例。
- 静态成员和其他遵守一样的访问权限规则。
{
class Base {
public:
static void statement();
};
class P : public Base {
void f(const P&);
};
// 定义f
void P::f(const P &p) {
Base::statement();//直接使用类名访问
P::statement();//直接只用派生类型访问
p.statement;//派生类型对象访问
statement;//使用this对象访问
}
}
8).派生类的声明
- 不包含派生列表。
- 和普通的类一样。
{
class Bulk_quote : public Quote;//错误
class bulk_quote;//正确
}
- 原因在于,一条声明语句的意义在于,令程序知晓某一个名字的存在,以及这个名字表示一个什么样的实体(函数,类,变量)。而派生列表以及定义相关的其他细节与类的主体一起出现。
9).被用作基类的类
- 如果我们想要将一个类作为基类,那么这个类必须已经定义了,而不是声明。原因就是,派生类包含了并且可以使用它从基类中继承而来的成员。
- 上面的规则隐式地指出,一个了不能派生它自身。
- 一个类可以是同时是基类,派生类。
{
class Base {...};
class D1 : public Base {...};
class D2 : public D1 {...};
}
- 以上的例子中。
Base
是D1
的直接基类,是D2
的间接基类。直接基类直接出现在派生列表中;而间接基类由派生类通过它直接基类继承而来。 - 每一个类都会继承它的基类的所有成员。对于最终一个基类而言。它既含有直接基类的所有成员,也会有直接基类的基类的成员…直到继承链的顶端。
10).防止继承的发生
- 目的,不想作为基类。
{
class ND final{..};//不能作为基类
// 但是它可以作为派生类
class ND final : Base {};
}
//3.类型转换和继承
1).智能指针和内置指针都支持派生类型向基类型的转换。
2).两种类型。
- **静态类型,**在编译时总是已知的,是变量的声明时的类型或者表达式生成的类型。
- **动态类型,**变量或者表达式表示的内存中的对象的类型,它只有运行时才会知道。
{
item.net_price();//item的静态类型就是Quote
// 动态类型取决于,item绑定的对象。
}
3).**如果不是引用或者指针,那么变量的的动态类型和静态类型,永远是一致的。**即,不存在基类和派生类 对象 之间的类型转换。
4).不存在基类向派生类的转换,因为派生类的成员基类不一定有。书中解释有误,或者太含糊。
- 有一点特殊。
{
Quote *itemp = &bulk;
Bulk_quote *p = itemp;//这也是非法的。
// 其实可以这么理解,就是Quote还是这个类型,它指向了基类所有的部分,而并没有进行转换。
// 第一条语句还是从基类到派生类的转换
// 自然就不可行了
}
5).编译器时无法确定某一个特定的转换在运行时候是否是安全的,这是因为编译器只能通过检查指针或者引用的静态类型来推断该转换是否合法。
- 如果在一个类中有一个或者多个隐函数,可以使用
dynamic_cast
请求一个类型转换,该转换的安全检查将会在运行时执行。 - 如果我们已知某一个基类向派生类的转换是安全的,则我们可以使用
static_cast
,来强制覆盖掉编译器的检查工作。
6).当我们用一个派生类对象为一个基类对象赋值或者初始化时,由于合成的或者我们自己定义的拷贝构造/赋值函数的参数都是const
引用,所以这样做是合法的。并且只有该派生类中的基类部分会被拷贝,赋值,它的派生类部分会被忽略掉。(或者说切掉)
- 所以,我们在很多的情况下可以通过拷贝或者赋值,进行一个简单的类型转换。
练习,
- 15.10,一个重要的例子就是
ifstream
就是继承自istream
的,或者说,istream
就是ifstream
的基类,所以我们可以使用istream
引用来接受一个ifstrean
的对象。并且可以对,ifstream
的对象使用istream
对象的操作。
/3.虚函数
1).为什么虚函数在每一个类中都需要有定义?
- 因为动态绑定。编译器编译时根本不知道会使用哪一个类中的版本。否则在运行时就会出现,没有函数可调用。
- 对虚函数的调用是在运行时才被解析。(当是引用或者指针时。)
- 实际类型动态类型动态绑定。
2).**多态性。**有继承关系的多个类型称为多态类型。
- 对于非虚函数,在编译时旧进行绑定。因为只有一个版本。不管是在哪一个派生类还是基类。
3).override
说明符
- 如果一个派生类定义了一个函数和基类中虚函数的名字相同但是形参列表不一样的函数,这仍然是合法的。编译器会认为新定义的函数和基类中原有的函数是相互独立的。这时,派生类中的函数并没有覆盖掉基类中的版本
- 编程习惯告诉我们,这是一种错误的行为。我们的预期是覆盖,但是不小心把列表弄错了。
- 解决,加上
override
关键字,使得程序员的意图更加明显;同时让编译器帮助我们检查。如果我们标记了override
,但是没有覆盖虚函数,将会报错。
const
也要匹配。返回类型和参数类型都需要匹配。- 只有虚函数才能覆盖
{
struct b {
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct B : b {
void f1(int) const override;//正确,匹配f1
void f2(int) override;//错误,没有匹配到f2(int)
// 如果没有关键字,他将会成为一个新函数。
void f3() override;//f3不是虚函数,
// 只有虚函数才能覆盖
void f4() override;//b中没有f4。
};
}
4).final
说明符。
{
struct c : b {
// c继承了b的f2,f3,覆盖了f1,并且不允许以他为基类的派生类覆盖f1
void f1(int) const final;
};
struct C : c {
void f2();//覆盖从b继承而来的f2
void f1(int) const;//错误,它是final
};
}
5).在虚函数中的默认实参
- 如果某一次调用使用了默认实参,它使用的是静态类型中定义的默认实参。
- 所以最好在所有类中的默认实参是一样的。
6).不要动态绑定地使用虚函数
- 使用作用域运算符号。
baseP->Quote::net_price(42);
- 强行调用了
Quote
中的版本,而不管baseP
的实际指向。函数在编译时解析。 - 通常只有在友元或者成员函数中的代码会用到。
- 如果在一个派生类中没有使用作用域运算符号,而调用一个基类的版本,会解析为对派生类版本自身的调用,会有无限循环??
练习,
- 15.10,在一个派生类中调用一个基类的虚函数,使用
Quote::debug();
/4.抽象基类
1).**纯虚函数,**表明当前的类中的net_price
函数是没有意义的。
- 纯虚函数无需定义。形式在函数体的位置书写
= 0;
就可以将一个虚函数说明为纯虚函数。**其中=0
只能出现在类的内部的虚函数的声明处。**我们可以为纯虚函数定义函数体,但是必须在类的外部。
{
struct Disc_quote : public Quote {
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 = 0;//折扣使用的数量
double discount = 0.0;//表示折扣
};
}
2).抽象基类
- 含有(未经覆盖直接继承)纯虚函数的类称为抽象基类。抽象基类负责定义接口,后续的其他类可以覆盖该接口。
- 不能直接创建抽象基类的对象, 即我们不能直接定义
Disc_quote
的对象,因为他将net_price
定义成纯虚函数。注意如果它的派生类没有覆盖纯虚函数,则这个派生类一样是纯虚函数,我们一样不可以创建对象。
3).理解初始化的过程。
- 派生类的对象构造过程,
- 传入参数到直接基类,直接基类再传入参数给它的直接基类直到到达最顶层。然后再依次递归回来。
- 初始化的方式和委托构造函数一摸一样。
4).重构概念。
- 负责重新设计类的体系以便将操作和数据从一个类移动到另一个类。例子就是,在
Quote
和Bulk_quote
的继承体系中加了一个Disc_quote;
??
/5.访问控制和继承
1).每一个类控制着自己成员的初始化过程;每一个类也控制着自己成员对于派生类而言是否是可访问的。
- 派生类可以继承定义在基类中的所有数据,函数成员;但是派生类的成员函数或者友元只能访问公有成员,受保护成员,而且派生类对象访问受保护成员,只能通过派生类的对象;但是不能访问私有成员。
{
Class Base {
protected:
int prot_mem;
};
class Sneaky : Public Base {
friend void clobber(Sneaky&);//可以访问protected
friend void clobber(Base&);//不可以访问
int j;
};
void clobber(Sneaky &a) {a.j = a.prot_mem = 0;//合法}
void clobber(Base &b) {b.prot_mem = 0;//不合法,不可以访问}
}
- 这样就导致了函数不是
Base
的友元,但是可以轻易访问它的成员。 - 用于对于基类的私有和受保护永远是不可见的。
2).派生列表中的访问说明符号对于它的直接派生类的成员,友元的访问权限是没有影响的。
- 影响的是派生类对对象的用户(包括它的派生类)
- 如果是
private
,继承而来的所有成员均为私有的。 - 如果是
protected
,继承而来的受保护,公有成员均为受保护的。 - 如果是
public
,继承而来的成员的属性不变。
3).访问控制和派生类向基类的转换。
- 只有D公有地继承自B,用户代码才可以使用该转换;如果是私有或者受保护继承的,则不可以使用该转换。
- 不论D是以什么方式继承B,D的成员函数和友元都可以使用该转换。
- 如果D是公有或者受保护继承B的,那么D的派生类的成员函数或者友元可以使用该转换。
- 总结,对于给定的代码,如果基类的公有成员是可访问的,就可以进行转换。
4).基类的组成。
- 实现部分->受保护(供给派生类使用)和私有
- 接口->公有(用户使用)。
- 它有三类的用户,类的设计者,派生类,普通用户。
5).友元关系不可以继承。
- 一旦某一个类被声明为另一个类的友元,那么它旧可以通过任何可以访问到该类的数据成员的方式进行访问,包括通过它的派生类进行访问。
{
class Base {
friend class Pal;
};
class Pal {
public:
int f(Base b) {return b.prot_mem;}//正确
int f1(Sneaky s) {return s.prot_men;}//正确,
int f2(Sneaky s) {return s.j;}//错误,不是友元。
};
}
- 每一个类控制自己的友元。
{
class D2 : public Pal {
public:
int mem(Base b) {
return b.prot_mem;//错误,友元不可以继承。
}
};
}
- 总结,该类的成员,该类的友元(可以用任何可以访问得到的方式进行访问。)可以访问它的所有成员。
6).改变个别成员的可访问性质。
- 使用
using
进行声明。 - 前提是派生类可以访问到该成员,可以是直接基类,也可以是间接基类。
- 改变之后的访问权限,只与当前类的访问说明符有关。
{
class Base {
public:
size_t size() const {return n;}
protected:
size_t n;
};
// 注意是私有继承
// 原本该类继承而来的成员都是私有的。
// 现在进行了更改
// size变为public
// n变为protected
class D : private Base {
public:
using Base::size;
protected:
using Base::n;
};
}
7).默认的继承列表关键字(默认派生运算符)。
- 由定义派生类的关键字来决定。
{
class Base {...};
class D2 : Base {...};//私有继承
struct D1 : Base {...};//公有继承
}
- 除了默认的派生运算符和默认类内的访问说明符之外。这两个关键字没有其他的差别。
练习,
- 15.18,分清三中代码,用户代码,派生类代码,友元或者成员函数代码。
/6.继承中的类作用域
1).派生类的作用域嵌套在直接基类里面。
2).使用指针,或者引用时,和直接使用对象来使用成员,进行的名字匹配是一样的,都是从静态类型开始解析。例如
{
Bulk_quote bulk;
Bulk_quote* p = &bulk;
Quote* itemp = &bulk;
p->discount_policy();//正确
// 在Disc_quote中匹配到名字。
itemp->discount_policy();//错误,之接从基类开始解析,并且没有匹配到名字。
}
3).由于是嵌套作用域,所以派生类定义的重名变量将会屏蔽直接或者间接基类。
- 由于基类的
mem
需要调用基类的构造函数进行初始化。这里初始化的是自身的mem
。
{
struct Base {
Base() : mem(0) {}
protected:
int mem;
};
struct D : Base {
D(int i) : mem(i) {}//基类中的mem执行默认初始化。
int gey_mem() {return mem;}
private:
int mem;
};
D d(1);
cout << d.get_mem();//打印1;
}
4).通过作用域运算符进行访问隐藏的成员。
- 注意一样的遵守访问控制规则。由于是
protected
,才可以访问
{
int get_mem() {return Base::mem;}
}
- 除了覆盖继承而来的虚函数,派生类最好不要使用重复的名字。
5).在继承关系中调用函数
- 在该对象对应的 静态类型 中查找该成员,如果没有找到,那么在它的直接基类中继续查找直到到达继承链的最顶端。没有查找到则报错。
- 查找到了,就进行类型检查,确保本次调用是合法的。
- 如果是合法的,
- 如果是虚函数而且是引用或者指针,则会进行动态绑定。最终的结果是根据它的动态类型,也就是它的真实类型。值得注意的是,和虚函数名字一样,但是形参列表不一致不是覆盖;此时它会继承直接基类的版本;也就是说此时会调用直接基类的虚函数版本,而不是该派生类的重名版本。
- 如果不是以上情况,则会进行常规的函数调用。
6).和一般的名字查找一样。名字查找是优于类型检查的;并且一个类就是一个作用域:所以,继承类之间的重名函数,是不会重载的。
- 调用基类的重名函数。
d.Base::memfuc();
7).虚函数的形参列表一样,调用形式就是一样的,于是用户代码是一样的,从而实现动态绑定。
8).是否是虚函数调用的差异。
- 同:都是从指针的静态类型开始搜索匹配。如果没有就失败,就算实际类型的派生类中有定义。
- 异:如果匹配到了,查看是否是虚函数;
- 是,直接匹配到实际类型的版本;
- 否,直接对当前匹配的函数进行解析。
9).是否是虚函数不是重载的条件。即,在重载时,是否有关键字virtual
无关紧要。
- 当我们需要基类中的重载函数组而不是覆盖或者屏蔽时,使用
using
声明。前提就是在该派生类中他们可见。对于派生类中没有重新定义的重载版本的访问其实就是对using
声明点的访问。 - 以上只能解决,所有的重载版本均可见。
- 疑问,如何解决只覆盖一些函数,还要求其他基类的重载函数组可见。因为使用
using
声明只需要指定一个名字而无需参数列表。书本是迷惑的。 - 根据以上。
using
声明不仅仅是改变访问说明符(访问权限),而且有再一次定义的效果。
/7.构造函数与拷贝控制
1).和普通的类一样,需要控制类对象的创建,拷贝,移动,赋值和销毁。如果没有定义,将会采用合成的版本。同样的会有删除的版本。
//1.虚析构函数
1).为什么需要将析构函数,定义成虚函数?
- 当我们
delete
一个指向对象的指针时。会执行该指针指向对象的析构函数。 - 而,当指针指向的是一个继承体系中的某一个类型时,将会又可能出现静态类型和动态类型不匹配的情形。此时按照和调用一个虚函数一样的思路进行匹配。
- 和其他的虚函数一样,析构函数的叙述性会被继承。不论它是合成的还是自己定义的,都将会是虚析构函数。因此只要基类中的析构函数是虚函数,就可以保证执行正确的析构函数。
- 如果基类的析构函数不是虚函数,那么
delete
一个指向派生类的基类指针将会产生未定义的行为。 - 此时,由于我们需要的是一个虚的析构函数。我们才显式地指出这个析构函数,并且它的函数体是空的。所以这是一个三五法则的例外。
2).虚析构函数使得,基类和派生类的合成移动操作都是删除的。
//2.合成拷贝控制与继承
1).对一个派生类执行拷贝,赋值,销毁,与普通的不一样的地方在于,对于直接基类的部分,它会调用直接基类的相应函数进行操作,直接基类调用它的直接基类…直到继承链的顶端。唯一的要求就是,相应的函数是可以访问的,不是删除的;是否是合成的还是自定义的并不重要。
2).被删除的的函数。(合成的默认构造函数和拷贝控制成员。)
- 和其他类一样的情况。
- 由于基类定义的方式导致的派生类成员被删除
- 如果基类的默认构造,拷贝构造,拷贝赋值,析构是删除或不可以访问的,那么派生类中的对应成员是被删除的。因为编译器不能调用相应的基类成员函数来执行派生类对象的基类部分的构造,拷贝,析构。
- 如果基类的析构是删除的,或者不可以访问的,那么派生类中合成的默认和拷贝构造将是被删除的。因为编译器无法销毁派生类对象中的基类部分。
- 基类析构是删除的或者不可以访问的,派生类移动操作也是删除的。理由同上。
- 如果在派生类中使用
=default
显式地请求合成默认的函数,而基类中是删除的。那么派生类该请求也会失效,最终就是一个删除的函数。
3).使用移动操作。
- 由于在基类中,通常需要定义一个虚析构函数,这导致了基类中合成的移动操作将会是删除的;从而派生类中的移动操作也是删除的。
- 所以我们如果需要移动操作,首先应该在基类中定义。可以使用合成的版本,但是必须显式地指出。于是我们在基类定义了6个
=default
的控制成员。这样该基类的派生类都具有以上的合成版本。除非由于其他的原因使得相应的操作失效。
练习,
- 15.25,对于默认构造函数,由于我们需要使用直接基类的构造函数,所以不可以使用默认构造函数。
//3.派生类的拷贝控制成员
1).拷贝,移动和赋值需要同时对基类的部分进行相应的操作。但是对于析构,只 需要显式地 负责销毁派生类自己分配的资源(例如delete
指针等。)。
2).在定义派生类的移动,拷贝…时,如果不显式地指出直接基类的相应函数,很大可能是不正确的定义;另一个方面也是因为,基类部分会是默认值,而派生类会是初始化的值,这不统一。可以使用派生类对像传递给基类函数的基类引用形参,并且自动完成基类部分的初始化。位置在初始化列表。
3).对于赋值。也是一样的处理。只是位置发生了改变,在函数体里。
{
D& D::operator=(const D &r) {
Base::operator=(r);
...
return *this;
}
}
4).析构的执行,与创建的顺序相反。从派生类开始,执行析构函数体,然后该类的成员隐式销毁,接着直接基类的析构函数…直到继承链的顶端。所以析构函数只负责自己成员的销毁就可以。
5).如果构造函数或者析构函数调用(间接调用,调用的函数里面有虚函数)了某一个虚函数,则我们应该执行与构造函数或者析构函数所属类型相对应得虚函数版本。否则会出现对象已经被销毁,或者对象尚未创建,但是却要调用它的成员的错误。当然这也是编译器的工作。
//4.继承的构造函数
1).新标准中,派生类可以重用其直接基类定义的构造函数。这些函数不是以常规地方式继承而来。
- 一个类只能直接初始化它的直接基类,出于相同的原因,一个类也只能继承其直接基类的构造函数。
- 类不能继承默认,拷贝,移动构造函数。
- 派生类继承基类的构造函数的方式就是,提供一条注明了基类名的
using
声明语句using Disc_quote::Disc_quote;//继承Disc_quote的构造函数(三种。)
。 using
此时,将会令编译器产生代码。对于基类中每一个构造函数,编译器都会生成一个与之对应的派生类构造函数。形式就是derived(args) : base(args) {};
{
// 例如,
Bulk_quote(const string &book,double price,size_t qty,double disc) :
Disc_quote(book,price,qty,disc) {}
// 如果派生类有自己的成员,那么这些成员就会被默认初始化。
// args由与直接基类的构造函数一致。
}
2).继承的构造函数的特点。
using
声明不会改变构造函数的访问级别。这一点和普通的成员不一样。(即,using
语句出现的位置不会改变它的访问级别。实际上是生成了代码的作用,而不是改变访问级别的作用。)- 并且
using
语句还不能指定explicit
或者constexpr
;也就是说基类的是constexpr
或者explicit
,那么继承的构造函数也有一样的属性。 - 如果一个基类的构造函数含有默认实参,这些实参并不会被继承。而是构造函数会获得多个继承的构造函数,其中每一个构造函数省略掉一个默认实参。例如,基类的构造函数接受两个形参,第二个形参含有默认实参,那么派生类中将会有两个版本,一个是只接受一个形参;另一个接受两个形参(没有默认实参)。
- 如果基类中含有多个构造函数,除了两个例外,大多数的派生类会继承它的所有构造函数。
- 派生类为一部分构造函数定义了自己的版本。这些版本的形参列表和基类的构造函数有一样的形参列表,它们将会替换继承而来的构造函数。
- 不会继承拷贝,移动,默认构造函数。继承的构造函数不会被视为用户定义的;也就是说,如果类中只有一个继承的构造函数,编译器会为它合成默认的构造函数。
/8.容器与继承
1).由于容器只能直接存放相同类型的元素。如何存放有继承关系的类?
- 如果直接将派生类存放在基类的容器中,那么将会发生“切断”。只有基类的部分被存放;派生类的部分被忽略掉。
2).解决。 - 使用智能指针,或者普通指针。
- 派生类的指针可以转为基类指针。(就是将派生类赋值给基类的指针。)
- 注意,同样的可以将
Bulk_quote
的智能指针赋值给Quote
的智能指针。
{
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("isbn",50));
// 这里是Bulk_quote的智能指针
basket.push_back(make_shared<Bulk_quote>("isbn",50,12,.25));
cout << basket.back()->net_price(15) << endl;
// 这里调用net_price虚函数,版本取决于动态类型。
}
//1.编写Basket类
1).存放交易信息。虽然是面向对象,但是更多地使用指针或者引用。指针,引用复杂度高,使用类辅助降低复杂度。
{
class Basket {
public:
//使用合成的构造和拷贝控制成员
void add_item(const shared_ptr<Quote> &sale) {
item.insert(sale);
}
double total_receipt(ostream &) const;
private:
// 由于shared_ptr没有定义小于运算符
static bool compare(const
shared_ptr<Quote> &l,const shared_ptr<Quote> &r) {return l->isbn() < r->isbn();}
//multiset保存多个报价,按照compare进行排序
// 这种形式的声明需要注意。也可以使用()
multiset<shared_ptr<Quote>,decltype(compare) *> items{compare};
};
}
2).定义类的成员print_total
。
**iter
表示的是一个基类对象或者一个派生类对象。由于指针的动态类型是不一定等于静态类型的。
{
double Basket::total_receipt(ostream &os) const {
double sum = 0.0;
// 一次处理同一本书。
// upper_bound返回的是该书籍的尾后迭代器。
// items.count返回的是该书的数量。
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;
}
}
3).定义类成员,add_item
{
// 第一个版本的是智能指针形参,
// 在传递参数时,不得不进行make_shared<Quote>或者make_shared<Bulk_quote>的操作
// 修改,让他接受一个对象
void add_item(const Quote &sale);//拷贝给定的对象
void add_item(Quote &&sale);//移动给定的对象。
// 问题是,由于set接受的是一个shared_ptr的类型,所以我们必须进行内存的管理,但是,
// 如果我们分配一个new Quote(sale);
// 进行移动或者拷贝,将会导致,sale的非基类部分被截断。
}
4).解决。模拟虚拷贝。
- 在
Quote
和Bulk_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{
public:
Bulk_quote* clone() const& override {
return new Bulk_quote(*this);
}
Bulk_quote* clone() && override {
return new Bulk_quote(std::move(*this));
}
};
class Basket {
public:
void add_item(const Quote &sale) {
items.insert(shared_ptr<Quote>(sale.clone()));
}
// 移动的版本
// 传参时进行一次移动
// 构造指针时进行第二次移动
// 这是移动的版本的特殊之处。
// 因为这个形参实际上还是一个左值,所以需要move成右值。
// 左值引用和右值引用都会进行动态绑定。
void add_item(Quote &&sale) {
item.insert(shared_ptr<Quote>(std::move(sale).clone()));
}
}
}
/9.文本查询程序的再探
//1.面向对象的解决方案
1).继承体系的设计
- 继承类是基类的一种情况
- 继承类含有基类
2).都需要的操作定义在基类中,并且定义成虚函数。由于对于与和非的操作是对于两个对象,所以,在Query_base
和AndQuoery
,OrQuery
之间加上一个抽象基类,BinaryQuery
。
3).设计一个公共的接口Query
类负责,建立相应的对象。(查询树)
//2.Query_base类和Query类
1).Query_base
类。含有纯虚函数,是一个抽象基类。
{
class Query_base {
friend class Query;
protected:
using line_no = TextQuery::line_no;
virtual ~Query_base() = default;
private:
virtual QueryResult eval(const TextQuery &) const = 0; //纯虚函数
virtual string rep() const = 0;//纯虚函数
};
}
- 我们不想用户使用这个类,而是统一使用
Query
。所以这里的成员均为private
对于用户而言。 - 析构函数是
protected
,这样派生类也可以隐式地调用它,保证不会是删除的。 line_no
是受保护的,仅仅在继承体系中使用。
2).Query
类。
- 负责提供对外的接口,隐藏继承体系。每一个
Query
对象中都含有指向Query_base
的shared_ptr
。
{
class Query {
// 将运算符号设置为友元,因为他们需要访问私有的构造函数。
friend Query operator~(const Query &);
firend Query operator|(const Query &,const Query &);
firend Query operator&(const Query &,const Query &);
public:
// 由于我们还没有构建WordQuery类,这里只能先声明,而不能直接定义。
Query(const string &);//构架一个新的WordQuery;
// 这两个接口是调用的base中的虚函数
// 是一个动态绑定
QueryResult eval(const TextQuery &t) const {
return q->eval(t);
}
string rep() const {
return q->rep();
}
private:
Query(shared_ptr<Query_base> query) : q(query) {}
shared_ptr<Query_bae> q;
};
}
3).Query
的输出运算符
{
ostream& operator<<(ostream &os,const Query &query) {
return os << query.rep();
}
// 使用时
Query andp = Query(sought1) & Query(sought2);
cout << andp << endl;
// 实际调用的是动态绑定的rep函数,
// 这里的实际类型就是and的rep
// 具体实现见下文
}
//3.派生类
1).WordQuery
类。
{
class WordQuery : public Query_base {
friend class Query;//Query使用它的构造函数,这些函数都是私有的。
WordQuery(const string &s) : query_word(s) {}
QueryResult eval(const TextQuery &t) const {
return t.query(query_word);
}
string rep() const {
return query_word;
}
string query_word;//要查找的单词
};
//定义Query的可见构造函数
inline
Query::Query(const string &s) : q(new WordQuery(s)) {}
}
2).NotQuery类和~运算符
{
class NotQuery : public Query_base {
friend Query operator~(const Query &);
NotQuery(const Query &q) : query(q) {}
// 必须定义继承而来的所有纯虚函数
// 注意query对rep的调用不是需调用,但是
// 函数体里面的q->rep()是一个虚调用。
string rep() const {
return "~(+" + query.rep() + ")";
}
QueryResult eval(const TextQuery &) const;
Query query;//便于每一个类返回对象的求值对象。
};
// 由于,该操作符号是一个友元
// 所以可以调用Query的私有构造函数
// 该构造函数接受一个shared_ptr的指针
// 下面的return语句实际行就是一个
// 拷贝初始化的过程。
inline Query operator~(const Query &operand) {
return shared_ptr<Query_ptr> (new NotQuery(operand));
}
}
3).BinaryQuery
类
- 它也是一个抽象基类,保存另外两个运算类所需要的运算操作。
- 注意
BinaryQuery
不直接定义eval
操作,而是直接继承,所以它隐式地包含一个纯虚函数。它也是一个抽象基类。
{
class BinaryQuery : public Query_base {
protected:
BinaryQuery(const Query &l,const Query &r,string s) :
lhs(l),rhs(r),opSym(s) {}
string rep() const {
return "(" + lhs.rep() + " "
+ opSym + " "
+ rhs.rep() + ")";
}
Query lhs,rhs;
string opSym;
};
}
5).AndQuery
和OrQuery
类。
{
class AndQuery : public BinaryQuery {
friend Query operator&(const Query &,const Query &);
// 构造函数。
// 对BinaryQuery的两个数据成员进行构造。
AndQuery(const Query &l,const Query &r) :
BinaryQuery(l,r,"&") {}
QueryResult eval(const TextQuery &t) const;
};
inline Query operator&(const Query &l,const Query &r) {
return shared_ptr<Query_base>(new AndQuery(l,r));
}
class OrQuery : public BinaryQuery {
friend Query operator|(const Query &,const Query &);
OrQuery(const Query &l, const Query &r) : BinaryQuery(l, r, "|") {}
QueryResult eval(const TextQuery &t) const;
};
inline
Query operator|(const Query &l.const Query &r) {
return shared_ptr<Query_base>(OrQuery(l,r));
}
}
练习,
- 15.37,体会使用
Query
公共接口的好处,将指针封装,同时保留一些信息。继承体系更加简单。 - 15.38,注意我们除了接口类
Query
之外的所有类的接口都是私有的,并且是运算返回的结果都是,Query
,没有办法实现从基类到派生类的转换。并且BinaryQuery
还是一个抽象基类。
//4.eval函数
1).OrQuery::eval
{
QueryResult
OrQuery::eval(const TextQuery &text) const {
// 这里是直接的继承BinaryQuery的两个成员rhs,lhs。
// 然后进行动态调用
// 返回的是实际的QueryResult
auto right = rhs.eval(text), left = lhs.eval(text);
// 得到左侧的运算结果。
auto ret_lines =
make_share<set<line_no>>(left.begin(), left.end());
ret.lines->insert(right.begin(), right.end());
// 由于set的特性自动取得并集。
return QueryResult(rep(),ret_lines,left.get_file());
}
}
2).AndQuery::eval
- 使用了标准库算法
set_section
接受五个迭代器,前面四个表示两个迭代器范围,算法将两个范围中的重复部分插入到目的迭代器中,这里的目的迭代器是一个插入迭代器。
{
QueryResult
AndQuery::eval(const TextQuery &text) const {
// 返回两个QueryResult的对象。
// 分别表示两个结果。
auto left = lhs.eval(text),right = rhs.eval(text);
//保存公共的行号
auto ret_lines = make_shared<set<line_no>>();
// 使用标准库的算法。
set_intersection(left.begin(), left.end(),
right.begin(), right.end(),
inserter(*ret_lines,ret_lines->begin()));
return QueryResult(rep(), ret_lines, left.get_file());
}
}
3).NotQuery::eval
{
QueryResult
NotQuery::eval(const TextQuery &text) const {
auto result = query.eval(text);
auto ret_lines = make_shared<set<line_no>>();
auto beg = result.begin(), end = result.end();
// 对于出现的每一行,如果没有出现在result中,则添加到ret_lines中。
auto sz = result.get_file()->size();
// set中的按照升序排列。
for (size_t n = 0; n != sz; ++n) {
if(beg == end || *beg != n)
ret_lines->insert(n);
else if (beg != end) {
++beg;
}
}
return ResultQuery(rep(), ret_lines, query.get_file());
}
}
/10.小结
1).构造,赋值,拷贝派生类对象,先从基类部分开始。析构派生类对象,则从派生类对象开始。