c++之旅:多态

多态

同一消息根据发送对象的不同而产生不同的行为,多态是建立的在封装和继承的基础之上

一个小案例引发的问题

#include <iostream>

using namespace std;

class Person {
    public:
        Person(){};
        void test() {cout << "Person test" << endl;}
        virtual void vTest() {cout << "Person vTest" << endl;}  //虚函数
};

class Worker: public Person {
    public:
        Worker(){};
        void test() {cout << "Worker test" << endl;}
        virtual void vTest() {cout << "Worker vTest" << endl;}  //虚函数
};


class Farmer: public Person {
    public:
        Farmer(){};
        void test() { cout << "Farmer test" << endl;}
        
        virtual void vTest() { cout << "Farmer vTest" << endl;}
};

int main(void) {
	Person* person = dynamic_cast<Person*>(new Worker());
    person->test();
    person->vTest();
    person =  dynamic_cast<Person*> (new Farmer());
    person->test();
    person->vTest();
}

上面的代码输出的是

Person test
Worker vTest
Person test
Farmer vTest

首先我们先对代码进行分析一下,有三个类,一个是Person,一个是Worker。Worker,Farmer类继承于Person类。在主函数中我们创建了一个Worker和Farmer对象,并将其返回值转为Person,然后分别调用test()和vTest()。有上面的现象的我们提出以下几个问题:

  • 静态绑定与动态绑定是什么,和本案例有啥关系
  • 多态到底是什么,如何体现
  • 虚函数的原理是什么

下面就来回答这三个问题,这三个问题弄懂了,多态就理解了就算理解了一半

静态绑定与动态绑定

静态绑定

在编译链接阶段需要确定函数的调用地址,即将函数的调用和函数的实现(函数体)关联起来,如果关联不到函数体,怎会报下面的错误,这也是我们经常遇到的

undefined reference to 'xxx()'

编译阶段将函数的调用和函数体关联起来我们称作静态绑定

在小案例中

 person->test();

就是静态绑定,编译器会将test()和person中test()方法体关联起来,所以结果输出的总是

Person test
动态绑定

当我们为方法添加virtual关键字后,编译器将不会执行静态绑定,因为该关键字给编译器一个暗示,将绑定推迟至运行时,这就是所谓的动态绑定,也叫运行时绑定。这样好处时什么呢?这样的做的话就可以调用同一接口而产生不同效果。

person->vTest()

上面的代码被调用了两次,但是却产生了不同的效果,这和静态绑定最大的区别 。那么动态绑定也是绑定,也要将方法调用和方法体绑定起来,那么它是根据什么绑定的呢?根据上下文来绑定,查看当前的指针指向的真实对象是什么。person开始指向的是Worker对象,那么就绑定该对象拥有的方法体,后来person指向了Farmer对象,那么就绑定Farmer对象拥有的方法体,等绑定完成之后就是开始执行方法体了

小结

动态绑定使得程序更加的灵活,因为如果使用静态绑定,那么一个接口不管执行多少次,其结果都是一样,但是使用动态绑定,在接口不变的情况下,我们只要更改对象就可以获取不同的效果

多态的含义

在本文的导语就说了多态是不同对象对同一消息会产生不同的行为。我们分解一下这句话就可以理解多态了

  • 同一消息:即同一函数调用,比如小案例中person->vTest()
  • 不同对象:不同对象很好理解了,比如Worker和Farmer就是不同的对象,但是有一个特点就是他们必须有共同基类
  • 产生不同行为:小案例中person->vTest()被调用了两次从而产生不同的输出结果就是不同的行为

多态的实质就是动态绑定,因为在同一消息的情况下产生不同的效果只能使用动态绑定来实现

虚函数

上面我们看到要实现多态必须借助虚函数,关于虚函数的使用如下:

  • 虚函数可以被子类继承
  • 如果要复写父类的虚函数则需要返回值,函数名和参数列表与父类保持一致
  • virtual不能修饰类成员方法(static)
  • 不能修饰内联函数

虚函数的原理是使用虚函数表和函数指针来实现的,当类中如果有虚函数时,类加载到内存会为其生成张虚函数表,虚函数表中记录了类中虚函数的位置,下面通过一个案例来说明:

Shape类及其虚函数表
class Shape {
    public:
       virtual double calcArea();
    protected:
       int m_iEege;
};

