`

学习虚函数和重载

    博客分类:
  • C++
 
阅读更多
数据封装和继承性共同构成了面向对象程序设计的三个重要机制。
    1.静态联编与动态联编

    由于函数重载的存在,当程序中出现调用同名函数时,编译器会根据函数的参数类型、个数决定调用执行哪一个同名函数的代码,这种把一个函数的调用与适当的函数实现代码联系在一起的过程,叫做联编。根据联编的实现阶段的不同,可将其分为静态联编和动态联编两种。

    静态联编是在程序编译阶段确定一个函数调用与函数实现代码间的对应关系,这种对应关系确定下来后,在程序运行过程中就根据这个对应关系去调用执行相应的函数代码,并且这种对应关系在程序运行过程中始终保持不变。

    而动态联编是在编译阶段不能决定执行哪个同名的被调函数,只在程序运行过程中根据需要处理的对象类型来决定执行哪个类的成员函数。

    2.多态性

    所谓多态性就是指同样的消息被类的不同对象接收时导致的完全不同的行为的一种现象。这里所说的消息即对类的成员函数的调用。

    函数的重载可以实现多态性,但这里要讲的多态性是通过虚函数来实现的,而虚函数又必须存在于继承的环境下。

    C++语言支持两种类型的多态:一种是编译时的多态(静态多态),另一种是运行时的多态(动态多态)。在编译时的多态是通过静态联编实现的,而在运行时的多态则是通过动态联编实现的。

    3.虚函数

    声明虚函数的方法是在基类中的成员函数原型前加上关键字virtual.格式如下:

    class 类名{

    ……

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

    ……

    };

    当一个类的成员函数声明为虚函数后,这就意味着该成员函数在派生类中可能有不同的实现,也就是说,该函数在派生类中可能需要定义与其基类虚函数原型相同的函数。

    虚函数是动态联编的基础,当用基类类型的指针或引用的方法指向不同派生类对象时,系统会在程序运行中根据所指向对象的不同自动选择适当的函数,从而实现了运行时的多态性。

    当通过基类指针或引用标识对象并调用成员函数时,由于基类指针可以指向该基类的不同派生类对象,因此存在需要动态联编的可能性,但具体是否使用动态联编,还要看所调用的是否是虚函数。

    虚函数可以在一个或多个派生类中被重新定义,但它要求在派生类中重新定义时必须与基类中的函数原型完全相同,包括函数名、返回值类型、参数个数和参数类型的顺序。

    只有类的成员函数才能声明为虚函数,但类的构造函数以及全局函数和静态成员函数不能声明为虚函数。

    4.用基类指针指向公有派生类对象

    指向基类的指针自然可以指向其公有派生类的对象。但是,由于基类指针本身的类型并没有改变,因此,基类指针仅能访问派生类中的基类部分。

    5.纯虚函数与抽象类

    在定义一个表达抽象概念的基类时,有时可能会无法给出某些成员函数的具体实现。这时,就可以将这些函数声明为纯虚函数。

    纯需函数的声明格式如下:

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

    声明了纯虚函数的基类只是用于继承,仅作为一个接口,具体功能在其派生类中实现。

    声明了纯虚函数的类,称为抽象类。抽象类只能用作基类来派生新类,而不能用来创建对象。



//例程1
#include <iostream>    
using namespace std;  
  
class Vehicle
{  
public:  
    Vehicle(float speed,int total)
    {
        Vehicle::speed=speed;
        Vehicle::total=total;
    }
    void ShowMember()
    {
        cout<<speed<<"|"<<total<<endl;
    }
protected:  
    float speed;
    int total;
};  
class Car:public Vehicle  
{  
public:  
    Car(int aird,float speed,int total):Vehicle(speed,total)  
    {  
        Car::aird=aird;  
    }
    void ShowMember()
    {
        cout<<speed<<"|"<<total<<"|"<<aird<<endl;
    }
protected:  
    int aird;
};  

void main()  
{  
    Vehicle a(120,4);
    a.ShowMember();
    Car b(180,110,4);
    b.ShowMember();
    cin.get();
}

  在c++中是允许派生类重载基类成员函数的,对于类的重载来说,明确的,不同类的对象,调用其类的成员函数的时候,系统是知道如何找到其类的同名成员,上面代码中的a.ShowMember();,即调用的是Vehicle::ShowMember(),b.ShowMember();,即调用的是Car::ShowMemeber();



但是在实际工作中,很可能会碰到对象所属类不清的情况,下面我们来看一下派生类成员作为函数参数传递的例子,代码如下:

//例程2
#include <iostream>    
using namespace std;  
  
class Vehicle
{  
public:  
    Vehicle(float speed,int total)
    {
        Vehicle::speed=speed;
        Vehicle::total=total;
    }
    void ShowMember()
    {
        cout<<speed<<"|"<<total<<endl;
    }
protected:  
    float speed;
    int total;
};  
class Car:public Vehicle  
{  
public:  
    Car(int aird,float speed,int total):Vehicle(speed,total)  
    {  
        Car::aird=aird;  
    }
    void ShowMember()
    {
        cout<<speed<<"|"<<total<<"|"<<aird<<endl;
    }
protected:  
    int aird;
};  

void test(Vehicle &temp)
{
    temp.ShowMember();
}

void main()  
{
    Vehicle a(120,4);
    Car b(180,110,4);
    test(a);
    test(b);
    cin.get();
}

  例子中,对象a与b分辨是基类和派生类的对象,而函数test的形参却只是Vehicle类的引用,按照类继承的特点,系统把Car类对象看做是一个Vehicle类对象,因为Car类的覆盖范围包含Vehicle类,所以test函数的定义并没有错误,我们想利用test函数达到的目的是,传递不同类对象的引用,分别调用不同类的,重载了的,ShowMember成员函数,但是程序的运行结果却出乎人们的意料,系统分不清楚传递过来的基类对象还是派生类对象,无论是基类对象还是派生类对象调用的都是基类的ShowMember成员函数。



为了要解决上述不能正确分辨对象类型的问题,c++提供了一种叫做多态性(polymorphism)的技术来解决问题,对于例程序1,这种能够在编译时就能够确定哪个重载的成员函数被调用的情况被称做先期联编(early binding),而在系统能够在运行时,能够根据其类型确定调用哪个重载的成员函数的能力,称为多态性,或叫滞后联编(late binding),下面我们要看的例程3,就是滞后联编,滞后联编正是解决多态问题的方法。

  代码如下:

//例程3
#include <iostream>    
using namespace std;  
  
class Vehicle
{  
public:  
    Vehicle(float speed,int total)
    {
        Vehicle::speed = speed;
        Vehicle::total = total;
    }
    virtual void ShowMember()//虚函数
    {
        cout<<speed<<"|"<<total<<endl;
    }
protected:  
    float speed;
    int total;
};  
class Car:public Vehicle  
{  
public:  
    Car(int aird,float speed,int total):Vehicle(speed,total)  
    {  
        Car::aird = aird;  
    }
    virtual void ShowMember()//虚函数,在派生类中,由于继承的关系,这里的virtual也可以不加
    {
        cout<<speed<<"|"<<total<<"|"<<aird<<endl;
    }
public:  
    int aird;
};

void test(Vehicle &temp)
{
    temp.ShowMember();
}

int main()  
{  
    Vehicle a(120,4);
    Car b(180,110,4);
    test(a);
    test(b);
    cin.get();
}

  多态特性的工作依赖虚函数的定义,在需要解决多态问题的重载成员函数前,加上virtual关键字,那么该成员函数就变成了虚函数,从上例代码运行的结果看,系统成功的分辨出了对象的真实类型,成功的调用了各自的重载成员函数。

  多态特性让程序员省去了细节的考虑,提高了开发效率,使代码大大的简化,当然虚函数的定义也是有缺陷的,因为多态特性增加了一些数据存储和执行指令的开销,所以能不用多态最好不用。



虚函数的定义要遵循以下重要规则:

  1.如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,那么即使加上了virtual关键字,也是不会进行滞后联编的。

  2.只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。

  3.静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。

  4.内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义定义,但是在编译的时候系统仍然将它看做是非内联的。

  5.构造函数不能是虚函数,因为构造的时候,对象还是一片位定型的空间,只有构造完成后,对象才是具体类的实例。

  6.析构函数可以是虚函数,而且通常声名为虚函数。

  说明一下,虽然我们说使用虚函数会降低效率,但是在处理器速度越来越快的今天,将一个类中的所有成员函数都定义成为virtual总是有好处的,它除了会增加一些额外的开销是没有其它坏处的,对于保证类的封装特性是有好处的。

  对于上面虚函数使用的重要规则6,我们有必要用实例说明一下,为什么具备多态特性的类的析构函数,有必要声明为virtual。

  代码如下:

#include <iostream>    
using namespace std;  
  
class Vehicle
{  
public: 
    Vehicle(float speed,int total)
    {
        Vehicle::speed=speed;
        Vehicle::total=total;
    }
    virtual void ShowMember()
    {
        cout<<speed<<"|"<<total<<endl;
    }
    virtual ~Vehicle()
    {
        cout<<"载入Vehicle基类析构函数"<<endl;
        cin.get();
    }
protected:  
    float speed;
    int total;
};  
class Car:public Vehicle  
{  
public:  
    Car(int aird,float speed,int total):Vehicle(speed,total)  
    {  
        Car::aird=aird;  
    }
    virtual void ShowMember()
    {
        cout<<speed<<"|"<<total<<"|"<<aird<<endl;
    }
    virtual ~Car()
    {
        cout<<"载入Car派生类析构函数"<<endl;
        cin.get();
    }
protected:  
    int aird;
};  

void test(Vehicle &temp)
{
    temp.ShowMember();
}
void DelPN(Vehicle *temp)
{
    delete temp;
}
void main()
{  
    Car *a=new Car(100,1,1);
    a->ShowMember();
    DelPN(a);
    cin.get();
}

  从上例代码的运行结果来看,当调用DelPN(a);后,在析构的时候,系统成功的确定了先调用Car类的析构函数,而如果将析构函数的virtual修饰去掉,再观察结果,会发现析构的时候,始终只调用了基类的析构函数,由此我们发现,多态的特性的virtual修饰,不单单对基类和派生类的普通成员函数有必要,而且对于基类和派生类的析构函数同样重要。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics