zoukankan      html  css  js  c++  java
  • 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++的角度看来都是虚函数,当然静态的成员方法除外。

  • 相关阅读:
    VS2005在使用membership的时候,如何连接Access数据库?
    今天想开始写计划的项目,可是就是静不下心来,乱糟糟的!
    今天想开始写计划的项目,可是就是静不下心来,乱糟糟的!
    有钱真好
    网页左边和上面的空隙如何设置成为0
    vim 配色方案(目测有上百个)
    Git 远程仓库的管理和使用
    vim 使用图
    Python 编程挑战
    python 网络爬虫
  • 原文地址:https://www.cnblogs.com/xidongyu/p/6930316.html
Copyright © 2011-2022 走看看