C++入门教程 06

学习链接

六、多态性

1. 多态性概述

多态性是面向对象程序设计的重要特性之一,从字面意思上可以简单理解就是:多种形态,多个样子。其实本质意思也是这样,在面向对象程序设计中,指同样的方法被不同对象执行时会有不同的执行效果。

具体来说,多态的实现又可以分为两种:编译时的多态运行时的多态。前者是编译的时候就确定了具体的操作过程。后者呢是在程序运行过程中才确定的操作过程。这种确定操作过程的就是联编,也称为绑定。

联编在编译和连接时确认的,叫做静态联编 ,前面我们学习的函数重载、函数模板的实例化就属于这一类。

另一种是在运行的时候,才能确认执行哪段代码的,叫做动态联编 ,这种情况是编译的时候,还无法确认具体走哪段代码,而是程序运行起来之后才能确认。

两者相比之下,静态联编由于编译时候就已经确定好怎么执行,因此执行起来效率高;而动态联编想必虽然慢一些,但优点是灵活。

两者各有千秋,有各自不同的使用场景。

下面,围绕静态联编,举个例子:

#include <iostream>
using namespace std;
#define PI 3.1415926

class Point
{
private:
    int x,y;

public:
    Point(int x=0,int y=0)
    {
        this->x = x;
        this->y = y;
    }
    double area()
    {
        return 0.0;
    }
};
class Circle:public Point
{
private:
    int r;
public:
    Circle(int x,int y,int R):Point(x,y)
    {
        r = R;
    }
    double area()
    {
        return PI*r*r;
    }
};
  
int main()
{
  
    Point A(10,10);
    cout<<A.area()<<endl;
    Circle B(10,10,20);
    cout<<B.area()<<endl;
    Point *p;
    p = &B;
    cout<<p->area()<<endl;
    Point &pp=B;
    cout<<pp.area()<<endl;
    return 0;
  
}

输出结果:image-20211023112723657

来分析四个输出:

  • 第一个cout输出A的面积,是Point类中的area方法,面积为0,没有问题。
  • 第二个cout输出B的面积,很明显是派生类Circle的area方法,面积自然按公式计算得出1256.64的值,也没问题。
  • 第三个cout输出的是Point类型指针p指向的Circle类对象的area方法,它输出了0很明显是执行了Point类里的area方法。这里C++实行的是静态联编,即在编译的时候就依据p的类型来定执行哪个area,因此是0
  • 第四种cout也同理,把Circle类型的对象赋给Point类型的引用,C++同样实行静态联编,也输出0

很明显,这不是我们期望的结果,实际上,对于指针、引用,我们更希望执行实际对象的方法,而不是因为这个指针、引用的类型而盲目的确定。这就是如果这么写存在的问题。

如果想达到我们的要求,即无论指针和引用是什么类型,都以实际所指向的对象为依据灵活决定。那么就要更改这种默认的静态联编的方法,采用动态联编,即在运行的时候灵活决定。

来看下一节。

2. 虚函数

虚函数(virtual function),这是一种什么函数呢? 简单讲,就是一个函数前面用virtual声明的函数,一般形式如下:

virtual 函数返回值  函数名(形参){
    函数体
}

虚函数的出现,允许函数在调用时与函数体的联系在运行的时候才建立,即所谓的动态联编。那么在虚函数的派生类的运行时候,就可以在运行的时候根据动态联编实现都是执行一个方法,却出现不同结果的效果,就是所谓的多态。这样解决上一节的问题就有了办法。

接下来,我们只需要把基类中的area方法声明为虚函数,那么主函数中无论Point类型的指针还是引用就都可以大胆调用,无用关心类型问题了。因为他们会依据实际指向的对象类型来决定调用谁的方法,来实现动态联编。

#include <iostream>
using namespace std;
#define PI 3.1415926

class Point
{
private:
    int x,y;

public:
    Point(int x=0,int y=0)
    {
        this->x = x;
        this->y = y;
    }
    virtual double area()
    {
        return 0.0;
    }
};
class Circle:public Point
{
private:
    int r;
public:
    Circle(int x,int y,int R):Point(x,y)
    {
        r = R;
    }
    double area()
    {
        return PI*r*r;
    }
};
  
int main()
{
  
    Point A(10,10);
    cout<<A.area()<<endl;
    Circle B(10,10,20);
    cout<<B.area()<<endl;
    Point *p;
    p = &B;
    cout<<p->area()<<endl;
    Point &pp=B;
    cout<<pp.area()<<endl;
    return 0;
  
}

