C++学习笔记-多态

多态

概述

所谓多态,其实就是:一个接口,多种方法;或者换种说法:发出同样的消息(函数调用),被不同类型的对象接收时(不同的函数拥有者),有可能导致完全不同的行为。

我们前面说过的函数重载(当然也包括运算符重载),就是一种多态,一个函数名(接口),对应多种具体的实现,它属于静态的多态,下一章要讲的模板也是静态多态,一套接口(函数/类)对应不同的数据类型或者行为(因为算法本身也可以作为模板参数);另外基于对象的编程也可以看成是静态多态,接口类里面是一些function,然后具体使用时,可以使用bind绑定不同的实现,类似于挂衣架,流水的衣服,铁打的挂衣架。

静态多态是指编译期就可以确定函数的入口地址,而本章要讲的是动态多态,它只有到运行期才能确定函数入口地址。动态多态在效率上要比静态多态差,因为有额外的运行时开销。

放到C++语言层面,多态其实就是指同一个操作作用于不同的对象会产生不同的响应

多态的优点:多态使得我们可以以一致的观点对待同一基类的所有派生类,减轻了分别设计的负担,提高了代码重用度。

C++通过虚函数来实现动态多态以及简单的RTTI。

静态联编与动态联编:

静态联编:
程序调用函数时,具体应使用哪个代码块是由编译器决定的。以函数重载为例,C++编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数,称为联编(binding)。

编译器可以在编译过程中完成这种联编,在编译过程中进行的联编叫静态联编(static binding)或早期联编(early binding)。

动态联编:
在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时完成选择,因此编译器必须提供这么一套称为“动态联编”(dynamic binding)的机制,也叫晚期联编(late binding),C++通过虚函数来实现动态联编。

虚函数

虚函数的定义和声明

将成员函数声明为虚函数,只需要在成员函数原型前加一个关键字virtual即可。

注:
1. 构造函数不能声明为虚函数
2. 虚函数不能声明为static函数

如果一个基类的成员函数定义为虚函数,那么,它在所有派生类中也保持为虚函数;即使在派生类中省略了virtual关键字,也仍然是虚函数。

派生类可根据需要对基类虚函数进行override,上一章说过,override的格式要求很严:

  • 与基类的虚函数有相同的参数个数
  • 与基类的虚函数有相同的参数类型
  • 与基类的虚函数有相同的返回类型

说白了,就是要一模一样,并且是虚函数。

为什么多态需要虚函数:
#include <iostream>
using std::cout;
using std::endl;

class Base
{
public:
    virtual      // 加与不加大不相同
    void disp()
    {
        cout << "Base::disp" << endl;
    }
};

class Child1 : public Base
{
public:
    void disp() // 派生类child1中定义的disp函数将base类中定义的disp函数隐藏
    {
        cout << "Child1::disp" << endl;
    }
};

class Child2 : public Base
{
public:
    void disp() // 派生类child2中定义的disp函数同样会隐藏base类中定义的disp函数
    {
        cout << "Child2::disp" << endl;
    }
};

void Display(Base* pb) // 目的是实现同一接口(函数),处理不同类对象
{
    pb->disp();
}

int main()
{
    Base *pBase = NULL; 

    Base obj_base;
    Child1 obj_child1;
    Child2 obj_child2;

    pBase = &obj_child1;
    pBase->disp(); // no virtual: Base::disp | virtual: Child1::disp

    // 通过指针调用
    Display(&obj_base); // no virtual: Base::disp | virtual: Base::disp
    Display(&obj_child1); // no virtual: Base::disp | virtual: Child1::disp
    Display(&obj_child2); // no virtual: Base::disp | virtual: Child2::disp
    (*pBase).disp(); // no virtual: Base::disp | virtual: Child1::disp

    getchar();
    return 0;
}

从代码中可以看到,只有虚函数,才能实现一个接口(void Display(Base*)),多种行为(Display(&obj_base), Display(&obj_child1), Display(&obj_child2)具有不同行为)。至于虚函数时怎样实现多态的我们后面会分析。

虚函数的访问
  1. 通过对象名访问:

    和普通函数一样,虚函数一样可以通过对象名来调用,此时编译器采用的是静态联编。

    通过对象名访问虚函数时, 调用哪个类的函数取决于定义对象名的类型。对象类型是基类时,就调用基类的函数;对象类型是子类时,就调用子类的函数。

  2. 通过指针访问:
    使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向的对象类型;

    使用指针访问虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数(动态联编),而与指针本身的类型无关。

  3. 通过引用访问
    使用引用访问虚函数,与使用指针访问虚函数类似;

    不同的是,引用一经声明后,引用变量本身无论如何改变,其调用的函数就不会再改变,始终指向其开始定义时的函数。

  4. 类成员函数中访问
    在类内的成员函数中访问该类层次中的虚函数,采用动态联编,要使用this指针。

  5. 通过构造函数或析构函数中访问:
    构造函数和析构函数是特殊的成员函数,在其中访问虚函数时,C++采用静态联编,即在构造函数或析构函数内,即使是使用“this->虚函数名”的形式来调用,编译器仍将其解释为静态联编的“本类名::虚函数名”。即它们所调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的是基类中的虚函数。但绝不会调用任何在派生类中重定义的虚函数

