虚函数与多态(一)

  • 多态性是面向对象程序设计的重要特征之一。
  • 多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。
  • 多态的实现:
    ①、函数重载
    ②、运算符重载
    ③、模板
    以上三种为静态绑定
    ④、虚函数【这节的重点】
    这个为动态绑定

  • 绑定过程出现在编译阶段,在编译期就已确定要调用的函数。
  • 绑定过程工作在程序运行时执行,在程序运行时才确定将要调用的函数。

  • 虚函数的概念:在基类中冠以关键字 virtual 的成员函数。
  • 虚函数的定义:
    virtual 函数类型 函数名称(参数列表);
    如果一个函数在基类中被声明为虚函数,则他在所有派生类中都是虚函数
  • 只有通过基类指针或引用调用虚函数才能引发动态绑定。
  • 虚函数不能声明为静态。【至于为什么,下面会说明原因】

下面看下代码:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void fun1() {
        cout<<"Base::fun1() ..."<<endl;
    }

    virtual void fun2() {
        cout<<"Base::fun1() ..."<<endl;
    }

    void fun3() {
        cout<<"Base::fun3() ..."<<endl;
    }
};

class Derived : public Base {
public:
    virtual void fun1() {
        cout<<"Derived::fun1() ..."<<endl;
    }

    virtual void fun2() {
        cout<<"Derived::fun2() ..."<<endl;
    }

    void fun3() {
        cout<<"Derived::fun3() ..."<<endl;
    }
};

int main(void) {
    Base* p;
    Derived d;
    p = &d;
    p->fun1();
    p->fun2();
    p->fun3();
    return 0;
}

编译运行:

从结果可以发现一个现象:fun1是虚函数,基类指向派生类对象,调用的是派生类对象的虚函数;而fun3是非虚函数,根据p指针指向实际类型来调用相应类的成员函数。

这就道出了虚函数的意义了,可以以一致的观点来看待所有的对象,这样也是体现多态的很好的表现。

那有一个细节需要注意,如果我把派生类的fun1()函数的virtual关键字给去掉,那它还是虚函数么,答案是肯定的,也就是如果父类是虚函数,则派生类同名的虚函数的关键字是可以省掉的。

  • 何时需要虚析构函数?
  • 当你可能通过基类指针删除派生类对象时。
  • 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数作为虚函数。

下面来看下代码:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void fun1() {
        cout<<"Base::fun1() ..."<<endl;
    }

    virtual void fun2() {
        cout<<"Base::fun1() ..."<<endl;
    }

    void fun3() {
        cout<<"Base::fun3() ..."<<endl;
    }

    Base() {
        cout<<"Base() ..."<<endl;
    }

    ~Base() {
        cout<<"~Base() ..."<<endl;
    }
};

class Derived : public Base {
public:
    virtual void fun1() {
        cout<<"Derived::fun1() ..."<<endl;
    }

    virtual void fun2() {
        cout<<"Derived::fun2() ..."<<endl;
    }

    void fun3() {
        cout<<"Derived::fun3() ..."<<endl;
    }

    Derived() {
        cout<<"Derived() ..."<<endl;
    }

    ~Derived() {
        cout<<"~Derived() ..."<<endl;
    }
};

int main(void) {
    Base* p;
    p = new Derived;

    p->fun1();
    delete p;//这时会调用基类的析构还是派生类呢?

    return 0;
}

编译运行:

由于Base类中的析构函数不是虚析构,所以它释放的还是指针指向的对象,这样就会造成派生类的内存泄漏了,这时虚析构函数就派上用场啦:

再次编译运行:

  •  虚函数的动态绑定是通过虚表来实现的。
  • 包含虚函数的类头4个字节存放指向虚表的指针。

下面用代码来探讨一下有了虚函数的类的数据模型:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void fun1() {
        cout<<"Base::fun1() ..."<<endl;
    }

    virtual void fun2() {
        cout<<"Base::fun1() ..."<<endl;
    }
    int data1_;
};

class Derived : public Base {
public:
    virtual void fun2() {
        cout<<"Derived::fun2() ..."<<endl;
    }
    int data2_;
};

int main(void) {
    cout<<sizeof(Base)<<endl;
    cout<<sizeof(Derived)<<endl;
    return 0;
}

先来打印一下基类与派生类的大小:

Base类中成员变量就只有一个4个字节的int数据,为啥大小为8呢?原因就是因为"包含虚函数的类头4个字节存放指向虚表的指针":

口说无评,下面用代码来验证上面画的数据模型:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void fun1() {
        cout<<"Base::fun1() ..."<<endl;
    }

    virtual void fun2() {
        cout<<"Base::fun1() ..."<<endl;
    }
    int data1_;
};

class Derived : public Base {
public:
    virtual void fun2() {
        cout<<"Derived::fun2() ..."<<endl;
    }
    int data2_;
};

typedef void (*FUNC)();