输出结果:image-20211023114212874

注意:

  1. 虚函数不能是静态成员函数,或友元函数,因为它们不属于某个对象。
  2. 内联函数不能在运行中动态确定其位置,即使虚函数在类的内部定义,编译时,仍将看作非内联
  3. 构造函数不能是虚函数,析构函数可以是虚函数,而且通常声明为虚函数。

3. 虚析构函数

在C++中,不能把构造函数定义为虚构造函数,因为在实例化一个对象时才会调用构造函数,且虚函数的实现,其实本质是通过一个虚函数表指针来调用的,还没有对象更没有内存空间当然无法调用了,故没有实例化一个对象之前的虚构造函数没有意义也不能实现。

但析构函数却是可以为虚函数的,且大多时候都声明为虚析构函数。这样就可以在用基类的指针指向派生类的对象在释放时,可以根据实际所指向的对象类型动态联编调用子类的析构函数,实现正确的对象内存释放。

#include <iostream>
using namespace std;
class Point
{
private:
    int x,y;
    int *str;
  
public:
    Point(int x=0,int y=0)
    {
        this->x = x;
        this->y = y;
        str = new int[100];
    }
    ~Point()
    {
        delete []str;
        cout<<"Called Point's Destructor and Deleted str!"<<endl;
    }
};
class Circle:public Point
{
private:
    int r;
    int *str;
public:
    Circle(int x,int y,int R):Point(x,y)
    {
        r = R;
        str = new int[100];
    }
    ~Circle()
    {
        delete []str;
        cout<<"Called Circle's Destructor and Deleted str!"<<endl;
    }
};
int main()
{
    Point *p;
    p = new Circle(10,10,20);
    delete p;
    
    return 0;
}
// 可以看到代码,基类中没有用virtual声明的析构函数,且基类和派生类当中都有动态内存开辟,那么我们在主函数中也动态开辟内存的方式创建一个Circle类,然后删除

输出结果:image-20211023115931999

可以清楚的看到,仅仅调用了基类的析构函数,这样一来派生类中new出来的4*100字节的内存就会残留,造成内存泄漏!

而如果把基类中析构函数声明为virtual,则结果大有不同!这个时候多态效应出现,会先调用释放派生类的空间,然后再释放基类的内存空间,完美结束:

#include <iostream>
using namespace std;
class Point
{
private:
    int x,y;
    int *str;
  
public:
    Point(int x=0,int y=0)
    {
        this->x = x;
        this->y = y;
        str = new int[100];
    }
    virtual ~Point()    // 注意这一行
    {
        delete []str;
        cout<<"Called Point's Destructor and Deleted str!"<<endl;
    }
};
class Circle:public Point
{
private:
    int r;
    int *str;
public:
    Circle(int x,int y,int R):Point(x,y)
    {
        r = R;
        str = new int[100];
    }
    ~Circle()
    {
        delete []str;
        cout<<"Called Circle's Destructor and Deleted str!"<<endl;
    }
  
};
int main()
{
    Point *p;
    p = new Circle(10,10,20);
    delete p;
    
    return 0;
}

输出结果:image-20211023120425113

以上,这就是虚析构函数带来的好处。

4. 纯虚函数与抽象类

纯虚函数,就是没有函数体的虚函数。

就是这样定义的函数:

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

可以看到,前面virtual与虚函数定义一样,后面加了一个=0。表示没有函数体,这就是一个纯虚函数。包含纯虚函数的类就是抽象类,一个抽象类至少有一个纯虚函数。

抽象类的存在是为了提供一个高度抽象、对外统一的接口,然后通过多态的特性使用各自的不同方法,是C++面向对象设计以及软件工程的核心思想之一。

抽象类的特点总结如下:

  1. 抽象类无法实例出一个对象来,只能作为基类让派生类完善其中的纯虚函数,然后再实例化使用。
  2. 抽象类的派生来依然可以不完善基类中的纯虚函数,继续作为抽象类被派生。直到给出所有纯虚函数的定义,则成为一个具体类,才可以实例化对象。
  3. 抽象类因为抽象、无法具化,所以不能作为参数类型、返回值、强转类型
  4. 接着第三条,但抽象类可以定义一个指针、引用类型,指向其派生类,来实现多态特性。

虚函数是C++中很重要的一部分内容,一定要深入理解!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ClimberCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值