【C++ Primer】第15章 面向对象程序设计 (3)


Part III: Tools for Class Authors
Chapter 15. Object-Oriented Programming


15.8 容器与继承

当使用容器存储继承体系中的对象时,通常必须间接地存储这些对象。不能直接地把继承相关类型的对象放入容器中,因为无法定义一个容纳不同类型元素的容器。

例如,想要定义一个 vector 容纳几本客户想要购买的书籍。
不能使用容纳 Bulk_quote 对象的 vector。因为不能将 Quote 对象转换为 Bulk_quote。
也不能使用容纳 Quote 类型对象的 vector。如果将 Bulk_quote 对象放入容器中,那么这些对象不再是 Bulk_quote 对象了。

vector<Quote> basket;
basket.push_back(Quote("0-201-82470-1", 50));
// ok, but copies only the Quote part of the object into basket
basket.push_back(Bulk_quote("0-201-54848-8", 50, 10, .25));
// calls version defined by Quote, prints 750, i.e., 15 * $50
cout << basket.back().net_price(15) << endl;

警告:因为当派生类对象赋值给基类对象时,派生类部分会被忽略,所以容器与继承相关的类型不能很好地混合使用。

在容器中放置(智能)指针,而不是对象

当需要一个容器来保存通过继承关联的对象时,通常会定义该容器来保存指向基类的指针(最好是智能指针)。这些指针指向的对象的动态类型可能是基类类型或从该基类派生的类型:

vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(make_shared<Bulk_quote>("0-201-54848-8", 50, 10, .25));
// calls the version defined by Quote; prints 562.5, i.e., 15 * $50 less the discount
cout << basket.back()->net_price(15) << endl;

可以将指向派生类型的普通指针转换为指向基类的指针,也可以将指向派生类型的智能指针转换为指向基类类型的智能指针。

编写 Basket 类

C++的面向对象编程的奇异之处之一是,无法直接使用对象来支持它。反而必须使用指针和引用。因为指针会增加程序的复杂性,所以常常定义辅助类来管理这种复杂情况。

class Basket {
public:
	// Basket uses synthesized default constructor and copy-control members
	void add_item(const std::shared_ptr<Quote> &sale) { items.insert(sale); }
	// prints the total price for each book and the overall total for all items in the basket
	double total_receipt(std::ostream&) const;
private:
	// function to compare shared_ptrs needed by the multiset member
	static bool compare(const std::shared_ptr<Quote> &lhs, const std::shared_ptr<Quote> &rhs) { return lhs->isbn() < rhs->isbn(); }
	// multiset to hold multiple quotes, ordered by the compare member
	std::multiset<std::shared_ptr<Quote>, decltype(compare)*> items{compare};
};

定义 Basket 成员

double Basket::total_receipt(ostream &os) const {
	double sum = 0.0;   // holds the running total
	// iter refers to the first element in a batch of elements with the same ISBN
	// upper_bound returns an iterator to the element just past the end of that batch
	for (auto iter = items.cbegin(); iter != items.cend(); iter = items.upper_bound(*iter)) {
		// we know there's at least one element with this key in the Basket
		// print the line item for this book
		sum += print_total(os, **iter, items.count(*iter));
	}
	os << "Total Sale: " << sum << endl; // print the final overall total
	return sum;
}

在 for 循环的“递增”表达式中,通过调用 upper_bound 跳过匹配当前关键字的所有元素,这样可令 iter 指向下一个关键字。

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>("345", 45, 3, .15));

重新定义 add_item,使它接受一个 Quote 对象而不是 shared_ptr。
add_item 的新版本会处理内存分配,这样用户就不需要处理了。

定义两个版本,一个复制给定对象,另一个使用移动操作。

void add_item(const Quote& sale);  // copy the given object
void add_item(Quote&& sale);       // move the given object

唯一的问题是 add_item 不知道分配什么类型。

模拟虚复制

要解决上面的问题,可以在 Quote 类中添加一虚函数,该函数分配自己本身的副本。

class Quote {
public:
	// virtual function to return a dynamically allocated copy of itself
	// these members use reference qualifiers; see §13.6
	virtual Quote* clone() const & {return new Quote(*this);}
	virtual Quote* clone() && {return new Quote(std::move(*this));}
	// other members as before
};
class Bulk_quote : public Quote {
	Bulk_quote* clone() const & {return new Bulk_quote(*this);}
	Bulk_quote* clone() && {return new Bulk_quote(std::move(*this));}
	// other members as before
};