炎炎夏日,来个例子清爽一下:

#include <iostream>

class Base
{
public:
    void Show()
    {
        Disp(); // <=> this->Disp()
    }
    virtual void Disp()
    {
        std::cout << "Base::Disp()" << std::endl;
    }
};

class Child : public Base
{
public:
    //void Show()
    //{
   
    //  Disp();
    //}
    void Disp() override
    {
        std::cout << "Child::Disp()" << std::endl;
    }
};

int main(void)
{
    Base base;
    Child child;

    // 直接通过对象调用,与普通继承效果一样
    base.Disp();         // Base::Disp()
    child.Disp();        // Child::Disp()
    child.Child::Disp(); // Child::Disp()
    child.Base::Disp();  // Base::Disp()

    // 通过基类指针调用,触发多态
    Base *p = &child;
    p->Disp();           // Child::Disp()

    Base *p2 = &base;
    p2 = &child;
    p2->Disp();          // Child::Disp()

    // 通过引用调用,与指针类似,但由于引用的特性,必须定义同时初始化,再不可更改绑定
    // 即使改了,也不是改的引用,而是引用的对象,这与指针不同。
    Base &ref = child;
    ref.Disp();          // Child::Disp()

    Base &ref2 = base;

    ref2 = child; // 引用还是指向base,相当于base = child; 
                  // 没有多态,存在对象切割,现在base就是child'体内'的那个Base了。
    ref2.Disp();         // Base::Disp()

    // 通过成员函数调用
    p = &child;
    p->Show();           // Child::Disp()
    p = &base;
    p->Show();           // Base::Disp()

    return 0;
}
虚析构函数

虽然构造函数不能被定义成虚函数,但析构函数可以定义为虚函数,一般来说,如果类中定义了虚函数,析构函数也应被定义为虚析构函数,尤其是类内有申请的动态内存,需要清理和释放的时候。

只需要在基类的析构函数前面加virtual即可,派生类的析构函数会自动变为虚函数的。

如果一个类要作为多态基类,就应当使用虚析构函数。

如果一个类永远不会被派生类继承,就不要定义成虚函数,无端增加数据复杂性。

#include <iostream>
using std::cout;
using std::endl;

class Base
{
private:
    char* data;

public:
    Base()
    {
        data = new char[64];
        cout << "Base::Base()" << endl;
    };

    virtual ~Base() // 虚析构函数
    {
        delete[] data;
        cout << "Base::~Base()" << endl;
    };
};

class Child : public Base
{
private:
    char* m_data;

public:
    Child() :Base()
    {
        m_data = new char[64];
        cout << "Child::Child()" << endl;
    };

    ~Child() // 析构函数,继承虚拟virtual,不需要再加virtual
    {
        delete[] m_data;
        cout << "Child::~Child()" << endl;
    };
};

class GrandChild :public Child
{
private:
    char* mm_data;

public:
    GrandChild() :Child()
    {
        mm_data = new char[64];
        cout << "GrandChild::GrandChild()" << endl;
    };

    ~GrandChild() // 虚析构函数,virtual从继承结构中得来
    {
        delete[] mm_data;
        cout << "GrandChild::~GrandChild()" << endl;
    };
};

int main()
{
    Base *pB = new Child;
    delete pB; // 调用的是指针指向的实际对象(Child对象)的析构函数,而
               // Child::Child()又会调用Base::Base(),这样就没有内存泄漏。

    cout << "=============" << endl;

    Child* pC = new GrandChild; 
    delete pC; // 调用的是指针指向的实际对象(GrandChild对象)的析构函数,而
               // GrandChild::GrandChild()又会调用Child::Child(),这样
               // 就没有内存泄漏。

    cout << "======不推荐下面这种用法=======" << endl;

    GrandChild *pG = (GrandChild *)new Base;
    delete pG; // 调用的是指针指向的实际对象(Base对象)的析构函数

  // -> 如果去掉基类析构函数前的virtual, 执行到
               // delete[] mm_data时调用的将是GrandChild::~GrandChild(),
               // 会报内存错误, 因为mm_data, m_data根本就没有new


    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值