《C++Primer》第十五章-面向对象编程-学习笔记(3)

《C++Primer》第十五章-面向对象编程-学习笔记(3)

日志:
1,2020-03-10 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
《C++Primer》第十二章-类-学习笔记(1)

纯虚函数

在第 15章所编写的 Disc_item 类提出了一个有趣的问题:该类从Item_base 继承了 net_price 函数但没有重定义该函数。因为对 Disc_item 类而言没有可以给予该函数的意义,所以没有重定义该函数。在我们的应用程序中,Disc_item 不对应任何折扣策略,这个类的存在只是为了让其他类继承
我们不想让用户定义 Disc_item 对象,相反,Disc_item 对象只应该作为Disc_item 派生类型的对象的一部分而存在。但是,正如已定义的,没有办法防止用户定义一个普通的 Disc_item 对象。这带来一个问题:如果用户创建一个Disc_item 对象并调用该对象的 net_price 函数,会发生什么呢?从前面章节的讨论中了解到,结果将是调用从 Item_base 继承而来的 net_price 函数,该函数产生的是不打折的价格。
很难说用户可能期望调用 Disc_item 的 net_price 会有什么样的行为。真正的问题在于,我们宁愿用户根本不能创建这样的对象。可以使 net_price 成为纯虚函数,强制实现这一设计意图并正确指出 Disc_item 的 net_price 版本没有意义的。在函数形参表后面写上 = 0 以指定纯虚函数

class Disc_item : public Item_base {
public:
double net_price(std::size_t) const = 0;
};

将函数定义为纯虚能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用重要的是,用户将不能创建 Disc_item 类型的对象。试图创建抽象基类的对象将发生编译时错误

// Disc_item declares pure virtual functions
Disc_item discounted; // error: can't define a Disc_item object
Bulk_item bulk; // ok: Disc_item subobject within Bulk_item

含有(或继承)一个或多个纯虚函数的类抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。

容器与继承

我们希望使用容器(或内置数组)保存因继承而相关联的对象。但是,对象不是多态的,这一事实对将容器用于继承层次中的类型有影响。
例如,书店应用程序中可能有购物篮,购物篮代表顾客正在购买的书。我们希望能够在 multiset中存储储购买物,要定义 multiset,必须指定容器将保存的对象的类型。将对象放进容器时,复制元素。如果定义 multiset 保存基类类型的对象:

multiset<Item_base> basket;  //购物篮是multiset
Item_base base;
Bulk_item bulk;
basket.insert(base); // ok: add copy of base to basket
basket.insert(bulk); // ok: but bulk sliced down to its base part

加入派生类型的对象时,只将对象的基类部分保存在容器中。记住,将派生类对象复制到基类对象时,派生类对象将被切掉
容器中的元素是 Item_base 对象,无论元素是否作为 Bulk_item 对象的副本而建立,当计算元素的 net_price 时,元素将按不打折定价。一旦对象放入了 multiset,它就不再是派生类对象了。
因为在容器中派生类对象在赋值给基类对象时会被“切掉”,所以容器与通过继承相关的类型不能很好地融合。
不能通过定义容器保存派生类对象来解决这个问题。在这种情况下,不能将Item_base 对象放入容器——没有从基类类型到派生类型的标准转换。可以显式地将基类对象强制转换为派生类对象并将结果对象加入容器,但是,如果这样做,当试图使用这样的元素时,会产生大问题:在这种情况下,元素可以当作派生类对象对待,但派生类部分的成员将是未初始化的。
唯一可行的选择可能是使用容器保存对象的指针。这个策略可行,但代价是需要用户面对管理对象和指针的问题,用户必须保证只要容器存在,被指向的对象就存在。如果对象是动态分配的,用户必须保证在容器消失时适当地释放对象。
下一节将介绍对这个问题更好更通用的解决方案。

句柄类与继承

C++ 中面向对象编程的一个颇具讽刺意味的地方是,不能使用对象支持面向对象编程,相反,必须使用指针或引用。例如,下面的代码段中:

void get_prices(Item_base object,const Item_base *pointer,const Item_base &reference)
{
// which version of net_price is called is determined at run time
cout << pointer->net_price(1) << endl;
cout << reference.net_price(1) << endl;
// always invokes Item_base::net_price
cout << object.net_price(1) << endl;
}