因为有 add_item 的复制和移动两个版本,所以定义了 clone 的左值和右值两个版本。

使用 clone 来写新版本的 add_item:

class Basket {
public:
	void add_item(const Quote& sale) // copy the given object
		{ items.insert(std::shared_ptr<Quote>(sale.clone())); }
	void add_item(Quote&& sale)      // move the given object
		{ items.insert(std::shared_ptr<Quote>(std::move(sale).clone())); }
	// other members as before
};

注意在右值版本中,尽管 sale 的类型是右值引用类型,但 sale(像其他变量那样)是一个左值。因此,调用 move 将一个右值引用绑定到 sale。


15.9 文本查询程序再探

扩展第12章中的文本查询应用程序,作为继承的例子。原来的类可以在文件中查找给定单词的出现情况。扩展系统支持更多复杂查询。

在下面小故事上允许查询程序:

Alice Emma has long flowing red hair. 
Her Daddy says when the wind blows 
through her hair, it looks almost alive, 
like a fiery bird in flight. 
A beautiful fiery bird, he tells her, 
magical but untamed. 
"Daddy, shush, there is no such thing," 
she tells him, at the same time wanting 
him to tell her more. 
Shyly, she asks, "I mean, Daddy, is there?"

新系统应支持以下查询:

  • 单词查询,查找与给定 string 匹配的所有行
Executing Query for: Daddy
Daddy occurs 3 times
(line 2) Her Daddy says when the wind blows
(line 7) "Daddy, shush, there is no such thing,"
(line 10) Shyly, she asks, "I mean, Daddy, is there?" 
  • 逻辑非查询,使用 ~ 运算符,生成与查询不匹配的所有行
Executing Query for: ~(Alice)
~(Alice) occurs 9 times
(line 2) Her Daddy says when the wind blows
(line 3) through her hair, it looks almost alive,
(line 4) like a fiery bird in flight.
... 
  • 逻辑或查询,使用 | 运算符,返回与任意一个匹配的行
Executing Query for: (hair | Alice)
(hair | Alice) occurs 2 times
(line 1) Alice Emma has long flowing red hair.
(line 3) through her hair, it looks almost alive,
  • 逻辑与查询,使用 & 运算符,返回与两个查询都匹配的行
Executing query for: (hair & Alice)
(hair & Alice) occurs 1 time
(line 1) Alice Emma has long flowing red hair.

此外,希望能够将这些操作结合起来,例如,fiery & bird | wind
使用通用的C++优先规则对类似上面的复杂表达式求值。

Executing Query for: ((fiery & bird) | wind)
((fiery & bird) | wind) occurs 3 times
(line 2) Her Daddy says when the wind blows
(line 4) like a fiery bird in flight.
(line 5) A beautiful fiery bird, he tells her,

面向对象的解决方案

使用 TextQuery 类表示单词查询,再从该类派生成其他查询,这种设计是有缺陷的。

建议将这 4 个不同的查询定义成独立的类,它们共享一个基类:

WordQuery // Daddy
NotQuery  // ~Alice
OrQuery   // hair | Alice
AndQuery  // hair & Alice

这些类只有两个操作:

  • eval,接受一个 TextQuery 对象,返回一个 QueryResult。eval 函数将使用给定的 TextQuery 对象找出查询匹配的行。
  • rep,返回基础查询的 string 表示形式。eval 将使用此函数来创建一个 QueryResult,表示匹配结果;输出运算符将使用 rep 来打印查询表达式。

抽象基类

四种查询关系之间不存在继承关系,但它们共享同一个接口,因此定义一个抽象基类表示这个接口。将这个基类命名为 Query_base。

Query_base 类将 eval 和 rep 定义成纯虚函数。表示特定查询的类必须覆盖这些函数。

Query_base 继承体系如下:

Query_base
WordQuery
NotQuery
BinaryQuery
AndQuery
OrQuery

关键概念:继承 VS 组合

有一条重要的基础设计指南,程序员应该要熟悉它。

