目录
- 5.1面向对象编程概念(Object-Oriented Programming Concepts)
- 5.2漫游:面向对象编程思维(A Tour of Object-Oriented Programming)
- 5.3不带继承的多态(Polymorphism without Inheritance)
- 5.4定义一个抽象基类(Defining an Abstract Base Class)
- 5.5定义一个派生类(Defining a Derived Class)
- 5.6运用继承体系(Using an Inheritance Hierarchy)
- 5.7基类应该多么抽象(How Abstract Should a Base Class Be?)
- 5.8初始化、析构、复制(Initialization,Destruction,and Copy)
- 5.9在派生类中定义一个虚函数(Defining a Derived Class Virtual Function)
- 虚函数的静态解析(Static Resolution)
- 5.10运行时的类型鉴定机制(Run-Time Type Identification)
5.1面向对象编程概念(Object-Oriented Programming Concepts)
面向对象编程概念的两项最主要特质是:继承(inheritance)
和多态(polymorphism)
。
继承:
使我们得以将一群相关的类组织起来,并让我们得以分享其间的共通数据和操作行为。
继承机制定义了父子(parent/child)关系。父类(parent)定义了所有子类(children)共通的公有接口(public interface)和私有实现(private implementation)。每个子类都可以增加或覆盖(override)继承而来的东西,以实现其自身独特的行为。
在C++中,父类被称为基类(base class),子类被称为派生类(derived class)。父类和子类之间的关系则称为继承体系(inheritance hierarchy)。
多态:
让我们在这些类之上进行编程时,可以如同操控单一个体,而非相互独立的类,并赋予我们更多弹性来加入或移除任何特定类。让基类的pointer或reference得以十分透明地(transparently)指向其任何一个派生类的对象。
抽象基类(abstract base class):LibMat。
LibMat用来定义图书馆借阅管理系统中所有馆藏的共通操作行为,包括:check_in()、check_out()、due_date()、find()、location(),等等。
LibMat并不代表图书馆借阅管理系统中实际存在的任何一个馆藏,仅仅是为了我们设计上的需要而存在。但事实上这个抽象十分关键。我们称之为“抽象基类”。
“图书馆借阅系统”中的“外借馆藏类继承体系”
void loan_check_in(LibMat &mat) {
// mat实际上代表某个派生类的对象(derived clas object)
// 诸如Book、RetalBook、Magazines,等等
mat.check_in();
if (mat.is_late())
mat.assess_fine();
if (mat.waiting_list())
mat.notify_available();
}
以上述的loan_check_in()为例,mat总是指向(代表)LibMat的某个派生对象。但究竟是哪一个?答案是除非程序实际执行的当下,否则无法确实。而且,loan_check_in()的每次执行情况可能不同。
每次loan_check_in()执行时,仅能在执行过程中依据mat所指的实际对象来决定调用哪一个check_in()。
5.2漫游:面向对象编程思维(A Tour of Object-Oriented Programming)
默认情形下,member function的解析(resolution)皆在编译时静态地进行。若要令其在运行时动态进行,我们就得在它的声明前加上virtual
。LibMat的声明表示,其destructor和print()皆为virtual(虚函数)。
class LibMat {
public:
LibMat() {
cout << "LibMat::LibMat() default constructor!\n";
}
virtual ~LibMat() {
cout << "LibMat::~LibMat() destructor!\n";
}
virtual void print() const {
cout << "LibMat::print() -- I am a LibMat object!\n";
}
};
现在,我们定义一个非成员函数print(),它接受一个形式为const LibMat reference的参数:
void print(const LibMat &mat) {
cout << "in global print(): about to print mat.print()\n";
// 下一行会依据mat实际指向的对象,
// 解析该执行哪一个print() member function。
mat.print();
}
第一次调用操作像下面这样:
LibMat libmat;
print(libmat);
以下是跟踪结果:
// 构造LibMat libmat
LibMat::LibMat() // 默认构造函数
// 处理print(libmat)
in global print(): about to print mat.print()
LibMat::print() -- I am a LibMat object!
// 析构Libmat libmat
LibMat::~LibMat() // 析构函数
当我们将Book对象传入print()时:
Book b("The Castle", "Franz Kafka");
print(b);
以下是跟踪结果:
// 构造Book b
LibMat::LibMat() // A1--默认构造函数
Book::Book(The Castle, Franz Kafka) // A2--构造函数
// 处理print(b)
in global print(): about to print mat.print()
Book::print() -- I am a Book object!
My title is: The Castle
My author is: Franz Kafka
// 析构Book b
Book::~Book() // B1--板构函数
LibMat::~LibMat() // B2--析构函数
在main()程序中重复调用print(),并依次将LibMat对象、Book对象、AudioBook对象当作参数传递给它。每次print()被执行,都会依据mat实际所指的对象,在LibMat、Book、AudioBook三者之间挑选正确的print()成员函数加以调用。
被调用的函数是Book::print()而非LibMat::print()。
当程序定义出一个派生对象,基类和派生类的构造函数都会被执行。当派生对象被销毁,基类和派生类的析构函数也都会被执行,但次序颠倒。
如何实现派生类Book呢?
class Book : public LibMat {
public:
Book(const string &title, const string &author): _title(title), _author(author) {
cout << "Book::Book( " << _title << ", " << _author << " ) constructor\n";
}
virtual ~Book() {
cout << "Book::~Book() destructor!\n";
}
virtual void print() const {
cout << "Book::print() -- I am a Book object!\n"
<< "My title is: " << _title << '\n'
<< "My author is: " << _author << endl;
}
const string& title() const { return _title; }
const string& author() const { return _author; }
protected:
string _title;
string _author;
};
Book中的print()覆盖(override)了LibMat的print()。这也正是mat.print()所调用的函数。title()和author()是两个所谓的访问函数(access function),都是non-virtual inline函数。被声明为protected
的所有成员都可以被派生类直接访问,除派生类之外,都不得访问protected成员。
如何实现AudioBook这个派生类呢?
class AudioBook : public Book {
public:
AudioBook(const string &title, const string &author, const string &narrator)
: Book(title, author), _narrator(narrator) {
cout << "AudioBook::AudioBook( " << _title
<< ", " << _author
<< ", " << _narrator
<< " ) constructor\n";
}
~AudioBook() {
cout << "AudioBook::~AudioBook() destructor!\n";
}
virtual void print() const {
cout << "AudioBook::print() -- I am an AudioBook object!\n"
// 注意,以下直接访问继承而来的data member: _title和_author
<< "My title is: " << _title << '\n'
<< "My author is: " << _author << '\n'
<< "My narrator is: " << _narrator << endl;
}
const string& narrator() const { return _narrator; }
protected:
string _narrator;
};
合并在一起,一个能够编译运行通过的完整例子:
#include <iostream>
using namespace std;
class LibMat {
public:
LibMat() {
cout << "LibMat::LibMat() default constructor!\n";
}
virtual ~LibMat() {
cout << "LibMat::~LibMat() destructor!\n";
}
virtual void print() const {
cout << "LibMat::print() -- I am a LibMat object!\n";
}
};
void print(const LibMat& mat) {// 非成员方法
cout << "in global print(): about to print mat.print()\n";
mat.print();
}
class Book : public LibMat {
public:
Book(const string& title, const string& author) : _title(title), _author(author) {
cout << "Book::Book( " << _title << ", " << _author << " ) constructor\n";
}
virtual ~Book() {
cout << "Book::~Book() destructor!\n";
}
virtual void print() const {
cout << "Book::print() -- I am a Book object!\n"
<< "My title is: " << _title << '\n'
<< "My author is: " << _author << endl;
}
const string& title() const {
return _title;
}
const string& author() const {
return _author;
}
protected:
string _title;
string _author;
};
class AudioBook : public Book {
public:
AudioBook(const string& title, const string& author, const string& narrator)
: Book(title, author), _narrator(narrator) {
cout << "AudioBook::AudioBook( " << _title
<< ", " << _author
<< ", " << _narrator
<< " ) constructor\n";
}
~AudioBook() {
cout << "AudioBook::~AudioBook() destructor!\n";
}
virtual void print() const {
cout << "AudioBook::print() -- I am an AudioBook object!\n"
// 注意,以下直接访问继承而来的data member: _title和_author
<< "My title is: " << _title << '\n'
<< "My author is: " << _author << '\n'
<< "My narrator is: " << _narrator << endl;
}
const string& narrator() const {
return _narrator;
}
protected:
string _narrator;
};
int main() {
LibMat libmat;
print(libmat);
Book book("X", "Y");
print(book);
AudioBook audiobook("a", "b", "c");
print(audiobook);
}
LibMat::LibMat() default constructor!
in global print(): about to print mat.print()
LibMat::print() -- I am a LibMat object!
LibMat::LibMat() default constructor!
————————————————————————
Book::Book( X, Y ) constructor
in global print(): about to print mat.print()
Book::print() -- I am a Book object!
My title is: X
My author is: Y
LibMat::LibMat() default constructor!
Book::Book( a, b ) constructor
————————————————————————
AudioBook::AudioBook( a, b, c ) constructor
in global print(): about to print mat.print()
AudioBook::print() -- I am an AudioBook object!
My title is: a
My author is: b
My narrator is: c
AudioBook::~AudioBook() destructor!
Book::~Book() destructor!
LibMat::~LibMat() destructor!
Book::~Book() destructor!
LibMat::~LibMat() destructor!
LibMat::~LibMat() destructor!
从以上可以看到,destructor是最后次序颠倒依次执行的。并且不像JAVA,JAVA的是abstract class,里面的方法可以是abstract的,也可以直接定义,abstract class不能直接被实例化,所以抽象方法不可能被调用。class才可实例化并调用方法。如果不继承实现抽象方法,是不可以实例化的。而C++中虚函数可以直接定义,也是可以直接调用的。
###对于【JAVA】###
// A.java
public abstract class A {
abstract int go();
int yes() {
return 1;
}
}
——————————————————————————
// B.java
public class B extends A {
@Override
int go() {
return 0;
}
}
// T.java
public class T {
public static void main(String[] args) {
A a = new A();// 报错:Cannot instantiate the type A
B b = new B();// 正确
b.go();
}
}
###对于【C++】###
去掉所有virtual
关键字,结果是一样的。
使用派生类时不必刻意区分“继承而来的成员”和“自身定义的成员”,两者的使用完全透明。
5.3不带继承的多态(Polymorphism without Inheritance)
上节中的num_sequence class模拟了多态的行为。
for (int i = 1; i < num_sequence::num_of_sequences(); ++i) {
ns.set_sequence(num_sequence::nstype(i));
int elem_val = ns.elem(pos);
// ...
}
对于ns,该类的每一个对象都含有data member _isa,用心记录它目前代表的数列类型。
class num_sequence {
public:
// ...
private:
vector<int> *_elem;// 指向一个vector,后者用来储存数列元素
PtrType _pmf;// 指向元素产生器(函数)
ns_type _isa;// 目前的数列类型
// ...
};
_isa被赋予一个常量名称,此名称用以表示某个数列类型。所有数列类型名称被放在一个名为ns_type的枚举类型(enumerated type)中:
class num_sequence {
public:
enum ns_type {
ns_unset, ns_fibonacci, ns_pell, ns_lucas,
ns_triangular, ns_square, ns_pentagonal
};
// ...
};
下面这个nstype()函数会检验其整数参数是否代表某一有效数列。如果参数有效,nstype()就返回对应的enumerator(枚举项),否则返回ns_unset:
class num_sequence {
public:
// ...
static ns_type nstype(int num) {
return num <= 0 || num >= num_seq ? ns_unset : static_cast<ns_type>(num);
}
};
static_cast
是个特殊转换记号,可将整数num转换为对应的ns_type枚举项。nstype()的结果再被我们传给set_sequence():
ns.set_sequence(num_sequence::nstype(i));
于是,set_sequence()将当前数列的_pfm、_isa、_elem等data member设定妥当:
void num_sequence::set_sequence(ns_type nst) {
switch(nst) {
default:
cerr << "invalid type: setting to 0\n";
// 刻意让它继续执行下去,不做break操作!
case ns_unset:
_pmf = 0;
_elem = 0;
_isa = ns_unset;
break;
case ns_fibonacci:
case ns_pell:
case ns_lucas:
case ns_triangular:
case ns_square:
case ns_pentagonal:
// 以下func_tbl是个指针表,每个指针指向一个成员函数
// 以下seq是个vector,其内又是一些vector,用来储存数列元素
_pmf = func_tbl[nst];
_elem = &seq[nst];
_isa = nst;
break;
}
}
为了让外界查询此对象目前究竟代表何种数列,还提供一个what_am_i()操作函数,返回一个表示数列类型的字符串。用法如下:
inline void display(ostream &os, const num_sequence &ns, int pos) {
os << "The element at position "
<< pos << " for the "
<< ns.what_am_i() << " sequence is "
<< ns.elem(pos) << endl;
}
what_am_i()系利用_isa对一个静态字符串数组进行索引操作,此数组按照ns_type枚举项的顺序,列出本程序支持的所有数列名称:
const char* num_sequence:: what_am_i() const {
static char *names[num_seq] = {
"notset", "fibonacci", "pell", "lucas", "triangular", "square", "pentagonal"
};
return names[_isa];
}
5.4定义一个抽象基类(Defining an Abstract Base Class)
重新设计上一节的num_sequence class。要为所有数列类设计出共享的抽象基类,然后继承它。
class num_sequence {
public:
// elem(pos): 返回pos位置上的元素
// gen_elems(pos): 产生直到pos位置的所有元素
// what_am_i(): 返回确切的数列类型
// print(os): 将所有元素写入os
// check_integrity(pos): 检查pos是否为有效位置
// max_elems(): 返回所支持的最大位置值
int elem(int pos);
void gen_elems(int pos);
const char* what_am_i() const;
ostream& print(ostream &os = cout) const;
bool check_integrity(int pos);
static int max_elems();
// ...
};
找出哪些操作作为与类型相关(type-dependent)——也就是说,有哪些操作作为必须根据不同的派生类而有不同的实现方式。这些操作行为应该成为整个类继承体系中的虚函数(virtual function)。
注意:
➀static member function无法被声明为虚函数。
➁基类的派生类无法访问基类的private member。
纯虚函数:
每个虚函数,要么得有其定义,要么可设为“纯”虚函数(pure virtual function)。将虚函数赋值为0,意思便是令它为一个纯虚函数:
virtual void gen_elems(int pos) = 0;
重新修整后的num_sequence class定义:
class num_sequence {
public:
virtual ~num_sequence() { };
virtual int elem(int pos) const = 0;
virtual const char* what_am_i() const = 0;
static int max_elems() { return _max_elems; }
virtual ostream& print(ostream &os = cout) const = 0;
protected:
virtual void gen_elems(int pos) const = 0;
bool check_integrity(int pos) const;
const static int _max_elems = 1024;
};
这种纯虚函数就和JAVA中的抽象类相似,任何一个类如果声明有一个或多个纯虚函数,那么,由于其接口不完整性,程序无法为它产生任何对象。这种类只能为派生类的子对象(subobject)使用,而且前提是这些派生类必须为所有虚函数提供确切的定义。
由于上述类没有任何non-static data member需要进行初始化操作,所以其constructor无存在价值。
不过,会为它设计destructor。根据一般规则,凡是基类定义有一个或多个虚函数,应该要将其destructor声明为virtual,像这样:
class num_sequence {
public:
virtual ~num_sequence();
// ...
};
为什么呢?考虑以下程序片段:
num_sequence *ps = new Fibonacci(12);
// ...使用数列
delete ps;
ps是基类num_sequence的指针,但它实际上指向派生类Fibonacci的对象。当delete表达式被应用于该指针,destructor会先应用于指针所指的对象上,于是将此对象占用的内存空间归还给程序的空闲空间(free store)。
于是,本例中,通过ps调用的destructor一定是Fibonacci的destructor,不是num_sequence的destructor。正确情况应该是“根据实际对象的类型选择调用哪一个destructor”,而此解析操作应该在运行时进行。为了促成正确行为的发生,我们必须将destructor声明为virtual。
但并不建议在这个基类中将其destructor声明为pure virtual——虽然它其实不具有任何实质意义的实现内容。对这类destructor而言,最好是提供空白定义,像下面这样:
inline num_sequence::~num_sequence() { }
下面列出num_sequence的output运算符和check_integrity()函数实现:
(在类之外对虚函数进行定义时,不必指明关键字virtual。)
bool num_sequence::check_integrity(int pos) const {
if (pos <= 0 || pos > _max_elems) {
cerr << "!!invalid position:" << pos << " Cannot honor request\n";
return false;
}
return true;
}
ostream& operator<<(ostream &os, const num_sequence &ns) {
return ns.print(os);
}
5.5定义一个派生类(Defining a Derived Class)
派生类由两部分组成:
➊基类构成的子对象(subobject),由基类的non-static data member组成。
➋派生类部分,由派生类non-static data member组成。
// 头文件含有基类的定义
#include "num_sequence.h"
class Fibonacci : public num_sequence {
public:
// ...
};
Fibonacci class必须为从基类继承而来的每个纯虚函数提供对应的实现。除此之外,它还必须声明Fibonacci class专属member。以下便是Fibonacci class的定义:
class Fibonacci : public num_sequence {
public:
Fibonacci(int len = 1, int beg_pos = 1): _length(len), _beg_pos(beg_pos) { }
virtual int elem(int pos) const;
virtual const char* what_am_i() const { return "Fibonacci"; }
virtual ostream& print(ostream &os = cout) const;
int length() const { return _length; }
int beg_pos() const { return _beg_pos; }
protected:
virtual void gen_elems(int pos) const;
int _length;
int _beg_pos;
static vector<int> _elems;
};
基类可以public、protected和private三种方式继承而来。甚至还有多重继承和虚继承。这些都是比较复杂而高级的主题,这里并不涵盖。
每个派生类都有长度和起始位置这两项data member。length()和beg_pos()这两个函数被声明为non-virtual,因为它们并无基类所提供的实体可供覆盖。但也因为它们并非基类提供的接口的一员,所以无法通过基类的pointer或reference来访问它们。
ps->what_am_i();// OK,通过虚函数机制,调用了Fibonacci::what_am_i()
ps->max_elems();// OK,调用继承而来的num_sequence::max_elems()
ps->length();// 错误,length()并非num_sequence接口中的一员
delete ps;// OK,通过虚函数机制调用Fibonacci destructor
如果“通过基类的接口无法访问length()和beg_pos()”会对我们造成困扰,那么我们应该回过头修改基类的接口。重新设计方式之一便是在基类num_sequence内加上两个纯虚函数length()和beg_pos()。这样一来便会“自动”派生类的beg_pos()和length()都成为虚函数——它们不再指定关键字virtual。如果必须加上关键字virtual,那么修改基类的虚函数就得大费周章:每个派生类都必须对它重新声明。
另一种重新设计方式是,将储存长度和起始位置的空间,由派生类抽离出来,移至基类,于是length()和beg_pos()都成了继承而来的inline nonvirtual function。
以下是elem()的实现。派生类的虚函数必须精确吻合基类中的函数原型。在类之外对虚函数进行定义时,不必指明关键字virtual:
int Fibonacci::elem(int pos) const {
if (!check_integrity(pos))
return 0;
if (pos > _elems.size())
Fibonacci::gen_elems(pos);
return _elems[pos - 1];
}
elem()调用继承来的check_integrity(),其形式仿佛后者为其自身成员一般。
一般来说,继承而来的public成员和protected成员,不论在继承体系中的深度如何,都可被视为派生类自身拥有的成员。
基类的public member在派生类中同样也是public,同样开放给【派生类的用户】使用。基类的protected member在派生类中同样也是protected,同样只能给后续【派生类】使用,无法给目前这个【派生类的用户】使用。至于基类的private member,则完全无法让派生类使用。(以上讨论仅限于public inheritance(公有继承)。如果是protected inheritance或private inheritance,情况就不一样了。此处不讨论后两种继承方式。)
class Base {
protected:
int b_;// line 3
public:
bool IsEqual(const Base& another) const {
// OK:access another instance's protected member
return another.b_ == b_;
}
};
class Derived : public Base {
protected:
Base base_;
public:
bool IsEqual_Another(const Derived& another) const {
return another.b_ == b_;// OK
}
void TestFunc() {
// ERROR-E0410:C++ protected member (declared at line 3) is not accessible through a pointer or object.
// ERROR-C2248:'Base::b_': cannot access protected member declared in class 'Base'.
int b = base_.b_;// ERROR
int x = b_;// OK
}
};
无法访问独立的Base实例的受保护成员,因为它是基于类的。C/C++访问控制是基于类而不是基于实例的。
总结:C++的访问修饰符的作用是以类为单位,而不是以对象为单位。
通俗的讲,同类的对象间可以“互相访问”对方的数据成员,只不过访问途径不是直接访问。步骤是:通过一个对象调用其public成员函数,此成员函数可以访问到自己的或者同类其他对象的public/private/protected数据成员和成员函数(类的所有对象共用),而且还需要指明是哪个对象的数据成员(调用函数的对象自己的成员不用指明,因为有this指针;其他对象的数据成员可以通过引用或指针间接指明)。
通过class scope运算符,我们可以明确告诉编译器,我们想调用哪一份函数实例。
以下是gen_elems()和print()的实现:
void Fibonacci::gen_elems(int pos) const {
if (_elems.empty()) {
_elems.push_back(1);
_elems.push_back(1);
}
if (_elems.size() <= pos) {
int ix = _elems.size();
int n_2 = _elems[ix - 2];
int n_1 = _elems[ix - 1];
for (; ix <= pos; ++ix) {
int elem = n_2 + n_1;
_elems.push_back(elem);
n_2 = n_1;
n_1 = elem;
}
}
}
ostream& Fibonacci::print(ostream &os) const {
int elem_pos = _beg_pos - 1;
int end_pos = elem_pos + _length;
if (end_pos > _elems.size())
Fibonacci::gen_elems(end_pos);
while (elem_pos < end_pos)
os << _elems[elem_pos++] << ' ';
return os;
}
这个操作必须写成Fibonacci::gen_elems(pos),不能简单地写gen_elems(),明确得很,不必等到运行时才进行gen_elems()的解析操作。这就是我们指明调用对象的原因。
我们该如何修改check_integrity()以使这个检验操作的确能检验出pos的有效性呢?一个可能的方式是为Fibonacci class编写一份check_integrity():
class Fibonacci : public num_sequence {
public:
// ...
protected:
bool check_integrity(int pos) const;
// ...
};
于是,在Fibonacci class内,对check_integrity()的每次调用都会被解析为派生类中的那一份函数实例。如下:
int Fibonacci::elem(int pos) const {
// 现在,调用的是Fibonacci的check_integrity()
if (!check_integrity(pos))
return 0;
// ...
}
每当派生类有某个member与其基类的member同名,便会遮掩住基类的那份member。也就是说,派生类内对该名称的任何使用,都会被解析为该派生类自身的那份member,而非继承来的那份member。这种情况下,如果要在派生类内使用继承来的那份member,必须利用class scope运算符加以限定。例如:
inline bool Fibonacci::check_integrity(int pos) const {
// 必须加上class scope运算符。
// 如果未经限定,会解析为派生类自身的函数!
if (!num_sequence::check_integrity(pos))
return false;
if (pos > _elems.size())
Fibonacci::gen_elems(pos);
return true;
}
在基类中check_integrity()并未被视为虚函数。于是,每次通过基类的pointer或reference来调用check_integrity(),解析出来的都是num_sequence那一份,并未考虑到pointer或reference实际指向的究竟是什么对象。如:
void Fibonacci::example() {
num_sequence *ps = new Fibonacci(12, 8);
ps->elem(1024);// 通过虚拟机动态解析为Fibonacci::elem()
ps->check_integrity(pos);// 喔欧:根据ps的类型,会静态解析为num_sequence::check_integrity()
不好的解决方法:在基类和派生类中提供同名的non-virtual函数。
基于此点而归纳出的结论或许是:基类中的所有函数都应该被声明为virtual。此结论并不能认为是个正确的结论,但它的确可以马上解决我们所面临的两难困境。
所谓设计,必须来来回回地借着程序员的经验和用户的反馈演进。本例中,较好的解决方案是重新定义check_integrity(),令它拥有两个参数:
bool num_sequence::check_integrity(int pos, int size) {
if (pos <= 0 || pos > _max_elems)
// 和先前相同...
}
if (pos > size)
// gen_elems()系通过虚拟机调用
gen_elems(pos);
return true;
}
新版本会如下方式被使用:
int Fibonacci::elem(int pos) {
if (!check_integrity(pos, _elems.size()))
return 0;
// ...
}
5.6运用继承体系(Using an Inheritance Hierarchy)
现在,我们有了一个两层的继承体系,其中包括抽象基类num_sequence,以及六个派生类。以下有个简单的display()函数:
inline void display(ostream &os, const num_sequence &ns, int pos) {
os << "The element at position "
<< pos << " for the "
<< ns.what_am_i() << " sequence is "
<< ns.elem(pos) << endl;
}
int main() {
const int pos = 8;
Fibonacci fib;
display(cout, fib, pos);
Pell pell;
display(cout, pell, pos);
Lucas lucas;
display(cout, lucas, pos);
Triangular trian;
display(cout, trian, pos);
Square square;
display(cout, square, pos);
Pentagonal penta;
display(cout, penta, pos);
}
The element at position 8 for the Fibonacci sequence is 21
The element at position 8 for the Pell sequence is 408
The element at position 8 for the Lucas sequence is 47
The element at position 8 for the Triangular sequence is 36
The element at position 8 for the Square sequence is 64
The element at position 8 for the Pentagonal sequence is 92
5.7基类应该多么抽象(How Abstract Should a Base Class Be?)
我们重新定义elem()和print(),使它们成为num_sequence的public member。
class num_sequence {
public:
virtual ~num_sequence(){}
virtual const char* what_am_i() const = 0;
int elem(int pos) const;
ostream& print(ostream &os = cout) const;
int length() const { return _length; }
int beg_pos() const { return _beg_pos; }
static int max_elems() { return 64; }
protected:
virtual void gen_elems(int pos) const = 0;
bool check_integrity(int pos, int size) const;
num_sequence(int len, int bp, vector<int> &re)
: _length(len), _beg_pos(bp), _relems(re) {}
int _length;
int _beg_pos;
vector<int> &_relems;
};
以下是改版后的Fibonacci类定义:
class Fibonacci : public num_sequence {
public:
Fibonacci(int len = 1, int beg_pos = 1);
virtual const char* what_am_i() const { return "Fibonacci"; }
protected:
virtual void gen_elems(int pos) const;
static vector<int> _elems;
};
5.8初始化、析构、复制(Initialization,Destruction,and Copy)
num_sequence扮演的角色是每个派生类对象的子对象。基于这个原因,我们将基类的constructor声明为protected而非public。
派生类对象的初始化行为:
先调用基类的constructor,然后再调用派生类自己的constructor。
派生类的constructor,不仅必须为派生类的data member进行初始化操作,还需要为其基类的data member提供适当的值。
本例中,基类num_sequence需要三个值,都需要通过member initialization list传入:
inline Fibonacci::
Fibonacci(int len, int beg_pos): num_sequence(len, beg_pos, _elems) {}
如果我们忽略了上述对num_sequence constructor的调用操作,这一份Fibonacci constructor定义就会被编译器视为错误。为什么?因为基类num_sequence要求我们指定调用哪一个constructor(本例为具有三个参数者)。在我们的设计中,这正是我们想要的。
另一种做法是,为num_sequence提供default constructor。不过,我们必须将_relems改为指针,并且在每次访问vector内容前,都检验这个指针是否不为null:
num_sequence::
num_sequence(int len = 1, int bp = 1, vector<int> *pe = 0)
: _length(len), _beg_pos(bp), _pelems(pe) {}
现在,如果派生类的constructor未能明确指出调用基类的哪一个constructor,编译器便会自动调用基类的default constructor。
当我们以某个Fibonacci对象作为另一个Fibonacci对象的初值时,会发生什么呢?
Fibonacci fib1(12);
Fibonacci fib2 = fib1;
如果我们为Fibonacci定义了一个copy constructor,以上便会调用该copy constructor。我们可能会以如下方式定义Fibonacci的copy constructor:
Fibonacci::
Fibonacci(const Fibonacci &rhs) : num_sequence(rhs) {}
其中rhs代表等号右边的派生类对象,它在member initialization list中被传给基类copy constructor。如果基类未自行定义copy constructor,那会怎样?不会太糟,因为default memberwise initialization程序会起来运行。而如果我们为基类定义了copy constructor,它便会被调用。
本例并不需要另行定义Fibonacci的copy constructor,因为默认行为便能够达到同等效果:首先,基类子对象会被逐一初始化,然后派生类的member亦会被逐一初始化。
Copy assignment operator的情形也一样。如果我们将某个Fibonacci对象赋值(assign)给另一个Fibonacci对象,而且Fibonacci class拥有明确定义的copy assignment operator,它便会在赋值操作发生时调用。以下便是copy assignment operator的一种定义方式,唯一棘手的地方是,必须明确调用基类的copy assignment operator:
Fibonacci& Fibonacci::
operator=(const Fibonacci &rhs) {
if (this != &rhs)
// 明确调用基类的copy assignment operator
num_sequence::operator=(rhs);
return *this;
}
}
5.9在派生类中定义一个虚函数(Defining a Derived Class Virtual Function)
如果我们继承了纯虚函数(pure virtual function),那么这个派生类也会被视为抽象类,也就无法为定定义任何对象。
基类what_am_i()被声明为const,派生类的what_am_i()却是non-const,这会造成什么重大影响吗?的确会。如下示范:
class num_sequence {
public:
virtual const char* what_am_i() const {
return "num_sequence\n";
}
};
class Fibonacci : public num_sequence {
public:
virtual const char* what_am_i() {
return "Fibonacci";
}
};
int main() {
Fibonacci b;
num_sequence p;
num_sequence *pp = &b;
cout << pp->what_am_i();// 预期输出:Fibonacci,but Not!
cout << b.what_am_i();
return 0;
}
num_sequence
Fibonacci
没有按预期输出,派生类所提供的函数,并未被用来覆盖基类所提供的同名函数。原因就是:两个函数并非完全吻合!省略掉中间或最后的任何一个const都不属于“完全吻合”。
“返回类型必须完全吻合”这一规则有个例外——当基类的虚函数返回某个基类形式(通常是pointer或reference)时:
class num_sequence {
public:
// 派生类的clone()函数可返回一个指针,
// 指向num_sequence的任何一个派生类。
virtual num_sequence *clone() = 0;
// ...
};
派生类中的同名函数便可以返回该基类所派生出来的类型:
class Fibonacci : public num_sequence {
public:
// ok:Fibonacci乃派生自num_sequence。
// 注意,在派生类中,关键字virtual并非必要
Fibonacci *clone() {
return new Fibonacci(*this);
}
// ...
};
虚函数的静态解析(Static Resolution)
有两种情况,虚函数机制不会出现预期行为:
(1)基类的constructor和destructor内;
(2)当我们使用的是基类对象,而非基类对象的pointer或reference时。
当构造派生类对象时,基类的constructor会先被调用。如果在基类的constructor中调用某个虚函数,会发生什么事?调用的应该是派生类所定义的那一份吗?
问题出在此刻派生类中的data member尚未初始化。如果此时调用派生类的那一份虚函数,它便有可能访问未经初始化的data member,这可不好!
基于这个原因,在基类constructor中,派生类的虚函数绝对不会被调用。
例如,num_sequence constructor内如果调用what_am_i(),无论如何一定会被解析为num_sequence自身的那一份what_am_i()。
如果在基类的destructor中调用虚函数,此规则同样成立。
考虑以下程序片段:(print()在类继承体系中是个虚函数)
void print(LibMat object, const LibMat *pointer, const LibMat &reference) {
// 以下必定调用LibMat::print()
object.print();
// 以下一定会通过虚函数机制来进行解析,
// 我们无法预知哪一份print()会被调用。
pointer->print();
reference.print();
}
为了能够“在单一对象中展现多种类型”,多态(polymorphism)需要一层间接性。在C++中,唯有用基类的pointer和reference才能够支持面向对象编程概念。
当我们为基类声明一个实际对象(例如上述print()函数的第一个参数),同时也就分配出了足以容纳该实际对象的内存空间。如果稍后传入的却是个派生类对象,那就没有足够的内存放置派生类中的各个data member。
例如,当我们将AudioBook对象传给print():
int main() {
AudioBook iWish("Her Pride of 10", "Stanley Lippman", "Jeremy Irons");
// void print(LibMat object, const LibMat *pointer, const LibMat &reference)
print(iWish, &iWish, iWish);
// ...
}
只有iWish内的“基类子对象(也就是属于LibMat的成分)”被复制到“为参数object而保留的内存”中。其他的子对象(Book成分和AudioBook成分)则被切掉(slice off)了。至于另两个参数,pointer和reference,则被初始化为iWish对象所在的内存地址。这就是它们能够指向完整的AudioBook对象的原因。
5.10运行时的类型鉴定机制(Run-Time Type Identification)
每个类都拥有一份what_am_i()函数,都返回一个足以代表该类的字符串:
class Fibonacci : public num_sequence {
public:
virtual const char* what_am_i() const {
return "Fibonacci";
}
// ...
};
另一种设计手法是只提供唯一一份what_am_i(),令各派生类通过继承机制加以复用。
这种设计可能的做法是为num_sequence增加一个string member,并令每一个派生类的constructor都将自己的类名作为参数,传给num_sequence的constructor。例如:
inline Fibonacci::
Fibonacci(int len, int beg_pos)
: num_sequence(len, beg_pos, _elems, "Fibonacci") {}
另一种实现便是利用typeid
运算符,这是运行时类型鉴定机制(Run-Time Type Indentification, RTTI)的一部分,由程序语言支持。它让我们得以查询多态化class pointer或class reference,获得其所指对象的实际类型。
#include<typeinfo>
inline const char* num_sequence::
what_am_i() const {
return typeid(*this).name();
}
typeid
运算符返回一个type_info
对象,其中储存着与类型相关的种种信息。
每一个多态类(polymorphic class),如Fibonacci、Pell等等,都对应一个type_info
对象,该对象的name()
函数返回一个const char*
,用以表示类名。
typeid(*this)
返回一个type_info对象,关联至“who_am_i()函数之中由this指针所指对象”的实际类型。
type_info class也支持相等和不等两个比较操作。
num_sequence *ps = &fib;
// ...
if (typeid(*ps) == typeid(fib))
// ok, ps的确指向某个Fibonacci对象
如果这么写:
ps->Fibonacci::gen_elems(64);
编译错误!因为ps并非一个Fibonacci指针——虽然我们知道它现在的确指向某个Fibonacci对象!
为了调用Fibonacci所定义的gen_elems(),我们必须指示编译器,将ps的类型转换为Fibonacci指针。static_cast
运算符可以担起这项任务:
if (typeid(*ps) == typeid(Fibonacci)) {
Fibonacci *pf = static_cast<Fibonacci*>(ps);// 无条件转换
pf->gen_elems(64);
}
static_cast其实有潜在危险,因为编译器无法确认我们所进行的转换操作是否完全正确。
dynamic_cast
运算符就不同,它提供有条件的转换:
if (Fibonacci *pf = dynamic_cast<Fibonacci*>(ps))
pf->gen_elems(64);
dynamic_cast
也是一个RTTI运算符,它会进行运行进检验操作。