通过 pointer 和 reference 进行的调用在运行时根据它们所绑定对象的动态类型而确定。
但是,使用指针或引用会加重类用户的负担。在前一节中讨论继承类型对象与容器的相互作用时,已经碰到了一种这样的负担。
C++ 中一个通用的技术是定义包装(cover)类句柄(handle)类句柄类存储和管理基类指针。指针所指对象的类型可以变化,它既可以指向基类类型对象又可以指向派生类型对象。用户通过句柄类访问继承层次的操作。因为句柄类使用指针执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化。因此,句柄的用户可以获得动态行为但无须操心指针的管理
包装了继承层次的句柄有两个重要的设计考虑因素:
• 像对任何保存指针(第 13 章)的类一样,必须确定对复制控制做些什么。包装了继承层次的句柄通常表现得像一个智能指针(第 13 章)或者像一个值(第 13章)。
• 句柄类决定句柄接口屏蔽还是不屏蔽继承层次,如果不屏蔽继承层次,用户必须了解和使用基本层次中的对象。
对于这些选项没有正确的选择,决定取决于继承层次的细节,以及类设计者希望程序员如何与那些类相互作用。下面两节将实现两种不同的句柄,用不同的方式解决这些设计问题。

指针型句柄

像第一个例子一样,我们将定义一个名为 Sales_item 的指针型句柄类,表示 Item_base 层次。Sales_item 的用户将像使用指针一样使用它:用户将Sales_item 绑定到 Item_base 类型的对象并使用 * 和 -> 操作符执行Item_base 的操作:

// bind a handle(连结句柄) to a Bulk_item object
Sales_item item(Bulk_item("0-201-82470-1", 35, 3, .20));
item->net_price(); // virtual call to net_price function

但是,用户不必管理句柄指向的对象,Sales_item 类将完成这部分工作。当用户通过 Sales_item 类对象调用函数时,将获得多态行为。

定义句柄

Sales_item 类有三个构造函数:默认构造函数复制构造函数接受Item_base 对象的构造函数。第三个构造函数将复制 Item_base 对象,并保证:
只要 Sales_item 对象存在副本就存在。当复制 Sales_item 对象或给Sales_item 对象赋值时,将复制指针而不是复制对象。像对其他指针型句柄类一样,将用使用计数来管理副本。
迄今为止,我们已经使用过的使用计数式类,都使用一个伙伴类来存储指针和相关的使用计数。这个例子将使用不同的设计,如图 所示。Sales_item类将有两个数据成员,都是指针:一个指针将指向 Item_base 对象,而另一个将指向使用计数。Item_base 指针可以指向 Item_base 对象也可以指向
Item_base 派生类型的对象。通过指向使用计数,多个 Sales_item 对象可以共享同一计数器。

 Sales_item 句柄类的使用计数策略

图1 . Sales_item 句柄类的使用计数策略

除了管理使用计数之外,Sales_item 类还将定义解引用操作符箭头操作符

// use counted handle class for the Item_base hierarchy
class Sales_item {
public:
// default constructor: unbound handle  
	Sales_item(): p(0), use(new std::size_t(1)) { }
// attaches a handle(柄) to a copy of the Item_base object
	Sales_item(const Item_base&);
// copy control members to manage the use count and pointers   复制控制成员
	Sales_item(const Sales_item &i):
	p(i.p), use(i.use) { ++*use; }
	~Sales_item() { decr_use(); }
	Sales_item& operator=(const Sales_item&);
// member access operators
	const Item_base *operator->() const { if (p) return p;
	else throw std::logic_error("unbound Sales_item"); }
	const Item_base &operator*() const { if (p) return *p;
	else throw std::logic_error("unbound Sales_item"); }
private:
	Item_base *p; // pointer to shared item
	std::size_t *use; // pointer to shared use count 使用计数
// called by both destructor and assignment operator to free pointers
	void decr_use()
	{ if (--*use == 0) { delete p; delete use; } }
};

使用计数式复制控制

复制控制成员适当地操纵使用计数和 Item_base 指针。复制 Sales_item对象包括复制两个指针和将使用计数加 1。析构函数将使用计数减 1,如果计数减至 0 就撤销指针。因为赋值操作符需要完成同样的工作,所以在一个名为decr_use 的私有实用函数中实现析构函数的行为。
赋值操作符比复制构造函数复杂一点:

// use-counted assignment operator; use is a pointer to a shared use count
Sales_item& Sales_item::operator=(const Sales_item &rhs)
{
	++*rhs.use;
	decr_use();
	p = rhs.p;
	use = rhs.use;
	return *this;
}