当定义一个类,它共有地继承自另一个类时,派生类应该反映与基类的“Is A”关系,即派生类“是”一种基类。如,Bulk_quote 是一种报价 (quote),只是价格策略不同。

类型之间的另一种常见关系是“Has A”关系,即某个类型“具有”另一种类型。与这种关系相关的类型蕴含着成员关系。如,书店类具有价格和ISBN。

在接口中隐藏层次关系

为了运行程序,需要能够创建查询。要想生成上面描述的的复合查询,编写代码如下:

Query q = Query("fiery") & Query("bird") | Query("wind"); 

用户级代码不会直接使用继承类。反而,定义一个名为 Query 的接口类,隐藏层次关系。
Query 类存储一个指向 Query_base 的指针,这个指针绑定 Query_base 派生类的对象。Query 类提供与 Query_base 相同的操作。

用户只能间接地通过 Query 对象的操作,创建和处理 Query_base 对象。
在 Query 对象上定义 3 个重载运算符,与一个接受 string 的构造函数。这些函数动态分配一个 Query_base 派生类的对象:

  • & 运算符生成一个 Query,绑定到新的 AndQuery。
  • | 运算符生成一个 Query,绑定到新的 OrQuery。
  • ~ 运算符生成一个 Query,绑定到新的 NotQuery。
  • 接受一个 string 的 Query 构造函数生成新的 WordQuery。

下图是由 Query 表达式 q 创建的对象示意图,每个方格表示一个 Query 对象:

Query
OrQuery
AndQuery
WordQuery (wind)
WordQuery (fiery)
WordQuery (bird)

理解这些类如何工作

查询程序设计概述:

  • 查询程序接口类与操作
类/操作说明
TextQuery类,读取给定文件并构建查找图。该类有一个 query 操作,接受一个 string 实参;返回一个 QueryResult,表示该 string 出现的行。
QueryResult类,存储 query 操作的结果。
Query接口类,指向 Query_base 派生类对象。
Query(q)将 Query q 绑定到一个保存 string s 的新的 WordQuery。
q1 & q2返回一个 Query,绑定到一个存储 q1 和 q2 的新的 AndQuery。
q1 | q2返回一个 Query,绑定到一个存储 q1 和 q2 的新的 OrQuery。
~q返回一个 Query,绑定到一个存储 q 的新的 NotQuery。
  • 查询程序实现类
说明
Query_base查询类的抽象基类。
WordQueryQuery_base 的派生类,查找给定单词。
NotQueryQuery_base 的派生类,表示其 Query 对象没有出现的行的集合。
BinaryQuery派生自 Query_base 的抽象基类,表示有两个 Query 运算对象的查询。
OrQueryBinaryQuery 的派生类,返回它的两个运算对象出现的行号的并集。
AndQueryBinaryQuery 的派生类,返回它的两个运算对象出现的行号的交集。

Query_base 与 Query 类

// abstract class acts as a base class for concrete query types; all members are private
class Query_base {
	friend class Query;
protected:
	using line_no = TextQuery::line_no; // used in the eval functions
	virtual ~Query_base() = default;
private:
	// eval returns the QueryResult that matches this Query
	virtual QueryResult eval(const TextQuery&) const = 0;
	// rep is a string representation of the query
	virtual std::string rep() const = 0;
};

Query 类

为了支持 &、| 和 ~ 操作符,Query 需要一个构造函数,接受指向 Query_base 的 shared_ptr 并存储其给定的指针。将这个构造函数定义成 private,因为不想普通用户定义 Query_base 对象。

// interface class to manage the Query_base inheritance hierarchy
class Query {
	// these operators need access to the shared_ptr constructor
	friend Query operator~(const Query &);
	friend Query operator|(const Query&, const Query&);
	friend Query operator&(const Query&, const Query&);
public:
	Query(const std::string&);  // builds a new WordQuery
	// interface functions: call the corresponding Query_base operations
	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 makes a virtual call through its Query_base pointer to rep()
	return os << query.rep();
}
Query andq = Query(sought1) & Query(sought2);
cout << andq << endl;

派生类

在 Query_base 派生类中,运算对象可以是 Query_base 派生的具体类中任意一个对象。
为了支持这样的灵活性,运算对象必须存储成指向 Query_base 的对象。这样可以将指针绑定到任一个需要的具体类。

