后台开发:核心技术与应用实践2.3 类的多态

简介:

2.3 类的多态


1.?多态


多态,顾名思义,是一个事物有多种形态的意思。在C++程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法);也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。

【例2.28】 基类和派生类成员函数同名覆盖时的执行选择。

#include<iostream>

using namespace std;

class A{

public:

    A(){}

    virtual void foo(){

        cout<<"This is A."<<endl;

    }

};

 

class B : public A{

public:

    B(){}

    void foo(){

        cout<<"This is B."<<endl;

    }

};

 

int main(){

    A a;

    a.foo();

    B b;

    b.foo();

    return 0;

}

程序的执行结果:

This is A.

This is B.

例2.28中声明了两个类(类A和类B),注意类A中的foo函数和类B中的foo函数不是重载函数,它们不仅函数名相同,而且函数类型和参数个数都相同,但两个同名函数不在同一个类中,而是分别在基类和派生类中,属于同名覆盖。若是重载函数,二者的参数个数和参数类型必须至少有一者不同,否则系统无法确定调用哪一个函数。而此处定义了一个A类的对象a和B类的对象b,有所区别,所以会分别执行各个类的foo函数。

人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。C++中的虚函数就是用来解决这个问题的。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

再看下面的例2.29,基类和派生类中有同名的函数display,就是使用虚函数,使得基类指针可以访问派生类中的同名函数。

【例2.29】 使用虚函数可以使得基类指针访问派生类中的同名函数。

#include <iostream>

#include <string>

using namespace std;

/*声明基类Box*/

class Box{

public:

    Box(int,int,int);                   // 声明构造函数

    virtual void display();                 // 声明输出函数

protected:                               // 受保护成员,派生类可以访问

    int length,height,width;

};

/*Box类成员函数的实现*/

Box:: Box (int l,int h,int w){              // 定义构造函数

    length =l;

    height =h;

    width =w;

}

void Box::display(){                  // 定义输出函数

    cout<<"length:" << length <<endl;

    cout<<"height:" << height <<endl;

    cout<<"width:" << width <<endl;

}

/*声明公用派生类FilledBox*/

class FilledBox : public Box{

public:

    FilledBox (int, int, int, int, string); // 声明构造函数

    virtual void display();                 // 虚函数

private:

    int weight;                         // 重量

    string fruit;                         // 装着的水果

};

/* FilledBox类成员函数的实现*/

void FilledBox :: display(){               // 定义输出函数

    cout<<"length:"<< length <<endl;

    cout<<"height:"<< height <<endl;

    cout<<"width:"<< width <<endl;

    cout<<"weight:"<< weight <<endl;

    cout<<"fruit:"<< fruit <<endl;

}

FilledBox:: FilledBox (int l, int h, int w, int we, string f ) : Box(l,h,w), weight(we),

fruit(f){}

int main(){                         // 主函数

    Box box(1,2,3);                  // 定义Student类对象stud1

    FilledBox fbox(2,3,4,5,"apple");        // 定义FilledBox类对象fbox

    Box *pt = &box;                      // 定义指向基类对象的指针变量pt

    pt->display( );

    pt = &fbox;

    pt->display( );

    return 0;

}

程序的执行结果是:

length:1

height:2

width:3

length:2

height:3

width:4

weight:5

fruit:apple

例2.25中声明了一个类Box和一个继承于类Box的类FilledBox。类Box中有一个成员函数display,类FilledBox中也有一个成员函数display,现在将基类Box中的成员函数display定义为虚函数,就能使得基类对象的指针变量既可以访问基类的成员函数display,也可以访问派生类的成员函数display。

例2.25就展现了虚函数的奇妙作用。现在用同一个指针变量(指向基类对象的指针变量),不但输出了box的全部数据,而且还输出了fbox的全部数据,这就说明已调用了fbox的display函数。这表明用同一种调用形式"pt->display()",而且pt是同一个基类指针,也可以调用同一类族中不同类的虚函数。这就是多态性,即对同一消息,不同对象有不同的响应方式。

说明:本来基类指针是用来指向基类对象的,如果用它指向派生类对象,则需要进行指针类型转换,即将派生类对象的指针先转换为基类的指针,所以基类指针指向的是派生类对象中的基类部分。如果基类中的display函数不是虚函数,是无法通过基类指针去调用派生类对象中的成员函数的。虚函数突破了这一限制,在派生类的基类部分中,派生类的虚函数取代了基类原来的虚函数,因此在使基类指针指向派生类对象后,调用虚函数时就调用了派生类的虚函数。要注意的是,只有用virtual声明了虚函数后才具有以上作用,如果不声明为虚函数,企图通过基类指针调用派生类的非虚函数则是不行的。

当把基类的某个成员函数声明为虚函数后,就允许在其派生类中对该函数重新定义,赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。虚函数实现了同一类族中不同类的对象可以对同一函数调用作出不同的响应的动态多态性。

虚函数的使用方法如下所述。