赋值操作符像复制构造函数一样,将右操作数的使用计数加 1 并复制指针
它也像析构函数一样,首先必须将左操作数的使用计数减 1,如果使用计数减至0 就删除指针。
像通常对赋值操作符一样,必须防止自身赋值。这个操作符通过首先将右操作数的使用计数减 1 来处理自身赋值。如果左右操作数相同,则调用 decr_use时使用计数将至少为 2。该函数将左操作数的使用计数减 1 并进行检查,如果使用计数减至 0,则 decr_use 将释放该对象中的 Item_base 对象和 use 对象。剩下的是从右操作数向左操作数复制指针,像平常一样,我们的赋值操作符返回左操作数的引用。
除了复制控制成员以外,Sales_item 定义的其他函数是是操作函数operator* 和 operator->,用户将通过这些操作符访问 Item_base 成员。因为这两个操作符分别返回指针和引用,所以通过这些操作符调用的函数将进行动态绑定。
我们只定义了这些操作符的 const 版本,因为基础 Item_base 层次中的成员都是 const 成员。
//这一段暂时没看懂

构造句柄

我们句柄有两个构造函数:默认构造函数创建未绑定的 Sales_item 对象,第二个构造函数接受一个对象,将句柄与其关联。
第一个构造函数容易定义:将 Item_base 指针置 0 以指出该句柄没有关联任何对象上。构造函数分配一个新的计数器并将它初始化为 1。
第二个构造函数难一点,我们希望句柄的用户创建自己的对象,在这些对象上关联句柄。构造函数将分配适当类型的新对象并将形参复制到新分配的对象中,这样,Sales_item 类将拥有对象并能够保证在关联到该对象的最后一个Sales_item 对象消失之前不会删除对象。

复制未知类型

要实现接受 Item_base 对象的构造函数,必须首先解决一个问题:我们不知道给予构造函数的对象的实际类型。我们知道它是一个 Item_base 对象或者是一个 Item_base 派生类型的对象。句柄类经常需要在不知道对象的确切类型时分配书籍对象的新副本。Sales_item 构造函数是个好例子。
解决这个问题的通用方法是定义虚操作进行复制,我们称将该操作命名为clone
为了句柄类,需要从基类开始,在继承层次的每个类型中增加 clone,基类必须将该函数定义为虚函数:

class Item_base {
public:
virtual Item_base* clone() const  //定义虚操作进行复制,我们称将该操作命名为 clone
{ return new Item_base(*this); }
};

每个类必须重定义该虚函数。因为clone函数的存在是为了生成类对象的新副本,所以定义返回类型为类本身:

class Bulk_item : public Item_base {
public:
Bulk_item* clone() const
{ return new Bulk_item(*this); }
};

第 15.2.3 节介绍过,对于派生类的返回类型必须与基类实例的返回类型完全匹配的要求,但有一个例外。这个例外支持像这个类这样的情况。如果虚函数的基类实例返回类类型的引用或指针,则该虚函数的派生类实例可以返回基类实例返回的类型的派生类(或者是类类型的指针或引用)。

定义句柄构造函数

一旦有了 clone 函数,就可以这样编写 Sales_item 构造函数:

Sales_item::Sales_item(const Item_base &item):
p(item.clone()), use(new std::size_t(1)) { }

像默认构造函数一样,这个构造函数分配并初始化使用计数,它调用形参的clone 产生那个对象的(虚)副本。如果实参是 Item_base 对象,则运行Item_base 的 clone 函数;如果实参是 Bulk_item 对象,则执行 Bulk_item 的clone 函数。

句柄的使用

使用 Sales_item 对象可以更容易地编写书店应用程序。代码将不必管理Item_base 对象的指针,但仍然可以获得通过 Sales_item 对象进行的调用的虚行为。
例如,可以使用 Item_base 对象解决第 15.7 节提出的问题。可以使用Sales_item 对象跟踪顾客所做购买,在 multiset 中保存一个对象表示一次购买,当顾客完成购买时,可以计算销售总数。

比较两个Sales_item 对象