实际上,这些类不存储 Query_base 指针,而是使用 Query 对象。使用接口类可以简化用户代码。

WordQuery 类

class WordQuery: public Query_base {
	friend class Query; // Query uses the WordQuery constructor
	WordQuery(const std::string &s): query_word(s) { }
	// concrete class: WordQuery defines all inherited pure virtual functions
	QueryResult eval(const TextQuery &t) const { return t.query(query_word); }
	std::string rep() const { return query_word; }
	std::string query_word;    // word for which to search
};

定义了 WordQuery 类之后,就可以定义接受 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) { }
	// concrete class: NotQuery defines all inherited pure virtual functions
	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));
}

上面重载运算符函数中的 return 语句(隐式地)使用接受 shared_ptr<Query_base> 的构造函数。即,该 return 语句等价于

// allocate a new NotQuery object
// bind the resulting NotQuery pointer to a shared_ptr<Query_base>
shared_ptr<Query_base> tmp(new NotQuery(expr));
return Query(tmp); // use the Query constructor that takes a shared_ptr 

BinaryQuery 类

class BinaryQuery: public Query_base {
protected:
	BinaryQuery(const Query &l, const Query &r, std::string s): lhs(l), rhs(r), opSym(s) { }
	// abstract class: BinaryQuery doesn't define eval
	std::string rep() const { return "(" + lhs.rep() + " " + opSym + " " + rhs.rep() + ")"; }
	Query lhs, rhs;    // right- and left-hand operands
	std::string opSym; // name of the operator
};

AndQuery 类与 OrQuery 类及其相关操作

class AndQuery: public BinaryQuery {
	friend Query operator& (const Query&, const Query&);
	AndQuery(const Query &left, const Query &right): BinaryQuery(left, right, "&") { }
	// concrete class: AndQuery inherits rep and defines the remaining pure virtual
	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, "|") { }
	QueryResult eval(const TextQuery&) const;
};

inline Query operator|(const Query &lhs, const Query &rhs) {
	return std::shared_ptr<Query_base>(new OrQuery(lhs, rhs));
}

eval 函数

假定 QueryResult 有 begin 与 end 成员,允许在 QueryResult 保存的行号 set 上迭代。
假定 QueryResult 有一个名为 get_file 的成员,返回一个 shared_ptr,指向查询运行的底层文件。

OrQuery::eval

// returns the union of its operands' result sets
QueryResult OrQuery::eval(const TextQuery& text) const {
	// virtual calls through the Query members, lhs and rhs
	// the calls to eval return the QueryResult for each operand
	auto right = rhs.eval(text), left = lhs.eval(text);
	// copy the line numbers from the left-hand operand into the result set
	auto ret_lines = make_shared<set<line_no>>(left.begin(), left.end());
	// insert lines from the right-hand operand
	ret_lines->insert(right.begin(), right.end());
	// return the new QueryResult representing the union of lhs and rhs
	return QueryResult(rep(), ret_lines, left.get_file());
}

AndQuery::eval

// returns the intersection of its operands' result sets
QueryResult AndQuery::eval(const TextQuery& text) const {
	// virtual calls through the Query operands to get result sets for the operands
	auto left = lhs.eval(text), right = rhs.eval(text);
	// set to hold the intersection of left and right
	auto ret_lines = make_shared<set<line_no>>();
	// writes the intersection of two ranges to a destination iterator
	// destination iterator in this call adds elements to 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

// returns the lines not in its operand's result set
QueryResult NotQuery::eval(const TextQuery& text) const {
	// virtual call to eval through the Query operand
	auto result = query.eval(text);
	// start out with an empty result set
	auto ret_lines = make_shared<set<line_no>>();
	// we have to iterate through the lines on which our operand appears
	auto beg = result.begin(), end = result.end();
	// for each line in the input file, if that line is not in result,
	// add that line number to ret_lines
	auto sz = result.get_file()->size();
	for (size_t n = 0; n != sz; ++n) {
		// if we haven't processed all the lines in result
		// check whether this line is present
		if (beg == end || *beg != n)
			ret_lines->insert(n);  // if not in result, add this line
		else if (beg != end)
			++beg; // otherwise get the next line number in result if there is one
	}
	return QueryResult(rep(), ret_lines, result.get_file());
}

【C++ primer】目录

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值