• 多态


    多态,以专业术语来讲,多态是一种运行期绑定(run-time binding)机制,通过这种机制,实现将函数名绑定到函数具体实现代码目的。

    多态就是就是将函数名称动态地绑定到函数入口地址的运行期绑定机制

     

    一个函数的名称和其入口地址是紧密相连的,入口地址是该函数在内存中的起始地址

    由于函数被调用时,到底应该执行哪一段代码是由编译器在编译阶段就决定了的,因此我们将这种对函数的绑定方式称为编译器绑定(compile-time  bindinig):专业术语:编译器将所以对函数的调用绑定到函数的入口地址

     

    与编译器绑定不同的时,运行期绑定是直到程序运行之时,才将函数名称绑定到其入口地址。

    如果对一个函数的绑定发生在运行期而非编译器,我们就称该函数是   多态

    在Smalltalk这样的纯面向对象语言中,所有函数都是多态的

    在C++这样混合语言中,函数既可以是多态的,也可以是非多态的,这要由绑定的时机是编译时刻还是运行时刻来决定

    在C++中,只有满足某些特定条件的成员函数才可能是多态的

     

    C++中多态有以下三个前提条件:

    1,必须存在一个继承体系结构

    2,继承体系结构中的一些类必须具有同名的virtual成员函数(virtual是关键字)

    3,至少有一个基类类型的指针或基类类型的引用。这个指针或引用可用来对virtual成员函数进行调用

    看例子:

     

    结果:

     

    基类类型的指针可以指向任何基类对象或派生类对象

     

    再看一个:

    结果:

     

    在sayHi的三个版本中,程序都使用了关键字virtual。这对练习使用virtual关键字是有益的,但实际上并没有必要,因为当声明了基类的一个成员函数为虚函数后,那么即使该成员函数没有在派生类中被显式地声明为虚函数,但它在所有派生类中也将自动称为虚函数。

    如果在派生类中sayHi成员函数的声明没有使用关键字virtual,该派生类的用户为了确定它是否为虚函数,不得不检查sayHi在基类中的声明,将函数在所有派生类中声明为虚函数,就可以避免这种不便

    如果虚函数在类声明外定义,关键字仅在函数声明时需要,不需在函数定义中使用virtual关键字

     

    C++仅允许将成员函数定义为虚函数,顶层函数(可以理解为全局函数,因为不在类中,所以定义为虚函数没有什么意义)不能为虚函数

     

    在派生类中虚成员函数也可以从基类继承

     

    C++使用vtable(虚成员函数表)来实现虚成员函数的运行期绑定。虚成员函数表存在的用途是支持运行时查询,使得系统可以将某一函数名绑定到虚成员函数表中的特定入口地址。需成员函数表的实现是与系统无关的。

     

    使用动态绑定的程序会影响效率,因为虚成员函数表需要额外的存储空间,而且对虚成员函数表进行查询也需要额外的时间。

    纯面向对象语言由于所有的函数都以动态方式运行,因而效率的降低会相当大,而在C++中,程序员可以选择性的执行哪些函数是虚成员函数,因而既不会导致太大的效率降低,又充分利用了运行期绑定机制

    class B{

    public:

              virtual void m1() { /*...*/}

          virtual void m2() { /*...*/ }

    };

    class D : public B{

    public:

             virtual void m1() {/*...*/}

    };

     

    构造函数不能是虚成员函数(因为如果是虚函数,必须在派生类提供一个同名的函数覆盖,或者不提供而继承下来,这两种情况都没有必要,因为构造函数只使用于类本身)

    析构函数可以是虚成员函数

     

     

    虚函数必须是基类函数中的非静态函数,访问权限可以是public或者protected

     

    看一个例子分析为什么析构函数可以为虚函数:

    #include <iostream>
    using namespace std;
    class A{
    public:
        A(){
            cout << endl << "A() firing" << endl;
            p = new char[5];
        }
        ~A(){
            cout << "~A() firing" << endl;
            delete []p;
        }
    private:
        char *p;
    };
    class Z : public A{
    public:
        Z() {
            cout << "Z() firing" << endl;
            q = new char[5000];
        }
        ~Z() {
            cout << "~Z() firinig" << endl;
            delete []q;
        }
    private:
        char *q;
    };
    void f();
    int main()
    {
        for(unsigned i = 0 ;i < 3 ;i ++)
            f();
        return 0;
    }

    void f(){
        A *ptr;
        ptr = new Z();
        delete ptr;
    }

    结果:

     

     

    看出来没有!!

    上述new操作符将导致构造函数A()和Z()被调用(Z的构造函数没有显式调用A的构造函数,但编译器会确保A的默认构造函数被调用),当我们通过ptr进行delete操作时,尽管ptr实际指向一个Z的对象,但只有~A()被调用,这是因为它们的析构函数不是虚函数,所以编译器实施的是静态绑定。编译器根据ptr的数据类型A*来决定调用哪一个析构函数,因此,仅调用了~A(),而没有调用~Z(),这样Z()中分配的5000字节就不会被释放。

    看修改后的程序:

    #include <iostream>
    using namespace std;
    class A{
    public:
        A(){
            cout << endl << "A() firing" << endl;
            p = new char[5];
        }
    virtual    ~A(){
            cout << "~A() firing" << endl;
            delete []p;
        }
    private:
        char *p;
    };
    class Z : public A{
    public:
        Z() {
            cout << "Z() firing" << endl;
            q = new char[5000];
        }
        ~Z() {
            cout << "~Z() firinig" << endl;
            delete []q;
        }
    private:
        char *q;
    };
    void f();
    int main()
    {
        for(unsigned i = 0 ;i < 3 ;i ++)
            f();
        return 0;
    }

    void f(){
        A *ptr;
        ptr = new Z();
        delete ptr;
    }
    结果:

     

    当然也可以把~Z()定义为virtual了,前面讲过

    现在由于析构函数已经声明为虚成员函数,当通过ptr来删除其所指向的对象时,编译器进行的是运行时期绑定。在这里,因为ptr指向Z类型的对象,所以~Z()被调用;我们随后看到~A()也被调用了,这是因为析构函数的调用是沿着继承树自下而上延伸的。通过将析构函数定义为虚函数,我们就保证了在调用f时不会产生内存遗漏

     

    通常来说,如果基类有一个指向动态分配内存的数据成员,并定义了负责释放这块内存的析构函数,就应该将这个析构函数声明为虚成员函数,这样做可以保证在以后添加该类的派生类时发挥多态性的作用。

    只有非静态成员函数才可以是虚成员函数。换句话说,只有对象成员函数才可以是虚成员函数。

     

     

    看两个例子:

     

     

     

     

    下面看一个大的程序,充分体现了虚成员函数和多态:

    程序总括:

    1,提供一个多态的成员函数input,可以从某个输入文件中读入有关Films、DirectorCuts和ForeignFilms的记录,输入文件中的每条记录都可映射到某个Film类层次中的对象,假设输入文件的格式是正确的

    2,依据输入文件中的记录功能动态的创建Film类层次对象

    3,将Film类层次中的input函数设计为虚函数,使其具有多态性。动态创建的对象通过input函数可从输入流中正确的读入数据

    4,将Film类层次中的output函数设计为虚函数,使其具有多态性。动态创建的对象通过output函数可将信息正确的输出到标准输出流

     

    头文件films.h

    #include <iostream>
    #include <fstream>
    #include <string>
    #include <cctype>

    using namespace std;

    class Film
    {
    public:
       Film()
       {
        store_title();
        store_director();
        store_time();
        store_quality();
       }
       void store_title(const string &t) { title = t;}
       void store_title(const char * t = "") { title = t;}
       void store_director(const string &d) { director = d;}
       void store_director(const char * d = "") { director = d;}
       void store_time(int t=0) { time = t;}
       void store_quality(int q = 0) { quality = q;}
       virtual void output();
       virtual void input(ifstream &);
       static bool read_input(const char *,Film *[],int);
    private:
       string title;
       string director;
       int time;
       int quality;
    };

    void Film::input(ifstream & fin)
    {
       string inbuff;
       getline(fin,inbuff);
       store_title(inbuff);
       getline(fin,inbuff);
       store_director(inbuff);
       getline(fin,inbuff);
       store_time(atoi(inbuff.c_str()));
       getline(fin,inbuff);
       store_quality(atoi(inbuff.c_str()));
    }

    void Film::output()
    {
       cout << "Title: " << title << endl;
       cout << "Director: " << director <<endl;
       cout << "Time: " << time << " mins" << endl;
       cout << "Quality: ";
       for(int i = 0 ; i < quality ; i ++)
          cout << '*';
       cout << endl;
    }

    class DirectorCut : public Film
    {
    public:
       DirectorCut()
       {
          store_rev_time();
          store_changes();
       }
       void store_rev_time(int t=0) { rev_time = t;}
       void store_changes(const string &c) {changes = c;}
       void store_changes(const char *c = "") { changes = c; }
       virtual void output();
       virtual void input(ifstream &);
    private:
       int rev_time;
       string changes;
    };

    void DirectorCut::input(ifstream &fin)
    {
       Film::input(fin);
       string inbuff;
       getline(fin,inbuff);
       store_rev_time(atoi(inbuff.c_str() ));
       getline(fin,inbuff);
       store_changes(inbuff);
    }

    void DirectorCut::output()
    {
       Film::output();
       cout << "Revised: time: " << rev_time << endl;
       cout << "Changes: " << changes << endl;
    }

    class ForeignFilm : public Film
    {
    public:
       ForeignFilm() { store_language(); }
       void store_language(const string &l) { language = l;}
       void store_language(const char *l = "") {language = l;}
       virtual void output();
       virtual void input(ifstream &);
    private:
       string language;
    };

    void ForeignFilm::input(ifstream & fin)
    {
       Film::input(fin);
       string inbuff;
       getline(fin,inbuff);
       store_language(inbuff);
    }

    void ForeignFilm::output()
    {
       Film::output();
       cout << "Language: " << language << endl;
    }

    bool Film::read_input(const char *file,Film * films[],int n)
    {
       string inbuff;
       ifstream fin(file);
       if(!fin)
        return false;
       int next = 0;
     
       while(getline(fin,inbuff) && next < n)
       {
          if(inbuff == "Film")
             films[next] = new Film();
          else if( inbuff == "ForeignFilm")
             films[next] = new ForeignFilm();
          else if( inbuff == "DirectorCut")
             films[next] = new DirectorCut();
          else
             continue;
          films[next ++] -> input(fin);
       }
       fin.close();
       return true;
    }

     

    测试文件和结果:

     

    我们所描述的多态函数指的是运行期进行绑定的函数,在C++中,仅有虚函数是在运行期进行绑定的,因此,仅有虚函数才具有真正意义上的多态

     

    参数个数和类型不同的同名函数被成为重载函数,都是顶层函数或者都是成员函数 ====== 重载与编译期绑定相对应,编译器依据函数签名来进行绑定

     

    假定基类B有一个成员函数m,其派生类D也有一个具有相同函数签名的成员函数m,如果这个成员函数是虚函数,则任何通过指针或引用对m的调用都会激活运行期绑定。对于这种情况,叫做派生类的成员函数D::m覆盖了其基类的成员函数B::m。如果成员函数不是虚函数,对m的任何调用均为编译期绑定

    下面看下面这若干例子:注意代码的细节:(有很多情况没有必要甚至重复,仅仅是列出来,做个比较,参考)

    1,

    2,

    3,

    4,

    5,

    6,

    7,

    8,

    9,

    10,

    11,

    12,

    13,

    14,

    15,

    16,

    17,

    18,

    19,

    20,

    21,

    22,

    23,

    24,

    只有知识点清楚,上面的都能解释!

     

     假定基类B拥有一个非虚函数m,其派生类D也有一个成员函数m,我们说函数D::m遮蔽了继承而来的函数B::m。如果派生类的同名成员函数与其基类的这个成员有不同的函数签名,那么这种遮蔽情况会相当复杂

    先看一个例子:

    看修改过的,之所以能修复,是因为是遮蔽,不是覆盖!!!

    虚函数和非虚函数都有可能产生名字遮蔽,实际上一旦派生类的虚函数不能覆盖基类的虚函数,就会产生虚函数遮蔽

    eg:

    与此相对比,再来看2个:

    1,NO1

    2,NO2

    看出点什么没有?

    名字共享:

    上面可以看到函数共享一个函数名,可能会引发一些问题,然而有时我们又希望几个函数共享一个函数名。

    下面几种情况就需要共享函数名:

    1,重载函数名的顶层函数。对于程序员来说,只使用一个函数名就可以执行不同的函数体,非常方便。也就是说,对于函数名相同但函数签名不相同的函数,使用起来是非常便利的。另外,我们通常将一些操作符设计为顶层重载函数 (可能指的是非类成员函数)

    2,重载构造函数。一个类经常有几个构造函数,这种情况也需要函数重载

    3,非构造函数是同一个类中名字相同的成员函数。这种方法和顶层函数的方式一样,知识处于不同的域

    4,继承层次中的同名函数(特别是虚函数)。为了发挥多态性的作用,虚函数必须具有相同的函数签名(具有相同的函数名)。在典型的多态情况下,派生类的虚函数覆盖了从基类继承来的虚函数,要形成覆盖,成员函数必须为函数签名相同的虚函数

    在类层次中共享函数名但函数签名不同时,将产生遮蔽,而遮蔽通常是非常危险的,因此要谨慎地运行这种遮蔽类型的名字共享机制

    抽象基类确保其派生类必须定义某些指定的函数,否则这个派生类就不能被实例化

    抽象基类之所以是抽象的,是因为不能实例化抽象基类,抽象基类可以用来指明某些必须被派生类覆盖的虚函数,如果这些派生类想要拥有对象的话。只有符合下面条件的类才可以称为抽象基类:

          类必须拥有一个纯虚成员函数

    在虚成员函数声明的结尾加上=0就可将这个函数定义为纯虚成员函数

    虽然不能创建一个抽象基类的对象,但抽象基类可以拥有派生类,从抽象基类派生出来的类必须覆盖基类的所有纯虚成员函数,否则派生类也是抽象类,因而也不能用来创建对象

    一个纯虚成员函数就可以使一个类成为抽象基类,一个抽象基类可以有其他不是纯虚成员函数或甚至不是虚函数的成员函数,还可以有数据成员。

    抽象基类的成员可以使private、protected或public

    只有虚函数才可以成为纯虚成员函数,非虚函数或顶层函数都不能声明为纯虚成员函数

    抽象基类的作用很大,通过这种机制,可以用来指明某些虚函数必须被派生类覆盖,否则这些派生类就不能拥有对象。从这种意义上来看,抽象基类实际上定义了一个公共接口,这个接口被所有从抽象基类派生的类共享

    因为抽象基类通常只有public成员函数,所以经常使用关键字struct来声明抽象基类

    微软的IUnknown接口:

    微软的COM(Componet Object Model)模型提供了一种应用程序构造框架

    IUnknown是一个标准的COM接口

    C++支持运行期类型识别(RTTI  Run-Time Type Identification),运行期类型识别提供如下功能:

    1,在运行期对类型转换操作进行检查

    2,在运行期确定对象的类型

    3,扩展C++提供RTTI

    在C++中,编译期合法的类型转换操作可能会在运行期引发错误,当转换操作涉及对象指针或引用时,更易发生错误。使用dynamic_cast操作符可用来在运行期对可疑的转换操作进行测试:

    一个基类指针不经过明确的转换操作,就能指向基类或派生类对象;反过来就大不一样了额,将一个派生类指针指向基类对象是一种相当不明智的做法。当然,通过明确的转型操作可以强制地做的这一点:

    static_cast是合法的,但这种转型操作是相当危险,可能会造成难以跟踪的运行期错误

    过程虽然不会导致编译错误,不过请注意,由于p现在指向一个B的对象,而B并没有成员函数m,这就导致一个运行期错误

    因此我们发现static_cast不能保证类型安全

    C++的dynamic_cast操作符可以再运行期检查某个转型动作是否类型安全。

    dynamic_cast和static_cast有相同的语法,不过dynamic_cast仅对多态类型(即至少有一个虚函数的类)有效

    上面这段代码有问题,因为它对非多态类型C(因为C不含虚函数)实施了dynamic_cast操作。dynamic_cast操作是否正确与转型的目标类型是否多态无关,但转型的源类型必须是多态。

    可做下面的改正:

    class C{

    public:

              virtual void m() {};

    };

    在<>中指定的dynamic_cast的目的类型必须是一个指针或引用。假设T是一个类,那么T*和T&对dynamic_cast来说都是有效的目的类型,而T不是

    看下面代码:

    编译时会出现一个警告:

         warning C4541: 'dynamic_cast' used on polymorphic type 'class B' with /GR-; unpredictable behavior

    执行的时候出现:

    如果dynamic_cast成功的话,p将会指向动态创建的B对象,若dynamic_cast失败p为NULL。本例失败

    看一个完整示例:

    #include <iostream>
    #include <string>
    using namespace std;

    class Book {
    public:
       Book(string t) {title = t;}
       virtual void printTitle() const{
          cout << "Title: " << title << endl; }
    private:
       Book();
       string title;
    };

    class Textbook : public Book {
    public:
       Textbook(string t,int l) : Book(t),level(l) {}
       void printTitle() const {
          cout << "Textbook " ;
          Book::printTitle();
       }
       void printLevel() const {
          cout << "Book level: " << level << endl; }
    private:
       Textbook();
       int level;
    };

    class PulpFiction : public Book{
    public:
       PulpFiction(string t) : Book(t) {}
       void printTitle() const{
          cout << "Pulp " ;
          Book::printTitle();
       }
    private:
       PulpFiction();
    };

    void printBookInfo(Book *);

    int main()
    {
       Book * ptr;
       int level;
       string title;
       int ans;
       cout << "Book's titles? (no white space) ";
       cin >> title;
       do{
          cout << "1 == Textbook, 2 == PulpFiction " << endl;
          cin >> ans;
       }while( ans < 1 || ans > 2);
       if(1 == ans){
          cout << "Level ?";
          cin >> level;
          ptr = new Textbook(title,level);
       }
       else
          ptr = new PulpFiction(title);
       printBookInfo(ptr);
       return 0;
    }

    void printBookInfo(Book * bookPtr)
    {
       bookPtr -> printTitle();
       Textbook * ptr = dynamic_cast<Textbook*>(bookPtr);
       if(ptr)
          ptr -> printLevel();
    }

    dynamic_cast的规则:

      dynamic_cast的规则很复杂,特别是在多重继承或进行与void*类型相关的转型操作时。在此主要讨论最基本的情况,简单起见,我们用指针而不是引用来说明这些规则。

    在单继承的类层次中,假定基类B具有多态性,而类D是直接或间接从类B派生而来的。通过继承,类D也因此具有多态性,在这种情况下:

    1,从派生类D*到基类B*的dynamic_cast可以进行,这称为向上转型(upcast)

    2,从基类B*到派生类D*的dynamic_cast不能进行,这称为向下转型(downcast)

    假定类A和类Z都具有多态性,但它们之间不存在继承关系,这种情况下:

    1,从A*到Z*的dynamic_cast不能进行

    2,从Z*到A*的dynamic_cast不能进行

    通常来说,向上转型可以成功,而向下转型不能成功。除了void *之外,无关类型之间的dynamic_cast也不会成功。

    dynamic_cast与static_cast小结:

    C++提供了不同的转换机制。一个static_cast可施加于任何类型,不管该类型是否具有多态性,dynamic_cast只能施加于具有多态性的类型,而且转换的目的类型必须是指针或引用。基于这个原因,static_cast比dynamic_cast的应用更广。但由于只有dynamic_cast才能实施运行期类型安全检查,因此从这个角度来说,dynamic_cast的功能更强大

    操作符typeid可用来确定某个表达式的类型,要使用这个操作符,必须包含头文件typeinfo

    操作符typeid返回一个type_info类对象的引用,type_info是一个系统类,用来描述类型,这个操作符可施加于类型名(包括类名)或C++表达式。

    下面仔细分析下面 bookPtr为Book*,但是typeid(*bootPtr)返回值代表对象类型Textbook

    语法正确但运行出错的转型动作不是类型安全的

    看下面一个例子

    typeid不能用于多态!!!

    看下面一个例子!!!!

    编译时:

    运行时:

    有人将多态分强多态和弱多态两种。强多态是指对覆盖的虚函数进行运行期绑定处理。弱多态行则有两种形式:顶层函数火成员函数的重载和类层次中非虚函数的函数名共享

    我们则用多态这个术语来描述覆盖的虚函数在运行期的绑定,而对C++结构中看似是运行期绑定而实际是编译器绑定的情况使用重载、函数名共享等术语

    常见的编程错误:::

    1,只有成员函数才可以声明为虚函数,顶层函数不能为虚函数

    2,静态成员函数不能为虚函数

    3,如果在类声明之外定义一个虚函数,只需在声明时使用关键字virtual,而在定义时不需要使用virtual

    4,声明任何构造函数为虚函数都是错误的,但析构函数可以是虚函数

    5,如果一个成员函数遮蔽了继承而来的成员函数,不指定其全名来调用继承的成员函数会导致错误

    6,不能对非虚函数进行运行期绑定

    7,如果派生类虚函数与基类虚函数签名不同,不能覆盖

    8,不能创建一个抽象基类的对象

    9,如果抽象基类的纯虚函数没有全部实现,则该派生类也是抽象类,不能创建对象

    10,dynamic_cast只能作用于多态性类型(至少有一个虚函数的类)

    11,dynamic_cast的目的类型必须是指针或引用

    12,typeid操作符不能针对非多态类型进行运行期类型检查

  • 相关阅读:
    为什么new的普通数组用delete 和 delete[]都能正确释放
    虚幻4属性系统(反射)
    CFileDialog类的默认路径
    把单一元素的数组放在一个struct的尾端
    在UE4中使用SVN作为source control工具
    单精度浮点数和有效位数为什么是7位
    Valid Number--LeetCode
    归并排序
    堆排序
    直接选择排序
  • 原文地址:https://www.cnblogs.com/lfsblack/p/2708472.html
Copyright © 2020-2023  润新知