C++ Primer笔记(十五)

本文详细探讨了C++中的面向对象编程概念,包括继承、虚函数、抽象基类和类型转换。重点讲解了基类与派生类的定义、虚函数的作用以及动态绑定的概念。此外,还讨论了访问控制在继承中的应用,以及构造函数和拷贝控制在继承中的行为。文章通过具体的例子解释了如何在实际编程中应用这些概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

面向对象程序设计

OOP

核心思想是抽象、继承和动态绑定,

  1. 通过继承联系在一起类构成一种层次关系,在层次关系的根部有一个基类,其他继承这个类而来的类被称为派生类,对于某些函数,基类希望它的派生类各自定义适合自己的版本,基类就将这些函数声明为虚函数。派生类必须通过类派生列表,指出是从哪个基类派生而来的,派生类在内部对其所有重新定义的虚函数进行声明,前面加virtual或者后面加override(override可以让编译器检查虚函数格式是否与基类相同)
class Bulk_quote : public Quote
{
public: 
	double new_price(std::size_t) const override;
}
  1. 可以通过动态绑定用一段代码处理基类和派生类的对象
double print_total(ostream &os, const Quote &item, size_t n)
{
	double ret = item.net_orice(n);
	os<<"ISBN:"<<item.isbn()<<"#sold:"<<n<<"total due:">>ret>>endl;
	return ret;
}

实际传入print_total的类型不同,可以执行不同的版本,但不需进行函数重载

定义基类和派生类

定义基类

class Quote
{
public:
	Quote() = default;
	Quote(const std::string &book, double sales_price):bookNo(book), price(sales_price){}
	std::string isbn() const {return bookNo;}
	virtual double new_price(std::size_t n) const {return n*price;}
	virtual ~Quote() = default;
private:
	std::string bookNo;
protected:
	double price = 0.0;
}

基类通常都应当定义一个虚析构函数

  1. 派生类可以继承其基类的成员,但遇到如net_price这样与类型相关的操作时必须对其重新定义,即提供自己的新定义以覆盖从基类继承而来的旧定义。基类通过在其成员函数的声明前加virtual使该函数执行动态绑定,构造函数之外的非静态函数都可以是虚函数,virtual只能出现在类内部声明语句之前而不能用于类外部函数定义。成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。
  2. 用protected来允许派生类访问而禁止其他用户访问, 常常将需要访问的基类成员设置为protected,以供派生类访问。

定义派生类

派生类需要使用类派生列表来指出它是从哪个基类继承而来的,每个基类前面可以有三种访问说明符中的一个,

class Bulk_quote : public: Quote
{
public:
	Bulk_quote() = default;
	Bulk_quote(const std::strning&, double, std::size_t, double);
	double net_price(std::size_t) const override;
private:
	std::size_t min_qty = 0;
	double discount = 0.0;
}

派生类需要将继承来的成员函数需要覆盖的进行重新声明。如果一个派生是共有的,则基类的公有成员也是派生类接口的组成部分,也能将公有类型的对象绑定到基类的引用或者指针上。

  1. 派生类并不是总会重新定义虚函数,如果没有,则虚函数类似于其他的普通成员
  2. 一个派生类对象包括自己定义的成员的子对象,和继承的基类对应的子对象,因此一方面可以将派生类对象当成基类对象使用,另一方面也能将基类的指针或引用绑定到派生类对象上的基类部分
Quote item;
Bulk_quote bulk;
Quote *p = &item//p指向了派生类对象的基类部分的子对象;
p=&bulk;
Quote &r = bulk;
  1. 派生类需要用基类的构造函数来初始化这些部分每个类控制它自己的成员初始化过程,基类部门和自己的数据成员都在构造函数初始化阶段执行初始化操作,
Bulk_quote(const std::string& book, double p,std::size_t qty,double disc):Quote(book,p),min_qty(qty),discount(disc){}

