前言
多态简单的说就是一个接口多种状态(方法), 这很像端口复用. 这里先来从虚函数入手, 再来分析多态.
虚函数
在类中的函数以virtual
开头的成员函数, 关键字virtual
就是告诉编译器我们的这个函数是虚函数.
virtual void print() { cout << "print"; }
这就是一个虚函数, 只需要一个关键字就行了.
下面分析会用到的例子
class temp
{
public:
virtual void print() { cout << "print"; }
virtual ~temp() { cout << "~temp"; }
};
class Temp : public temp
{
public:
virtual void print() { cout << "virtual Print"; }
virtual ~Temp() { cout << "~Temp"; }
};
{
temp t; t.print();
Temp T; T.print();
}
虚函数的作用?
- 虚函数为子类实现自己与基类方法名相同但是不同功能的可能. 就如temp基类实现了
print
方法, 现在子类Temp
也需要调用该方法, 但就是只多个实现方法, 不同的子类对这个方法有不同的实现. 如果了解一点硬件的应该知道端口复用, 多态就是类似于端口复用.
虚函数和重载一样吗?
- 首先虚函数不是重载, 其次重载是要求函数名相同但是传入参数的类型或者参数数量不一样. 而虚函数是函数名, 参数数量, 参数类型都是一样的, 所以虚函数和重载不一样.
但是如果你将基类和子类virtual
的关键字去掉, 程序也能正常的执行, 而且调用程序输出与之前也一样, 那么虚函数的作用体现在哪里?
-
虚函数体现在基类的指针或者引用指向(引用)子类的实现, 即
temp *ptr_t = &T;
. 这样ptr_t
调用虚函数时不会调用基类的函数体了, 而是调用子类重新实现的虚函数功能.ptr_t->print(); // virtual Print
-
体现在虚析构. 当这样的
temp *ptr_t = &T;
使基类指向子类, 在对象析构的时候, 只会调用基类的析构函数, 子类的析构并不会调用, 这就造成了子类内存泄露, 很大的问题, 所以为了子类的析构也能调用, 就一定要用上虚函数.class temp { public: ~temp() { cout << "~temp"; } }; class Temp : public temp { public: ~Temp() { cout << "~Temp"; } }; { Temp T; temp *t = &T; } // ~temp // 内存泄漏, 子类并没有被完全的释放, 基类的析构函数必须为虚函数才行
以上提到了析构能定义为析构函数, 构造函数能成为虚函数吗?
- 不能. 虚函数调用是只需要知道"部分的"信息, 即函数接口(虚指针)就行, 不必知道类的具体类型, 而类在构造的时候就必须知道类的具体类型. 同样, 函数接口(虚指针)必须要在构造函数执行了才知道, 如果构造函数是虚函数这就是一个悖论了.
从上到下, 我们对虚函数也有了一定的认识了. 至于虚指针
并不是我们现在谈论的问题, 这个问题我们之后来解答.
纯虚函数和抽象类
上面介绍了虚函数, 那么对纯虚函数也就容易理解了.
virtual void print() = 0;
上面就是一个纯虚函数. 它有三个特征.
- 函数后面接
= 0;
最后要以;
结尾 (这里的=0, 这是为了告诉编译器这是一个纯虚函数而已.) - 基类该函数不能有函数体. 只能在基类实现.
- 子类必须对该函数实现.
而具有纯虚函数的类就是抽象类. 抽象类不能定义其对象, 但是可以通过其指针或引用指向子类.
抽象类为什么不能有对象进行实例化?
- 抽象类是一个公共的接口, 大家都要继承它, 对公共函数实现不同的功能. 也就定义抽象类不能有构造函数. 没有构造函数就不能对类进行实例化, 也就不存在对象的实例化了.
多态
多态 : 一个接口多种状态(方法).
什么是多种状态(方法)?
- 就是只多个实现方法, 不同的子类对这个方法有不同的实现. 如果了解一点硬件的应该知道端口复用, 多态就是类似于端口复用. 举一个例子 : 你就是一个接口, 在家你是孩子, 在学校你是学生, 在公司你是员工, 你(接口)没有变, 但是你的身份(方法)在不同的地方(子类)一直在改变. 一个接口, 多种方法.
怎么实现多种状态(方法)?
- 通过虚函数实现.
多态的作用
- 封装使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用. 而多态的目的是为了接口重用. 也就是说, 不论传递过来的究竟是哪个类的对象, 函数都能够通过同一个接口调用到适应各自对象的实现方法.
上面讲了很多, 现在再回过头看概念应该都明白多态的意义了. 因为有虚函数, 所以才有了多种状态.
这里就再说一下多态的绑定. 一般的成员函数在调用时, 其函数地址是在编译期间就静态绑定了不能进行修改, 而虚函数则是在运行的时候才确定调用的是哪一个类的虚函数.
总结
这一节对多态有了一个基本的认识, 它的实现是以虚函数为基础. 以基类的虚函数定义一个公共接口, 子类便可以通过覆盖基类的虚函数重新实现功能, 就实现了多种方法, 但是构造函数不能定义为虚函数. 这篇我们也扩展探讨了抽象类, 抽象类不能有实例, 因为它不能有构造函数, 同时析构函数也能够定义为虚函数. 接下来我们从内存布局的角度再来分析多态.