C++面向对象总结——多态

引言

了解c++的三大特性是对c++的整体的认识。

  • 封装性: 类将成员变量和成员函数封装在类的内部,根据需要设置访问权限,通过成员函数管理内部状态(用访问修饰符设置)

  • 继承:继承所表达的是类之间相关的关系,这种关系使得对象可以继承另外一类对象的特征和能力。作用:避免公用代码的重复开发,减少代码和数据冗余。
  • 多态:多态性可以简单地概括为“一个接口,多种方法”,字面意思为多种形态。程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。比如函数重载、运算符重载、虚函数等

前些章已经介绍了继承,重载,本篇就在此基础上详说一下多态。


 一,C++ 多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

下面的实例中,基类 Shape 被派生为Rectangle类,如下所示:

#include <iostream> 
using namespace std;

class Shape {
protected:
    int width, height;
public:
    Shape(int a,int b):width(a),height(b){}
    int area()
    {
        cout << "Parent class area :" << endl;
        return 0;
    }
};
//将Rectangle类继承Shape类
class Rectangle : public Shape {
public:
    Rectangle(int a,int b) :Shape(a, b) { }
    int area()
    {
        cout << "Rectangle class area :" <<width*height<< endl;
        return 0;
    }
};

// 程序的主函数
int main()
{
    Shape* shape;//定义shpae类指针
    Rectangle rec(10, 7);//派生类对象
    // 基类指针指向派生类对象(存储矩形的地址)
    shape = &rec;
    // 调用矩形的求面积函数 area
    shape->area();
    return 0;
}

 可以发现运行结果和我们期望的不一样。什么原因造成的呢?

我们直观上认为,如果指针指向了派生类对象,那么就应该使用派生类的成员变量和成员函数,这符合人们的思维习惯。但是本例的运行结果却告诉我们,当基类指针 shape指向派生类 Rectangle的对象时,虽然使用了 Rectangle的成员变量,但是却没有使用它的成员函数,导致输出结果不符合我们的预期。

换句话说,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。

为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)。使用虚函数非常简单,只需要在函数声明前面增加 virtual 关键字。

但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,如下所示:

class Shape {
protected:
    int width, height;
public:
    Shape(int a,int b):width(a),height(b){}
   virtual int area()
    {
        cout << "Parent class area :" << endl;
        return 0;
    }
};

修改后,当编译和执行前面的实例代码时,它会产生以下结果:(运行成功!)

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。

二,虚函数

虚函数对于多态具有决定性的作用,有虚函数才能构成多态,这节我们来重点说一下虚函数的注意事项。

  • 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
  • 为了方便,可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽(覆盖)关系的同名函数都将自动成为虚函数。

C++继承时的名字遮蔽

1️⃣如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。(即使用派生类新增的成员)

2️⃣基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。


  •  当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
  • 只有派生类的虚函数遮蔽基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。
  • 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
  • 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。

🧡构成多态的条件

下面是构成多态的条件:

  • 必须存在继承关系;
  • 继承关系中必须有同名的虚函数,并且它们是遮蔽(覆盖)关系。
  • 存在基类的指针,通过该指针调用虚函数。

下面的例子对各种混乱情形进行了演示:

#include <iostream> 
using namespace std;

//基类Base
class base
{
public:
    virtual void func()
    {
        cout << "void Base::func()" << endl;
    }
    virtual void func(int)
    {
        cout << "void Base::func(int)" << endl;
    }
};
//派生类Derived
class Dervied :public base
{
public:
    void func()
    {
        cout << "void Derived::func()" << endl;
    }
    void func(char*str)
    {
        cout << "void Derived::func(char *shr)" << endl;
    }
};
int main()
{
    base* p = new Dervied();//创建基类指针*p指向派生类对象
    p->func();
    p->func(10);
    //p->func("学习c++");//报错
}

输出结果:

在基类 Base 中我们将void func()声明为虚函数,这样派生类 Derived 中的void func()就会自动成为虚函数。p 是基类 Base 的指针,但是指向了派生类 Derived 的对象。

语句p -> func();调用的是派生类的虚函数,构成了多态(由于派生类遮蔽了基类函数)

语句p -> func(10);调用的是基类的虚函数,因为派生类中没有函数遮蔽它。

语句p -> func("学习c++");出现编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。

💙纯虚函数和抽象类

如果我们想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数,语法格式为:

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

 我们可以把基类中的虚函数 area() 改写如下:

class Shape {
protected:
    int width, height;
public:
    Shape(int a,int b):width(a),height(b){}
    //纯虚函数
    virtual int area() = 0;
};

area()= 0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。

包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

纯虚函数使用举例:

#include <iostream> 
using namespace std;

//基类 line
class line
{
public:
    line(float len):m_len(len){}//初始化列表
    virtual float area() = 0;//纯虚函数
    virtual float volume() = 0;//纯虚函数
protected:
    float m_len;
};

//派生类 rect:基类 line
class rect :public line
{
public:
    rect(float len,float width):line(len),m_width(width){}
    float area()
    {
        return m_len * m_width;
    }
protected:
    float m_width;
};

//派生类 cuboid:基类 rect
class cuboid :public rect
{
public:
    cuboid(float len, float width, float height) :rect(len, width), m_height(height) {}
    float volume()
    {
        return m_len * m_width * m_height;
    }
protected:
    float m_height;
};
int main()
{
    line* p = new cuboid(10,20,30);//基类指针指向派生类对象
    cout << "The area of Cuboid is " << p->area() << endl;
    cout << "The volume of Cuboid is " << p->volume() << endl;
}

 本例定义了三个类,其继承关系为:line->rect->cuboid。

line是一个抽象类,也是最顶层的基类,在 line类中定义了两个纯虚函数 area() 和 volume()。

  • 在 rect类中,实现了 area() 函数;所谓实现,就是定义了纯虚函数的函数体。但这时 rect类仍不能被实例化,因为它没有实现继承来的 volume() 函数,volume() 仍然是纯虚函数,所以 rect也仍然是抽象类。
  • 直到 cuboid类,才实现了 volume() 函数,才是一个完整的类,才可以被实例化。

可以发现,line类表示“线”,没有面积和体积,但它仍然定义了 area() 和 volume() 两个纯虚函数。这样的用意很明显:line类不需要被实例化,但是它为派生类提供了“约束条件”,派生类必须要实现这两个函数,完成计算面积和体积的功能,否则就不能实例化。
在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。

 抽象基类除了约束派生类的功能,还可以实现多态。指针 p 的类型是 line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 line类中将这两个函数定义为纯虚函数;如果不这样做,后面的代码都是错误的。我想,这或许才是C++提供纯虚函数的主要目的。

关于纯虚函数的几点说明:

1) 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
2) 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。

posted @ 2021-07-29 16:13  唯有自己强大  阅读(712)  评论(0编辑  收藏  举报