moc 发表于 2018-9-7 15:17:30

035-C++之多态

本帖最后由 moc 于 2018-9-7 19:00 编辑

1、多态基础
1.多态的实现效果
        同样的调用语句有多种不同的表现形态。
        一个函数在类族中穿梭的时候,会表现出不同的行为。

        根据实际的对象类型来判断重写函数的调用。
        如果父类指针指向的是父类对象则调用父类中定义的函数。
        如果父类指针指向的是子类对象则调用子类中定义的重写函数。
2.多态的意义
        封装可以使得代码模块化;
        继承可以在原有的代码基础上扩展,他的目的都是为了代码重用。
        多态则是为了接口重用,也就是说,不论传递过来的究竟是哪个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法,是更高意义上的代码复用。
        多态:设计模式的基础。
3.静态联编与动态联编
class Base
{
public:
    /*virtual*/ void fun(){ cout << "Base::fun()" << endl; }
};

class Derived : public Base
{
public:
    void fun(){ cout << "Derived::fun()" << endl; }
};

void FunTest()
{
    Base b;
    Derived d;
    b.fun();         //调用基类的fun()打印Base::fun()
    d.fun();         //子类虽然继承了基类的fun()但是子类本身中fun(),此时构成了重定义,即基类中的fun()被隐藏,因此打印子类的fun()
    d.Base::fun();      //若想调用基类的fun()需要加类的作用域限定符,打印Base::fun()

    Base* pb = &b;
    Derived* pd = &d;
    pb->fun();       //pb指向基类,打印Base::fun()
    pd->fun();       //pd指向子类,打印Derived::fun()

    pb = &d;       pb->fun();   //父类指针pb指向子类,却打印Base::fun()
    //同理引用也是
    Base& rb = b;
    Derived& rd = d;
    rb.fun();      //rb引用基类,打印Base::fun()
    rd.fun();      //rd引用子类,打印Derived::fun()

    Base& rd2 = d;       rd2.fun();    //父类引用rd2引用子类,却打印Base::fun()
}

int main()
{
    FunTest();
    return 0;
}
        C++的继承,有赋值兼容性原则,即父类指针可以指向子类,那么为什么还会出现父类指针指向子类或者父类对象引用子类对象,却调用父类自己的fun函数打印Base::fun()呢?    其中的原因就是静态联编,在编译时期就将函数实现和函数调用关联起来,不管是引用还是指针在编译时期都是Base类的自然调用Base类的fun()。
动态联编:也即动态多态,只有在程序运行期间(非编译期)才判断所引用对象的实际类型,根据其实际类型调用相应的方法。即根据实际的对象类型来判断重写函数的调用,也即在运行时期就将函数实现和函数调用关联起来,即迟绑定、晚绑定。
4.virtual关键字
       场景1:虚继承    多继承中为了防止从不同的路径中对同一个的了的多次继承。
       场景2:虚函数    告诉编译器这个函数要支持多态;不要根据指针类型判断如何调用,而是根据指针所指向的实际对象的类型来判断如何掉调用。
       场景3:纯虚函数    ****
5.多态实现的三个条件
        ① 有继承
        ② 有虚函数重写
        ③ 有父类指针(引用)指向子类对象
6.实现多态的理论基础
        函数指针做函数参数。
        C函数指针是C++至高无上的荣耀;C++函数指针一般有正、反两种用法。
7.多态的注意点
        ①基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
        ②静态成员函数和友元函数不能定义为虚函数。
        ③如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加。

2、重载、重写、重定义
重载:指在同一作用域中允许有多个同名函数,而这些函数的参数列表不同,包括参数个数不同,类型不同,次序不同,需要注意的是返回值相同与否并不影响是否重载。在编译期间确定啦。

重写::分为普通函数重写(重定义)和 虚函数重写(虚函数重写),父子(类)之间,他们的成员函数的三要素(函数名、函数参数、函数返回类型)完全一样,注意virtual是关键字,不是返回类型。
        重定义:即普通函数重写,父类中的函数没有virtual关键字,会发生名称覆盖,即隐藏父类中的同名函数,如果要使用父类中的函数需要加域作用符::,属于静态联编,编译期间确定;
        虚函数重写:父类中的函数冠以virtual关键字,属于动态联编,会发生多态现象。

