六、多态性
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;
}
输出结果:
来分析四个输出:
- 第一个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;
}
输出结果:
注意:
- 虚函数不能是静态成员函数,或友元函数,因为它们不属于某个对象。
- 内联函数不能在运行中动态确定其位置,即使虚函数在类的内部定义,编译时,仍将看作非内联
- 构造函数不能是虚函数,析构函数可以是虚函数,而且通常声明为虚函数。
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类,然后删除
输出结果:
可以清楚的看到,仅仅调用了基类的析构函数,这样一来派生类中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;
}
输出结果:
以上,这就是虚析构函数带来的好处。
4. 纯虚函数与抽象类
纯虚函数,就是没有函数体的虚函数。
就是这样定义的函数:
virtual 返回值 函数名(形参)=0;
可以看到,前面virtual与虚函数定义一样,后面加了一个=0。表示没有函数体,这就是一个纯虚函数。包含纯虚函数的类就是抽象类,一个抽象类至少有一个纯虚函数。
抽象类的存在是为了提供一个高度抽象、对外统一的接口,然后通过多态的特性使用各自的不同方法,是C++面向对象设计以及软件工程的核心思想之一。
抽象类的特点总结如下:
- 抽象类无法实例出一个对象来,只能作为基类让派生类完善其中的纯虚函数,然后再实例化使用。
- 抽象类的派生来依然可以不完善基类中的纯虚函数,继续作为抽象类被派生。直到给出所有纯虚函数的定义,则成为一个具体类,才可以实例化对象。
- 抽象类因为抽象、无法具化,所以不能作为参数类型、返回值、强转类型
- 接着第三条,但抽象类可以定义一个指针、引用类型,指向其派生类,来实现多态特性。
虚函数是C++中很重要的一部分内容,一定要深入理解!