5-面向对象编程风格

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并不代表图书馆借阅管理系统中实际存在的任何一个馆藏,仅仅是为了我们设计上的需要而存在。但事实上这个抽象十分关键。我们称之为“抽象基类”。

LibMat
Book
ChildToys
Manazines
Films
RentalBook
AudioBook
CDIBook
             “图书馆借阅系统”中的“外借馆藏类继承体系”
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运算符,它会进行运行进检验操作。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值