c 的 多态
面向对象有三个基本特征:封装、继承、多态。其中,封装可以使得代码模块化,忽略或者隐藏实现细节,优化整个程序的结构;继承可以扩展已存在的代码模块(类),避免重复造轮子;而多态则是为了实现接口的重复使用。这里我们着重学习多态的概念,使用和其实现的原理。
什么是多态?
同一个方法在派生类和基类中的行为是不同的。换句话说,方法的行为应取决于调用该方法的对象。这种复杂的行为称为多态—具有多种形态,就是指同一个方法的行为将随环境而异。
和多态相关联概念
- 虚函数:
用virtual关键字申明的函数 - 纯虚函数:
虚函数再加上 = 0;如:virtual void fun()=0;即抽象类!必须在派生类实现内容 - 虚表:
存在虚函数的类都有一个一维的虚函数表称之为虚表 - 虚表指针:
类的对象有一个指向虚表开始的指针称之为虚表指针 - 函数名联编(binding):
将源代码中的函数调用解释为可执行特定的函数代码块被称为函数名联编。 - 静态联编(早期联编,early binding):
编译器在编译过程中,查看函数参数以及函数名确定调用的函数,这种联编称为静态联编又称早起联编。 - 动态联编(晚期联编,late binding):
相对静态联编,编译器在程序运行时选择正确的虚拟方法的代码,这种动态联编,又称晚期联编。
动态联编能够重新定义类的方法,而静态联编在这方面很差。那为什么不摒弃静态联编呢?是效率和概念模型的需求。静态联编有调用速度快,效率高的优点。另一方面,在设计时,可能包含一些不在派生类重新定义的成员函数,以提高效率或者明确不需要在派生类中重新定义。 - 向上强制转换:
将派生类强制引用或指针转换为基类的引用或指针被称为向上强制转换(upcasting),这是共有继承不需要进行显示类型转换,是is-a规则。 - 向下强制转换:
将基类指针或引用转换为派生类指针或引用称之为向下强制转换(downcasting)。必须使用显示的转换。
以上是多态使用中会涉及到的一些概念。
多态从实现上的分类
多态从实现的角度可以分为两类:编译时的多态和运行时的多态。
- 编译时的多态(静态多态)是在静态联编阶段,通过函数重载和模板实现的。利用函数重载机制,在调用同名函数时,编译器会根据实参的具体情况确定要调用的是哪个函数。
- 运行时的多态(动态多态)是用动态联编阶段,通过虚函数来实现。
动态多态使用的两个条件:
1、类的定义时存在继承关系,存在虚机制,派生类重写虚函数
2、 隐式向上强制转换使基类指针或引用可以指向基类对象或者派生类对象,这里需要动态联编,在运行时,根据不同的环境,产生不同的行为。
多态使用实例
下面我们通过实例来看一下多态的使用
/*
c 多态测试实例
*/
#include
#include
using namespace std;
//基类
class base {
public:
virtual void vfunc1()
{
printf("base::vfunc1\r\n");
}
virtual void vfunc2()
{
printf("base::vfunc2\r\n");
}
virtual void vfunc3()
{
printf("base::vfunc3\r\n");
}
virtual void vfunc4()
{
printf("base::vfunc4\r\n");
}
void func1()
{
printf("base::func1\r\n");
}
void func2()
{
printf("base::func2\r\n");
}
private:
int m_data1, m_data2;
};
//派生类a
class a: public base {
public:
virtual void vfunc1()
{
printf("a::vfunc1\r\n");
}
void func1(int a)
{
printf("a::func1 with %d\r\n",a);
}
void func1()
{
printf("a::func1\r\n");
}
private:
int m_data3;
};
//派生类b
class b : public base {
public:
virtual void vfunc1()
{
printf("b::vfunc1\r\n");
}
virtual void vfunc3()
{
printf("b::vfunc3\r\n");
}
void func2()
{
printf("b::func2\r\n");
}
private:
int m_data1, m_data4;
};
int main()
{
//静态联编调用
a object_a;
object_a.vfunc1(); //调用a类虚函数vfunc1
object_a.vfunc2(); //调用a类未重写vfunc2,调用继承基类虚函数vfunc2
object_a.func1(); //调用a类调用方法func1
object_a.func1(100); //调用a类调用重载方法func1,实现静态多态
//动态联编调用,实现动态多态
b object_b;
base* p = &object_a; //隐式向下转换基类指针p 指向类a对象object_a
p->vfunc1(); //基类指针指向对象a,调用a类虚函数vfunc1
p = &object_b; //隐式向下转换基类指针p 指向类b对象object_b
p->vfunc1(); //基类指针指向对象b,调用b类虚函数vfunc1
system("pause");
}
运行结果:
如上面代码和注释所示,实现了静态多态和动态多态。
动态多态实现原理
动态多态是如何实现的?运行时的动态联编到底做了什么?
带着问题,我们首先改造一下上面的实例。在实例中复制一个base类,命名为basewithoutv,将basewithoutv类中的虚函数关键字都去掉,其他保持不变。我们来看看这两个类的size。代码如下:
/*
c 多态测试实例
*/
#include
#include
using namespace std;
//基类
class base {
public:
virtual void vfunc1()
{
printf("base::vfunc1\r\n");
}
virtual void vfunc2()
{
printf("base::vfunc2\r\n");
}
virtual void vfunc3()
{
printf("base::vfunc3\r\n");
}
virtual void vfunc4()
{
printf("base::vfunc4\r\n");
}
void func1()
{
printf("base::func1\r\n");
}
void func2()
{
printf("base::func2\r\n");
}
private:
int m_data1, m_data2;
};
//无虚函数的基类
class basewithoutv {
public:
void vfunc1()
{
printf("basewithoutv::vfunc1\r\n");
}
void vfunc2()
{
printf("basewithoutv::vfunc2\r\n");
}
void vfunc3()
{
printf("basewithoutv::vfunc3\r\n");
}
void vfunc4()
{
printf("basewithoutv::vfunc4\r\n");
}
void func1()
{
printf("basewithoutv::func1\r\n");
}
void func2()
{
printf("basewithoutv::func2\r\n");
}
private:
int m_data1, m_data2;
};
//派生类a
class a: public base {
public:
virtual void vfunc1()
{
printf("a::vfunc1\r\n");
}
void func1(int a)
{
printf("a::func1 with %d\r\n",a);
}
void func1()
{
printf("a::func1\r\n");
}
private:
int m_data3;
};
//派生类b
class b : public base {
public:
virtual void vfunc1()
{
printf("b::vfunc1\r\n");
}
virtual void vfunc3()
{
printf("b::vfunc3\r\n");
}
void func2()
{
printf("b::func2\r\n");
}
private:
int m_data1, m_data4;
};
int main()
{
printf("base size is = %d\r\n", sizeof(base));
printf("basewithoutv size is = %d\r\n", sizeof(basewithoutv));
base base;
basewithoutv basewithoutv;
//静态联编调用
a object_a;
object_a.vfunc1(); //调用a类虚函数vfunc1
object_a.vfunc2(); //调用a类未重写vfunc2,调用继承基类虚函数vfunc2
object_a.func1(); //调用a类调用方法func1
object_a.func1(100); //调用a类调用重载方法func1,实现静态多态
//动态联编调用,实现动态多态
b object_b;
base* p = &object_a; //隐式向下转换基类指针p 指向类a对象object_a
p->vfunc1(); //基类指针指向对象a,调用a类虚函数vfunc1
p = &object_b; //隐式向下转换基类指针p 指向类b对象object_b
p->vfunc1(); //基类指针指向对象b,调用b类虚函数vfunc1
system("pause");
}
运行结果:
我们看到含有虚函数的base类比没有虚函数的basewithoutv多了4个字节(32位程序)。多了什么?
通过调试程序,如上图,忽略成员变量,我们发现含有虚函数的基类多了一个虚函数表指针。
c 规定了虚拟函数的行为,但将实现交给了编译器处理。通常,编译器处理虚函数的方法是:给每一个对象添加一个隐藏的成员—虚函数表指针。
动态多态就是通过虚函数和虚函数表来实现的。
凡是含有虚函数的类的对象内部就会有指向类内部的虚表指针。通过这个指针索引虚函数。虚函数的调用会被编译器根据环境动态的转换为对虚函数表的访问,类似如下过程:
base->vfunc(); //ptr代表this指针,vfunc是虚函数
*(ptr->__vfptr[虚函数索引])(ptr);
知道了这个原理,我们再来看看实力中动态多态的使用。
//动态联编调用,实现动态多态
a object_a;
…
b object_b;
base* p = &object_a; //隐式向下转换基类指针p 指向类a对象object_a
p->vfunc1(); //基类指针指向对象a,调用a类虚函数vfunc1
p = &object_b; //隐式向下转换基类指针p 指向类b对象object_b
p->vfunc1(); //基类指针指向对象b,调用b类虚函数vfunc1
运行结果:
定义的基类base指针p,分别向下强制转换指向派生a类和b类,实现了对a,b类中虚函数vfunc1()的分别调用。这样就实现了不同对象去完成同一行为时,展现出不同的形态。
p指向a类时:
p指向b类时:
程序运行调用虚函数时,程序将查看存储在对象中的虚函数表指针,然后转向相应的函数地址表,索引到调用函数执行。
从调用过程中可以看到,使用虚函数时,在内存和执行速度方面有一定的成本包括:
每个对象增加存储地址的空间
对每个类,编译器都要创建一个虚拟函数地址表
每个函数的调用都需要执行一步额外的操作,即到表中查找函数地址
这也印证了上面我们提到的静态联编相对于动态联编效率高的优点。
虚函数在汇编中又是怎么个样子呢?我会在另一片文章中说明。