在编写函数计算销售总数之前,需要定义比较 Sales_item 对象的方法。要用 Sales_item 作为关联容器的关键字,必须能够比较它们(第 10.3.1 节)。
关联容器默认使用关键字类型的小于操作符,但是,基于第 14.3.2 节讨论过的有关原始 Sales_item 类型的同样理由,为 Sales_item 句柄类定义operator >可能是个坏主意:当使用 Sales_item 作关键字时,只想考虑 ISBN,但确定相等时又想要考虑所有数据成员。
幸好,关联容器使我们能够指定一个函数或函数对象用作比较函数,这样做类似于第 11.2.3 节中将单独函数传给 stable_sort 算法的方式。在那种情况下,只需要将附加的实参传给 stable_sort 以提供比较函数,代替 < 操作符的。覆盖关联容器的比较函数有点复杂,因为,正如我们将看到的,在定义容器对象时必须提供比较函数。
让我们比较容易的部分开始,定义一个函数用于比较 Sales_item 对象:

// compare defines item ordering for the multiset in Basket 
inline bool compare(const Sales_item &lhs, const Sales_item &rhs)
{
	return lhs->book() < rhs->book();
}
//我们的 compare 函数与小于操作符有两样的接口,它接受两个 Sales_item对象的 const 引用,通过比较 ISBN 而比较形参,返回一个 book 值。

该函数使用 Sales_item 的 -> 操作符,该操作符返回 Item_base 对象的指针,那个指针用于获取并运行成员 book,该成员返回 ISBN。

使用带关联容器的比较器

如果考虑一下如何使用比较函数,就会认识到,它必须作为容器的部分而存储。任何在容器中增加或查找元素的操作都要使用比较函数。原则上,每个这样的操作可以接受一个可选的附加实参,表示比较函数。但是,这种策略容易导致出错:如果两个操作使用不同的比较函数,顺序可能会不一致。不可能预测实际上会发生什么。
要有效地工作,关联容器需要对每个操作使用同一比较函数。然而,期望用户每次记住比较函数是不合理的,尤其是,没有办法检查每个调用使用同一比较函数。因此,容器记住比较函数是有意义的。通过将比较器存储在容器对象中,可以保证比较元素的每个操作将一致地进行。
基于同样的理由,容器需要知道元素类型,为了存储比较器,它需要知道比较器类型。原则上,通过假定比较器是一个函数指针,该函数接受两个容器的key_type 类型的对象并返回 bool 值,容器可以推断出这个类型。不幸的是,这个推断出的类型可能限制太大。首先,应该允许比较器是函数对象或是普通函数。即使我们愿意要求比较器为函数,这个推断出的类型也可能仍然太受限制了,毕竟,比较函数可以返回 int 或者其他任意可用在条件中的类型。同样,形参类型也不需要与 key_type 完全匹配,应该允许可以转换为 key_type 的任意形参类型。所以,要使用 Sales_item 的比较函数,在定义 multiset 时必须指定比较器类型。在我们的例子中,比较器类型是接受两个 const Sales_item 引用并返回 bool 值的函数。
首先定义一个类型别名,作为该类型的同义词(第 7.9 节):

// type of the comparison function used to order the multiset
typedef bool (*Comp)(const Sales_item&, const Sales_item&);

这个语句将 Comp 定义为函数类型指针的同义词,该函数类型与我们希望用来比较 Sales_item 对象的比较函数相匹配。
接着需要定义 multiset,保存 Sales_item 类型的对象并在它的比较函数中使用这个 Comp 类型。关联容器的每个构造函数使我们能够提供比较函数的名字。可以这样定义使用 compare 函数的空multiset:

std::multiset<Sales_item, Comp> items(compare);

这个定义是说,items 是一个 multiset,它保存 Sales_item 对象并使用Comp 类型的对象比较它们。multiset 是空的——我们没有提供任何元素,但我们的确提供了一个名为 compare 的比较函数。当在 items 中增加或查找元素时,将用 compare 函数对 multiset 进行排序。

容器与句柄类

既然知道了怎样提供比较函数,我们将定义名为 Basker 的类,以跟踪销售并计算购买价格:

class Basket {
// type of the comparison function used to order the multiset
	typedef bool (*Comp)(const Sales_item&, const Sales_item&);
public:
// make it easier to type the type of our set
	typedef std::multiset<Sales_item, Comp> set_type;
// typedefs modeled after corresponding container types
	typedef set_type::size_type size_type;
	typedef set_type::const_iterator const_iter;
	Basket(): items(compare) { } // initialze the comparator 传递函数,上面定义那个
	void add_item(const Sales_item &item)
	{ items.insert(item); }
	size_type size(const Sales_item &i) const
	{ return items.count(i); }
	double total() const; // sum of net prices for all items in the basket
private:
	std::multiset<Sales_item, Comp> items;
};