将前两个参数传递给Quote的构造函数,接下来初始化派生类数据列表。

  1. 派生类可以访问基类的公有成员和受保护成员,但是应当遵循基类的接口,调用基类的构造函数来初始化成员。
  2. 如果基类定义了一个静态成员,则在整个继承体系中都只存在该成员的唯一定义,无论从基类中派生出多少个类,对每个静态成员来说都只存在唯一的实例。静态成员遵循同样的访问控制规则,如果基类中成员private,派生类无权访问,如果可访问,那么可通过基类或者派生类使用它。
  3. 如果想将某个类用作基类,那么这个类必须已经定义而非仅仅声明,因为派生类要使用基类当中的成员,另外,这个规定隐含表示了:一个类不能派生它本身。
  4. 一个类可以同时是基类和派生类一个类可以同时是基类和派生类:
class Base{};
class D1:public Base{};
class D2:public D1{};

Base是D1的直接基类,是D2的间接基类,最终的派生链末端的类会包含直接基类的子对象以及每个间接基类的子对象。

  1. 可以通过class Base final {};来阻止其他类来继承该类

类型转换与继承

  1. 存在继承关系,我们可以将一个基类的引用或者指针绑定到派生类对象上, 当使用存在继承关系的类型时,需将静态类型与表达式表示对象的动态类型区分开来:表达式的静态类型在编译时总是已知的,是变量声明的类型或表达式生成的类型;动态类型是变量或者表达式表示的内存中对象的类型
    但如果表达式既不是引用也不是指针,那么静态类型永远与动态类型一致。

  2. 不存在从基类向派生类的隐式类型转换

每个派生类对象包含一个基类部分,才能实现派生类向基类的隐式转换,所以一个基类不能隐式地转换为派生类

Quote base;
Bulk_quote* bulkP=&base;//错误
BUlkquote& bulkRef = base;//错误 

另外一个特殊的是即使一个基类指针绑定在派生类对象上,我们也不能执行基类向派生类的转换,因为编译器判断的是指针或者引用的静态类型,

Bulk_quote bulk;
Quote *itemP = &bulk;
Bulk_quote *bulkP = itemP;//错误
  1. 对象之间不存在类型转换
    派生类向基类的自动类型转换只针对指针或者引用,我们初始化或者赋值一个类类型的对象,实际上是在调用某个函数,这些函数可以接受一个引用作为参数,允许我们向基类形参传递一个派生类的对象,又由于这些不是虚函数,显然这些函数只能处理基类的对象,派生类自有的成员被切掉了。

虚函数

当使用基类的引用或者指针调用一个虚成员函数时会执行动态绑定。因为执行时才知道会调用哪个版本的虚函数,所以所以虚函数都必须定义。

  1. 当某个虚函数通过指针或者引用调用时,直到运行时才能确定调用哪个版本的函数,被调用的是与指针或者引用的动态类型匹配的哪个,需要注意的是只有通过指针或者引用调用虚函数才会发生动态绑定。通过对象调用的虚函数版本在编译时确定。
  2. 总结:C++的多态性,具有继承关系的多个类型称为多态类型,当使用基类的引用或者指针调用基类中定义的一个函数时,不知道对象的类型,如果是虚函数,则直到运行时才会确定调用函数的版本,依据就是指针或者引用的动态类型。而对非虚函数或者通过对象调用的虚函数在编译时确定版本,且只依据静态类型
  3. 一旦某个函数被声明为虚函数,则在所有派生类中都是虚函数,覆盖的版本形参类型、返回类型必须一致,(但如果返回的是类的引用或者指针时,可以不一样)
struct B
{
	virtual void f1(int) const;
	virtual void f2();
	void f3();
};
struct D1:B
{
	void f1(int) const override;//正确
	void f2(int) override;//错误 基类中f2不接受参数
	void f3() override;//不是虚函数
	void f4() override;//没有这个函数
}

还能将某个函数指定为final 不允许覆盖该函数

