虚函数与多态

(一):

多态

静态绑定与动态绑定

虚函数

虚表指针

object slicing与虚函数

overload、override 、overwrite


多态性是面向对象程序设计的重要特征之一。

多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。

关于多态:


多态的实现:

静态绑定:函数重载、运算符重载、模板

动态绑定:虚函数


静态绑定

绑定过程出现在编译阶段,在编译期就已确定要调用的函数。

动态绑定

绑定过程工作在程序运行时执行,在程序运行时才确定将要调用的函数。


虚函数的概念:在基类中冠以关键字 virtual 的成员函数

虚函数的定义:

virtual 函数类型 函数名称(参数列表);

如果一个函数在基类中被声明为虚函数,则他在所有派生类中都是虚函数

只有通过基类指针或引用调用虚函数才能引发动态绑定

#include <iostream>
using namespace std;
 
 
class Base
{
public:
    virtual void Fun1()
    {
        cout<<"Base::Fun1 ..."<<endl;
    }
 
    virtual void Fun2()
    {
        cout<<"Base::Fun2 ..."<<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();       // Fun1是虚函数,基类之指针指向派生类对象,调用的是派生类对象的虚函数
    p->Fun2();
    p->Fun3();       // Fun3非虚函数,根据p指针实际类型来调用相应类的成员函数
 
    return 0;
}

虚函数不能声明为静态  

不能够是友元函数

只能是成员函数

这个跟虚函数的动态绑定是有关的


虚析构函数

何时需要虚析构函数?

1、当你可能通过基类指针删除派生类对象时

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

注:删除子类对象的同时其实也删除了父类对象!因为是继承关系


如果析构函数不是虚的,调用的析构函数是基类的虚构函数

#include <iostream>
using namespace std;
 
 
class Base
{
public:
    virtual void Fun1()
    {
        cout<<"Base::Fun1 ..."<<endl;
    }
 
    virtual void Fun2()
    {
        cout<<"Base::Fun2 ..."<<endl;
    }
 
    void Fun3()
    {
        cout<<"Base::Fun3 ..."<<endl;
    }
 
    Base()
    {
        cout<<"Base ..."<<endl;
    }
    // 如果一个类要做为多态基类,要将析构函数定义成虚函数
    virtual ~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;
}

虚表指针


虚函数的动态绑定是通过虚表来实现的。

包含虚函数的类头4个字节存放指向虚表的指针

让虚表指针指向虚表

(注:与前面的虚基类表是不一样的,这个是虚表,上节课是虚基类表指针,这节课是虚表指针)


关于函数指针。。。

‍typedef void (*FUNC)();

1.定义函数指针类型:

typedef int (*fun_ptr)(int,int);

2.申明变量,赋值:

fun_ptr max_func=max;

也就是说,赋给函数指针的函数应该和函数指针所指的函数原型是一致的。

#include<stdio.h>
void FileFunc()
{
    printf("FileFunc\n");
}
void EditFunc()
{
    printf("EditFunc\n");
}
void main()
{
    typedef void (*funcp)();
    funcp pfun= FileFunc;
    pfun();
    pfun = EditFunc;
    pfun();
}
内存模型:

#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:
    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();
    cout<<endl;
     
    Derived d;
    p = (long**)&d;
    fun = (FUNC)p[0][0];
    fun();
    fun = (FUNC)p[0][1];
    fun();
    fun = (FUNC)p[0][2];
    fun();
 
    Base* pp = &d;
    pp->Fun2();
    return 0;
}


派生类可以将虚函数给覆盖掉

基类指针指向派生类,取出对象的头四个字节,然后进行偏移,就调用派生类的函数,达到动态绑定的目的


静态函数不能声明为静态的原因:比如:Base:Fun2(),是直接访问的,他没有this指针,就不是对象的一部分,就没办法取出对象,就没办法调用虚函数


1、为什么C++不支持普通函数为虚函数?      

  普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思, 因此编译器会在编译时邦定函数。  2、为什么C++不支持构造函数为虚函数?        

这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始 化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论) 

 3、为什么C++不支持内联成员函数为虚函数?        

其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是 为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)  

4、为什么C++不支持静态成员函数为虚函数?        

这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码, 他也没有要动态邦定的必要性。  

5、为什么C++不支持友元函数为虚函数?        

因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。


object slicing与虚函数

#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;
    }
    CDocument()
    {
        cout<<"CDocument()"<<endl;
    }
    //提供了拷贝构造函数  则类的构造函数也要有
    CDocument(const CDocument& other)
    {
        cout<<"CDocument(const CDocument& other)"<<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();      //并不是指针的指向,而是Object slicing 派生类的特有成员会消失
    ​    ​    ​    ​    ​    ​    ​    ​    ​//会调用拷贝构造函数  基类到派生类上一节已经提到
                                    //mydoc对象强制转换为CDocument对象,向上转型
                                    //完完全全将派生类对象转化为了基类对象
 
    return 0;
}

成员函数被重载的特征:overload

(1)相同的范围(在同一个类中);

(2)函数名字相同;

(3)参数不同;

(4)virtual关键字可有可无。

覆盖是指派生类函数覆盖基类函数,特征是:  override

(1)不同的范围(分别位于派生类与基类);

(2)函数名字相同;

(3)参数相同;

(4)基类函数必须有virtual关键字。

重定义(派生类与基类)  overwrite

(1)不同的范围(分别位于派生类与基类);

(2)函数名与参数都相同,无virtual关键字

(3)函数名相同,参数不同,virtual可有可无


(二):纯虚函数、抽象类、多态

纯虚函数

虚函数是实现多态性的前提,需要在基类中定义共同的接口,接口要定义为虚函数。