这个类在 Sales_item 对象的 multiple 中保存顾客购买的商品,用multiple 使顾客能够购买同一本书的多个副本。
该类定义了一个构造函数,即 Basket 默认构造函数。该类需要自己的默认构造函数,以便将compare 传给建立 items 成员的 multiset 构造函数。Basket 类定义的操作非常简单:add_item 操作接受 Sales_item 对象引用并将该项目的副本放入 multiset;对于给定 ISBN,size 操作返回购物篮中该ISBN 的记录数。除了操作,Basket 还定义了三个类型别名,这样使用它的multiset 成员就比较容易了。

使用句柄执行虚函数

Basket 类唯一的复杂成员是 total 函数,该函数返回购物篮中所有物品的价格:

double Basket::total() const
{
	double sum = 0.0; // holds the running total
/* find each set of items with the same isbn and calculate
* the net price for that quantity of items
* iter refers to first copy of each book in the set
* upper_bound refers to next element with a different isbn
*/
	for (const_iter iter = items.begin();iter != items.end(); iter =items.upper_bound(*iter))
{
//for 循环中的“增量”表达式很有意思。与读每个元素的一般循环不同,我们推进 iter 指向下一个键。
// we know there's at least one element with this key in the Basket
// virtual call to net_price applies appropriate discounts,if any
	sum += (*iter)->net_price(items.count(*iter));
}
	return sum;
}

total 函数有两个有趣的部分:对 net_price 函数的调用,以及 for 循环结构。我们逐一进行分析。
调用 net_price 函数时,需要告诉它某本书已经购买了多少本,net_price函数使用这个实参确定是否打折。这个要求暗示着我们希望成批处理multiset——处理给定标题的所有记录,然后处理下一个标题的所有记录,以此类推。幸好,multiset 非常适合处理这个问题。
for 循环开始于定义 iter 并将 iter 初始化为指向 multiset 中的第一个元素。我们使用 multiset 的 count 成员(第 10.3.6 节)确定 multiset 中的多少成员具有相同的键(即,相同的 isbn),并且使用该数目作为实参调用net_price 函数。
**for 循环中的“增量”表达式很有意思。与读每个元素的一般循环不同,我们推进 iter 指向下一个键。**调用 upper_bound 函数以跳过与当前键匹配的所有元素,upper_bound 函数的调用返回一个迭代器,该迭代器指向与 iter 键相同的最后一个元素的下一元素,即,该迭代器指向集合的末尾或下一本书。测试iter 的新值,如果与 items.end() 相等,则跳出 for 循环,否则,就处理下一本书。
for 循环的循环体调用 net_price 函数,阅读这个调用需要一点技巧:

sum += (*iter)->net_price(items.count(*iter));
//对 iter 解引用获得基础 Sales_item 对象
//对该对象应用 Sales_item 类重载的箭头操作符,该操作符返回句柄所关联的基础 Item_base 对象

对 iter 解引用获得基础 Sales_item 对象,对该对象应用 Sales_item 类重载的箭头操作符,该操作符返回句柄所关联的基础 Item_base 对象,用该Item_base 对象调用 net_price 函数,传递具有相同 isbn 的图书的 count 作为实参。net_price 是虚函数,所以调用的定价函数的版本取决于基础Item_base 对象的类型。

总结

继承和动态绑定的思想,简单但功能强大。
继承使我们能够编写新类,新类与基类共享行为但重定义必要的行为
动态绑定使编译器能够在运行时根据对象的动态类型确定运行函数的哪个版本
继承和动态绑定的结合使我们能够编写具有特定类型行为而又独立于类型的程序。

在 C++ 中,动态绑定仅在通过引用或指针调用时才能应用于声明为虚的函数。C++ 程序定义继承层次接口的句柄类很常见,这些类分配并管理指向继承层次中对象的指针,因此能够使用户代码在无须处理指针的情况下获得动态行为。

继承对象由基类部分和派生类部分组成。继承对象是通过在处理派生部分之前对对象的基类部分进行构造、复制和赋值,而进行构造、复制和赋值的。因为派生类对象包含基类部分,所以可以将派生类型的引用或指针转换为基类类型的引用或指针。
即使另外不需要析构函数,基类通常也应定义一个虚析构函数。如果经常会在指向基类的指针实际指向派生类对象时删除它,析构函数就必须为虚函数

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104761278