struct D2:B
{
	void f1(int) const final;
}
struct D3:D2
{
	void f2();
	void f1(int) const;
}
  1. 虚函数也可以有默认实参,但是实参值由该次调用的静态类型决定,也就是如果通过基类的引用或指针调用虚函数,则使用基类虚函数定义的默认实参,不管动态类型如何。
  2. 某些情况下希望强制执行虚函数的某个特定版本,可以使用作用域运算符,强制调用某个派生类或者基类的虚函数double undiscounted = baseP->Quote::net_price(42) 通常是派生类的虚函数需要调用其覆盖的基类的虚函数,会用到作用域运算符。
  3. 依赖于对象才能调用,指向虚函数表的指针是存在于对象当中的,且这个函数必须能取地址(因为地址存在虚函数表中)所以呢,这些函数不能成为虚函数;
    a. 内联函数:内联函数只是在函数调用点将其展开,它不能产生函数符号,所以不能往虚表中存放,自然就不能成为虚函数。
    b. 静态函数:定义为静态函数的函数,这个函数只和类有关系,它不完全依赖于对象调用,所以也不能成为虚函数。
    c. 构造函数:当调用了构造函数,这个对象才能产生,如果把构造函数写成虚函数,对象就没有办法生成,就更不能调用调用了。所以构造函数不能成为虚函数。

抽象基类

纯虚函数无需定义,通过在函数声明语句的分号之前书写=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;
protected:
	std::size_t quantity=0;
	double discount =0.0;
}
  1. 含有纯虚函数的类是抽象基类,它负责定义接口,而其他类可以覆盖该接口,我们不能直接创建抽象基类的对象,但是可以定义其派生类的对象,前提是这些类覆盖了纯虚函数。
class Bulk_quote : public Disc_quote
{
public:
	Bulk_quote()=default;
	Bulk_quote(const std::string& book, double price, stdLLsize_t qty, double disc):
	Disc_quote(book,price, qty,disc){}
	double net_price(std::size_t) const override;
}

在这个类的对象初始化时, 接受四个参数,传递给Disc_quote的构造函数,随后Disc_quote继续调用Quote的构造函数,

访问控制与继承

每个类分别控制自己成员的初始化过程和成员可访问特性

  1. (protected)受保护的成员,对类用户来说是不可访问的,对于派生类和友元来说是可访问的。派生类的成员或者友元只能通过派生类对象来访问基类的受保护成员!
class Base
{
	protected:
		int prot_mem;
};
class Sneaky:public Base
{
	friend void clobber(Sneaky&);//可以通过派生类对象访问基类受保护成员
	friend void clobber(Base&);//不行
}
  1. 继承的访问控制,派生访问说明符对派生类的成员和友元能否访问其直接基类的成员无影响,只与基类中访问说明符有关。派生访问说明符是为了控制派生类用户和派生类的派生类在内对基类成员的访问权限,如果继承是public的,则尊循原来的访问说明符,如果继承是private的,则所有继承成员都是私有的,如果继承是受保护的,则继承的公有成员都是受保护的
  2. 当派生类公有继承基类时,用户代码才能实现派生类向基类的转换,否则不能;无论什么方式继承,派生类的成员函数和友元都可以使用派生类向基类的转换;如果D是公有或者受保护继承B,则D的派生类的成员和友元可以使用向基类B的类型转换
  3. 友元关系是不能继承的,派生类的友元不能随意访问基类成员,但是基类的友元在派生类中可以访问内嵌在派生类中的基类成员,即每个类控制自己成员的访问权限。
class Base
{
	friend class Pal;
};
class Pal
{
public:
	int f(Base b) {return b.prot_mem;}//可以
	int f2(Sneaky s){return s.j;}//错误, 不是友元
	int f3(Sneaky s) {return s.prot_mem;} //尽管Sneaky不是Base的友元,但是访问的成员是属于Base的,所以也行
}
  1. 有的时候需要改变继承的某个名字的访问级别,用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;
};