如果基类的接口没办法实现怎么办?如形状类Shape

解决方法:将这些接口定义为纯虚函数


基类之指针指向派生类之对象,调用的是派生类的虚函数,这就使得我们可以以一致的观点看待不同的派生类对象

Shape

    Draw()

Circle

    Draw()

Square

    ​Draw()


在基类中不能给出有意义的虚函数定义,这时可以把它说明成纯虚函数,把它的定义留给派生类来做

定义纯虚函数:

class 类名{

        virtual 返回值类型 函数名(参数表) = 0;

    };

纯虚函数不需要实现   虚函数是必须要实现的!

只要一个类有一个纯虚函数,那么这个类就是一个抽象类


抽象类

作用:

1、抽象类为抽象和设计的目的而声明,将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。

2、对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。

注意:

1、抽象类只能作为基类来使用。

2、不能声明抽象类的对象。

构造函数不能是虚函数,析构函数可以是虚函数。

注:因为虚表的存在,析构函数声明虚函数可以避免内存泄漏


3、抽象类不能用于直接创建对象实例,可以声明抽象类的指针和引用

4、可使用指向抽象类的指针支持运行时多态性

4、派生类中必须实现基类中的纯虚函数,否则它仍将被看作一个抽象类


多态优点

多态性有助于更好地对程序进行抽象

1、控制模块能专注于一般性问题的处理

2、具体的操作交给具体的对象去做

多态性有助于提高程序的可扩展性

1、可以把控制模块与被操作的对象分开

2、可以添加已定义类的新对象,并能管理该对象

3、可以添加新类(已有类的派生类)的新对象,并能管理该对象 OCP原则

//可以由等下的程序看出来  只需要增加对象就可以了

#include <iostream>
#include <vector>
#include <string>
using namespace std;
 
//抽象类   是不能定义对象的  是现实世界中不存在的实物
//仅仅只是一个抽象的东西  派生类必须要实现纯虚函数
class Shape
{
public:
    //纯虚函数
    virtual void Draw() = 0;
    virtual ~Shape() {}
};
 
 
class Circle : public Shape
{
public:
    void Draw()
    {
        cout<<"Circle::Draw() ..."<<endl;
    }
    ~Circle()
    {
        cout<<"~Circle ..."<<endl;
    }
};
 
class Square : public Shape
{
public:
    void Draw()
    {
        cout<<"Square::Draw() ..."<<endl;
    }
    ~Square()
    {
        cout<<"~Square ..."<<endl;
    }
};
 
class Rectangle : public Shape
{
public:
    void Draw()
    {
        cout<<"Rectangle::Draw() ..."<<endl;
    }
    ~Rectangle()
    {
        cout<<"~Rectangle ..."<<endl;
    }
};
 
void DrawAllShapes(const vector<Shape*>& v)
{
    vector<Shape*>::const_iterator it;
    //it中的内容是指针  所以*it是指针
    for (it=v.begin(); it!=v.end(); ++it)
    {
        (*it)->Draw();
    }
}
 
void DeleteAllShapes(const vector<Shape*>& v)
{
    vector<Shape*>::const_iterator it;
    for (it=v.begin(); it!=v.end(); ++it)                      
    {
        delete(*it);
    }
}
 
// 简单工厂模式
class ShapeFactory
{
public:
    static Shape* CreateShape(const string& name)
    {
        Shape* ps = 0;
        if (name == "Circle")
        {
            ps = new Circle;
        }
        else if (name == "Square")
        {
            ps = new Square;
        }
        else if (name == "Rectangle")
        {
            ps = new Rectangle;
        }
 
        return ps;
    }
};
 
int main(void)
{
    //Shape s;      //Error,不能实例化抽象类
    vector<Shape*> v;
    //Shape* ps;
    //ps = new Circle;
    //v.push_back(ps);
    //ps = new Square;
    //v.push_back(ps);
    //ps = new Rectangle;
    //v.push_back(ps);
 
    Shape* ps;
    ps = ShapeFactory::CreateShape("Circle");
    v.push_back(ps);
    ps = ShapeFactory::CreateShape("Square");
    v.push_back(ps);
    ps = ShapeFactory::CreateShape("Rectangle");
    v.push_back(ps);
 
    DrawAllShapes(v);
    DeleteAllShapes(v);
    return 0;
}

注:要让新建的代码在某一个地方   就只需要工厂模式

//如果没有用工厂模式,那么需要在很多个地方进行修改,比如是用

ps = new Circle;  如果Circle进行了改变  要在代码的多个地方进行修改 不方便

//则只需要在工厂中进行修改就可以  只需要修改一个地方

//面向对象要消除if else 或者switch    用了虚函数已经减少了判断

下节课讲解动态创建技术,消除if else


纯虚析构函数

在某些类里声明纯虚析构函数很方便。纯虚函数将产生抽象类——不能实例化的类(即不能创建此类型的对象)。有些时候,你想使一个类成为抽象类,但刚好又没有任何纯虚函数。怎么办?因为抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。

这里是一个例子:

class awov {

public:

virtual ~awov() = 0;      // 声明一个纯虚析构函数

};

这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义:

awov::~awov() {}           // 纯虚析构函数的定义

这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。

#include <iostream>
using namespace std;
 
 
// 对于一个没有任何接口的类,如果想要将它定义成抽象类,只能将虚析构函数声明为纯虚的
// 通常情况下在基类中纯虚函数不需要实现
// 例外是纯虚析构函数要给出实现。(给出一个空的实现即可)
class Base
{
public:
    virtual ~Base() = 0
    {
 
    }
};
 
class Drived : public Base
{
 
};
 
int main(void)
{
    Drived d;
    return 0;
}










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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值