C++虚函数(3) 学习总结

 1   多态性可分为两类:静态多态和动态多态。函数重载和运算符重载实现的多态属于静态多态,动态多态性是通过虚函数实现的。每个含有虚函数的类有一张虚函数表(vtbl),表中每一项是一个虚函数的地址, 也就是说,虚函数表的每一项是一个虚函数的指针。没有虚函数的C++类,是不会有虚函数表的。两张图


2 .   C++的虚函数主要是为了实现多态。基类定义的虚函数,派生类重新定义该函数,这样基类对象指针调用哪一个函数是判断该基类指针的对象的类型。虚函数用virtual关键字修饰,同时开启动态联编技术,动态联编只有程序运行时才能确定调用的函数,在编译阶段是不知道要调用哪一个函数。

3 .  当父类指针或父类引用指向子类对象,而子类中又覆盖了父类的函数,希望用父类指针或父类引用,调用到子类版本中覆盖父类的函数,需要在父类中把该覆盖的成员函数声明为虚函数,在函数返回类型前加virtual。

4 .   如果不将基类中一个或多个在派生类中被重新定义的成员函数前加virtual,那么在将子类对象赋给父类引用或者使父类指针指向子类对象时,那么当父类引用或父类指针调用被子类覆盖的重名函数时,该函数的实现仍然是基类中定义的内容,不会调用派生类中重新定义的成员函数。

5 .  注意:将子类对象赋给父类引用或使父类指针指向子类对象时,父类指针或父类引用只能访问调用父类自身的成员或调用子类中覆盖父类的的同名函数(在父类中该函数前加virtual的情况下),不能访问子类自身拓展的成员即在父类中没有被定义过的成员。如果想让父类引用或者是父类指针调用自身版本且已经被子类重新覆盖过同名函数,需要用“父类名::被覆盖的成员函数”来进行调用父类版本被覆盖的成员函数。

6.   如果函数的参数是传值方式,形参是父类对象,实参是子类对象,则在函数的内部用形参调用成员函数依然是父类版本,因为传值只是用子类对象给父类对象赋值。但是如果形参是父类对象的引用或父类对象的指针,实参是子类对象或子类对象的地址,当子类中覆盖了父类的成员函数且父类中被覆盖的成员函数已被定义为虚函数时,   虽然父类引用或父类指针只能调用访问自身定义过的成员不能访问子类拓展的成员但当它直接调用被子类覆盖的成员函数时 就不再调用父类版本的此函数而直接调用子类中重新定义的重名函数。

7.   使用虚函数的三个必要条件:

第一 个是子类中对父类的某个成员函数进行了重新定义即覆盖了父类的成员函数。(覆盖的意思就是子类与父类的某个成员函数函数名和函数的参数列表(即参数个数、类型、出现顺序)完全相同,而返回值可以相同也可以不同);第二个是父类中被子类覆盖的成员函数前面加virtual 被声明为虚函数。第三个条件是把子类对象直接赋值给(或通过函数参数传给)父类引用,要么取子类对象的地址格式如“&子类对象”赋给(或传给)父类类型的指针。这三个条件缺一不可,只有在满足了这三个必要条件的情况下,当父类引用或父类指针调用被子类覆盖的成员函数时,才会调用子类的函数内容进行实现。

8.   使用虚函数(具有覆盖,virtual,父类引用或父类指针三个必要条件)与不是虚函数(不同时满足三个必要条件)的相同点与区别如下:他们的相同之处是当父类对象、父类引用或父类指针被用子类对象,取子类对象地址进行赋(传)值时,都只能调用父类自身定义过的成员,不能调用子类拓展的成员(成员名不同于父类)。他们的不同之处在于使用虚函数时,指向子类的父类指针(引用)直接调用被子类覆盖的成员函数时会实现子类中的这个函数的内容,如果想调用自身版本被覆盖的函数时需要用“父类引用.父类名::被覆盖的函数名”或者是