int main(void) {
    cout<<sizeof(Base)<<endl;
    cout<<sizeof(Derived)<<endl;
    Base b;
    long** p = (long**)&b;
    FUNC fun = (FUNC)p[0][0];
    fun();
    return 0;
}

编译运行:

果真如模型所画,虚表里面的第一项存放的就是第一个虚函数,如果我们将修饰符改为私有呢,还能正常调用么?

#include <iostream>
using namespace std;

class Base {
private:
    virtual void fun1() {
        cout<<"Base::fun1() ..."<<endl;
    }

    virtual void fun2() {
        cout<<"Base::fun1() ..."<<endl;
    }
    int data1_;
};

class Derived : public Base {
public:
    virtual void fun2() {
        cout<<"Derived::fun2() ..."<<endl;
    }
    int data2_;
};

typedef void (*FUNC)();

int main(void) {
    cout<<sizeof(Base)<<endl;
    cout<<sizeof(Derived)<<endl;
    Base b;
    long** p = (long**)&b;
    FUNC fun = (FUNC)p[0][0];
    fun();
    return 0;
}

答案是肯定的,因为我们是用内存模型的方式来直接调用的,跟公有私有木有任何关系,同样的方法我们可以调用第二个虚函数:

编译运行:

不要被输出结果吓倒,因为我们的猜想并没有错,而只是代码日志打错了,如下:

粗心呀~~将其改过来:

再来看结果:

下面来分析下它的派生类Derived的数据模型,在分析之前,为了更好的说明问题,在派生类中再声明一个自己的虚函数:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void fun1() {
        cout<<"Base::fun1() ..."<<endl;
    }

    virtual void fun2() {
        cout<<"Base::fun2() ..."<<endl;
    }
    int data1_;
};

class Derived : public Base {
public:
    virtual void fun2() {
        cout<<"Derived::fun2() ..."<<endl;
    }
    virtual void fun3() {
        cout<<"Derived::fun3() ..."<<endl;
    }
    int data2_;
};

typedef void (*FUNC)();

int main(void) {
    cout<<sizeof(Base)<<endl;
    cout<<sizeof(Derived)<<endl;
    Base b;
    long** p = (long**)&b;
    FUNC fun = (FUNC)p[0][0];
    fun();
    fun = (FUNC)p[0][1];
    fun();
    return 0;
}

先画下它的数据模型,呆会再用代码来验证:

输出结果:

所以之前为什么说“虚函数不能声明为静态”,因为它没有this指针,不是对象的一部分,也就没办法取出对象的前4个字节虚表指针进而找到虚表。

以上的这个代码是节选自MFC框架进行简写来说明其问题,下面按上面来编写代码:

#include <iostream>
using namespace std;

class CObject {
public:
    virtual void serialize() {
        cout<<"CObject::serialize..."<<endl;
    }
};

class CDocument : public CObject {
public:
    int data1_;
    void func() {
        cout<<"CDocument::func..."<<endl;
        serialize();
    }
    virtual void serialize() {
        cout<<"CDocument::serialize..."<<endl;
    }
};

class CMyDoc : public CDocument {
public:
    int data2_;
    virtual void serialize() {
        cout<<"CMyDoc::serialize..."<<endl;
    }
};


int main(void) {
    CMyDoc mydoc;
    CMyDoc* pmydoc = new CMyDoc;

    cout<<"#1 testing"<<endl;
    mydoc.func();

    cout<<"#2 testing"<<endl;
    ((CDocument*)(&mydoc))->func();

    cout<<"#3 testing"<<endl;
    pmydoc->func();

    cout<<"#4 testing"<<endl;
    ((CDocument)mydoc).func();

    return 0;
}

编译运行:

其中tesing4才是体现object slicing的地方,mydoc对象强制转换成CDocument对象,也就是向上转型,完完全全将派生类对象转换为了基类对象,其虚表也会发生变化,所以输出结果都是输出的基类相关的。实际上在转换成CDocument对象时,是调用了它的拷贝构造函数,为了验证这点,我们来重写拷贝构造既可:

编译运行:

提示CMyDoc没有合适的默认构造函数,这是由于CDocument类由于重写了拷贝构造函数之后,没有默认构造函数了,而CMyDoc没有显示提供到底怎么构造父类CDocument,默认就是调用CDocument的无参的默认构造函数,所以在CDocument类中需要声明出来:

再次编译运行:

  • 成员函数被重载的特征:【overload】
    (1)相同的范围(在同一个类中);
    (2)函数名字相同;
    (3)参数不同;
    (4)virtual关键字可有可无。
  • 覆盖是指派生类函数覆盖基类函数,特征是:【override】
    (1)不同的范围(分别位于派生类与基类);
    (2)函数名字相同;
    (3)参数相同;
    (4)基类函数必须有virtual关键字。
  • 重定义(派生类与基类)【overwrite】
    (1)不同的范围(分别位于派生类与基类);
    (2)函数名与参数都相同,无virtual关键字
    (3)函数名相同,参数不同,virtual可有可无

posted on 2016-07-12 22:13  cexo  阅读(210)  评论(0)    收藏  举报

导航