C++在兼容C(procedural programming)的基础上实现了Object-oriented programming的编程范式,与object-oriented programming相关的三个核心思想就是封装、继承、多态。
C++是没有GC(Garbage Collection)的语法机制的。对于动态内存的管理(如何避免泄露,特别是引发异常时),RAII(Resource Acquisition Is Initialization,资源获取就是初始化)就是指利用类的封装和对象的自动构造和自动析构的语法机制来实现对资源的有效管理(初始化与释放)。
Object-oriented programming的核心思想是“organizing types into hierarchies to express their relationships directly in code”(将类型组织到层次结构中,以直接在代码中表示它们之间的关系)。继承就是一种纵向的“is-a“关系。在继承的层次关系中,通过虚函数来实现多态。与此同时,虚函数的语法机制中还实现了简单的的RTTI(Run Time Type Identification,运行时类型识别)。
1 RAII(Resource Acquisition Is Initialization)
RAII是C++语言的一种管理资源(如堆内存、文件句柄、网络连接套接字等)、避免泄漏的语法机制。C++保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
C++社群在赋予技术神秘的缩略语和古怪的名字方面有着悠久而自豪的传统。RAII就是一个例子,它既古怪又神秘。RAII表示“资源获取即初始化"(resource acquisition is initializa-tion), 而不是某些人会以为的“初始化即资源获取"(initialization is resource acquisition)。 一句闲话,如果你打算搞怪,那就干脆怪到底,否则达不到效果。
RAII是一项很简单的技术,利用C++对象生命期的概念来控制程序的资源。RAII的基本技术原理很简单。 如果希望对某个重要资源进行跟踪,那么创建一个对象,并将资源的生命期和对象的生命期相关联。如此一来,就可以利用C++复杂老练的对象管理设施来管理资源。最简单的 RAII形式是,创建这样的一个对象,使其构造函数获取一份资源,而析构函数则释放这份资源:
class Resource {} ;
class ResourceHandle {
public:
explicit ResourceHandle( Resource *aResource )
: r_(aResource) {} //获取资源
~ResourceHandle ()
{ delete r_;} //释放资源
Resource *get()
{ return r_;} //访问资源
private:
ResourceHandle( const ResourceHandle &);
ResourceHandle &operator = ( const ResourceHandle &) ;
Resource *r_;
} ;
2 RTTI(Run Time Type Identification)
看如下范例:
Human *phuman = new Men(); // 基类指针指向一个派生类对象
Human &r = *phuman; // 基类引用绑定到派生类对象上
这里的静态类型就是变量声明时的类型,静态类型在编译时就是已知的,如上面代码中的phuman、r,它们的静态类型就是Human类型指针和Human类型引用。
当基类的指针或引用指向派生类对象,会出现只有在运行的时候(执行到这行代码的时候)才能确定的类型型态称为动态类型。在C++中,当基类存在虚函数,基类的指针或引用指向派生类对象,产生多态时才会出现动态类型。如果不是多态的上下文,则静态类型与动态类型应该永远都是一致的。
在C++ 环境中﹐头文件(header file) 含有类之定义(class definition)亦即包含有关类的结构信息(representational information)。但是﹐这些信息只供编译器(compiler)使用﹐编译完毕后并未留下来﹐所以在执行时期(at run-time) ﹐无法得知对象的类信息﹐包括类名称、数据成员名称与类型、函数名称与类型等等。例如﹐两个类Figure和Circle﹐互相之间是继承关系。
若有如下指令﹕
Figure *p;
p = new Circle();
Figure &q = *p;
在执行时﹐p指向一个对象﹐但欲得知此对象之类信息﹐就有困难了。同样欲得知q 所参考(reference) 对象的类信息﹐也无法得到。RTTI(Run-Time Type Identification)就是要解决这困难﹐也就是在执行时﹐您想知道指针所指到或参考到的对象类型时﹐该对象有能力来告诉您。随着应用场合之不同﹐所需支持的RTTI范围也不同。最单纯的RTTI包括﹕
I 类识别(class identification)──包括类名称或ID。
II 继承关系(inheritance relationship)──支持执行时期的「往下变换类型」(downward casting)﹐亦即动态变换类型(dynamic casting) 。
在对象数据库存取上﹐还需要下述RTTI﹕
I 对象结构(object layout) ──包括属性的类型、名称及其位置(position或offset)。
II 成员函数表(table of functions)──包括函数的类型、名称、及其参数类型等。
其目的是协助对象的I/O 和持久化(persistence) ﹐也提供调试信息等。
若依照Bjarne Stroustrup 之建议,C++ 还应包括更完整的RTTI﹕
I 能得知类所实例化的各对象 。
II 能参考到函数的源代码。
III 能取得类的有关在线说明(on-line documentation) 。
其实这些都是C++ 编译完成时所丢弃的信息﹐如今只是希望寻找一个途径来将之保留到执行期间。然而﹐要提供完整的RTTI﹐将会大幅提高C++ 的复杂度。
在C++中,最单纯的RTTI使用了两个运算符:
dynamic_cast,将父类的指针或引用安全地转换为子类的指针或引用。
typeid:返回指针或者引用所指对象的实际类型。
特别需要注意的是,上述两个运算符要能够正常地如所期望的那样工作,父类中至少要有一个虚函数,不然这两个运算符工作的结果很可能与预期的不一样。因为只有虚函数的存在,这两个运算符才会使用指针或引用所指对象的类型(new时的类型)。
Java中任何一个类都可以通过反射机制来获取类的基本信息(接口、父类、方法、属性、Annotation等),而且Java中还提供了关键字instanceof,可以在运行时判断一个类是不是另一个类的子类或者是该类的对象,Java可以生成字节码文件,再由JVM(Java虚拟机)加载运行,字节码文件中可以含有类的信息。
在C++ 程序中﹐若类含有虚函数﹐则该类会有个虚函数表(Virtual Function Table﹐ 简称VFT )。为了提供RTTI﹐C++ 就将在VFT 中附加个指针﹐指向typeinfo对象﹐这对象内含RTTI资料。
由于该类所实例化之各对象﹐皆含有个指针指向VFT 表﹐因之各对象皆可取出typeinfo对象而得到RTTI。例如﹐
Figure *f1 = new Square();
Figure *f2 = new Square();
const typeinfo ty = typeid(*f2);
其中﹐typeid(*f2) 的动作是﹕
I 取得f2所指之对象。
II 从对象取出指向VFT 之指针﹐经由此指针取得VFT 表。
III 从表中找出指向typeinfo对象之指针﹐经由此指针取得typeinfo对象。
这typeinfo对象就含有RTTI了。经由f1及f2两指针皆可取得typeinfo对象﹐所以 typeid(*f2) == typeid(*f1)。
对于C++的多态,如果父类中有一个虚函数,并且在子类中覆盖了这个虚函数,那么,当父类指针指向子类对象的时候,调用的虚函数就是子类里的虚函数。如下面实例中的vt()函数。
也是可以通过RTTI来实现同样的功能,如下面实例中的vt2RTTI()函数。
如果子类中有一个父类中没有的普通成员函数(非虚函数),那么,即使是父类指针指向了该子类对象,但也没办法用父类指针调用子类是的这个普通成员函数。那么如果就想用父类指针调用子类中的这个普通成员函数,该怎样做呢?
I 把这个普通成员函数改写成虚函数(在父类中只要是虚函数,在子类中自然就是虚函数)。但是,如果子类中每增加一个新成员函数就要在父类中增加等同的虚函数,这样的解决方案怎是让人不太满意。
II 替代虚函数方案的办法就是RTTI,使用dynamic_cast进行类型转换。如下面实例中的 doSomething()函数:
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class Movable
{
public:
virtual void move()=0;
};
class Bus : public Movable
{
public:
virtual void move()
{
cout << "Bus -- move" << endl;
}
void carry()
{
cout << "Bus -- carry" << endl;
}
};
class Tank :public Movable
{
public:
virtual void move()
{
cout << "Tank -- move" << endl;
}
void fire()
{
cout << "Tank -- fire" << endl;
}
};
void vt2RTTI(Movable *obj)
{
if(typeid(*obj) == typeid(Bus)) // 如果去掉move()的虚函数声明也无法RTTI
{
Bus *bus = dynamic_cast<Bus*>(obj);//注意这两个语句
bus->move();
}
if(typeid(*obj) == typeid(Tank))
{
Tank *tank=dynamic_cast<Tank*>(obj);//注意这两个语句
tank->move();
}
}
void vt(Movable *obj)
{
obj->move();
}
void doSomething(Movable *obj)
{
if(typeid(*obj) == typeid(Bus)) // 如果去掉move()的虚函数声明也无法RTTI
{
Bus *bus = dynamic_cast<Bus*>(obj);//注意这两个语句
bus->carry();
}
if(typeid(*obj) == typeid(Tank))
{
Tank *tank=dynamic_cast<Tank*>(obj);//注意这两个语句
tank->fire();
}
}
int main(void)
{
Bus b;
Tank t;
cout<<typeid(b).name()<<endl;
cout<<typeid(t).name()<<endl;
vt2RTTI(&b);
vt2RTTI(&t);
vt(&b);
vt(&t);
doSomething(&b);
doSomething(&b);
return 0;
}
/*
3Bus
4Tank
Bus -- move
Tank -- move
Bus -- move
Tank -- move
Bus -- carry
Tank -- fire
*/
虽然vt()和vt2RTTI()皆能达到多态化(polymorphism) ﹐但使用复选方法的vt2RTTI()﹐常导致违反著名的「开放╱封闭原则」(open/closed principle)。反之﹐使用虚函数方法的vt()则可合乎这个原则。
当Movable类体系再派生出子类时﹐vt2RTTI() 函数的内容必须多加个if指令。因而违反了「开放╱封闭原则」。
想一想,如果C++ 并未提供RTTI﹐则程序员毫无选择必须使用虚函数来支持move() 函数的多态,使用vt()函数:
void vt(Movable *obj)
{
obj->move();
}
如此﹐Movable类体系能随时派生类﹐而不必修正vt() 函数。亦即﹐Movable体系有个稳定的接口(interface) ﹐vt() 使用这接口﹐使得vt() 函数也稳定﹐不会随Movable类体系的扩充而变动。这是封闭的一面。而这稳定的接口并未限制Movable体系的成长﹐这是开放的一面。因而合乎「开放╱封闭」原则﹐软件的结构会更具弹性﹐更易于随环境而不断成长。
ref
https://wwuhn.github.io/witisoPC/34ccpp/CPP的RTTI观念、使用场合及实现来源.html
-End-