“父类指针—>父类名::被覆盖的函数名”来进行自身被覆盖函数的调用。而非虚函数,当指向子类对象的父类引用(父类指针)或者被子类对象赋过值的父类对象调用被覆盖的成员函数时,只会实现自身版本的内容。就等于说虚函数能直接利用父类指针(引用)来调用子类中覆盖父类的成员函数。又能通过::来调用父类自身版本的被覆盖的函数,就是说虚函数比非虚函数多了一子类版本的覆盖函数。

9.   把子类对象不论以何种方式传递给父类,当用父类调用时都只能调用自身定义过的成员。也就是说把子类对象中从父类继承下来的成员(成员变量和成员函数)又传给了父类,而子类自身拓展的成员被屏蔽掉了 不会传给父类对象。就是说子类对象仅把从父类继承来的基因交给父类对象保管。虚函数就是在此基础之上,又把子类重新定义过的基因(基因突变,基因名相同而性质不同)直接交给父类对象调用。

10.   如果父类的一个虚函数被自身的其他普通成员函数调用,子类的版本也会被正确调用。就是说对于虚函数而言,当指向子类对象的父类引用(父类指针)调用父类中的这个普通成员函数(但是函数体内对被子类重新定义覆盖过且用virtual声明过的父类成员函数进行了调用)时,当实现到调用包含的虚函数时也会自动调用子类版本的覆盖函数。但是对于构造函数和析构函数则例外,当父类的虚函数被自身的构造函数或析构函数调用时。实现的仍然是父类版本的虚函数。

11.   如果一个类有子类,则这个父类的析构函数必须是虚函数,即虚析构。如果父类的析构不是虚析构,则当(用delete)删除一个指向由动态分配生成子类对象的父类指针时,则将调用父类版本的析构函数,子类只释放了来自于父类的那部分成员变量,而没有释放子类扩展的那部分成员变量,造成内存泄露。相反如果在父类中将析构函数用virtual进行声明,那么删除指向子类对象的父类指针时系统就等于默认为子类覆盖了父类的析构函数,就会调用子类的析构函数,因为调用子类析构函数时,系统会先自动调用父类析构函数删除父类成员,然后再删除子类成员。所以不会造成错误。虚析构在父类声明即可。切记:只有指向由动态分配生成子类对象的父类指针才有必要用delete进行删除。如果没有用new动态生成例如声明子类对象再取子类对象的地址赋给父类指针等等都是属于局部变量类型将由系统按照栈后进先出的顺序自动调用析构函数。


例1 虚函数被类的构造析构函数和成员函数调用虚函数的执行过程,代码如下

#include<iostream>
class base{
public:
    base()
    {
        std::cout<<std::endl;
        std::cout<<"base constructor"<<std::endl;
        func1();
        std::cout<<std::endl;
    }
    virtual ~base()
    {
        std::cout<<std::endl;
        std::cout<<"base distructor"<<std::endl;
        func1();
        std::cout<<std::endl;
    }
    virtual void func1()
    {
        std::cout<<"base virtural func1"<<std::endl;
    }
    void func2()
    {
        std::cout<<"base member func2"<<std::endl;
        func1();
        std::cout<<std::endl;
    }
};
class derived:public base{
public:
    derived()
    {
        std::cout<<std::endl;
        std::cout<<"derived constructor"<<std::endl;
        func1();
        std::cout<<std::endl;
    }
    virtual ~derived()
    {
        std::cout<<std::endl;
        std::cout<<"derived distructor"<<std::endl;
        func1();
        std::cout<<std::endl;
    }
    virtual void func1()
    {
        std::cout<<"derived virtual func1"<<std::endl;
    }
};
int main()
{
    base *point = new derived();
    point->func2();
    delete point;
    return 0;
}

运行结果如下


即使func1是虚函数,在base类和derived的构造函数和析构函数里面,都是调用自己类里面的func1。而在普通成员函数func2调用func1,就会走虚函数的流程

 










你可能感兴趣的:(C++虚函数(3) 学习总结)