Shape类中有个虚函数calcArea,当Shape类被加载到内存中后,加载器会为其创建一张虚函数表(一个类对应一张虚函数表,如果类中没有虚函数则不会创建虚函数表)。当我们在程序中创建Shape对象后,该对象中将会有一个隐藏的指针指向虚函数表,如下图所示:

11

在图中虚函数表的地址时0xCCFF,那么创建的Shape对象其虚函数表指针vftable_ptr将会指向0xCCFF。在虚函数表中有个函数指针为calcArea_ptr指向0x3355内存区域,该区域其实存储了calcArea()函数体。

Circle类及其虚函数表
class Circle:public Shape {
   protected:
   int m_dR;
}

Circle继承于Shape类,那么也会继承Shape类的虚函数。同时Circle类被加载到内存后也会为其创建虚函数表,但是有一点稍微不同的地方。

11

在上图中Circle对象中也会有个指针指向自己的虚函数表,在虚函数表中有个指针会指向calcArea()函数,我们看到这个函数的内存地址没有发生变化,仍然是0x3355。当我们复写了calcArea()后,这个指针才会执行新的函数的位置。

注意:如果类中有虚函数,则创建的对象将会多出四个字节用来保持虚函数表指针

通过上面的分析,我们就可以理解如何根据上下文来进行动态绑定了。

Person* person = dynamic_cast<Person*>(new Worker());
person->vTest();

person实际指向的是Worker对象,那么在调用vTest()的时候首先会从Worker对象查找虚函数表的位置,在虚函数表中查找vTest()在内存中的位置,找到之后调用vTest()方法

纯虚函数

纯虚函数的格式如下,纯虚函数是不能有函数体的,拥有纯虚函数的类被称为抽象类。由于纯虚函数没有函数体,那么在虚函数表中指向纯虚函数的指针将会被赋值为0

class Person {
virtual void work() = 0;
};

含有纯虚函数的类的不能被实例化

RTTI

RTTI被称为运行时类型信息,用来检查一个指针指向对象的具体类型。由于在设计中为了解耦,往往存在向上转型(把一个子类对象赋给父类指针)。但是有时我们需要通过指针检查该指针指向对象的具体类型,那么可以通过typeid查看。下面是一个简单的例子:

#include <iostream>
#include <typeinfo>
using namespace std;
class Person {
    virtual void test(){};
};

class Worker:public Person {
};

int main(void) {
    Person* person = dynamic_cast<Person*>(new Worker());
    cout << typeid(*person).name() << endl;
    if (typeid(*person) == typeid(Worker))
        cout << "match successful!" << endl;
    else
        cout << "match failed!" << endl;
}

输出结果为

Worker
match successful!

上面的代码中我们查看了person指向的对象到底是哪个类实例化出来的,这样就能进行类型的检查了。

注意:typeid传入的值一般都是对象而不是指向对象的指针,这样才能检测该对象的实际类型;typeid传入的对象其父类必须有虚函数,如果没有虚函数则会返回父类类型。如果把上面的代码中Person的类中的virtual去掉,则输出的结果为:

Person
match failed

这是因为typeid主要用于多态,但是Person中没有虚函数表示我们对于该类我们放弃的多态的特性,所有typeid并不承担该责任。这在编程中需要注意。

typeid其实是个函数,其返回值是指向type_info对象的引用,关于type_info的东东可以百度或谷歌

异常

通过一个小案例说明异常

#include <iostream>
using namespace std;
class Exception {
    public:
        Exception(){};
        virtual void printException() = 0;
        virtual ~Exception(){};
};

class IndexException : public Exception {
    public:
        virtual void printException() {cout << "IndexException" << endl;}
        IndexException(){};
        virtual ~IndexException(){};
};

void test() {
    throw IndexException();
}

int main(void) {
    try {
        test();
    } catch (Exception& e) {
        e.printException();
    }
}

总结

多态的实质就是动态绑定,而动态绑定与虚函数是息息相关的。所以要有多态的特性必须在父类中出现虚函数或这纯虚函数。在java中是天然支持多态的,所以java中所有的成员方法在C++的角度看来都是虚函数,当然静态的成员方法除外。

posted @ 2017-06-01 18:44  被罚站的树  阅读(225)  评论(0编辑  收藏  举报