C++ Pirmer第十五章⑥
C++ Primer
面向对象程序设计
文本查询程序再探
我们要拓展之前的文本查询程序,用来作为说明继承的最后一个例子。上一版程序中,我们实现的功能时查询在文件中某个指定单词的出现情况。我们将在本节拓展该程序使其支持更多更复杂的查询操作。在后面的例子中,我们将针对下面这个美丽的小故事展开查询:
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
... -
逻辑或查询,使用|运算符返回匹配两个条件中任意一个的行:
Executing Query for: (hair | Alice)
(hair | Alice) occurs 2 times
... -
逻辑与查询,使用&运算符返回匹配全部两个条件的行: Executing Query for: (hair & Alice)
(hair & Alice) occurs 1 times
...
我们还希望能够混合使用这些运算符,例如:
fiery & bird | wind 意思是这行要么fiery和bird都有,要么有wind
面向对象的解决方案
我们的第一反应肯定是,在原来那个类的基础上派生出这样一个查询类,okay,我们来试试,我们有一个窍门别忘了,就是假设类已经设计好了,我们来直接用用看,就能发现它设计得好不好,我们来考虑一下逻辑非查询:
比如我们要找~Alice,那我们肯定先以Alice为关键字查找,找到所有这些含有Alice的行之后,再有一个取反的操作(把整个文本中含有Alice的行去掉),同样的,逻辑与和逻辑或也是这样,都要有一个合并的操作:
既然这样的话,我们索性把逻辑与、或、非分别建类,这些类共享一个公共基类,通过这样的方式会让我们的类设计更简单,这些类只需要两个操作就好了:
- eval,接受一个TextQuery对象并返回一个QueryResult,eval函数使用给定的TextQuery对象查找与之匹配的行
- rep,返回基础查询的string表示形式,eval函数使用rep创建一个表示匹配结果的QueryText,输出运算符使用rep打印查询表达式
继承体系的设计非常复杂,这里只要记住一条准则:当我们让一个类公有地继承另一个类时,派生类与基类的关系应该是"Is A",”是一种“关系
抽象基类
这四种查询并不存在继承关系,它们在功能上应该是兄弟关系,它们都共享一个公共接口,所以我们应该定义一个抽象基类Query_base,表示它是整个查询继承体系的根节点
自然的,在Query_base中应该把eval和rep定义为纯虚函数,那四个类必须要覆盖这两个函数,因为逻辑与和逻辑或要包含两个运算对象,我们再给它们加一层,最后我们得到的设计结果就是下图了:
将层次关系隐藏于接口类中
我们来创建一下查询命令,下面这种方式就很容易让熟悉C++的同学使用:
Query q = Query("fiery") & Query("bird") | Query("wind");
我们来想想这里的Query是什么角色,其实啊,我们的用户是不知道我们有这个继承体系的,他们不需要知道,我们来定义一个名为Query的接口类,来隐藏整个继承体系:
Query类将保存一个Query_base指针,该指针绑定到Query_base的派生类对象上。Query类于Query_base类提供的操作时相同的:eval用于求查询结果,rep用于生成查询的string版本,Query还会定义一个重载的输出运算符用于显示查询,接下来的部分说得太精彩,于是你懂得,我又偷懒地给你们看原文了:
这里面的类还包括了咱们最初版那个查询的程序类TextQuery和QueryResult,如果忘了的话回头去看看
Query_base类和Query类
下面我们就要开始撸起袖子好好干了,首先定义Query_base类:
//抽象基类
class Query_base
{
firend class Query; //友元比派生更亲近哦,可以访问private
protected:
using line_no = TextQuery::line_no;
virtual ~Query_base() = default; //合成虚析构函数
private:
virtual QueryResult eval(const TextQuery&) const = 0; //纯虚函数,返回类型是QueryResult
virtual string rep() const = 0;
};
Query类
class Query
{
//这些运算符要访问接受shared_ptr的构造函数(私有的,后面private会定义)
firend Query operator~(const Query &);
firend Query operator|(const Query&, const Query&);
firend Query operator&(const Query&, const Query&);
public:
Query(const string&); //构造函数,构建一个新的WordQuery
QueryResult eval(const TExtQuery &t) const //接口函数用来调用对应的Query_base函数
{
return q->eval(t);
}
string rep() const {return q->rep();}
private:
Query(shared_ptr<Query_base> query) : q(query) {}
shared_ptr<Query_base> q;
};
Query还要一个输出运算符:
ostream& operator<<(ostream &os, const Query &query)
{
return os << query.rep();
}
当我们打印一个Query时,输出运算符调用Query类的rep
派生类
接下来我们来写派生类(有没有发现我们其实是自顶向下写的):
class WordQuery : public Query_base
{
friend class Query; //Query要使用WordQuery的构造函数,因为它保存了一个可能指向它的对象呀
//sadstory,朋友不是互相的
WordQuery(const string &s) : query_word(s) {} //构造函数
//继承的虚函数
QueryResult eval(const TextQuery &t) const
{
return t.query(query_word); //调用原来TextQuery的查找方法
}
string rep() const {return query_word;}
string query_word; //要查找的单词
};
有了这个类后,我们就可以定义接受string的Query构造函数了(仔细看下面的定义):
inline Query::Query(const string &s) : q(new WordQuery(s)) {}
NotQuery类及~运算符
~运算符生成一个NotQuery,其中保存着一个需要对其取反的Query(注意~运算符和NotQuery类的关系):
class NotQuery : public Query_base
{
friend Query operator~(const Query &); //友元函数
NotQuery(const Query &q) : query(q) {} //构造函数
//继承的纯虚函数
QueryResult eval(const TextQuery&) const; //先写声明,定义比较复杂,待会介绍
string rep() const {return "~(" + query.rep() + ")"} //明确查询优先级
Query query;
};
//~运算符动态分配一个新的NotQuery对象,
//return语句隐式地接受一个shared_ptr<Query_base>的Query构造函数(要看懂哦)
inline Query operator~(const Query &operand)
{
return shared_ptr<Query_base>(new NotQuery(operand));
}
BinaryQuery类
BinaryQuery类也是一个抽象基类,它保存操作两个运算对象的查询类型所需的数据:
class BinaryQuery : public Query_base
{
protected: //为了给派生类访问
BinaryQuery(const Query &l, const Query &r, string s) : lhs(l), rhs(r), opSym(s) {}
//抽象类:BinaryQuery不定义eval函数
string rep() const {return "(" + lhs.rep() + " " + opSym + " " + rhs,rep() + ")";}
Query lhs, rhs; //左侧和右侧运算对象
string opSym; //运算符的名字
};
AndQuery类、OrQuery类及相应的运算符
class AndQuery : public BinaryQuery
{
firend Query operator&(const Query&, const Query&);
AndQuery(const Query &left, const Query &right) : BinaryQuery(left, right, "&") {}
//覆盖了eval(是覆盖不是继承哦,因为它的直接基类没有这个函数)
QueryResult eval(const TextQuery&) const; //只是个声明
};
inline Query operator&(const Query &lhs, const Query &rhs)
{
return shared_ptr<Query_base>(new AndQuery(lhs, rhs));
}
class OrQuery : public BinaryQuery
{
firend Query operator|(const Query&, const Query&);
AndQuery(const Query &left, const Query &right) : BinaryQuery(left, right, "|") {}
//覆盖了eval(是覆盖不是继承哦,因为它的直接基类没有这个函数)
QueryResult eval(const TextQuery&) const; //只是个声明
};
inline Query operator|(const Query &lhs, const Query &rhs)
{
return shared_ptr<Query_base>(new OrQuery(lhs, rhs));
}
eval函数
可以看出eval函数就是我们这个查询系统的核心了,实现它我们就大功告成了
#C++工程师#