zoukankan      html  css  js  c++  java
  • C++多态中虚函数的深入理解

    c++中动态多态性是通过虚函数来实现的。静态多态性是通过函数的重载来实现的,在程序运行前的一种早绑定,动态多态性则是程序运行过程中的一种后绑定。根据下面的例子进行说明。

    #include <iostream>
    #include <string>
    
    using namespace std;
    class Shape//形状类
    {
    public:
        double calcArea()
        {
            cout<<"calcArea"<<endl;
            return 0;
        }
    };
    //////////////////////////
    class Circle1:public Shape      //公有继承自形状类的圆形类
    {
    public:
        Circle1(double r):m_dR(r){}//参数初始化表初始化构造函数
        double calcArea();
    
    private:
        double m_dR;
    };
    
    
    
    double Circle1::calcArea()
    {
        cout<<"the area of circle is "<<3.14*m_dR*m_dR<<endl;
        return 3.14*m_dR*m_dR;
    }
    
    ///////////////////////////
    class Rect1:public Shape       //公有继承自形状类的矩形类
    {
    public:
        Rect1(double width,double height);
        double calcArea();
    
    private:
        double m_dWidth;
        double m_dHeight;
    };
    Rect1::Rect1(double width,double height)
    {
       m_dWidth= width;
       m_dHeight=height;
    }
    
    double Rect1::calcArea()
    {
        cout<<"the area of circle is "<<m_dWidth*m_dHeight<<endl;
        return m_dWidth*m_dHeight;
    }
    
    int main()
    {
    
        Shape *s1=new Circle1(4.0);
        Shape *s2=new Rect1(3.0,5.0);
        s1->calcArea();
        s2->calcArea();
        return 0;
    }

    运行结果为:

    calcArea
    calcArea

    这里并没有得到我们需要的面积,基类指针是s1,s2是用来指向基类(Shape)对象的,这里我们用基类指针指向了派生类对象的时候,系统会自动进行指针类型转换,将派生类的指针先转化为基类的指针,这样,基类指针s1,s2就指向了派生类对象中的基类部分,所以会得到上面的运行结果。虚函数可以突破这一限制。

      将基类中calcArea()函数前加上virtual后可以解决这一问题,此时可以得到我们所需要的输出结果。

    the area of circle is 50.24
    the area of Rect is 15

      注意将基类中calcArea()函数定义为虚函数后,其派生类后重新定义的calcArea()函数均为虚函数,在派生类的同名函数声明的时候,virtual可以加也可以不加。由此就可以得到我们所需要的多态效果。 这里注意理解这句代码Shape *s1=new Circle1(4.0),S1是基类指针,而Circle1是派生类,这里就涉及到了基类和派生类之间指针类型转换了。这里讲派生类指针向基类指针转换定义为向上类型转换,程序运行时自动完成的,也可以使用dynamic_cast进行强制类型转换,系统中默认的就是向上类型转换,向上类型转换是安全的、隐式的;向下类型转换是不安全的,这里不做讨论。

       派生类继承了基类的成员变量和成员函数,但是构造函数和析构函数却是不能继承的,派生类对象构造函数初始化的时候:1、先调用基类中的构造函数(如果有多个基类,根据继承时声明的顺序进行初始化)2、再调用成员类中的构造函数(如果有多个成员类,根据其声明的顺序进行初始化)3、最后初始化派生类本身的构造函数。具体可以参考http://blog.csdn.net/Helloguoke/article/details/21826309,析构函数顺序则和构造函数相反,即先构造的后析构,后构造的先析构。

    虚析构函数:

      在多态中存在的一个严重的问题就是内存的泄露问题。当派生类的对象从内存中撤销的时候一般先调用派生类的析构函数,然后在调用基类的析构函数。但是,如果用new运算符建立了临时对象,并且定义了一个指向该基类的指针变量,在程序用带指针参数的delete运算符撤销对象时,系统会只执行基类的析构函数,而不执行派生类的析构函数,由此造成内存的泄露。

    #include <iostream>
    #include <string>
    
    using namespace std;
    class Shape//形状类
    {
    public:
        Shape(){}
        ~Shape(){cout<<"destruct Shape"<<endl;}
     virtual  double calcArea()
        {
            cout<<"calcArea"<<endl;
            return 0;
        }
    };
    //////////////////////////
    class Circle1:public Shape      //公有继承自形状类的圆形类
    {
    public:
    
        Circle1(double r):m_dR(r){}//参数初始化表初始化构造函数
        ~Circle1(){cout<<"destruct Circle1"<<endl;}
        double calcArea();
    
    private:
        double m_dR;
    };
    
    
    double Circle1::calcArea()
    {
        cout<<"the area of circle is "<<3.14*m_dR*m_dR<<endl;
        return 3.14*m_dR*m_dR;
    }
    
    ///////////////////////////
    class Rect1:public Shape       //公有继承自形状类的矩形类
    {
    public:
        Rect1(double width,double height);
        ~Rect1(){cout<<"destruct Rect1"<<endl;}
        double calcArea();
    
    private:
        double m_dWidth;
        double m_dHeight;
    };
    Rect1::Rect1(double width,double height)
    {
       m_dWidth= width;
       m_dHeight=height;
    }
    
    double Rect1::calcArea()
    {
        cout<<"the area of Rect is "<<m_dWidth*m_dHeight<<endl;
        return m_dWidth*m_dHeight;
    }
    
    int main()
    {
    
        Shape *s1=new Circle1(4.0);
        Shape *s2=new Rect1(3.0,5.0);
        s1->calcArea();
        s2->calcArea();
        delete s1;
        delete s2;
        return 0;
    }

    运行结果为:

    the area of circle is 50.24
    the area of Rect is 15
    destruct Shape
    destruct Shape

    解决这一问题我们可以在基类的析构函数前加上virtual关键字进行修饰,这样基类指针指向那个对象,在销毁对象时,对象的析构函数就会先执行,然后执行基类的析构函数。

       基类析构函数加上virtual后,程序运行结果如下:

    the area of circle is 50.24
    the area of Rect is 15
    destruct Circle1
    destruct Shape
    destruct Rect1
    destruct Shape

      要深入理解虚函数就需要对虚函数的实现进行深入理解。

      虚函数表指针vptr:类中除了定义的函数成员,还有一个成员是虚函数表指针vptr(占四个字节),这个指针指向一个虚函数表(vbtl)的起始位置,这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数。每一个带有虚函数类的实例,都拥有一个虚函数指针——vptr,在类的对象初始化完毕后,它将指向虚函数表。

            当实例化一个该类的子类对象的时候,(如果)该类的子类并没有定义虚函数,但是却从父类中继承了虚函数,所以在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表是子类的虚函数表,但是记录的子类的虚函数地址却是与父类的是一样的。所以通过子类对象的虚函数表指针找到自己的虚函数表,在自己的虚函数表找到的要执行的函数指针也是父类的相应函数入口的地址。

            如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时子类定义了自己的(从父类那继承来的)相应函数,所以它的虚函数表当中管于这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,换句话说就是指向了自己定义的相应函数,这样如果用父类的指针,指向子类的对象,就会通过子类对象当中的虚函数表指针找到子类的虚函数表,从而通过子类的虚函数表找到子类的相应虚函数地址,而此时的地址已经是该函数自己定义的虚函数入口地址,而不是父类的相应虚函数入口地址,所以执行的将会是子类当中的虚函数。这就是多态的原理。

    函数的覆盖和隐藏

    基类和派生类出现同名函数称为隐藏。

    • 基类对象.函数函数名(...);     //调用基类的函数
    • 派生类对象.函数名(...);           //调用派生类的函数  
    • 派生类对象.基类名::函数名(...);//派生类调用从基类继承来的函数。

    基类和派生类出现同名虚函数称为覆盖

    • 基类指针=new 派生类名(...);基类指针->函数名(...);//调用派生类的虚函数。(系统自动向上类型转换)

    虚析构函数的实现原理

    虚析构函数的特点:当我们在基类中通过virtual修饰析构函数之后,通过基类指针指向派生类对象,通过delete删除基类指针就可以释放掉派生类对象

    理论前提:执行完派生类的析构函数就会执行基类的析构函数

      如果基类当中定义了虚析构函数,那么基类的虚函数表当中就会有一个基类的虚析构函数的入口指针,指向的是基类的虚析构函数,派生类虚函数表当中也会产生一个派生类的虚析构函数的入口指针,指向的是派生类的虚析构函数,这个时候使用基类的指针指向派生类的对象,delete接基类指针,就会通过指向的派生类的对象找到派生类的虚函数表指针,从而找到虚函数表,再虚函数表中找到派生类的虚析构函数,从而使得派生类的析构函数得以执行,派生类的析构函数执行之后系统会自动执行基类的虚析构函数。这个是虚析构函数的实现原理。

    virtual在函数中的使用限制

    • 普通函数不能是虚函数,也就是说这个函数必须是某一个类的成员函数,不可以是一个全局函数,否则会导致编译错误。
    • 静态成员函数不能是虚函数 static成员函数是和类同生共处的,他不属于任何对象,使用virtual也将导致错误。
    • 内联函数不能是虚函数 如果修饰内联函数 如果内联函数被virtual修饰,计算机会忽略inline使它变成存粹的虚函数。
    • 构造函数不能是虚函数,否则会出现编译错误。

    纯虚函数:

    纯虚函数没有函数体,同时在定义的时候函数名后面要加“=0”。

    class Shape
    {
    public:
        virtual  double calcArea()//虚函数
        {....}
        virtual  double calcPerimeter()=0;//纯虚函数
        ....
    };

    含有纯虚函数的类被称为抽象类

          含有纯虚函数的类被称为抽象类,比如上面代码中的类就是一个抽象类,包含一个计算周长的纯虚函数。哪怕只有一个纯虚函数,那么这个类也是一个抽象类,纯虚函数没有函数体,所以抽象类不允许实例化对象,抽象类的派生类也可以是一个抽象类。抽象类派生类只有把抽象类当中的所有的纯虚函数都做了实现才可以实例化对象。

    对于抽象的类来说,我们往往不希望它能实例化,因为实例化之后也没什么用,而对于一些具体的类来说,我们要求必须实现那些要求(纯虚函数),使之成为有具体动作的类。

  • 相关阅读:
    利用GitHub和Hexo打造免费的个人博客 coder
    Android基础——项目的文件结构(二) coder
    25个Android酷炫开源UI框架 coder
    MarkDown使用教程(In Atom) coder
    Android基础——项目的文件结构(一) coder
    25类Android常用开源框架 coder
    Android Activity启动黑/白屏原因与解决方式 coder
    我的window phone 开发第一步
    Entity Framework 4 In Action 读书笔记
    最近在制作一套ASP.NET控件,已初见雏形
  • 原文地址:https://www.cnblogs.com/xiaodingmu/p/7348345.html
Copyright © 2011-2022 走看看