16 多态版扑克牌游戏
16.1 多种牌墩
皮纳克尔牌墩:只使用9到A的牌,但要使用两副牌
这个牌只要48张牌,牌24到47重复牌0到23 的牌点和花色
class PinochleDeck {
public:
PinochleDeck();
Card deal_a_card();
private:
int cards[48];
int nCard;
void shuffle();
};
PinochleDeck::PinochleDeck() {
srand(time(nullptr));
for (int i =0; i < 48; ++i) {
cards[i] = i;
}
shuffle();
}
void PinochleDeck::shuffle() {
nCard = 0;
for (int i = 47; i > 0; --i) {
/*
int j = rand() % (i + 1);
swap(cards[i], cards[j]); */
random_shuffle(cards, cards + 48);
}
}
Card PinochleDeck::deal_a_card() {
if (nCard > 47) {
cout << endl << "正在重新洗牌..." << endl;
shuffle();
}
int r = (cards[nCard] % 6)+7; //r=9-A
//牌墩分为一半(%24)再除以6来生成0-3的花色
int s = (cards[nCard++]%24) / 6;
return Card(r, s);
}
在运行时切换牌墩
将my_deck声明为多个类的对象,为每个牌墩都创建一个对象,即使只使用其中一种类型——麻烦,浪费资源!
多态
在调用函数时,能自动调用当前对象的对应实现,即使实现不知道确定的类型
如果一个函数时多态的,那么总是能在运行调用该函数的正确版本
C++支持多态函数,必须无条件满足两个前提条件
- 涉及的类必须通过继承来关联,一个必须从另一个派生,或者都从一个通用基类派生
- 函数必须在基类中声明为virtual
派生类自动继承基类的所有成员,无法访问基类的私有成员
第三种访问级别protected:为所有的派生类(包括派生类的派生类)赋予访问权限
任何由派生类重写(override)的函数必须声明为virtual
构造函数不能为虚
实现多态性的;另一种方式是从一个通用基类(或成为“接口”)派生出各种牌墩类。作为抽象类的接口不能实例化,但可将一个派生类型的对象的地址传给一个基类型的指针:1. 创建对象,2. 获取他的地址,3. 将地址传给一个指针
Ideck *pDeck; //指向基类型Ideck
//创建派生类型的对象,把他的地址赋给指针pDeck
pDeck = new PinochleDeckl; //new动态分配内存
...
aCard[i] = pDeck->deal_a_card;
//等价于aCard[i] = (*pDeck).deal_a_card;
接口(即基类)的指针可以再编译的时候指向一个派生类的对象,也可以在运行时根据实际情况指向不同类型的对象
class IDeck {
public:
virtual Card deal_a_card() = 0;
};
在派生类声明的第一行中,必须在基类名称前附加public前缀,这种情况下可以使用private和protected
继承和虚函数的终极目标:1. 任何对象都可以在运行时选定,只要他的类从一个通用基类派生;2. 将调用每个虚函数的正确实现
虚函数的代价:性能和空间
需要维一个vtable指针,性能损失来源于需要更多时间发出间接函数调用,空间损失来源于vptr和表自身占用的字节)
代价小
16.2 “纯虚”和其他抽象事项
接口(或抽象类)包含纯虚函数,纯虚函数既不需要,也不期待有一个具体实现,使用**=0**记号法来标识纯虚函数。例如:
class Number{
protected:
virtual void normalize = 0;
};
16.3 抽象类和接口
包含一个或多个纯虚函数(原型包含=0)的类称为抽象类。
抽象类的一个重要规则是不可以实例化,即不能用它声明对象
抽象类主要是为他的子类起到一个规范作用,从抽象类派生出子类,实现所有纯虚函数,最后用子类实例化对象
用子类实例化(创建)对象之前,子类必须为所有纯虚函数提供定义。
任何一个函数没有实现,当前类就还是抽象类,不可以实例化
- 每个子类都可采取它想要的任何方式实现所有服务(即纯虚函数)
- 每个服务都需要实现,否则类不能实例化
- 每个类都要要严格遵守类型规定,包括返回类型和每个实参的类型,这为继承层次结构制定了严格的纪律,编译器能提早发现意外
16.4 面向对象和I/O
使用各种流类来扩展输出
cout无限可扩展
要使一个类可打印,只需要operator<<函数
ostream &operator<<(ostream &os,const Fraction &fr){
os<<fr.num<<"/"<<fr.den;
}
使用各种I/O流不仅能输出到控制台(cout),还可输出到任何文件或字符串
Fraction fr(1,1);
cout << "The value is: " <<fr1;
cout不是多态的
较为具体的东西(子类)总是能传给较常规的东西(基类)
总结:
- 函数声明为虚,在所有子类中同样为虚,在子类中重写函数时不需要添加virtual关键字
- 可能被重写的任何成员函数都应声明为虚
- 纯虚函数在声明他的类中通常无具体实现(即没有函数定义)。用=0记号法声明虚函数:
virtual void normalize = 0;
- 含有至少一个纯虚函数的类称为抽象类,不可用它实例化对象,虽然子类可以
- 抽象类主要用于创建接口,子类通过实现所有虚函数来提供一组服务
- 多态性解放了对象,避免他们相互依赖,因为对象自内建了执行服务的逻辑,正是“面向对象”一词的由来,主要面向对象而非只是,面向类