zoukankan      html  css  js  c++  java
  • C++之虚函数与多态

    C++之虚函数与多态

    引言:初学C++对于多态的基本不存在什么概念,虚函数(Virtual)又是什么?什么是重写(override)?两者之间有什么联系?多态的运用场景又是什么?

    什么是多态(Polymorphism)

    • 多态按照字面的意思即多种形态,子类(派生类)继承与同一个父类(基类),父类与子类包含有同名的成员函数,该成员函数因子类不同而实现不同。也就是经常说的:一个接口,多种方法。一个接口即指的是父类的该成员函数,多种方法指的是不同子类的同名实现函数。

    • 面向对象中接口的多种不同实现方式即为多态。多态允许子类类型的指针赋值给父类类型的指针,其实有一句话总结的相当好:多态即调用同名函数却因上下文子类的不同而实现不同,我觉得这样更加贴切,还加入了多态三要素:(同名函数)、(依据上下文)、(实现不同)。

    多态的应用

    • C++中父类指针是指向基类对象的,如果将子类的类型转化赋予父类指针,那么该父类指针即指向了子类中的基类部分,可以访问子类中的基类部分,但却不能访问派生类中的成员函数。而多态中虚函数的引入突破了该限制。当子类中含有与父类同名的虚函数时,父类指针即可访问该子类中的虚函数成员函数。

    • 父类比作为电脑外设接口USB, 子类比作外设,此时我的子类外设有U盘、MP3等,它们2个都可以存储但却各不相同,对于不同的外设(子类),我在写驱动时,只需在子类重写父类的读取设备(功能实现函数)的虚函数即可。那么此时外设接口(父类)只需一个,如果不这样写,对于每一个子类都表示一个外设接口,那么就需要N个外设接口,明显不合理。所以,用父类的指针指向子类,是为了面向接口编程。大家都遵循该接口,准确的说“一个接口,多种实现”。

    虚函数与虚函数表

    基类的成员函数(非静态)用virtual关键字声明即为虚函数,需在派生类中重新定义该函数。

    #include<iostream>
    using namespace std;
    class A
    {
        public:
        int value;
         void fun()
            {
            }
    //     virtual void fun()
    //        {
    //        }
    };
    int main()
    {
        A a;
        cout << sizeof(a)<<endl;
    }
    输出结果:
    void fun --> 4
    virtual void fun --> 8
    

    • 如图所示:所有的类若是有虚函数,都会比正常类大一点,因为所有包含virtual类的对象在内存存储时,都会在头上自动加个隐藏的指针(4字节),它指向一张表,该表称为Vtable,Vtable里包含了所有类中virtual函数的地址。

    静态与动态

    区别:动态多态的成员函数被virtual关键字声明,静态多态则没有

    静态多态

    静态多态是指程序在编译时候就被确定,不随程序的运行而动态改变,称为早绑定。

    
    静态多态的成员函数调用举例:
    #include <iostream>
    using namespace std;
    class base
    {
      public :
        void fun()
        {
           cout<<"it is base"<<endl;
        }
    };
    class A:public base
    {
      public:
        void fun()
        {
           cout<<"it is A"<<endl;
        }
    };
    class B:public base
    {
      public:
        void fun()
        {
           cout<<"it is B"<<endl;
        }
    };
    int main()
    {
        //多态访问
        base *Pbase;
        A myA;
        B myB;
    
        Pbase = &myA;
        Pbase->fun();
    
        Pbase = &myB;
        Pbase->fun();
        return 0;
    }
    

    当上述代码编译执行时,会产生以下结果:

    • "it is base"

    静态编译:多态函数fun()在编译期间就被静态的设置为基类中的版本,此时编译器看的是指针的类型。

    动态多态(virtual)

    动态多态是指程序在运行时,随基类指针的内容而动态改变,称为晚绑定。

    动态态多态的成员函数调用举例:
    
    #include <iostream>
    using namespace std;
    class base
    {
      public :
        virtual void fun()
        {
           cout<<"it is base"<<endl;
        }
    };
    class A:public base
    {
      public:
        void fun()
        {
           cout<<"it is A"<<endl;
        }
    };
    class B:public base
    {
      public:
        void fun()
        {
           cout<<"it is B"<<endl;
        }
    };
    int main()
    {
        //多态访问
        base *Pbase;
        A myA;
        B myB;
    
        Pbase = &myA;
        Pbase->fun();
    
        Pbase = &myB;
        Pbase->fun();
        return 0;
    }
    

    当上述代码编译执行时,会产生以下结果:

    • "it is A"
    • "it is B"

    动态编译:多态函数fun()在运行期间,会根据指针的内容进行不同的函数实现。
    Note:

    • C++规定,当父类的成员函数被定义为虚函数后,其子类中的同名函数会自动称为虚函数,virtual关键字在子类成员函数中可声明也可不声明。
    • virtual关键字让子类与父类之间的同名函数有了联系,实现了动态绑定。

    虚函数定义规则

    1. 虚函数在父类子类中,名字相同、形式参数相同、返回类型相同,否则不构成多态。

    2. 只有类的成员函数才能说明为虚函数,因为虚函数仅适合于有继承关系的类对象。

    3. 类的静态成员函数不能声明为虚函数,因为静态函数的特点不受限制于某个对象。

    4. 内联(inline)函数不能为虚函数,因为内联函数不能在运行中动态的确定位置,即使虚函数有类的成员函数的定义概念,但是编译系统编译时仍然认为虚函数是作为非内联的。

    5. 构造函数不能是虚函数,因为构造时,对象还是一片未定型的空间,只有构造完成后,对象才是具体类的实例。

    6. 析构函数可以是虚函数,且通常被声明为虚函数。

    7. 虚函数在基类中定义,在子类中可重写,也可以选择不重写。

    //虚函数也可选择不重载
    #include <iostream>
    using namespace std;
    class base
    {
      public :
        virtual void fun()
        {
           cout<<"it is base"<<endl;
        }
    };
    class A:public base
    {
      public:
        void TEST()
        {
    
        }
    };
    int main()
    {
        //多态访问
        base *Pbase;
        A myA;
        Pbase = &myA;
        Pbase->fun();
        return 0;
    }
    

    当上述代码编译执行时,会产生以下结果:

    • "it is base"
      现象分析:虚函数在子类中若没有被重写,则会调用父类的虚函数。

    虚析构函数

    1. 当子类的对象从内存删除时,一般会先调用子类的析构函数释放子类对象的子类内存,再调用父类的析构函数释放父类对象的父类内存。

    2. 但是当存在基类的指针指向了一个子类的对象时,base *p = new child;此时若利用deleter p来删除p指向的动态内存时,只会调用父类的析构函数来释放堆内存中的基类部分,并不会调用子类的析构函数释放子类的堆内存。此时造成内存泄露。

    3. 解决办法:为了避免上述现象,将基类的析构函数声明为虚函数,此时当deleter p时,就会先调用父类的析构函数来释放堆内存中的基类部分,再调用子类的析构函数释放子类的堆内存。

    4. 如果将基类的析构函数声明为虚函数,那么子类的析构函数自动为虚函数。

    虚函数与纯虚函数

    1. 纯虚函数在基类中定义,没有函数主体,在基类中没有实现意义,仅声明为一个接口,仅在派生类中重写并实现。

    2. 包含纯虚函数的类不能实例化,它的作用仅作为一个共同基类,提供给一个公共的接口。

    3. 基类定义了纯虚函数,子类没有去具体实现,那么在派生类中该函数也为纯虚函数,不可调用。(甚至有的编译器报错)

    //纯虚函数必须在子类中重写,且含纯虚函数的基类不能有实例对象
    #include <iostream>
    using namespace std;
    class base
    {
      public :
        virtual void fun() = 0;
    };
    class A:public base
    {
      public:
        void TEST()
        {
    
        }
    };
    
    int main()
    {
        //多态访问
        base *Pbase;
        A myA;
        Pbase = &myA;
        Pbase->fun();
        return 0;
    }
    

    当上述代码编译执行时,编译器会认为语法错误。
    现象分析:纯虚函数在子类中若没有被重写,则会编译出错。

    //子类实例化纯虚函数
    #include <iostream>
    using namespace std;
    class base
    {
      public :
        virtual void fun() = 0;
    };
    class A:public base
    {
      public:
    	void fun()
    	{
    		cout<<"it is ok"<<endl;
    	}
        void TEST()
        {
    
        }
    };
    
    int main()
    {
        //多态访问
        base *Pbase;
        A myA;
        Pbase = &myA;
        Pbase->fun();
        return 0;
    }
    当上述代码编译执行时,会产生以下结果:
    "it is ok"
    

    纯虚函数在基类中的声明形式:"virtual int Fun()=0"

    • 最后面的“=0”的作用告诉编译器这是一个纯虚函数。
    • 纯虚函数只有函数名称,不具备函数功能,因此不能被调用,只有在派生类中被重新定义才可调用。
    • 纯虚函数用来规范子类的行为,即接口,包含纯虚函数的类被称为抽象类。
  • 相关阅读:
    Java 语义网编程系列二: 本体
    Java 语义网编程系列三: 现实世界中的知识建模
    Windows编程--线程和内核对象的同步-等待定时器内核对象
    Windows编程--虚拟内存的使用
    Windows编程--线程和内核对象的同步-事件内核对象
    Python 中文问题
    Windows编程--线程和内核对象的同步-信标(信号量)内核对象
    Windows编程--伪句柄
    Windows编程-- 线程和内核对象的同步 - 互斥对象内核对象
    Windows编程-- Windows的内存结构
  • 原文地址:https://www.cnblogs.com/retry/p/9285536.html
Copyright © 2011-2022 走看看