多态性

  • 目的:不同对象在接收到相同消息时(操作/函数)做不同响应
  • 现象:对应同样成员函数名称,执行不同函数体

多态性的实现

  • 虚函数:使用virtual关键字声明成员函数
  • 声明格式: virtual 函数返回值 函数名称(参数列表); (派生类重新实现时加不加virtual都行)
  • 指向派生类的基类指针会调用对应派生类实现的虚函数
  • 如果派生类不实现虚函数,则继承基类的虚函数实现
  • 当类中有虚函数的时候,会维持一个虚表指针,虚表指针指向虚表中对应到该类的虚函数的函数入口地址(运行时确定),这叫做函数的动态绑定(非虚函数是静态绑定)
    • 静态类型:对象在声明时采用的类型,在编译期既已确定;

    • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;

    • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;

    • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

    • 对象的动态类型可以更改,但是静态类型无法更改;

    • 要想实现多态,必须使用动态绑定;

    • 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;

    • 建议:绝对不要重新定义继承而来的非虚(non-virtual)函数(《Effective C++ 第三版》条款36),因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,也这将给程序留下不可预知的隐患和莫名其妙的BUG;

    class A
    {
    public:
    /*virtual*/ void func(){ std::cout << "A::func()\n"; }
    };
    class B : public A
    {
    public:
    void func(){ std::cout << "B::func()\n"; }
    };
    class C : public A
    {
    public:
    void func(){ std::cout << "C::func()\n"; }
    };
    C* pc = new C(); //pc的静态类型是它声明的类型C*,动态类型也是C*;
    B* pb = new B(); //pb的静态类型和动态类型也都是B*;
    A* pa = pc; //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*;
    pa = pb; //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*;
    C *pnull = NULL; //pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL;
    pa->func();      //A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类,都是直接调用A::func();
    pc->func(); //C::func() pc的动、静态类型都是C*,因此调用C::func();
    pnull->func(); //C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;
    • 如果注释掉类C中的func函数定义,其他不变,即
    class C : public A
    {
    };

    pa->func(); //A::func() 理由同上;
    pc->func(); //A::func() pc在类C中找不到func的定义,因此到其基类中寻找;
    pnull->func(); //A::func() 原因也解释过了;
    • 如果为A中的void func()函数添加virtual特性,其他不变,即
    class A
    {
    public:
    virtual void func(){ std::cout << "A::func()\n"; }
    };

    pa->func(); //B::func() 因为有了virtual虚函数特性,pa的动态类型指向B*,因此先在B中查找,找到后直接调用;
    pc->func(); //C::func() pc的动、静态类型都是C*,因此也是先在C中查找;
    pnull->func(); //空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;
    • 建议:绝对不要重新定义一个继承而来的virtual函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率),而virtual函数却是动态绑定。
    class E
    {
    public:
    virtual void func(int i = 0)
    {
    std::cout << "E::func()\t"<< i <<"\n";
    }
    };
    class F : public E
    {
    public:
    virtual void func(int i = 1)
    {
    std::cout << "F::func()\t" << i <<"\n";
    }
    };

    void test2()
    {
    F* pf = new F();
    E* pe = pf;
    pf->func(); //F::func() 1 正常,就该如此;
    pe->func(); //F::func() 0 哇哦,这是什么情况,调用了子类的函数,却使用了基类中参数的默认值!
    }

纯虚函数

  • 充当占位函数,没有任何实现
  • 派生类负责实现其具体功能
  • 声明格式: virtual 函数返回值 函数名称(参数列表) = 0;
  • 纯虚函数的虚表指针是存在的,只不过该指针指向0地址

抽象类(纯虚类)

  • 带有纯虚函数的类
  • 作为类继承层次的上层
  • 不能构造抽象类的对象,但可以存在抽象类的指针或引用

虚析构函数

  • 保持多态性需要虚析构函数,以保证能够正确释放对象
  • 当一个类有子类时,该类的析构函数必须是虚函数,否则可能会有资源释放不完全的情况(因为非虚函数是静态绑定的);
class A
{
public:
~A(){ std::cout << "~A()!" << std::endl; }
}
class B : public A
{
public:
~B(){ std::cout << "~B()!" << std::endl; }
}
class C : public A
{
public:
~C(){ std::cout << "~C()!" << std::endl; }
}

int main()
{
A* pa1 = new B();
A* pa2 = new C();
delete pa1;
delete pa2;
}
// 只会调用父类的析构函数,如果这时子类的析构函数中有关于内存释放的操作,将会造成内存泄露。
//~A()!
//~A()!
  • 需要给父类的析构函数加上virtual
class A
{
public:
virtual ~A(){ std::cout << "~A()!" << std::endl; }
}
// 输出如下
//~B()!
//~A()!
//~C()!
//~A()!