多态是面向对象的三大特征之一,是在我看来面向对象中最难的部分。多态可以分为重载多态、强制多态、包含多态和参数多态四类。 重载多态主要是通过函数重载和算符重载来实现的。强制多态是指对一个变量进行强制类型转换,以达到某个函数或操作的要求,例如一个整型数据和浮点型数据相加时,会将整型数据强制转换为浮点型数据,再进行加法运算。参数多态通过模板来实现,以后可能会写。最后就是今天的主角:包含重载,包含重载主要是通过虚函数来实现的,也就是今天的主角。
虚函数是指用virtual来修饰的函数,声明的语法如下:
virtual 返回类型 函数名(参数列表);虚函数在通过父类指针指向子类对象时才能发挥作用,例如存在一个机器基类,他是所有机器类的父类:
/*这是一个机器基类*/ class Machine{ public: void start(){cout << "机器启动了" << endl;} //virtual void start(){cout << "机器启动了" << endl;} void stop(){cout << "机器停止了" << endl;} //virtual void stop(){cout << "机器停止了" << endl;} virtual ~Machine(){cout << "Machine destructor" << endl;} };在工厂里一共有三台机器,其中生产部件A的机器实现如下:
class PartAMachine:public Machine{ private: string mod = "PartA"; public: void start(){cout << "正在生产部件A" << endl;} void stop(){cout << "停止生产部件A" << endl;} ~Machine(){cout << "PartAMachine destructor" << endl;} };main函数如下:
int main(){ Machine *machine; machine = new PartAMachine();//让父类指针指向子类对象 //通过父类指针来调用函数 machine->start(); machine->stop(); return 0; }当基类中的start和stop函数未使用virtual进行修饰时,运行上述代码我们可以在运行结果中看到
机器启动了 机器停止了这是因为在未使用virtual进行修饰时,父类指针调用的函数是父类中的函数。我们可以将父类中的start和stop函数用virtual进行修饰,就像我注释中的代码那样,接下来再运行一次就可以看到子类的函数被正确调用了。
可能会有一些小伙伴开始想,为什们我一定要通过父类的指针来调用子类对象的成员函数呢?我可以为子类创建一个对象,再通过子类对象来调用子类的成员函数呀,这样就不需要使用虚函数了。 这是因为,如果采用了如下代码:
int main(){ PartAMachine a; a.start(); a.stop(); return 0; }当我生产了足够多的组件A,这个时候我想生产组件B了,这个时候如果我是通过子类的对象来调用子类的成员函数,我就需要在每次调用子类成员函数的位置将组件A的对象换成B的对象,在这个例子中可能只需要修改两个地方,但在实际的工作中,可能就需要我们修改几十次,甚至上百次。这也就体现了多态的优点,它提高了代码的复用性。
细心的小伙伴可能从上述的例子中发现了,我在一开始就在父类中显示的声明了一个虚的析构函数。
这是因为,子类继承了父类的所有成员,但他们有可能派生出一些只属于他们自己的成员,例如PartAMachine类中的数据成员mod。如果使用了非虚析构函数,这可能会导致一些可怕的事情发生,那就是内存泄漏!这将会导致在父类指针被delete的时候,调用的是父类的析构函数,而父类并没有成员mod,所以父类的析构函数并不会正确的将mod所占用的内存进行释放。只有使用了虚析构函数才能正确的调用子类的析构函数释放内存。
所以,我们应该在编写代码的时候,多留心一下,自己写的这个类是否有可能用于多态,如果有可能,那我们就应该尽可能的将析构函数声明为虚函数! 当然,我们也不需要为每个类都加上虚析构函数,尽管这样编译器并不会报错。这是因为,当我们使用了虚函数的时候,编译器会自动的添加一个指向vtable的指针(有兴趣可以找找关于虚函数底层实现的文章),这样就增加了类所占用的内存。一般情况下,我只在类中出现虚函数时,才会为该类声明虚析构函数。
学习过java的小伙伴可能发现,C++中好像没有接口(interface)这个关键字,那么C++是怎么实现接口的呢?那就是纯虚函数,顾名思义就是完全抽象的函数,我们不需要为它编写具体的实现,只需要知道有这个方法即可。
纯虚函数声明语法如下:
virtual 返回类型 函数名(参数列表) = 0;函数后面的“=0”就是纯虚函数与普通虚函数语法格式上的区别,当我们将一个函数声明为纯虚函数之后,就不能再给出这个函数的实现。
抽象类是指,类中定义了纯虚函数的类,只要类中定义了一个及以上的纯虚函数,这个类就被称为抽象类,也就是接口类。抽象类不能够实例化,但是可以定义抽象类的指针和引用!
我们可以利用抽象类来实现多态,例如前面例子中的Machine类中,start和stop函数就可以被声明为纯虚函数,因为对于机器来说,我们只知道他们有启动和停止两种操作,不同的机器有不同的实现方式,只有对特定的机器例如PartAMachine类,我们才能知道启动和停止具体是怎么实现的。此时,我们便可以让不同的机器都继承自Machine类,然后通过Machine类的指针来调度不同的机器类来进行生产。
可以将上述例子修改为:
#include <iostream> using namespace std; class Machine { public: virtual void start() = 0; virtual void stop() = 0; virtual ~Machine() {} }; class PartAMachine: public Machine { private: string mod = "PartA"; public: void start() { cout << "正在生产部件A" << endl; } void stop() { cout << "停止生产部件A" << endl; } }; int main() { Machine *m = new PartAMachine(); m->start(); m->stop(); return 0; }在之前学习继承的时候其实就有接触过virtual关键字,有兴趣的小伙伴可以另外找找关于“钻石继承”的文章看看。今天就写到这吧,买可乐去了。