代码改变世界

白话C++系列(27) -- RTTI:运行时类型识别

2016-06-20 20:43  Keiven_LY  阅读(673)  评论(0编辑  收藏  举报

RTTI—运行时类型识别

RTTI:Run-Time Type Identification。

那么RTTI如何来体现呢?这就要涉及到typeid和dynamic_cast这两个知识点了。为了更好的去理解,那么我们就通过一个例子来说明。这个例子大家已经非常熟悉了,如下:

首先定义一个Flyable类,在这个类当中有两个纯虚函数:takeoff(起飞)和land(降落)。我们又定义了一个鸟类,并且公有继承了Flyable类,既然public继承了Flyable,就要去实现起飞和降落这两个函数,此外,作为鸟类来说,还有一个自己特有的函数foraging(觅食)。同时,我们还定义了另外一个类Plane,其也以public方式继承了Flyable,并且也实现了起飞和降落这两个函数,此外,作为飞机类来说,其还有一个自己特有的函数Carry(运输)。

在使用的时候,我们假设有如下一个函数dosomething,它的传入参数是Flyable的一个指针,如下:

在这个函数当中,我们可以使用obj这个指针去调用起飞和降落这两个函数。同时,我们可以想一想,如果我们能够对传入的这个指针再做进一步的判断,如如说,我们判断出如果它是一个Bird对象指针,那么我们是不是就可以用这个指针去调用觅食这个函数呢?同理,如果我们判断出它是一个Plane对象指针,那么我们是不是也就可以用这个指针去调用运输这个函数呢?如果想要做到这样的效果,那么就要用到这节课开始提到的知识:运行时类型识别(RTTI)。

我们可以看到,当我们去实现dosomething这个函数的时候,如下:

我们在这调用了起飞函数,最后一行代码调用了降落函数。我们在调用完起飞这个函数之后,我们通过typeid(*obj).name()这样的方法就可以将当前的obj这个指针指向的实际的对象类型打印出来了(比如传入的是飞机,打印出来的就是Plane;如果传入的是Bird,那么打印出来的就是Bird)。当然,我们可以还可以通过if语句对类型进行比对,如果我们想要判断当前的obj是不是一个Bird类型,我们就可以通过上面的if判断语句的方法进行比对,比对完成之后,我们就可以将obj通过dynamic_cast的方式,将其转换为Bird指针。转换的时候,需要注意的是,dynamic_cast<Bird *>(obj),尖括号中是目标类型。转换完之后,我们就可以用bird这个指针去调用觅食这个函数。

总结:

dynamic_cast注意事项:

  • l  只能应用于指针和引用的转换
  • l  要转换的类型当中必须包含虚函数(如果没有虚函数,转换就会失败)
  • l  转换成功返回子类的地址,失败返回NULL

typeid的注意事项:

  • l  type_id返回一个type_info对象的引用
  • l  如果想要通过基类的指针获得派生类的数据类型,基类必须带有虚函数
  • l  只能获取对象的实际类型(也就是说,即便这个类含有虚函数,也只能判断当前对象是基类还是子类,而没有办法判断当前指针是基类还是子类)

下面我们来看一看type_info中的内容,如下:

对于type_info这个类来说,当中我们用到一个name()函数。我们在之前的例子当中,通过typeid(*obj)获取到的就是一个type_info的引用,通过这个引用就可以调用name()这个成员函数(typeid(*obj).name()),那么这个被调用的name()成员函数就是在这所看到的name()。语句bool operator == (const type_info& rhs) const;是一个运算符重载(这部分内容后面介绍),大家只要知道,在进行了运算符重载之后,这里的==就可以使得前面的例子中两个type_info对象的比对了(typeid(*obj) = = typeid(Bird))。

RTTI代码实践

/* *************************************************  */

/* RTTI

      1. Flyable类,成员函数:takeoff()和land()

      2. Plane类,成员函数:takeoff()、land()和carry()

      3. Bird类,成员函数:takeoff()、land()和foraging()

      4. 全局函数dosomething(Flyable *obj)

*/

/* *************************************************  */

程序结构:

头文件(Flyable.h

#ifndef FLYABLE_H
#define FLYABLE_H

//在Flyable这个类中定义两个纯虚函数takeoff()和land()
class Flyable
{
public:
    virtual void takeoff() = 0;
    virtual void land() = 0;
};

#endif

头文件(Bird.h

#ifndef Bird_H
#define Bird_H

#include "Flyable.h"
#include <string>
using namespace std;

class Bird:public Flyable //公有继承了Flyable
{
public:
    void foraging();//对于Bird类来说,其具有一个特有的成员函数foraging(觅食)
    virtual void takeoff(); //实现了Flyable中的虚函数takeoff和land
    virtual void land();
};

#endif

源程序(Bird.cpp

#include <iostream>
#include "Bird.h"

using namespace std;

void Bird::foraging()
{
    cout << "Bird --> foraging()" << endl;
}

void Bird::takeoff()
{
    cout << "Bird --> takeoff()" << endl;
}

void Bird::land()
{
    cout << "Bird --> land()" << endl;
}

头文件(Plane.h

#ifndef PLANE_H
#define PLANE_H

#include "Flyable.h"
#include <string>
using namespace std;

class Plane:public Flyable  //公有继承了Flyable
{
public:
    void carry(); //Plane具有一个特有的成员函数carry(运输)
    virtual void takeoff(); //实现了Flyable中的虚函数takeoff和land
    virtual void land();

};

#endif

源程序(Plane.cpp

#include <iostream>
#include "Plane.h"

using namespace std;

void Plane::carry()
{
    cout << "Plane --> carry()" << endl;
}

void Plane::takeoff()
{
    cout << "Plane --> takeoff()" << endl;
}

void Plane::land()
{
    cout << "Plane --> land()" << endl;
}

主调程序(demo.cpp

#include <iostream>
#include "stdlib.h"
#include "Bird.h"
#include "Plane.h"

using namespace std;
void dosomething(Flyable *obj)
{
    cout << typeid(*obj).name() << endl;  //打印传入的对象指针究竟是什么类型的对象
    obj->takeoff();
    if(typeid(*obj) == typeid(Bird)) //这里判断obj这个指针所指向的对象是不是Bird类型
    {
        Bird *bird = dynamic_cast<Bird *>(obj); //将obj这个指针通过dynamic_cast强制转换为Bird指针,并且将这个指针赋值给一个新的指针bird
        bird->foraging(); //通过这个bird指针来调用foraging(觅食)这个成员函数
    }
    if(typeid(*obj) == typeid(Plane)) //这里判断obj这个指针所指向的对象是不是Bird类型
    {
        Plane *plane = dynamic_cast<Plane *>(obj); //将obj这个指针通过dynamic_cast强制转换为Plane指针,并且将这个指针赋值给一个新的指针plane
        plane->carry(); //通过这个plane指针来调用carry(运输)这个成员函数
    }
    obj->land();
}

我们来到主调函数main()下面,先实例化一个Bird对象b,然后通过调用dosomething函数来传入Bird这个对象b,由于dosoething这个函数传入的参数是一个独享指针,所以这里传入的应该是对象b的地址(&b),如下:

int main()
{
    Bird b;
    dosomething(&b);

    system("pause");
    return 0;
}

我们按一下F5,看一下运行结果:

通过运行的结果来比较相应的程序,看一看是如何来运行的。

首先打印出的第一行是 “class Bird”,其是通过dosomething函数中的cout语句打印出来的;接下来打印出的是“Bird –> takeoff()”,其是通过代码obj->takeoff();实现的;第三行打印出的是“Bird –> foraging()”,其运行的一定时dosomething函数中的第一个if判断语句,因为其通过bird这个指针调用了foraging(觅食)这个函数;可见,当前传入的这个obj指针所指向的对象就是一个Bird对象(如果这里我们指向的是一个Plane对象,那么显而易见就会执行第二个if判断语句,从而就会通过plane指针去调用carry(运输)这个函数);最后一行打印出的是“Bird –> land()”,其是通过代码obj->land();实现的。

这里,如果我们实例化一个Plane对象,并将对象指针传入dosomething函数,如下:

int main()
{
    Plane p;
    dosomething(&p);

    system("pause");
    return 0;
}

运行结果:

从我们的打印结果就可以反推出RTTI所做的这些工作。

接下来,再通过一些代码再来展示一下关于typeid以及dynamic_cast使用的注意事项。

我们先来看一看typeid,对于typeid来说,它能够看任何一个对象或者指针的类型(包括基本的数据成员的类型)。比如,我们定义一个变量i,就可以通过cout来看一看我们定义的i究竟是什么类型,如下:

int main()
{
    int i = 0;
    cout << typeid(i).name() << endl;

    system("pause");
    return 0;
}

我们按F5来看一下运行结果:

打印结果就是int,这就说明i这个变量的数据类型就是int类型(如果我们写成double i;),那么打印出来的就是double类型,如下:

那么,对于typeid来说,它能够打印的指针是指针本身的类型。我们再来看一看typeid打印指针和打印对象的不同之处。

首先,我们用Flyable去定义一个指针p,并且用指针p去指向Bird这样的一个对象(Flyable *p = new Bird();),指向这个对象之后,我们分别来看一看p和*p通过typeid所打印出来的结果如何,看如下代码:

int main()
{
    Flyable *p = new Bird();
    cout << typeid(p).name() << endl;
    cout << typeid(*p).name() << endl;

    system("pause");
    return 0;
}

按一下F5,看一看运行结果:

我们看到,p通过typeid打印出来的结果是“class Flyable *”,也就是说,p是一个Flyable *的数据类型,而对于*p来说,它打印出来的则是“class Bird”,也就是说*p是一个Bird对象。

接着我们再来看一看dynamic_cast有什么使用限制。

为了看到这些限制,我们需要改造一下前面的代码。

修改后的Flyable.h文件如下:

#ifndef FLYABLE_H
#define FLYABLE_H

//在Flyable这个类中定义两个纯虚函数takeoff()和land()
class Flyable
{
public:
    void takeoff(){}
    void land(){}
};

#endif

修改后的Bird.h文件如下:

#ifndef Bird_H
#define Bird_H

#include "Flyable.h"
#include <string>
using namespace std;

class Bird:public Flyable //公有继承了Flyable
{
public:
    void foraging();//对于Bird类来说,其具有一个特有的成员函数foraging(觅食)
    void takeoff();
    void land();
};

#endif

此时,对于Bird和Flyable来说,它们之间只是一种普通的子类和父类的关系

那么,当我们用父类的指针去指向一个子类的对象(Flyable *p = new Bird();)也是可以的。那么,我们还能不能通过dynamic_cast来进行指针的转换呢?我们一起来看一看:

int main()
{
    Flyable *p = new Bird();
    Bird *b = dynamic_cast<Bird *>p; //将Flyable的指针转换为Bird指针,并且将转换完的指针赋值给Bird的一个指针b

    system("pause");
    return 0;
}

此时,我们按F7看一看编译是否通过:

我们看到系统提示“dynamic_cast”:“Flyable”不是多态类型,也就是说,对于Flyable来说,它要求转换的目标类型以及被转换的数据类型都应该是具有虚函数的,如果没有就会报错;当然也不能直接转对象这样的类型,比如说,将Flyable的对象p直接转换为Bird的对象b,如下:

int main()
{
    Flyable p;
    Bird b = dynamic_cast<Bird>p;

    system("pause");
    return 0;
}

我们看一看这样是否可行,按F5:

我们看到,这样依然会报错,报错提示依然是“dynamic_cast”:“Flyable”不是多态类型的原因,当然它也不是一个正常的数据类型,因为必须待是引用和指针才可能进行转换,其次还要加上一个条件:这个类当中必须含有虚函数。