15.8 容器与继承
当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式。原因是 C++ 不允许在容器中保存不同类型的元素(Python 原生支持在 slice 中保存不同类型的元素;Golang 可以通过将容器接收数据的类型定义为 interface{} 来支持存放不同类型的元素)。所以我们不能把具有继承关系的多种类型的对象直接存放在容器当中。
例如,假如我们定义一个保存 Bulk_quote 的 vector,令其保存用户准备购买的几种书籍。显然我们不应该用这个 vector 来保存 Bulk_quote 对象,因为无法将 Quote 对象转换成 Bulk_quote 对象,所以我们能无法将 Quote 对象放置到这个 vector 当中。
也不应该使用 vector 来直接保存 Quote 对象,虽然这样做可以同时把 Quote 和 Bulk_quote 放置到 vector 当中,但此时保存的 Bulk_quote 对象不再是 Bulk_quote 对象了,而是类型转换之后的 Quote 对象(Bulk_quote 对象是 Quote 的派生类,Bulk_quote 向 Quote 的类型转换相当于将 Bulk_quote 内部的 Quote 部分提取出来,并抛弃其它派生类部分):
vector<Quote> basket;
basket.push_back(Quote("0-201-82470-1", 50));
basket.push_back(Bulk_quote("0-201-54848-8", 50, 10, .25));
// 👆 正确, 但是是将 Bulk_quote 对象的 Quote 部分拷贝给了 basket
正如上面所提到的,由于 basket 元素是 Quote 对象,因此当我们向这个 vector 添加一个 Bulk_quote 对象时,它的派生类部分被忽略掉了。
在容器中放置(智能)指针而非对象
当我们希望在容器中存放具有继承关系的对象时,实际上存放的通常是基类的指针。和往常一样,这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型:
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-1", 50, 10, .25));
cout << basket.back() -> net_price(15) << endl;
实际调用的 net_price 版本依赖于指针所指对象的动态类型。
值得注意的是,我们将 basket 定义成 shared_ptr<Quote>,但是在第二个 push_back 中传入的是一个 Bulk_quote 对象的 shared_ptr。正如我们可以将一个派生类的普通指针转换成基类指针一样,也可以把一个派生类的智能指针转换成基类的智能指针。
在此例中,make_shared<Bulk_quote> 返回一个 shared_ptr<Bulk_quote> 对象,当我们调用 push_back 时该对象又被转换为 shared_ptr<Quote> 对象。
15.8.1 编写 Basket 类
对于 C++ 面向对象编程而言,一个悖论是我们无法直接使用对象进行面向对象编程。相反,我们必须使用指针和引用。指针会增加程序的复杂性,因此我们经常会定义一些辅助的类来处理这种复杂情况:
class Basket {
public:
// Basket 使用合成的默认构造函数和拷贝控制成员
void add_item(const std::shared_ptr<Quote> &sale)
{ items.insert(sale); }
// 打印每本书的总价和购物篮中所有书的总价
double total_receipt(std::ostream&) const;
private:
// 该函数用于比较 shared_ptr, multiset 成员会用到它
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);
// 由于 Quote 并非内置类型, 因此 compare 的定义相当于给出了 shared_ptr<Quote> 的比较规则
// 这样才能够将它存储到 multiset 这个关联容器当中
};
// 👇同时重新给出我们之前定义的 Quote 及其派生了 Bulk_quote
class Quote {
public:
Quote() = default; // 默认构造函数
Quote(const std::string &book, double sales_price):
bookNo(book), price(sales_price) { }
std::string isbn() { return bookNo; }
virtual double net_price(std::size_t n) const
{ return n * price; }
virtual ~Quote() = default; // 对析构函数进行动态绑定
private:
std::string bookNo;
protected:
double price = 0.0;
};
class Bulk_quote: public Quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double);;
double net_price(std::size_t) const override;
// 👆 覆盖基类的函数版本以实现基于大量购买的折扣政策
private:
std::size_t min_qty = 0;
double discount = 0.0;
};
我们的类使用一个 multiset 来存放交易信息,这样我们就可以保存同一本书的多条交易记录,而且对于一本给定的数据,它所有的交易信息都将保存在一起(因为 multiset 使用 compare 对 isbn 进行排序,使得 isbn 相同的记录被连续地排列在一起)。
multiset 的元素是 shared_ptr,由于 shared_ptr 没有自己定义的小于运算符,因此为了对元素进行排序,我们必须提供自己的比较运算符。在此例中我们定义了一个名为 compare 的私有静态成员,该成员负责比较 shared_ptr 所指对象的 isbn。
我们初始化 multiset,通过类内初始值调用比较函数:
std::multiset<std::shared_ptr<Quote, decltype(compare)*> items(compare);
定义 Basket 成员
Basket 类只定义了两个操作,第一个成员是我们在类的内部定义的 add_item 成员,该成员接受一个指向动态分配的 Quote 的 shared_ptr,然后将这个 shared_ptr 放置在 multiset 中。
第二个成员是 total_receipt,它负责将购物篮内的内容逐项打印成清单,然后返回购物篮中所有物品的总价格:
double Basket::total_receipt(ostream &os) const {
double sum = 0.0;
for(auto iter = item.cbegin();
iter != item.cend();
iter = item.upper_bound(*iter)) {
sum += print_total(os, **iter, items.count(*iter));
}
os << "Total Sale: " << sum << endl;
return sum;
}
该 for 循环中的“递增”表达式与通常的循环语句依此读取每个元素不同。我们直接令 iter 指向下一个关键字,调用 upper_bound 函数可以令我们跳过与当前关键字相同的所有元素。对于 upper_bound 函数来说,它返回一个迭代器,该迭代器指向所有与 iter 关键字相同的元素的最后一个元素的下一个位置。因此,调用 iter = upper_bound(*iter) 的结果或是指向下一本书籍,或是指向 multiset 的尾后迭代器。
在 for 内部调用 print_total 来打印购物篮中每本书的细节:
sum += print_total(os, **iter, item.count(*iter));
print_total 的实参包括一个用于写入数据的 ostream、一个待处理的 Quote 对象和一个计数值。当我们解引用 iter 后将得到一个指向准备打印的对象的 shared_ptr。为了进一步得到对应的对象,我们应该进一步对 shared_ptr 进行解引用,因此 **iter 是一个 Quote 对象。使用 multiset 的 count 成员来统计在 multiset 中有多少元素的键值相同(即 isbn 相同)。
隐藏指针
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(这样可以使 Busket 的用户方便地使用 add_item 成员来向 Basket 添加数据)。
新版本的 add_item 将负责处理内存分配,这样用户不必困扰于使用哪个版本的 make_shared 了。此处定义两个版本,一个拷贝它给定的对象,另一个采取移动操作(使用右值引用):
void add_item(const Quote& sale); // 拷贝给定的对象
void add_item(Quote&& sale); // 移动给定的对象
// 👆 需要重点注意的是, 上面给出的两个版本不是构造函数, 它们的返回值是 void, 它们是成员函数
唯一的问题在于 add_item 不知道要分配的类型。当 add_item 进行内存分配时,它将拷贝(或移动)它的 sake 参数,在某处可能会有如下的 new 表达式:
new Quote(sale);
上述表达式可能是错误地,因为 sale 实际指向的可能是 Bulk_quote 对象。此时,该对象被迫被切掉了一部分(即派生类的部分,只保留了基类 Quote 的部分)。
模拟虚拷贝
为了解决上述问题,我们可以为 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{
Bulk_quote* clone() const & { return new Bulk_quote(*this); }
Bulk_quote* clone() && { return new Bulk_quote(std::move(*this)); }
/* ... ... ... */
};
因为我们声明了 add_item 的拷贝和移动版本,所以我们分别定义 clone 的左值(用于拷贝)和右值(用于移动)版本。每个 clone 函数分配当前类型的一个新对象,其中,const 左值引用成员将它拷贝给新分配的对象,右值引用成员则将自己移动到新数据当中。
可以通过 clone 写出新版本的 add_item:
class Basket {
public:
void add_item(const Quote& sale) // 拷贝给定的对象
{ items.insert(std::shared_ptr<Quote>(sale.clone())); }
void add_item(Quote&& sale) // 移动给定的对象
{ items.insert(std::shared_ptr<Quote>(std::move(sale).clone())); }
// 动态绑定: 在 C++ 当中, 当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定.
// add_item 的参数是 Quote 的左值或右值引用, 因此会根据调用对象的动态类型来选择执行哪个版本
};
:容器与继承&spm=1001.2101.3001.5002&articleId=144549206&d=1&t=3&u=3a2786733f6f4da4b09cb1b10b204ea3)
2465

被折叠的 条评论
为什么被折叠?