(1)在基类用virtual关键字声明成员函数为虚函数。

这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加virtual关键字。

(2)在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。

C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual关键字,也可以不加,但一般习惯在每一层声明该函数时都加virtual关键字,使程序更加清晰。如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。

(3)定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。

通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。

(4)通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数,只要先用基类指针指向即可。如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。正如前面所说,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。

需要说明以下几点:①有时在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;②如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。

2.?虚函数的使用

(1)使用虚函数时,有两点要注意,如下所述。

1)只能用virtual关键字声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中。

2)一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。

(2)根据什么考虑是否把一个成员函数声明为虚函数呢?主要考虑以下几点。

1)首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能,一般应该将它声明为虚函数。

2)如果成员函数在类被继承后的功能不需要被修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。

3)应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。

4)有时,在定义虚函数时并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。

需要说明的是:使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表,它是一个指针数组,用于存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态是高效的。

3.?纯虚函数

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,如下所示:

virtual void funtion()=0;

而虚函数的定义是:

virtual void funtion();

为了方便使用多态特性,常常需要在基类中定义虚函数。但在很多情况下,用基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但用动物本身生成对象明显不合常理。为了解决上述问题,从而引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都是错误的,因为抽象基类是不能被直接调用的,而必须被子类继承重载以后,再根据要求调用其子类的方法,且在子类中一定要实现纯虚函数的定义,不然编译时会出错。

【例2.30】 纯虚函数的使用举例。

class Animail{

public:

    virtual void GetColor()  = 0;

};

class Dog : public Animail{

public:

    virtual void GetColor() {cout <<"Yellow"endl;};

};

class Pig : public Animail{

public:

 virtual void GetColor() {cout <<"White"<<endl;};

};

int main(){

    Animail cAnimail;

    return 0;

}

该程序编译失败,因为在例2.30中声明了一个动物类(Animail),类中有一函数GetColor可取得动物颜色,但动物有很多很多种,颜色自然无法确定,所以就把它声明为纯虚函数,也就是只声明函数名不去定义(实现)它,不能通过编译。有一点需要注意,纯虚函数不能实例化,但可以声明指针,所以上面的程序编译时,编译器会告诉你:由于它的成员的原因,无法抽象类Animail,并且警告你GetColor()没有定义,所以报错。

4.?析构函数

在C++中,构造函数不能声明时为虚函数,这是因为编译器在构造对象时,必须知道确切类型,才能正确地生成对象;其次,在构造函数执行之前,对象并不存在,无法使用指向此对象的指针来调用构造函数。然而,析构函数可以声明为虚函数;C++明确指出,当derived class对象经由一个base class指针被删除、而该base class带着一个non-virtual析构函数,会导致对象的derived成分没被销毁掉,如例2.31所示。

【例2.31】 析构函数不是虚函数容易引发内存泄漏。

#include<iostream>

using namespace std;

class Base{

public:

    Base(){ std::cout<<"Base::Base()"<<std::endl; }

    ~Base(){ std::cout<<"Base::~Base()"<<std::endl; }

};

class Derive:public Base{

public:

    Derive(){ std::cout<<"Derive::Derive()"<<std::endl; }

    ~Derive(){ std::cout<<"Derive::~Derive()"<<std::endl; }

};

int main(){

    Base* pBase = new Derive();

    /*这种base classed的设计目的是为了用来“通过base class接口处理derived class对象”*/

    delete pBase;

    return 0;

}

程序的执行结果是:

Base::Base()

Derive::Derive()

Base::~Base()

例2.31中声明了两个类Base和类Derive,类Derive继承于类Base,两个类各自有构造函数和析构函数,并且基类和派生类的析构函数都是非虚函数。从上面的执行结果可以看出,析构函数的调用结果是存在问题的,也就是说析构函数只做了局部销毁工作,这可能形成资源泄漏、损坏数据结构等问题。而解决此问题的方法很简单,只要给基类一个virtual析构函数即可,如例2.32所示。

【例2.32】 基类的析构函数为虚函数。

#include<iostream>

using namespace std;

class Base{

public:

    Base(){ std::cout<<"Base::Base()"<<std::endl; }

    virtual ~Base(){ std::cout<<"Base::~Base()"<<std::endl; }

};

 

class Derive:public Base{

public:

    Derive(){ std::cout<<"Derive::Derive()"<<std::endl; }

    ~Derive(){ std::cout<<"Derive::~Derive()"<<std::endl; }

};

 

int main(){

    Base* pBase = new Derive();

    delete pBase;

    return 0;

}

输出结果是:

Base::Base()

Derive::Derive()

Derive::~Derive()

Base::~Base()

例2.32与例2.31的区别只在于是否把基类的析构函数声明为虚函数。例2.32中派生类和基类都能正常析构了,这样的结果正是我们所希望的。虚函数是多态的基础,在C++中没有虚函数就无法实现多态特性;因为不声明为虚函数就不能实现“动态联编”,就不能实现多态。