本来是私有继承,继承来的成员应当是Derived的私有成员,但是用using声明改变了成员的可访问性。通过在类内部使用using 我们将类直接或者间接基类的可访问成员标记出来,using名字的访问权限由该语句之前的访问说明符决定

  1. 如果不显式说明派生运算符种类,则class默认为私有继承,struct默认为公有继承

继承中的类作用域

每个类定义自己的作用域,定义类的成员,存在继承关系时,派生类的作用域嵌套在其基类的作用域之内,
Bulk_quote bulk; cout<<bulk.isbm()名字解析方式:首先在Bulk_quote中查找名字isbn(),因为是Disc_quote的派生类,所以在其中查找,还没找到,因为Disc_quoteQuote的派生类,继续查找,找到了isbn()

  1. 静态类型决定了该对象的哪些成员是可见的(尽管动态类型可能不一致)也就是哪些可以让用户代码使用
class Disc_quote : public Quote
{
public:
	std::pair<size_t, double> discount_policy() const {return { quantity,discount};}
}
Bulkquote bulk;
Bulk_quote *bulkP = &bulk;
Quote *itemP = &bulk;
bulkP->discount_policy();
itemP->discount_policy();//错误,搜索的是静态成员作用域

尽管itemP中是有名为discount_policy的成员,但是它的静态类型是Quote是不可见该成员的,

  1. 派生类可重用定义在直接基类或者间接基类中的名字,此时在派生类的名字将隐藏基类当中的名字,如果一个名字被隐藏了,可以用作用域运算符来使一个名字可见(不推荐在派生类中覆盖基类中的名字)
  2. p->mem()对于这个的名字查找过程,遵循如下步骤:首先确定p的静态类型;在静态类的作用域中查找mem() 若没有,则在直接基类和间接基类中查找直到继承链的顶端,不会在派生类中查找一旦找到了mem就进行类型检查,如果调用合法,编译器再根据是否调用的虚函数来确定代码。
  3. 声明在内层作用域中的函数不会重载声明在外层作用域的函数,因此派生类的成员也不会重载基类中的成员,而是隐藏掉外层作用域成员。所以下面的调用是错误的:
struct Base
{
	int memfcn();
};
struct Derived:Base
{
	int memfcn(int);
};
Derived d;
Base b;
d.memfcn();//派生类的成员隐藏了基类的无参成员,该调用错误
d.Base::memfcn();//显式调用基类无参成员
class Base
{
public:
	virtual int fcn();//虚函数
};
class D1:public Base
{
public:
	int fcn(int);//未覆盖基类的fcn()
	virtual void f2();
}
class D2:public D1
{
public:
	int fcn(int);//覆盖了fcn()
	int fcn();//覆盖了基类的虚函数
	void f2();//覆盖了D1的虚函数
}
Base bobj;D1 d1obj;D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn();//虚调用,调用基类中虚函数
bp2->fcn();//虚调用 因为派生类中未覆盖基类的虚函数,故直接调用D1中从基类继承来的虚函数
bp3->fcn();//虚调用 调用D2覆盖的函数
D1 *d1p = &d1obj;D2 *d2p & d2obj;
bp2->f2();//在静态类型中查找f2() 找不到
d1p->f2();//调用D1当中f2() 
d2p->f2();//调用D2当中f2()
  1. 成员函数无论是否虚函数都能被重载,派生类可以覆盖(也可以不)覆盖重载的实例,如果一个类只覆盖重载集合当中的一些而非全部函数,可以为重载的成员提供一条using声明语句,指定函数名而不指定形参列表,所以基类成员函数的using就可以将该函数的所有重载版本添加到派生类中,派生类只需定义自己的版本。(对未重载版本的访问实际上是对usings声明点的访问)

构造函数与拷贝控制

虚析构函数

继承关系对基类拷贝控制影响的是通常基类应当定义一个虚析构函数,以实现根据动态类型调用析构函数。如果指针的静态类型与动态类型不符,则析构函数的执行会遇到问题(行为未定义)

class QUote
{
public:
	virtual ~Quote() = default;
};