3、多态原理探究
        C/C++都是静态编译型语言。在编译时,编译器会自动根据指针的类型判断指向的是一个什么样的对象,因为程序没有运行,所以编译器不可能知道父类对象具体指的是父类对象还是子类对象,为了安全,编译器只能认为父类指针指向的是父类对象。
为了实现在运行时父类指针可以根据其指向的对象来判断是调父类的函数还是子类的函数,C++编译器需要做一些特殊处理:
        ① 当类中声明虚函数时,C++编译器会在自动在类中隐式创建一个VPTR指针,同时为该类生成一个虚函数表,让VPTR指针指向该虚函数表。
        ② 虚函数表是一个存储类的虚成员函数指针的数据结构,由编译器自动生成和维护,virtual成员函数都会被编译器自动放入虚函数表。
        ③ 当类中存在虚函数时,每个对象都有一个指向虚函数表的指针(vptr指针),同一个类只有一个虚函数表(即父类和子类都有他们自己的虚函数表),被所有对象共享。
原理图解:

说明1:通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。因此在效率上,虚函数的效率要低很多。
说明2:出于效率考虑,没有必要将所有的成员函数都声明为虚函数。

4、多态遇上构造函数与析构函数
1.VPTR指针在什么时候被初始化?
        在创建对象时,VPTR指针的指向是在运行完构造函数后才最终确定的。
具体步骤:
        ① 在创建子类对象,子类会先调用父类的构造函数,初始化从父类继承的成员,这个时候子类的VPTR指针指向父类的虚函数表;
        ② 当父类的构造函数执行完毕后,再执行子类的构造函数时,子类的VPTR指针才指向自己的虚函数表。
结论:
        不要在构造函数中使用虚函数,构造函数中不能实现多态。当然,也没有虚构造函数啦。
        相仿也不要再析构函数中使用虚函数。
2.虚析构函数
class AA
{
public:
        AA(int a = 0)
        {
                this->a = a;
                print();   
        }

        /*virtual*/ ~AA()// 设为虚析构函数便能正常析构子类
        { cout << "父类的析构" << endl; }

        virtual void print()
        { cout << "父类的" << a << endl; }

protected:
        int a;       
};

class BB: public AA
{
public:
        BB(int a = 0, int b = 0)
        {
                this->a = a;
                this->b = b;
        }

        ~BB()
        {
                cout << "子类的析构" << endl;
        }

        virtual void print()
        {
                cout << "子类的" << a << b << endl;
        }

private:
        int b;
};

void howToDelete(AA *pBase)
{
        delete pBase;
}

int main()
{
        BB *b1 = new BB(1, 2);
        b1->print();
        // delete b1;可以正常释放
        howToDelete(b1);// 不可以,子类的析构函数没有被调用
        system("pause");
        return 0;
}
申请内存空间创建子类对象时返回的子类指针,在把该指针传给父类的指针,然后用父类的指针取释放内存空间时,会导致子类的析构函数没有被调用,如果要想该子类的析构函数被正常调用,需要在父类的虚构函数前面加virtual关键字,使之成为虚析构函数,造成这种现象的原因用静态编译来说明。
结论: 最好将基类的析构函数声明为虚函数。(析构函数比较特殊,因为派生类的析构函数跟基类的析构
函数名称不一样,但是构成覆盖,这里编译器做了特殊处理)。
如果想通过父类指针 执行 所有子类对象的析构函数,那么需要在父类的析构函数前加上virtual关键字,即把父类析构函数变为虚析构函数。
5、子类对象和父类指针的混搭
指针的步长问题在C++中仍然有效,如果子类中有新成员,会导致子类指针和父类指针的步长不一样,如果再对于指向子类对象的父类指针进行++等移动指针时会出错。
结论:不要用父类指针做辅助指针变量,去遍历一个子类的数组。

hogen 发表于 2019-10-25 09:43:53

受益匪浅感谢

ShaeZhang 发表于 2020-4-26 10:15:55

多态的内容很详细。写的很清楚 *_**_**_**_*
感谢^_^!!

1000小千哥 发表于 2020-5-6 07:39:44

道行不够,看的不是很明白

ISHEEPI 发表于 2020-6-30 17:02:34

写的很棒   不过可以把 子类不能正常析构的危害写出来就更好了
页: [1]
查看完整版本: 035-C++之多态