Chapter 5 面向对象编程风格
5.1 面向对象编程概念
**继承(inheritance):**让我们得以将一群相关的类组织起来,分享其间的共同数据和操作行为。
定义了父子关系,父类定义了所有子类共通的公有接口和私有实现。每个子类都可以增加或覆盖继承而来的东西,以实现其自身独特的行为。
**多态(polymorphism):**让我们在这些类之上进行编程时,可以如同操控单一个体,而非相互独立的类,并赋予我们更多弹性来加入或移除任何特定类。
让基类的pointer或reference得以十分透明地指向其任何一个派生类的对象
在面向对象应用程序中,我们会间接利用“指向抽象基类”的pointer或reference来操作系统中的各个实际对象。这让我们在不更改旧有程序的前提下,加入或移除任何一个派生类。
void loan_check_in(LibMat &mat)
{
// mat实际代表某个派生类的对象
mat.check_in();
// ...
}
**动态绑定(dynamic binding):**指解析操作会延迟至运行时
// 在非面向对象的编程风格中,有:
mat.check_in();
// 编译器在编译时就根据mat所属的类决定究竟执行哪一个check_in()函数,由于在执行之前就已经解析出应该调用哪一个函数,因此被称为静态绑定(static binding)。
// 在面型对象编程方法中,编译器无法得知究竟那一份check_in()函数会被调用,每次loan_check_in()执行时,仅能在执行过程中根据mat所知的实际对象来决定究竟调用哪个check_in();找出实际被调用的是哪一个派生类的check_in()函数,这一解析操作会延迟至运行时才进行,即动态绑定
总结来说:
继承特性让我们得以定义一整群互有关联的类,并共享共通的接口;
多态则让我们得以用一种与类型无关的方式来操作这些类对象。我们通过抽象基类的pointer或reference来操纵其共通接口,而实际执行起来的操作则需要等到运行时,依据pointer或reference所指的实际对象的类型才能决定。也就是说,多态和动态绑定的特性只有在使用pointer或reference时才能发挥。
5.2 漫游:面向对象编程思维
默认情形下,member function的解析皆在编译时静态的进行。若要令其在运动时动态进行,就得在他的声明前加上关键字virtual
LibMat
的声明表示,其destructor和print()
皆为虚函数
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(); //会根据mat实际指向的对象解析该执行哪一个print()成员函数
}
// 接下来,在主程序中依次将`LibMat`对象,`Book`对象,`AudioBook`对象当作参数传递给它,每次print()执
// 行,都会根据`mat`实际所指的对象,在`LibMat`,`Book`,`AudioBook`三者中选择正确的print()成员函数
// 使用
int main()
{
// 1
cout<<"\n"<<"Creating a Libmat object to print()\n";
LibMat libmat;
print(libmat);
// 2
cout<<"\n"<<"Creating a Book object to print()\n";
Book b("The Castle","Franz Kafka");
print(b); // 调用的是Book.print()而非LibMat::print()
// 3 AudioBook 省略
return 0;
}
结果分别为:
Creating a Libmat object to print()
LibMat::LibMat() default constructor!
in global print():about to print mat.print()
LibMat::print() -- I am a LibMat object
LibMat::~LibMat() destructor!
---------------------------------
Creating a Book object to print()
LibMat::LibMat() default constructor!
Book::Book( The Castle , Franz Kafka ) constructor
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::~Book() destructor!
LibMat::~LibMat() destructor!
联系上图,可以看到,通过mat.print()
进行的虚拟调用(virtual invocation)操作的确有效!被调用的是Book::print(
)而非LibMat::print()
;另外,当程序定义出一个派生类,基类和派生类的constructor都会被执行,当派生对象被销毁,destructor也都全部执行(但次序颠倒)。
// 如何实现派生类Book?为了标示这个新类是继承自一个已存在的类,其名称之后必须接一个冒号,然后紧跟着关键字public和基类的名称
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()覆盖了LibMat
的print()。这也正是mat.print()所调用的函数。
protected
关键字:被声明为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";
}
virtual ~AudioBook(){
cout<<"AudioBook::AudioBook destructor!\n";
}
virtual void print() const {
cout<<"Book::print() -- I am a AudioBook object!\n"
<<"My title is: "<<_title<<'\n'
<<"My author is: "<<_author<<'\n'
<<"My narrator is: "<<_narrator<<endl;
}
const string& narrator() const {return _narrator;}
protected:
string _narrator;
};
5.3 不带继承的多态
略
5.4 定义一个抽象基类
重新设计4.11节中的num_sequence
class,为所有数列类设计出共享的抽象基类,然后继承它
1.定义抽象类的第一个步骤就是找出所有子类共同的操作
class num_sequence{
public:
int elem(int pos); //返回pos位置上的元素
void gen_elems(int pos); //产生直到pos位置的所有元素
const char* what_am_i() const; //返回一个字符串,代表数列类型
ostream& print(ostream &os); //将所有元素写入os
bool check_intergrity(int pos); //检查pos是否为有效位置
static int max_elems(); //返回所支持的最大位置值
};
2. 设计抽象基类的下一步,设法找出哪些操作行为与类型相关,也就是说,有哪些操作必须根据不同的派生类而有不同的实现方式,这些操作行为应该成为整个类继承体系中的虚函数
例如每个数列类都必须提供他们自己的gen_elems()实现,但check_intergrity()就不会因为类型的不同而有任何差异。
注意:static member function无法被声明为虚拟函数
3.找出每个操作行为的访问层级(access level)。
若某个操作行为应该让一般程序皆能访问,就将他声明为public;例如elem(), max_elems()…
如果某个操作行为在基类之外不需要被用到,就声明为private,即使是该基类的派生类,也无法访问基类中的private member;
protected,这种层级的操作行为可以让派生类访问,却不允许一般程序使用,如check_intergrity(),gen_elems()一般程序不会用到
// 重新修改后num_sequence class的定义
class num_sequence{
public:
virtual ~num_sequence() {
cout<<"this is num_sequence destructor"<<endl;
};
virtual int elem(int pos) const = 0; //返回pos位置上的元素
virtual const char* what_am_i() const = 0; //返回一个字符串,代表数列类型
static int max_elems() {return _max_elems;} //返回所支持的最大位置值
virtual ostream& print(ostream &os = cout) const = 0; //将所有元素写入os
protected:
// 下面=0 否则会出错
virtual void gen_elems(int pos) const = 0; //产生直到pos位置的所有元素
bool check_intergrity(int pos,int size) const; //检查pos是否为有效位置
const static int _max_elems = 1024;
};
ostream& operator<<(ostream& os,const num_sequence &ns)
{
return ns.print(os);
}
- 每个虚函数,要么有其定义,要么可设为**”纯“虚函数**
如果对于该类而言,这个虚函数并无实际意义的话,例如gen_elelms()
之于num_sequence
class,便可将虚函数赋值为0,令其为纯虚函数。
任何类如果声明有一个或多个纯虚函数,那么由于其接口的不完整性(纯虚函数没有函数定义,是谓不完整),程序无法为它产生任何对象,这种类只能作为派生类的子对象使用,而且前提是这些派生类必须为所有虚函数提供确切的定义。
根据一般规则,凡基类定义有一个(或多个)虚函数,应该要将其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)
non-virtual函数在编译时就已完成解析,即根据该对象被调用时的类型来判断。
本例中通过ps调用的destructor一定是Fibonacci的destructor,而非num_sequence的destructor。
正确情况应该是”根据实际对象的类型选择调用哪一个destructor“,而此解析操作应该在运行时进行,为了促成正确行为的发生,必须将destructor声明为virtual
与此同时,不建议在该基类中将其destructor声明为纯虚函数(虽然它其实不具有任何实质意义的实现内容),对这类destructor而言,最好是提供空白定义,如
inline ::~num_sequence() {}
5.5 定义一个派生类
派生类由两部分组成:
- 基类构成的子对象(subobject),由基类的non-static data member组成
- 派生类的部分,由派生类的non-static data member组成
派生类的名称后紧跟着冒号、关键字public,以及基类的名称。唯一规则是,类进行继承声明之前,其基类的定义必须已经存在。
#include "num_sequence.h"
class Fibonacci : public num_sequence{
public:
// ...
};
Fibonacci
class必须为从基类继承而来的每个纯虚函数提供对应的实现,还必须声明Fibnacci
class专属的member,如下所示:
class Fibonacci:public num_sequence
{
public:
// virtual ~Fibonacci() {
// cout<<"this is Fibonacci destructor"<<endl;
// }
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;
};
// elem的实现,派生类的虚函数必须精确吻合基类中的函数原型,在类之外对虚函数进行定义时候,不必指明关键字
// virtual
int Fibonacci::elem(int pos) const
{
if( ! check_intergrity(pos,_elems.size()))
{
cout<<"wrong pos"<<endl;
return 0;
}
if(pos > _elems.size())
Fibonacci::gen_elems(pos);
// 由于我们在Fibonacci::elem()中想调用的就是
// Fibonacci::gen_elems(),因此不必等到运行时才进行 `
// gen_elems()的解析,即希望跳过虚函数机制,使该函数在编译时
// 就完成解析,这就是指明调用对象的原因,于是,运行时发生的虚
// 拟机制便被遮掩了
return _elems[pos-1];
}
下面是 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;
}
由于以上两个函数都会检查_elems存有的元素是否足够,现为Fibonacci
class 编写一份check_interigrity():
class Fibonacci:public num_sequence{
public:
bool check_intergrity(int pos) const;
// ...
};
// 如果要在派生类能使用继承来的那份member,必须利用class scope运算符加以限定,例如:
inline bool Fibonacci::
check_intergrity(int pos) const
{
if(!num_sequence::check_intergrity(pos))
return false;
if(pos > _elems.size())
Fibonacci::gen_elems();
return true;
}
问题:在基类中check_intergrity()并未视为虚函数,因此每次通过pointer或reference来调用check_intergrity(),解析出来都是num_sequence的那一份,未考虑pointer或reference究竟指向的是什么对象,例如
num_sequence *ps =new Fibonacci(12,8);
ps -> elem(1024); //通过虚拟机制动态解析为Fibonacci::elem()
ps -> check_intergrity(pos); // 被静态解析为num_sequence::check_intergrity()
因此,一般而言在基类和派生类使用同名的non-virtual函数并不是好的解决办法
造成这种两难的原因是,当派生类与检查其自身状态的完整性时,已实现完成的基类缺乏足够的知识。
较好的解决方案是重新定义check_intergrity()
bool num_sequence::
check_intergrity(int pos,int size) const
{
if(pos<0||pos>_max_elems)
{
cerr<<"invalid position: "<<pos<<"cannot honor request\n";
return false;
}
if(pos>size)
gen_elems(pos);
return true;
}
// 测试程序
vector<int> Fibonacci::_elems;
int main()
{
Fibonacci fib;
cout<<fib<<endl;
Fibonacci fib2(16);
cout<<fib2<<endl;
Fibonacci fib3(10,1);
cout << fib3<<endl;
}
5.6 运用继承体系
略
5.7 基类应该多么抽象
目前,抽象基类提供的是接口,并未实提供任何实现。每个派生类不仅必须提供本身专属的元素产生算法,还必须支持特定元素的搜索,打印,数列长度和起始位置的维护等任务。
如果,不会常有其他派生类需要加入此继承体系内,以上设计没问题。反之则会使派生类的加入工作变得更为复杂。
以下是基类的另一个设计方式,将所有派生类共有的实现内容剥离出来,移至基类内。接口依旧没有变动。
class num_sequence{
public:
virtual ~num_sequence() {}
int elem(int pos) const;
virtual const char* what_am_i() const = 0;
ostream& print(ostream &os = cout) const;
int length() {return _length;} // 提升到num_sequence层次,一般程序能够读取
int beg_pos() {return _beg_pos;} // 提升到num_sequence层次,一般程序能够读取
static int max_elems() {return 64;}
protected:
virtual void gen_elems(int pos) const = 0;
bool check_intergrity(int pos,int size) const;
num_sequence(int len,int bp,vector<int> &re)
:_length(len),_beg_pos(bp),_relems(re){}
int _length;// 提升到num_sequence层次,派生类能够读取
int _beg_pos;// 提升到num_sequence层次,派生类能够读取
vector<int> & _relems; // 用来指向派生类中某个static vector
// data member如果是reference,必须在constructor 的
// member intialization list中加以初始化。一旦初始化,就无法指向另一个对象
// pointer无此限制,可以先初始化为null,稍后再令它指向某个有效的内存地址
};
我们可以重新定义elem()
和print()
,使他们成为num_sequence
的public member
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 初始化、析构、复制
现在的num_sequence
具有实际的data member,必须为其提供初始化行为。可以将初始化操作留给每个派生类,但有潜在的危机(此坑待解释)。较好的设计方式是,为基类提供constructor,并利用这个constructor处理基类所声明的所有data member的初始化操作。
由于num_sequence是一个抽象基类,无法为其定义任何对象。num_sequence扮演的角色是每个派生类对象的子对象,因此,我将基类的constructor声明为protected而非public
派生类对象中含有多个子对象;由基类constructor初始化的“基类子对象”,由派生类constructor所初始化的“派生类子对象”,例如5.2节中的AudioBook,其对象包含三个子对象,分别由对应的constructor加以初始化。
派生类的constructor,不仅要为派生类的data member进行初始化操作,还需要为其基类的data member提供适当的值。
// 这是之前类的定义
// 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;
// };
// 还需要为基类提供适当的值,否则会报错undefined reference to `Fibonacci::Fibonacci(int, int)'
inline Fibonacci::
Fibonacci(int len,int beg_pos)
:num_sequence(len,beg_pos,_elems)
{}
若忽略对num_sequence
的调用操作,则这一份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。copy constructor定义可能如下:
Fibonacci::Fibonacci(const Fibonacci &rhs)
:num_sequence(rhs)
{
cout<<"design"<<endl; // 这行用来验证确实是用了自己设计的copy constructor,实际没有这行
}
// rhs代表等号右边的派生类对象,在成员初始化列表中传给基类的copy constructor
// 只需要在之前的Fibonacci class加一个声明,如下
class Fibonacci:public num_sequence
// {
// public:
// Fibonacci(int len=1,int beg_pos=1);
Fibonacci(const Fibonacci &rhs);
// virtual const char* what_am_i() const {return "Fibonacci";}
// protected:
// virtual void gen_elems(int pos) const;
// static vector<int> _elems;
// };
如果基类没有定义copy constructor,默认的default memberwise initialization程序会执行(见4.2节),若我们为基类定义了copy constructor,它便会被调用。
**tips:**本例不需要定义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;
}
同上,无需为Fibonacci
设计copy assignment operator。4.2和4.8节讨论了何时该提供copy constructor和copy assignment constructor
基类的destructor会在派生类的destructor结束后被自动调用,无需在派生类中对它做明确的调用操作。
5.9 在派生类中定义一个虚函数
定义派生类时,需要决定是将基类的虚函数覆盖掉还是加以继承
1. 如果继承了纯虚函数,那么这个派生类也会被视为抽象类,故无法为它定义任何对象
2. 如果覆盖基类提供的虚函数,那么派生类提供的新定义的函数原型必须完全符合基类所声明的函数原型,包括:参数列表、返回类型、常量性(const
-ness)
否则可能会出现的问题:
class num_sequence{
public:
virtual const char* what_am_i() const
{return "num_sequence\n";}
};
class Fibonacci:public num_sequence{
public:
// 派生类没有const
virtual const char * what_am_i()
{return "Fibonacci\n";}
};
int main()
{
Fibonacci b;
num_sequence p;
num_sequence *pp = &b;
cout<<pp->what_am_i();
cout<<b.what_am_i();
return 0;
}
output:
num_sequence
Fibonacci
在上例中,Fibonacci
的what_am_i()
是non-const
,和基类冲突,因此派生类所提供的函数并未用来覆盖基类所提供的同名函数
”返回类型必须完全吻合“这一规则有一个例外——当基类的虚函数返回某个基类形式(通常是pointer或reference)时,派生类的同名函数可以返回该基类所派生出来的类型
class num_sequence{
public:
virtual num_sequence *clone() = 0;
};
class Fibonacci:public num_sequence{
public:
Fibonacci *clone() {return new Fibonacci(*this);}
};
如上代码所示,当在派生类中为了覆盖基类的某个虚函数而进行声明操作时,不一定得加上关键字virtual
,编译器会依据两个函数的原型声明,决定某个函数是否会覆盖其基类中的同名函数
虚函数的静态解析(Static Resolution)
有两种情况,虚函数机制不会出现预期行为:
(1)基类的constructor和destructor内
(2)当我们使用的是基类的对象,而非基类对象的pointer或reference时
Question:当构造派生类对象时,基类的constructor会先被调用,如果在基类的constructor中调用某个虚函数,调用的是派生类的那一份吗?
由于此刻派生类的data member尚未初始化。如果此时调用派生类的那一份虚函数,便可能访问未经初始化的data member,这显然不是一件好事。基于这个原因,在基类的constructor中,派生类的虚函数绝对不会被调用,例如num_sequence内调用what_am_i(),一定会被解析为num_sequence
自身的那一份what_am_i()。如果在基类的destructor中调用虚函数,此规则同样成立。
可看下例:
class num_sequence{
public:
num_sequence()
{
cout<<"base constructor\n";
cout<<what_am_i()<<endl;
}
virtual const char* what_am_i() const
{return "num_sequence\n";}
};
class Fibonacci:public num_sequence{
public:
Fibonacci()
{
cout<<"subobject constuctor\n";
cout<<what_am_i()<<endl;
}
virtual const char * what_am_i() const
{return "Fibonacci\n";}
};
int main()
{
Fibonacci b;
return 0;
}
// output:
base constructor
num_sequence
subobject constuctor
Fibonacci
在单一对象中展现多种类型
为了能够"在单一对象中展现多种类型",多态(polymorphism)需要一层间接性,在C++中,只有用基类的pointer和reference才能够支持面向对象编程概念,可看下例:
void print(LibMat object,const LibMat *pointer,const LibMat &reference)
{
// 以下必定调用LibMat::print()
cout<<"---------"<<endl;
object.print();
cout<<"---------"<<endl;
// 以下一定会通过虚函数机制来进行解析
// 我们无法预知哪一份print()会被调用,在下面的main()中,我们传入的是AudioBook,因此调用的也是
// AudioBook的print()
pointer -> print();
cout<<"---------"<<endl;
reference.print();
cout<<"---------"<<endl;
}
int main()
{
AudioBook iwish("price",
"Stanley","Jeremy");
print(iwish,&iwish,iwish);
// 当我们为基类声明了一个实际对象(例如print()的第一参数iwish),同时也就分配除了足以容纳该实际对象
// (LibMat)的内存空间。如果稍后传入的却是派生类对象(AudioBook),那就没有足够内存放置派生类中的各
// 个data member
// 也就是说,只有iwish内的"基类子对象(也就是属于LibMat的成分)"被复制到“为参数object而保留的内存
// 中”,其他子对象(Book成分和AudiBook成分)则被切掉了。
// 至于另两个参数,pointer和reference,则被初始化为iwish对象所在的内存地址,这就是他们能够指向完整
// 的AudioBook对象的原因
}
// output:
LibMat::LibMat() default constructor!
Book::Book( price , Stanley ) constructor
AudioBook::AudioBook( price , Stanley , Jeremy ) constructor
---------
LibMat::print() -- I am a LibMat object
---------
Book::print() -- I am a AudioBook object!
My title is: price
My author is: Stanley
My narrator is: Jeremy
---------
Book::print() -- I am a AudioBook object!
My title is: price
My author is: Stanley
My narrator is: Jeremy
---------
LibMat::~LibMat() destructor!
AudioBook::AudioBook destructor!
Book::~Book() destructor!
LibMat::~LibMat() destructor!
5.10 运行时的类型鉴定机制
what_am_i()
返回一个足以代表该类的字符串,另一种实现是利用typeid运算符,这是运行时类型鉴定机制(Run-time Type Identification,RTTI)的一部分,由程序语言支持。他让我们得以查询多态化的class pointer或class reference,获得其所指对象的实际类型。
#include<typeinfo>
class num_sequence{
public:
virtual const char* what_am_i() const;
};
inline const char* num_sequence::what_am_i() const
{
return typeid(*this).name();
// type(*this)返回一个type_info对象,关联至“what_am_i()函数
// 之中由this指针所指对象”的实际类型。
}
class Fibonacci:public num_sequence {};
int main()
{
int a;
cout<<typeid(a).name()<<endl;
num_sequence n1;
cout<<n1.what_am_i()<<endl;
Fibonacci fib;
cout << fib.what_am_i()<<endl;
}
// output:
i
12num_sequence
9Fibonacci
// 这里的12,9留个坑,知道的旁友们评论区解释一下 ^_^
使用 typeid 运算符之前,必须先包含头文件typeinfo
, typeid 运算符会返回与一个type_info
对象,其中储存着与类型相关的种种信息。每一个多态类,如Fibonacci
,Pell
等,都有一个type_info
对象,该对象的name()
函数会返回一个const char*,用以表示类名。
type_info
class 也支持相等和不等两个比较操作,例如下面代码决定ps
是否指向某个Fibonacci
对象:
num_sequence *ps = &fib;
// ...
if(typeid(*ps) == typeid(Fibonacci))
// ...
注意:
注意:
注意:
下面的这些操作在我的环境下是可以成功的(gcc --version -> MinGW-W64 8.1.0;-std -> c++11)
从上面的检验中我们知道ps
确实指向某个Fibonacci
对象,但直接在此通过ps
调用Fibonacci
的gen_elems()
函数,会产生编译错误。
// 错误,ps并非一个Fibonacci指针,虽然确实指向某个Fibonacci对象!
ps -> gen_elems(64);
为了能够完成调用,须指示编译器将ps
的类型转换为Fibonacci
指针(非常朴实无华的想法),可采用static_cast运算符:
if(typeid(*ps) == typeid(Fibonacci))
{
Fibonacci *pf = static_cast<Fibonacci*>(ps); //无条件转换
pf -> gen_elems();
}
static_cast运算符有潜在危险,因为编译器无法确认我们所进行的转换操作是否完全正确,因此一般需要if(typeid(*ps) == typeid(Fibonacci))
使用dynamic_cast运算符(这也是一个RTTI运算符),会进行运行时检验操作,检验ps
所指对象是否属于Fibonacci
类。若是,则进行转换操作,pf
则指向该Fibonacci
对象;若否,该运算符则返回0,那么对Fibonacci
的gen_elems()
所进行的静态调用操作也不会发生。