笔记:
15.1 OOP概述
面向对象程序设计的核心思想:数据抽象、继承、动态绑定。
当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定。(也称运行时绑定)
15.2 定义基类和派生类
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。
派生类的声明包含类名但是不包含它的派生列表。
当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。
如果表达式既不是引用类型也不是指针类型,则它的动态类型永远与静态类型一致。
15.4 抽象基类
在函数体的位置(即在声明语句的分号之前)书写=0将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处。
不能(直接)创建一个抽象基类的对象。
每个类各自控制其对象的初始化过程。
15.5 访问控制与继承
派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
在C++中,名字查找发生在类型检查之前。
15.7 构造函数与拷贝控制
虚析构函数将阻止合成移动操作。
课后习题:
练习15.1:什么是虚成员?
答:在类中被声明为virtual的成员,基类希望这种成员在派生类中重定义。除了构造函数外,任意非static成员都可以为虚成员。
练习15.2:protected 访问说明符与private 有何区别?
答:protected为受保护的访问标号,protected成员可以被该类的成员、友元和派生类成员(非友元)访问,而不可以被该类型的普通用户访问。而private成员只能被基类的成员和友元访问,派生类不能访问。
练习15.3:定义你自己的Quote 类和print_total 函数。
// 练习 15.3
#include <iostream>
#include <string>
using namespace std;
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; // 书籍的ISBN编号
protected:
double price = 0.0; //代表普通状态下不打折的价格
};
double print_total(ostream &os, const Quote &item, size_t n)
{
//根据传入item形参的对象类型调用Quote::net_price
//或者Bulk_quote::net_price
double ret = item.net_price;
os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: "
<< ret << endl;
return ret;
}
练习15.4:下面哪条声明语句是不正确的?请解释原因。
class Base { ... }
(a)class Derived : public Derived { ... };
(b)class Derived : public Base { .. };
(c)class Derived : public Base;
答:(a)错误,一个类不能派生它本身。
(b)正确。
(c)错误,类的声明不应该包括派生列表。
练习15.5:定义你自己的Bulk_quote 类。
// 练习 15.5
#include <iostream>
#include <string>
using namespace std;
class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const string &book, double p, size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) { }
//如果达到了够爱书籍的某个最低限量值,就可以享受折扣价格了
double net_price(size_t cnt) const override
{
if (cnt >= min_qty)
{
return cnt * (1 - discount) * price;
}
else
{
return cnt * price;
}
}
private:
size_t min_qty;
double discount;
};
练习15.6:将Quote 和Bulk_quote 的对象传给15.2.1 节(第529 页)练习中的print_total 函数, 检查该函数是否正确。
// 练习 15.6
#include <iostream>
#include <string>
using namespace std;
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; // 书籍的ISBN编号
protected:
double price = 0.0; //代表普通状态下不打折的价格
};
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;
}
class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const string &book, double p, size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) { }
//如果达到了够爱书籍的某个最低限量值,就可以享受折扣价格了
double net_price(size_t cnt) const override
{
if (cnt >= min_qty)
{
return cnt * (1 - discount) * price;
}
else
{
return cnt * price;
}
}
private:
size_t min_qty;
double discount;
};
int main()
{
Quote quote("倚天屠龙记", 25.0);
Bulk_quote bquote("倚天屠龙记", 25.0, 5, 0.2);
print_total(cout, quote, 3);
print_total(cout, bquote, 5);
system("pause");
return 0;
}
练习15.7:定义一个类使其实现一种数量受限的折扣策略, 具体策略是: 当购买书籍的数量不超过一个给定的限量时享受折扣,如果购买量一旦届过了限量,则超出的部分将以原价销售。
// 练习 15.7
class Limit_quote : public Quote
{
public:
Limit_quote() = default;
Limit_quote(const string &book, double price, size_t max_cnt, double discount) :
Quote(book, price), max_cnt(max_cnt), discount(discount) { }
double net_price(size_t cnt) const override
{
if (cnt <= max_cnt)
{
return cnt * (1 - discount) * price;
}
else
{
return max_cnt * (1 - discount) * price + (cnt - max_cnt) * price;
}
}
private:
size_t max_cnt;
double discount;
};
练习15.8:给出静态类型和动态类型的定义。
答:静态类型在编译时就已经确定了,它是变量声明时的类型或表达式生成的类型;而动态类型则是变量或表达式表示的内存中的对象的类型,动态类型直到运行时才能知道。如:Quote *pQuote = new Bulk_quote;,指针pQuote的静态类型是Quote,在编译时就已经确定了。但是它的动态类型是Bulk_quote,知道运行时才能知道它指向的是基类还是派生类。如果一个变量非指针也非引用,则它的静态类型和动态类型永远一致。但基类的指针或引用的静态类型可能与其动态类型不一致。
练习15.9:在什么情况下表达式的静态类型可能与动态类型不同?请给出三个静态类型与动态类型不同的例子。
基类的指针或引用的动态类型可能与静态类型不一致。
Bulk_quote bulk;
Quote *pQuote = &bulk;
Quote &rQuote = bulk;
//传递给item的如果是派生类对象,即是静态类型和动态类型不同的情况。
double print_total(ostream &os, const Quote &item, size_t n);
练习15.10:回忆我们在8.1 节(第279 页)进行的讨论,解释第284 页中将ifstream递给Sales_data 的read 函数的程序是如何工作的。
答:在要求使用基类的地方,可以使用派生类型的对象来代替,是静态类型和动态类型不同的典型例子。
练习15.11:为你的Quote 类体系添加一个名为debug 的虚函数,令其分别显示每个类的数据成员。
// 练习 15.11
#include <iostream>
#include <string>
using namespace std;
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 void debug()
{
cout << "bookNo = " << bookNo << " price = " << price << endl;
}
virtual ~Quote() = default; //对析构函数进行动态绑定
private:
string bookNo; // 书籍的ISBN编号
protected:
double price = 0.0; //代表普通状态下不打折的价格
};
class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const string &book = " ", double p = 0, size_t qty = 0, double disc = 0) :
Quote(book, p), min_qty(qty), discount(disc) { }
//如果达到了够爱书籍的某个最低限量值,就可以享受折扣价格了
double net_price(size_t cnt) const override
{
if (cnt >= min_qty)
{
return cnt * (1 - discount) * price;
}
else
{
return cnt * price;
}
}
virtual void debug()
{
Quote::debug(); //bookNo变量为private,所以不能直接访问bookNo
//只能调用基类的debug()函数来显示
cout << "min_qty = " << min_qty << " discount = " << discount << endl;
}
private:
size_t min_qty;
double discount;
};
练习15.12:有必要将一个成员函数同时声明成override 和final 吗? 为什么?
答:override:在C+=11新标准中我们可以使用override关键字来说明派生类中的虚函数。这么做的好处是在使得我们的意图更加清晰明确地告诉编译器我们想要覆盖掉已存在的虚函数。如果定义了一个虚函数与基类中的名字相同但是形参列表不同,在不使用override关键字的时候这种函数定义是合法的,在使用了override关键字之后这种行为是非法的,编译器会提示出错。
final:如果我们将某个函数定义成final,则不允许后续的派生类来覆盖这个函数,否则会报错。
因此同时将一个成员函数声明成override和final能够使我们的意图更加清晰。
练习15.13:给定下面的类,解释每个print 函数的机理:
class base
{
public:
string name() { return basename; }
virtual void print(ostream &os) { os << basename; }
private:
string basename;
};
class derived : public base
{
public:
void print(ostream &os) { print(os); os << " " << i; }
private:
int i;
};
在上述代码中存在问题吗?如果有,你该如何修改它?
// 练习 15.13
#include <iostream>
#include <string>
using namespace std;
class base
{
public:
base(string szNm) : basename(szNm) { }
string name() { return basename; }
virtual void print(ostream &os) { os << basename; }
private:
string basename;
};
class derived : public base
{
public:
derived(string szName, int iVal) : base(szName), mem(iVal) { }
void print(ostream &os) { base::print(os); os << " " << mem; }
private:
int mem;
};
int main()
{
base r1("机器学习");
r1.print(cout);
derived r2("机器学习", 10);
r2.print(cout);
system("pause");
return 0;
}
练习15.14:给定上一题中的类以及下面这些对象,说明在运行时调用哪个函数:
base bobj; base &bp1 = &bobj; base *br1 = bobj;
derived dobj; base *bp2 = &dobj; base &br2 = dobj;
(a) bobj.print(); (b) dobj.print(); (c) bp1->name();
(d) bp2->name(); (e) br1.print(); (f) br2.print();
(a) bobj.print();用的是基类的print函数。
(b) dobj.print();用的是派生类的print函数。
(c) bp1->name();用的是基类的name函数。
(d) bp2->name();用的是基类的name函数。
(e) br1.print();用的是基类的print函数。
(f) br2.print();用的是派生类的print函数。
练习15.15:定义你自己的Disc_quote 和Bulk_quote。
// 抽象基类
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 = 0; //折扣适用的购买量
double discount = 0.0; //表示折扣的小数值
};
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 cnt) const override
{
if (cnt >= quantity)
{
return cnt * (1 - discount) * price;
}
else
{
return cnt * price;
}
}
};
练习15.16:改写你在15. 2.2 节(第533 页)练习中编写的数量受限的折扣策略,令其继承Disc_quote 。
class Limited_quote : public Disc_quote
{
public:
Limited_quote() = default;
Limited_quote(const string &book, double price, size_t qty, double disc) :
Disc_quote(book, price, qty, disc) { }
double net_price(size_t cnt) const override
{
if (cnt <= quantity)
{
return cnt * (1 - discount) * price;
}
else
{
return quantity * (1 - discount) * price + (cnt - quantity) * price;
}
}
};
练习15.17:尝试定义一个Disc_quote 的对象, 看看编译器给出的错误信息是什么?
错误信息如下:
练习15.18:假设给定了第543 页和第544 页的类,同时己知每个对象的类型如注释所示,判断下面的哪些赋值语句是合法的。解释那些不合法的语句为什么不被允许:
Base *p = &d1; // d1的类型是Pub_Derv,合法
p = &d2; // d2的类型是Priv_Derv,非法
p = &d3; // d3的类型是Prot_Derv,非法
p = &dd1; // dd1的类型是Derived_from_Public,合法
p = &dd2; // dd2的类型是Derived_from_Private,非法
p = &dd3; // dd3的类型是Derived_from_Protected,非法
答:只有d1和dd1才能够赋值。这是因为:只有当派生类公有地继承基类时,用户代码才能使用派生类向基类的转换;也就是说,如果派生类继承基类的方式是受保护的或者私有的,则用户代码不能使用该转换。
在题中,只有d1和dd1类是公有地继承基类,故只有它们才能完成向基类的转换。
练习15.19:假设543 页和544 页的每个类都有如下形式的成员函数:
void memfcn(Base &b) { b = *this; }
对于每个类,分别判断上面的函数是否合法。
答:
Derived_from_Private:private Priv_Derv这个类的函数不合法。
原因如下:
1、无论派生类以什么方式继承基类,派生类的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和函数来说永远是可访问的。
2、如果派生类继承基类的方式是公有的或者受保护的,则派生类的成员和友元可以使用派生类向基类的类型转换;反之,如果派生类继承基类的方式是私有的,则不能使用。
练习15.20:编写代码检验你对前面两题的回答是否正确。
// 练习 15.20
#include <iostream>
using namespace std;
class Base
{
public:
void pub_mem();
protected:
int prot_mem;
private:
char priv_mem;
};
struct Pub_Derv : public Base
{
int f() { return prot_mem; }
void memfcn(Base &b)
{
b = *this;
cout << "Pub_Derv" << endl;
}
};
struct Priv_Derv : private Base
{
int f1() const { return prot_mem; }
void memfcn(Base &b)
{
b = *this;
cout << "Priv_Derv" << endl;
}
};
struct Prot_Derv : protected Base
{
int f2() { return prot_mem; }
void memfcn(Base &b)
{
b = *this;
cout << "Prot_Derv" << endl;
}
};
struct Derived_from_Public : public Pub_Derv
{
int use_base() { return prot_mem; }
void memfcn(Base &b)
{
b = *this;
cout << "Derived_from_Public" << endl;
}
};
struct Derived_from_Protected : protected Pub_Derv
{
int use_base() { return prot_mem; }
void memfcn(Base &b)
{
b = *this;
cout << "Derived_from_Protected" << endl;
}
};
int main(int argc, const char *argv[])
{
Pub_Derv d1;
Priv_Derv d2;
Prot_Derv d3;
Derived_from_Public dd1;
//Derived_from_Private dd2;
Derived_from_Protected dd3;
Base base;
Base *p = new Base;
p = &d1;
//p = &d2;
//p = &d3;
p = &dd1;
//p = &dd2;
//p = &dd3;
d1.memfcn(base);
d2.memfcn(base);
d3.memfcn(base);
dd1.memfcn(base);
//d2.memfcn(bade);
dd3.memfcn(base);
system("pause");
return 0;
}
练习15.21:从下面这些一般性抽象概念中任选一个(或者选一个你自己的〉,将其对应的一组类型组织成一个继承体系:
(a)图形文件格式(如gif、tiff、jpeg 、bmp )
(b)图形基元(如方格、圆、球、圆锥)
(c)C++语言中的类型(如类、函数、成员函数)
对(b)中的几何图元组织成一个继承层次:1)公共基类Figure,表示几何图元;2)类Rectangle、Circle、Sphere和Cone分别表示矩形、圆、球形和锥形等图元,这些类可以定义为Figure类的派生类。
练习15.22:对于你在上一题中选择的类,为其添加合适的虚函数及公有成员和受保护的成员。
class Figure
{
public:
Figure(double, double);
protected:
double xSize, ySize;
};
class Figure_2D : public Figure{
public:
Figure_2D(double, double);
virtual double area() = 0;
virtual double pcrimeter() = 0;
};
class Figure_3D : public Figure
{
public:
Figure_3D(double, double, double);
virtual double cubage() = 0;
protected:
double zSize;
};
class Rectangle : public Figure_2D
{
public:
Rectangle(double, double);
virtual double area();
virtual double pcrimeter();
};
class Circle : public Figure_2D
{
public:
Circle(double, double);
virtual double area();
virtual double pcrimeter();
};
class Sphere : public Figure_3D
{
public:
Sphere(double, double, double);
virtual double cubage();
};
class Cone : public Figure_3D
{
public:
Cone(double, double, double);
virtual double cubage();
};
练习15.23:假设第550 页的D1类需要覆盖它继承而来的 fcn 函数,你应该如何对其进行修改?如果你修改之后 fcn 匹配了Base 中的定义,则该节的那些调用语句将如何解析?
答:1.将D1类的fcn函数更改为int fcn()。
2.p2->fcn(42),这一条调用语句将会出错。
练习15.24:哪种类需要虚析构函数?虚析构函数必须执行什么样的操作?
答:作为基类使用的类应该具有虚析构函数,以保证在删除指向动态分配的对象的基类指针时根据指针实际指向的对象所属的类型运行适当的析构函数。
虚析构函数可以为空,即不执行任何操作。一般而言,析构函数的主要作用是清除本类中定义的数据成员。如果该类没有定义指针类成员,则使用合成版本即可;如果该类定义了指针成员,则一般需要自定义析构函数以对指针成员进行适当的清除。因此,如果有虚析构函数必须执行的操作,则就是清除本类中定义的数据成员的操作。
练习15.25:我们为什么为Disc_quote 定义一个默认构造函数?如果去除掉该构造函数的话会对Bulk_quote 的行为产生什么影响?
答:因为Disc_quote的默认构造函数会运行Quote的默认构造函数,而Quote默认构造函数会完成成员的初始化工作。
如果去掉该函数的话,Bulk_quote的默认构造函数无法完成Disc_quote的初始化工作。
练习15.26:定义Quote 和Bulk_quote 的拷贝控制成员,令其与合成的版本行为一致。为这些成员以及其他构造函数添加打印状态的语句,使得我们能够知道正在运行哪个程序。使用这些类编写程序,预测程序将创建和销毁l哪些对象。重复实验, 不断比较你的预测和实际输出结果是否相同,直到预测完全准确再结束。
// 练习 15.26
#include <iostream>
#include <string>
#include <ostream>
using namespace std;
class Quote
{
public:
Quote() = default;
Quote(const string &book = "", double sales_price = 0.0) :
bookNo(book), price(sales_price)
{
cout << "Quote constructor is running." << endl;
}
string isbn() const
{
return bookNo;
}
//返回给定数量的书籍的销售总额,派生类改写并使用不同的折扣计算方法
virtual double net_price(std::size_t n) const
{
return n * price;
}
virtual void debug()
{
cout << "bookNo = " << bookNo << "price = " << price << endl;
}
friend ostream &operator <<(ostream&, Quote&);
private:
string bookNo; //书籍的ISBN编号
protected:
double price = 0.0; //代表普通状态下不打折的价格
};
ostream & operator <<(ostream &os, Quote &)
{
os << "\tUsing operator << (ostream &, Quote &):" << endl;
return os;
}
class Bulk_quote :public Quote
{
public:
Bulk_quote(const string &book = "", double sales_price = 0.0, size_t qty = 0, double disc = 0.0) :
Quote(book, sales_price), min_qty(qty), discount(disc)
{
cout << "Bulk_constructor is running" << endl;
}
double net_price(size_t cnt) const
{
if (cnt > min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
~Bulk_quote()
{
cout << "Bulk_quote destructor is running" << endl;
}
private:
size_t min_qty;
double discount;
};
ostream &operator <<(ostream &os, Bulk_quote &bq)
{
os << "\tUsing operator <<(ostream &, Bulk_quote &)" << endl;
return os;
}
int main(int argc, char **argv)
{
Quote base("C++ primer", 128.0);
Bulk_quote bulk("Core Python Programming", 89, 5, 0.19);
cout << base << endl;
cout << bulk << endl;
return 0;
}
练习15.27:重新定义你的Bulk_quote 类, 令其继承构造函数。
// 练习 15.27
class Disc_quote : public Quote
{
public:
Disc_quote(const string bookNo = "", double sales_price = 0.0, size_t qty = 0, double disc = 0.0) :
Quote(book, sales_prices), quantity(qty), discount(disc) { }
double net_price(size_t cnt) const = 0;
protected:
size_t quantity;
double discount;
};
class Bulk_uote : public Disc_quote
{
public:
using Disc_quote::Disc_quote;
double net_price(size_t cnt) const
{
if (cnt > quantity)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
};
练习15.28:定义一个存放Quote 对象的vector,将Bulk_quote 对象传入其中。计算vector 中所有元素总的net_price。
// 练习 15.28
#include <iostream>
#include <string>
#include <vector>
#include "Discount.h"
using namespace std;
int main(int argc, char *argv[])
{
vector<Quote> itemVec;
for (size_t i = 0; i != 10; ++i)
{
Bulk_quote item("C++ Primer", 6, 5, 0.5);
itemVec.push_back(item);
}
double sum = 0;
for (vector<Quote>::iterator iter = itemVec.begin(); iter != itemVec.end(); ++iter)
{
sum += iter->net_price(10); //调用Quote::net_price
}
cout << sum << endl; //输出600
system("pause");
return 0;
}
练习15.29:再运行一次你的程序, 这次传入Quote 对象的shared_ptr 。如果这次计算出的总额与之前的程序不一致,解释为什么;如果一致,也请说明原因。
// 练习 15.29
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include "Discount.h"
using namespace std;
int main(int argc, char *argv[])
{
vector<shared_ptr<Quote>> itemVec;
for (size_t i = 0; i != 10; ++i)
{
itemVec.push_back(make_shared<Bulk_quote>("C++ Primer", 6, 5, 0.5));
}
double sum = 0;
for (vector<shared_ptr<Quote>>::iterator iter = itemVec.begin(); iter != itemVec.end(); ++iter)
{
sum += (*iter)->net_price(10); //调用Quote::net_price
}
cout << sum << endl; //输出300
system("pause");
return 0;
}
练习15.30:编写你自己的Basket 类,用它计算上一个练习中交易记录的总价格。
class Basket
{
public:
void add_item(const shared_ptr<Quote> &sales)
{
items.insert(sales);
}
double total_receipt(std::ostream&) const; // 打印每本书的总价和购物篮中所有书的总价
private:
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 };
};
double Basket::total_receipt(std::ostream &os) const
{
double sum = 0.0;
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;
}
练习15.31:已知sl 、s2 、s3 和s4 都是string,判断下面的表达式分别创建了什么样的对象:
(a)Query(s1) | Query(s2) & ~Query(s3);
(b)Query(s1) | (Query(s2) & ~Query(s3));
(c)(Query(s1) & (Query(s2)) | (Query(s3) & Query(s4)));
答:(a)共创建12个对象:6个Query_base对象以及其相关联的句柄。6个Query_base对象分别是3个WordQuery对象,1个NotQuery对象,1个AndQuery对象,1个OrQuery对象。
(b)与(a)相同。
(c)共创建14个对象:7个Query_base对象以及其相关联的句柄。7个Query_base对象分别是4个WordQuery对象,2个AndQuery对象,1个OrQuery对象。
练习15.32:当一个Query 类型的对象被拷贝、移动、赋值或销毁时,将分别发生什么?
答:Query类未定义自己的拷贝/移动控制成员,当进行这些操作时,执行默认语义。而其唯一的数据成员是Query_base的shared_ptr,因此,当拷贝、移动、赋值或销毁一个Query对象时,会调用shared_ptr的对应控制成员,从而实现多个Query对象正确共享一个Query_base。而shared_ptr的控制成员调用Query_base的控制成员时,由于指向的可能是Query_base的派生类对象,因此可能在类层次中进行相应的拷贝移动操作,调用Query_base的派生类的相应控制成员。
练习15.33:当一个Query_base 类型的对象被拷贝、移动、赋值或销毁时, 将分别发生什么?
答:Query_base是一个虚基类,不允许直接声明其对象。
当其派生类对象进行这些操作时,会调用Query_base的相应控制成员。而Query_base没有定义自己的拷贝移动控制成员,实际上它没有任何数据成员,无须定义这些操作,因此进行这些操作时,执行默认语义,什么也不会发生。
练习15.34:针对图15.3(第565 页)构建的表达式:
(a)列举出在处理表达式的过程中执行的所有构造函数。
(b)列举出cout<<q 所调用的rep。
(c)列举出q.eval()所调用的eval。
答:(a)处理表达式Query("fiery")&Query("bird")|Query("wind")所执行的构造函数如下:
WordQuery(std::string&)
Query(const std::string&)
WordQuery(std::string&)
Query(const std::string&)
WordQuery(std::string&)
Query(const std::string&)
BinaryQuery(Query, Query, std::string)
AndQuery(Query, Query)
BinaryQuery(Query, Query, std::string)
Query(std:;shared_ptr<Query_base> query)
BinaryQuery(Query, Query, std::string)
OrQuery(Query, Query)
Query(std::shared_ptr<Query_base> query)
(b)执行cout<<q各个类的rep函数的调用次序为:
BinaryQuery、Query、WordQuery、Query、BinaryQuery、Query、WordQuery、Query、WordQuery、BinaryQuery、Query、WordQuery、Query、WordQuery、BinaryQuery、Query、WordQuery、Query、BinaryQuery、Query、WordQuery、Query、WordQuery、Query、BinaryQuery、Query、WordQuery、Query、BinaryQuery、Query、WordQuery、Query、WordQuery
(c)计算q.eval时所调用的eval函数如下:
Query类的eval,OrQuery类的eval,AndQuery类的eval,WordQuery类的eval
练习15.35:实现Query 类和Query_base 类,其中需要定义rep 而无须定义eval。
class Query
{
friend Query operator~(const Query &);
friend Query operator|(const Query&, const Query&);
friend Query operator&(const Query&, const Query&);
public:
Query(const std::string&);
QueryResult eval(const TexQuery &t) const { return q->eval(); }
std::string rep() const { return q->rep(); }
private:
Query(std::shared_ptr<Query_base> query) : q(query) { }
std::shared_ptr<Query_base> q;
};
class Query_base
{
friend class Query;
protected:
using line_no = TextQuery::line_no;
virtual ~Query_base() = default;
private:
virtual QueryResult eval(const TexQuery&) const = 0;
virtual std::string rep() const = 0;
};
练习15.36:在构造函数和rep 成员中添加打印语句,运行你的代码以检验你对本节第一个练习中( a )、( b )两小题的回答是否正确。
答:验证即可。
练习15.37:如果在派生类中含有shared_ptr < Query_base >类型的成员而非Query类型的成员, 则你的类需要做出怎样的改变?
答:书中的实现方式是用Query类封装了Query_base指针,管理实际查询处理用到的不同Query类型对象。如果不使用Query类,则涉及使用Query类型的地方,都要改成Query_base指针。如创建单个词查询时,就必须创建WordQuery类而不是Query对象。几个重载的布尔运算符也不能再针对Query对象,而需针对Query_base指针,从而复杂的查询请求无法写成目前的简单形式,而需逐个运算完成,将结果赋予Query_base指针,然后再进行下一步运算。资源管理方面也需要重新设计。因此,当前的设计仍是最佳方式。
练习15.38:下面的声明合法吗?如果不合法, 请解释原因;如果合法,请指出该声明的含义。
BinaryQuery a = Query("fiery") & Query("bird");
AndQuery b = Query("fiery") & Query("bird");
OrQuery c = Query("fiery") & Query("bird");
第一条声明不合法,因为BinaryQuery中的eval是纯虚函数。
第二条声明不合法,不能将Query转换为AndQuery。
第三条声明不合法,不能将Query转换为OrQuery。
练习15.39:实现Query 类和Query_base 类,求图15.3 (第5 6 5 页)中表达式的值并打印相关信息, 验证你的程序是否正确。
#include "Query.h"
#include <iostream>
#include <algorithm>
using namespace std;
QueryResult OrQuery::eval(const TextQuery& text) const
{
// call Query::eval() --> Query_base::eval() --> QueryResult::eval()
QueryResult right = rhs.eval(text), left = lhs.eval(text);
auto ret_line = make_shared<set<line_no>>(left.begin(), left.end());
ret_line->insert(right.begin(), right.end());
return QueryResult(rep(), ret_line, left.get_file());
}
QueryResult AndQuery::eval(const TextQuery& text) const
{
QueryResult right = rhs.eval(text), left = lhs.eval(text);
// auto ret_line = make_shared<set<line_no>> (left.begin(), left.end());
shared_ptr<std::set<line_no>> ret_lines =
std::make_shared<std::set<line_no>>(left.begin(), left.end());
set_intersection(left.begin(), left.end(), right.begin(), right.end(), inserter(*ret_lines, ret_lines->begin()));
return QueryResult(rep(), ret_lines, left.get_file());
}
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();
auto sz = result.get_file().size();
for (size_t i = 0; i < sz; ++ i)
{
if (beg == end || *beg != i)
{
ret_lines->insert(i);
}
else if (beg != end)
{
++ beg;
}
}
return QueryResult(rep(), ret_lines, result.get_file());
}
int main()
{
Query q = Query("firey") & Query("bird") | Query("wind");
cout << q;
}
// 打印的结果 ((firey & bird) | wind)
练习15.40:在OrQuery 的eval 函数中,如果rhs 成员返回的是空集将发生什么?如果lhs 是空集呢?如果lhs 和rhs 都是空集又将发生什么?
答:OrQuery的eval从lhs和rhs获取范围来构造set,而set的构造和插入操作可以正确处理空范围,因此无论lhs和rhs的结果是否为空集,eval都能得到正确结果。
练习15.41:重新实现你的类,这次使用指向Query_base 的内置指针而非shared_ptr 。请注意,做出上述改动后你的类将不能再使用合成的拷贝控制成员。
class Query
{
public:
Query(const std::string&);
Query(const Query& query) : q(query.q), uc(query.uc) {++*uc;}
Query& operator=(const Query& query);
~Query();
private:
Query(Query_base* query) : q(query), uc(new int(l)) { }
Query_base* q;
int* uc;
};
inline Query::Query(const std::string &s) : q(new WordQuery(s)), uc(new int(l)) { }
inline Query::~Query()
{
if(--*us == 0)
{
delete q;
delete uc;
}
}
inline
Query& Query::operator=(const Query& query)
{
++*query.uc;
if (--*us == 0)
{
delete q;
delete uc;
}
q = query.q;
uc = query.uc;
return *this;
}
inline Query operator&(const Query &lhs, const Query &rhs)
{
return new AndQuery(lhs, rhs);
}
inline Query operator|(const Query &lhs, const Query &rhs)
{
return new OrQuery(lhs, rhs);
}
inline Query operator~(const Query &operand)
{
return new NotQuery(operand);
}
练习15.42:从下面的儿种改进中选择一种,设计并实现它:
(a)按句子查询并打印单词,而不再是按行打印。
(b)引入一个历史系统,用户可以被编号查阅之前的某个查询,并可以在其中增加内容或者将其与其他查询组合。
(c)允许用户对结果做出限制,比如I从给定范围的行中挑出匹配的进行显示。
//a
TextQuery::TextQuery(ifstream &is) : file(new vector<string>)
{
char ws[] = {'\t', '\r', '\v', '\f', '\n'};
char eos[] = {'?', '.', '!'};
set<char> whitespace(ws, ws+5);
set<char> endOfSentence(eos, eos+3);
string sentence;
char ch;
while(is.get(ch))
{
if(!whitespace.count(ch))
sentence+=ch;
if(endOfSentence.count(ch))
{
file->push_back(sentence);
int n = file->size() -1;
istringstream is(sentence);
string word;
while(is >> word)
{
auto &lines = wm[word];
if (!lines)
lines.reset(new set<line_no>);
lines->insert(n);
}
sentence.assign("");
}
}
}
//b
bool get_word(string &s1)
{
cout << "enter a word to search for, or q to quit, or h to history: ";
cin >> s1;
if (!cin || s1 == "q") return false;
else return true;
}
int main(int argc, char **argv)
{
TextQuery file = get_file(argc, argv);
vector<array<string, 3>> h;
while(true)
{
string sought1, sought2, sought3;
if(!get_word(sought)) break;
if(sought1 != "n")
{
cout << "\nenter second and third words: ";
cin >> sought2 >> sought3;
Query q = Query(sought1) &Query(sought2) | Query(sought3);
h.push_back({sought1, sought2, sought3});
cout << "\nRxcuting Query for: " << q <<endl;
const auto results = q.eval;
print(cout, results);
}
else
{
cout << "\nenter Query no.: ";
int i;
cin >> i;
if (i<1 ||i > h.size())
cout<< "\nBad Query no." << endl;
else
{
Query q = Query(h[i-1][0]) & Query(h[i-1][1]) | Query(h[i-1][2]);
cout << "\nExcuting Query for: " << q <<endl;
const auto results = q.eval(file);
print(cout, results);
}
}
}
return 0;
}
//c
ostream &print(ostream &os, const QueryResult &qr, int beg, int end)
{
os << qr.sought<< " occurs "<<qr.lines->size() << " " << make_plural(qr.lines->size(), "time", "s") <<endl;
for(auto num : *qr.lines)
if(num +1 >=beg && num + 1 <= end)
os << "\t(line " << num + 1 << ")" << *(qr.file->begin() + num) <<endl;
return os;
}