多态
概述
所谓多态,其实就是:一个接口,多种方法;或者换种说法:发出同样的消息(函数调用),被不同类型的对象接收时(不同的函数拥有者),有可能导致完全不同的行为。
我们前面说过的函数重载(当然也包括运算符重载),就是一种多态,一个函数名(接口),对应多种具体的实现,它属于静态的多态,下一章要讲的模板也是静态多态,一套接口(函数/类)对应不同的数据类型或者行为(因为算法本身也可以作为模板参数);另外基于对象的编程也可以看成是静态多态,接口类里面是一些function,然后具体使用时,可以使用bind绑定不同的实现,类似于挂衣架,流水的衣服,铁打的挂衣架。
静态多态是指编译期就可以确定函数的入口地址,而本章要讲的是动态多态,它只有到运行期才能确定函数入口地址。动态多态在效率上要比静态多态差,因为有额外的运行时开销。
放到C++语言层面,多态其实就是指同一个操作作用于不同的对象会产生不同的响应。
多态的优点:多态使得我们可以以一致的观点对待同一基类的所有派生类,减轻了分别设计的负担,提高了代码重用度。
C++通过虚函数来实现动态多态以及简单的RTTI。
静态联编与动态联编:
静态联编:
程序调用函数时,具体应使用哪个代码块是由编译器决定的。以函数重载为例,C++编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数,称为联编(binding)。
编译器可以在编译过程中完成这种联编,在编译过程中进行的联编叫静态联编(static binding)或早期联编(early binding)。
动态联编:
在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时完成选择,因此编译器必须提供这么一套称为“动态联编”(dynamic binding)的机制,也叫晚期联编(late binding),C++通过虚函数来实现动态联编。
虚函数
虚函数的定义和声明
将成员函数声明为虚函数,只需要在成员函数原型前加一个关键字virtual即可。
注:
1. 构造函数不能声明为虚函数
2. 虚函数不能声明为static函数
如果一个基类的成员函数定义为虚函数,那么,它在所有派生类中也保持为虚函数;即使在派生类中省略了virtual关键字,也仍然是虚函数。
派生类可根据需要对基类虚函数进行override,上一章说过,override的格式要求很严:
- 与基类的虚函数有相同的参数个数;
- 与基类的虚函数有相同的参数类型;
- 与基类的虚函数有相同的返回类型。
说白了,就是要一模一样,并且是虚函数。
为什么多态需要虚函数:
#include <iostream>
using std::cout;
using std::endl;
class Base
{
public:
virtual // 加与不加大不相同
void disp()
{
cout << "Base::disp" << endl;
}
};
class Child1 : public Base
{
public:
void disp() // 派生类child1中定义的disp函数将base类中定义的disp函数隐藏
{
cout << "Child1::disp" << endl;
}
};
class Child2 : public Base
{
public:
void disp() // 派生类child2中定义的disp函数同样会隐藏base类中定义的disp函数
{
cout << "Child2::disp" << endl;
}
};
void Display(Base* pb) // 目的是实现同一接口(函数),处理不同类对象
{
pb->disp();
}
int main()
{
Base *pBase = NULL;
Base obj_base;
Child1 obj_child1;
Child2 obj_child2;
pBase = &obj_child1;
pBase->disp(); // no virtual: Base::disp | virtual: Child1::disp
// 通过指针调用
Display(&obj_base); // no virtual: Base::disp | virtual: Base::disp
Display(&obj_child1); // no virtual: Base::disp | virtual: Child1::disp
Display(&obj_child2); // no virtual: Base::disp | virtual: Child2::disp
(*pBase).disp(); // no virtual: Base::disp | virtual: Child1::disp
getchar();
return 0;
}
从代码中可以看到,只有虚函数,才能实现一个接口(void Display(Base*)
),多种行为(Display(&obj_base), Display(&obj_child1), Display(&obj_child2)
具有不同行为)。至于虚函数时怎样实现多态的我们后面会分析。
虚函数的访问
通过对象名访问:
和普通函数一样,虚函数一样可以通过对象名来调用,此时编译器采用的是静态联编。
通过对象名访问虚函数时, 调用哪个类的函数取决于定义对象名的类型。对象类型是基类时,就调用基类的函数;对象类型是子类时,就调用子类的函数。
通过指针访问:
使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向的对象类型;使用指针访问虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数(动态联编),而与指针本身的类型无关。
通过引用访问
使用引用访问虚函数,与使用指针访问虚函数类似;不同的是,引用一经声明后,引用变量本身无论如何改变,其调用的函数就不会再改变,始终指向其开始定义时的函数。
类成员函数中访问
在类内的成员函数中访问该类层次中的虚函数,采用动态联编,要使用this指针。通过构造函数或析构函数中访问:
构造函数和析构函数是特殊的成员函数,在其中访问虚函数时,C++采用静态联编,即在构造函数或析构函数内,即使是使用“this->虚函数名”的形式来调用,编译器仍将其解释为静态联编的“本类名::虚函数名”。即它们所调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的是基类中的虚函数。但绝不会调用任何在派生类中重定义的虚函数。
炎炎夏日,来个例子清爽一下:
#include <iostream>
class Base
{
public:
void Show()
{
Disp(); // <=> this->Disp()
}
virtual void Disp()
{
std::cout << "Base::Disp()" << std::endl;
}
};
class Child : public Base
{
public:
//void Show()
//{
// Disp();
//}
void Disp() override
{
std::cout << "Child::Disp()" << std::endl;
}
};
int main(void)
{
Base base;
Child child;
// 直接通过对象调用,与普通继承效果一样
base.Disp(); // Base::Disp()
child.Disp(); // Child::Disp()
child.Child::Disp(); // Child::Disp()
child.Base::Disp(); // Base::Disp()
// 通过基类指针调用,触发多态
Base *p = &child;
p->Disp(); // Child::Disp()
Base *p2 = &base;
p2 = &child;
p2->Disp(); // Child::Disp()
// 通过引用调用,与指针类似,但由于引用的特性,必须定义同时初始化,再不可更改绑定
// 即使改了,也不是改的引用,而是引用的对象,这与指针不同。
Base &ref = child;
ref.Disp(); // Child::Disp()
Base &ref2 = base;
ref2 = child; // 引用还是指向base,相当于base = child;
// 没有多态,存在对象切割,现在base就是child'体内'的那个Base了。
ref2.Disp(); // Base::Disp()
// 通过成员函数调用
p = &child;
p->Show(); // Child::Disp()
p = &base;
p->Show(); // Base::Disp()
return 0;
}
虚析构函数
虽然构造函数不能被定义成虚函数,但析构函数可以定义为虚函数,一般来说,如果类中定义了虚函数,析构函数也应被定义为虚析构函数,尤其是类内有申请的动态内存,需要清理和释放的时候。
只需要在基类的析构函数前面加virtual即可,派生类的析构函数会自动变为虚函数的。
如果一个类要作为多态基类,就应当使用虚析构函数。
如果一个类永远不会被派生类继承,就不要定义成虚函数,无端增加数据复杂性。
#include <iostream>
using std::cout;
using std::endl;
class Base
{
private:
char* data;
public:
Base()
{
data = new char[64];
cout << "Base::Base()" << endl;
};
virtual ~Base() // 虚析构函数
{
delete[] data;
cout << "Base::~Base()" << endl;
};
};
class Child : public Base
{
private:
char* m_data;
public:
Child() :Base()
{
m_data = new char[64];
cout << "Child::Child()" << endl;
};
~Child() // 析构函数,继承虚拟virtual,不需要再加virtual
{
delete[] m_data;
cout << "Child::~Child()" << endl;
};
};
class GrandChild :public Child
{
private:
char* mm_data;
public:
GrandChild() :Child()
{
mm_data = new char[64];
cout << "GrandChild::GrandChild()" << endl;
};
~GrandChild() // 虚析构函数,virtual从继承结构中得来
{
delete[] mm_data;
cout << "GrandChild::~GrandChild()" << endl;
};
};
int main()
{
Base *pB = new Child;
delete pB; // 调用的是指针指向的实际对象(Child对象)的析构函数,而
// Child::Child()又会调用Base::Base(),这样就没有内存泄漏。
cout << "=============" << endl;
Child* pC = new GrandChild;
delete pC; // 调用的是指针指向的实际对象(GrandChild对象)的析构函数,而
// GrandChild::GrandChild()又会调用Child::Child(),这样
// 就没有内存泄漏。
cout << "======不推荐下面这种用法=======" << endl;
GrandChild *pG = (GrandChild *)new Base;
delete pG; // 调用的是指针指向的实际对象(Base对象)的析构函数
// -> 如果去掉基类析构函数前的virtual, 执行到
// delete[] mm_data时调用的将是GrandChild::~GrandChild(),
// 会报内存错误, 因为mm_data, m_data根本就没有new
return 0;
}