析构函数的虚属性会被继承,因此无论Quote的派生类使用的是合成的析构函数还是定义的自己的析构函数,都是虚析构函数,因此只需基类是虚析构函数,就可以确保delete指针时执行正确的版本。

合成拷贝控制与继承

基类或者派生类的合成拷贝控制成员的行为与其他类似:

  • 合成派生类默认构造函数运行了直接基类的默认构造函数,直接基类又运行了间接基类的构造函数
  • 先执行基类的默认构造函数,再执行派生类构造函数
  • 在上文继承体系中所有类都使用合成的析构函数,派生类隐式使用,基类显式使用,派生类的析构函数释放成员,销毁直接基类
  1. 如果基类的默认构造函数、拷贝控制成员、析构函数是删除或者不可访问,则派生类中对应的成员是删除的,因为没办法执行对基类的操作;如果基类的析构函数是删除的,则派生类拷贝控制成员和移动构造函数是删除的,因为没法销毁基类对象;编译器不会合成删除掉的移动操作,如果基类的移动操作是删除的,则派生类当中的也是删除的
class B
{
public:
	B();
	B(const B&) = delete;
};
class D:public B
{
};
D d;//正确 使用合成的默认构造函数
D d2(d);//错误 基类的拷贝构造函数被删除了,D的合成拷贝构造函数也被删除
D d3(std::move(d));//错误 没移动构造函数,隐式使用拷贝构造函数,但是它又被删除了
  1. 大多数基类定义一个虚析构函数,因此默认下基类不含合成的移动操作,派生类也没有移动操作,当确实需要时应在基类中定义,并同时定义拷贝操作
class Quote
{
pubic:
	Quote() = default;
	Quote(const Quote&) = default;
	QUote(Quote&&) = defualt;
	Quote& operator=(const Quote&) = default;
	Quote& operator=(Quote&&) = default;
	virtual ~Quote() = default;
}

派生类的拷贝控制成员

派生类的拷贝控制成员不仅拷贝自身成员,也负责了拷贝基类部分的成员

class Base {};
class D:public Base
{
public:
	D(const D& d):Base(d){}
	D(D&& d):Base(std::move(d)){} 
	D& operator=(const D &rhs)
	{
		Base::operator=(rhs);
		return *this;
	}
	~D() {/*清除派生类成员*/}
};

在拷贝构造函数中,如果不将d绑定到基类的拷贝构造函数,则导致基类默认初始化,未拷贝。在赋值运算符中先调用基类的赋值运算符为基类部分赋值;派生类的析构函数只负责销毁由派生类自己分配的资源。

  1. 在构造过程中,先构建基类,再构建派生类,因此在执行基类构造函数时,对象派生类部分是未被初始化的,销毁的顺序相反,销毁基类对象时,派生类部分已经被销毁掉了。所以如果在基类构造函数或者析构函数中调用虚函数时,应当控制执行版本,与构造函数所属类型一致。

继承的构造函数

新标准中派生类可以重用直接基类定义的构造函数,不能继承默认构造函数和拷贝,移动构造函数,如果不定义,编译器负责合成。

class Bulk_quote:public Disc_quote
{
	using Disc_quote::Disc_quote;//继承Disc_quote的构造函数
	double net_price(std::size_t) const;
};

using作用于编译器时令其产生代码,对于基类的每个构造函数,派生类都生成一个形参列表完全对应的

  • 构造函数的using不会改变构造函数的访问级别;
  • 当基类构造函数含有默认实参,实参不会被继承,而是生成一个形参列表中除去默认实参的构造函数。P558 ;
  • 如果派生类定义了与基类构造函数形参相同的构造函数,则不会继承该构造函数,而是替换

容器与继承