5.?单例模式

要理解单例模式,只需要一个实例就可以了。比如,一台计算机上可以连好几台打印机,但是这个计算机上的打印程序只能有一个,这里就可以通过单例模式来避免两个打印作业同时输出到打印机中,即在整个的打印过程中只有一个打印程序的实例。对于这种问题,《设计模式》一书中给出了一种很不错的实现,定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法来获取该实例。单例模式的作用就是保证在整个应用程序的生命周期中的任何一个时刻,单例类的实例都只存在一个(当然也可以不存在)。

单例模式通过类本身来管理其唯一实例,唯一的实例是类的一个普通对象,但设计这个类时,让它只能创建一个实例并提供对此实例的全局访问,具体可以参见下面的例2.33。

【例2.33】 单例模式使用举例。

#include<iostream>

using namespace std;

class CSingleton{

private:

    CSingleton(){                           // 构造函数是私有的

    }

    static CSingleton *m_pInstance;

public:

    static CSingleton * GetInstance(){

        if(m_pInstance == NULL)               // 判断是否第一次调用

            m_pInstance = new CSingleton();

        return m_pInstance;

    }

};

CSingleton * CSingleton::m_pInstance=NULL; // 初始化静态数据成员

int main(){

    CSingleton *s1= CSingleton::GetInstance();

    CSingleton *s2= CSingleton::GetInstance();

    if(s1==s2){

        cout<<"s1=s2"<<endl;             // 程序的执行结果是输出了s1=s2

    }

    return 0;

}

程序的执行结果是:

s1=s2

例2.33中,用户访问实例的唯一方法只有GetInstance()成员函数。如果不通过这个函数,任何创建实例的尝试都将失败,因为类的构造函数是私有的。GetInstance()的返回值是当这个函数首次被访问时被创建的,所有GetInstance()之后的调用都返回相同实例的指针。

单例类CSingleton有以下特征:①有一个指向唯一实例的静态指针m_pInstance,并且是私有的;②有一个公有的函数,可以获取这个唯一的实例,并且在需要的时候创建该实例;③其构造函数是私有的,这样就不能从别处创建该类的实例。

相关文章
|
12天前
|
设计模式 架构师 Java
Java架构师的秘密武器:掌握设计模式的终极指南
【4月更文挑战第7天】资深架构师李先生向年轻工程师张女士阐述设计模式在构建可扩展、高效Java应用中的作用。他比喻设计模式如同建筑蓝图,是解决编程问题的标准方案。李先生介绍了单例、工厂、建造者、原型和适配器等模式,并强调理解模式意图和应用场景的重要性。通过实践与学习,设计模式能提升代码质量和团队沟通效率,成为开发者解决复杂问题的有力工具。
|
30天前
|
设计模式 算法 数据库
【C++ 继承】C++继承解密:一步步引领您从基础到实战
【C++ 继承】C++继承解密:一步步引领您从基础到实战
63 0
|
9月前
|
缓存 搜索推荐 前端开发
项目实战典型案例21——面向对象复用、面向对象实现、立体化权限落地
项目实战典型案例21——面向对象复用、面向对象实现、立体化权限落地
58 0
|
5月前
|
Java
探秘面向对象编程:封装、继承、多态的精髓与实践
探秘面向对象编程:封装、继承、多态的精髓与实践
【项目实战典型案例】21.面向对象复用、面向对象实现、立体化权限落地
【项目实战典型案例】21.面向对象复用、面向对象实现、立体化权限落地
|
11月前
|
安全 Java 编译器
Java编程最佳实践之多态
多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。 多态提供了另一个维度的接口与实现分离,以解耦做什么和怎么做。多态不仅能改善代码的组织,提高代码的可读性,而且能创建有扩展性的程序——无论在最初创建项目时还是在添加新特性时都可以“生长”的程序。
80 0
|
开发框架 Java 中间件
java程序设计与j2ee中间件技术/软件开发技术(I)-实验二-类与对象
java程序设计与j2ee中间件技术/软件开发技术(I)-实验二-类与对象
122 1
java程序设计与j2ee中间件技术/软件开发技术(I)-实验二-类与对象
|
存储 编译器 C++
C++继承和多态核心重点知识刨析,一文必拿下
C++继承和多态核心重点知识刨析,一文必拿下
C++继承和多态核心重点知识刨析,一文必拿下
|
Java
Java编程核心之继承
继承的优点: 1. 优化代码,减少代码量 2. 方便修改维护,让思路更清晰 ## 如何实现继承 1. 创建父类抽取共有的属性和方法 class Demo{ //公共的方法和属性 } 2.创建一个子类 class GZ extends{ //子类特有的属性和方法 } - 声明继承的关系函数:extends - 创建一个父类和两个子类对比一下
110 0
Java编程核心之继承
|
存储 Java
Java面向对象基础(二)
Java面向对象基础(二)
148 0
Java面向对象基础(二)