容器如果存放继承体系中的对象,一般间接存储(因为不允许在容器中存放不同类型的元素) 如果直接存储,相当于将派生类对象赋值给了基类对象,其中派生类的部分会被切掉,因此容器和存在继承关系的类型无法兼容。

  1. 为解决这一问题,在容器中存放继承关系的对象,实际存放的是基类的指针,所指对象的类型(动态类型)可能是基类类型或者派生类型
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201",50));
basket.push_back(make_shared<Bulk_quote>("0-201",50));//将派生类的指针转换为基类的指针
class Basket
{
public:
	void add_item(const std::shared_ptr<Quote> &sale) { items.insert(sale); }
	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(); }
	std::multiset < std::shared_ptr<Quote>, decltype(compare)*> items{ compare };
};

该类定义了两个操作,add_item和total_receipt:

double Basket::total_receipt(ostream &os)const
{
	double sum = 0.0;
	for(auto iter = items.cbegin();iter!=items.cend();iter = items.upper_bound(*iter))
	{
		sum+=print_total(0s,**iter,items.count(*iter));
	}
	os<<"Total Sale:"<<sum<<endl;
	return sum;
}

在for循环中,我们通过upper_bound来使iter直接指向下一个关键字,因为这个函数返回的是指向所有与iter关键字相等的元素中最后一个元素的下一个位置的迭代器,所以要么指向下一位置,要么指向集合的末尾。

  1. Basket的用户需要自己创建智能指针,为了避免让用户管理内存,我们把add_item改写为:
void add_item(const Quote& sale);
void add_item(Quote&& sale);

但是在分配对象时,不能用new Quote(sale)来分配,因为传入的可能是派生类的对象,这时分配的内存会小,对象会被切掉一部分。为了解决该问题,我们给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 &{ return new Bulk_quote(*this);}
	Bulk_quote* clone() && {return new Bulk_quote(std::move(*this));}
};

定义了clone的左值和右值版本,因此add_item可以根据传入的智能指针绑定的动态类型调用不同的虚函数,就不会有分配内存不足的问题了。

文本查询程序再探

我们将不同的查询建模成独立的类,共享一个公共操作,包含两个操作 eval接受一个TextQuery对象返回QueryResult 使用给定的对象查找与之匹配的行,rep操作返回基础查询的string表达形式。
不同的查询操作派生类不具有继承关系,所以应当定义一个抽象基类来表示接口,将eval和rep定义为纯虚函数,又因为AndQuery和OrQuery有两个运算对象,所以定义了BinaryQuery这个抽象基类表示含有两个运算对象的查询。在这里插入图片描述

  1. 我们的程序通过如下表达式实现复合查询Query q = Query("fiery") & Query("bird") | Query("wind");Query类保存了Query_base指针,绑定到Query_base的派生类对象上,它负责隐藏整个继承体系,而我们通过Query的操作间接地创建并处理Query_base对象。我们定义三个重载运算符和构造函数:
  • &运算符生成绑定到AndQuery上的Query对象
  • |运算符生成绑定到OrQuery上的Query对象
  • ~运算符生成绑定到NotQuery上的Query对象
  • 接受string参数的Query构造函数,生成wordQuery
    由此得到了程序的接口和隐藏实现:
    在这里插入图片描述

Query_base和Query类

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 std::string rep()const  = 0;
};

两个操作都是纯虚函数,所以该类是个抽象基类,所有对该类的使用都需要经过Query类,故成为该类的友元

  1. Query类含有一个指向Query_base类的shared_ptr因为Query是Query_base的唯一接口,所以必须定义自己版本的eval和rep操作,接受string作为参数的Query构造函数创建一个WordQuery对象,然后将私有的指针绑定到这个对象上,& | ~分别创建AndQuery OrQuery 和 NotQuery对象,为了创建对象并绑定指针,这三个重载运算符需要一个以指针为形参的Query的构造函数,但我们不希望这个构造函数被用户所访问,故设置为私有的。
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 TextQuery& t)const {return q->eval(t);}//在派生类中以.方式调用,但实际还是虚调用!!!
	std::string rep()const {return q->rep();}
private:
	std::shared_ptr<Query_base> q;
	Query(std::shared_ptr<Query_base> query):q(query){}	
};

eval和rep通过操作指针调用动态类型的函数来保证能够执行不同查询类型的虚函数。

  1. 输出运算符,
ostream& operator<<(std::ostream &os,const Query &query)
{
	return os<<query.rep();//调用Query的公有成员,但是这是个引用,实际调用的是不同类的虚函数。
}

4.当一个Query的对象

  • 被拷贝: 调用合成版本拷贝构造函数,拷贝指向Query_base的智能指针,递增计数器。
  • 被移动: 调用合成的移动构造函数。它将数据成员移动到新对象中。在这种情况下,来自新创建的对象的共享指针将指向原始共享指针所指向的地址。在移动操作之后,新对象中的共享指针的使用计数是1,而来自原始对象的指针变为nullptr。
  • 被赋值: 合成的赋值运算符,将右侧对象指针地址拷贝到左侧指针指向地址,递增计数器
  • 被销毁:销毁赋值运算符,计数器递减

而Query_base的对象,完全由合成版本的控制成员管理

派生类实现

  1. WordQuery查找一个给定string
class WordQuery:public Query_base
{
	friend class Query;
	WordQuery(const std::string &s):query_Word(s){}
	QueryResult eval(const TextQuery &t) const {return t.query(query_word);}
	std::string rep() const {return query_word;}
	std::string query_word;
};
inline Query::Query(const std::string &s):q(new WordQuery(s)){}

没有公有成员,并且必须定义继承而来的虚函数eval和rep

class NotQuery:public Query_base
{
	friend Query operator~(const Query&);
	NotQuery(const Query &q):query(q){}
	std::string rep() const {return "~("+query.rep()+")";}//形式上是非虚调用,但实际上调用了Query中的rep:q->rep() 实际还是虚调用
	QueryResult eval(const TextQuery&) const;
	Query query;
};
inline Query operator~(const Query &operand)
{
	return std::shared_ptr<Query_base>(new NotQuery(operand));
}

因为成员都是私有的,所以将、~运算符设置为友元,~运算符在return语句中动态分配一个NotQuery对象,

  1. 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;//此处调用两次Query的构造函数
	std::string opSym;
};

这个类中两个数据是相应的对象和运算符号,rep创建了一个表达式来返回BinaryQuery,该类直接继承了eval,是个纯虚函数,所以该类是个抽象基类,不能创建该类型的对象

  1. AndQuery类
class AndQuery:public BinaryQuery
{
	friend Query operator&(const Query&, const Query&);
	AndQuery(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 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));//隐式调用接受智能指针的Query的构造函数,返回Query对象
}

eval函数

不同派生类的eval的逻辑也不同。

  1. OrQuery:eval
QueryResult OrQuery::eval(const TextQuery& text) const 
{
	auto right = rhs.eval(text);
	auto left = lhs.eval(text);
	auto ret_lines = make_shared<set<line_no>>(left.begin(), left.end());
	ret_lines->insert(right.begin(),right.end());
	return QueryResult(rep(),ret_lines, left.get_file());
}

用set表示行号,先复制lhs的查询结果,再将右侧查询结果插入到set中,因为不是multiset,所以去除了重复行号

  1. AndQuery:eval
QueryResult AndQuery::eval(const TextQuery& text) const 
{
	auto right = rhs.eval(text);
	auto left = lhs.eval(text);
	auto ret_lines = make_shared<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());
}

set_intersection接受五个迭代器,前四个表示两个输入序列,最后一个表示目的位置(上文中传入的是插入迭代器)

  1. 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();
	auto end = result.end();
	auto sz = result.get_file()->size();//标识文件总的行数
	for(size_t n=0;n!=sz;++n)
	{
		if(beg==end||*beg!=n)
			ret_lines->insert(n);//如果不在result 就添加这一行
		else if(beg!=end)
			++beg;
	}
	return QueryResult(rep(),re_lines,result.get_file());
}

QueryResult中包含的是运算对象出现的行号,但是想要的是运算对象未出现的行号,所以需要遍历整个文件,找到不存在于QueryResult中的行号,放到结果集合ret_lines中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值