一、多态的概念:
用相同的指令可以调用不同的函数。即对不同类的对象发出相同的消息将会有不同的行为。
在了解了虚函数的意思之后,再考虑什么是多态就很容易了。看11.1代码
//例11.1
class A
{
public:
virtual void foo() { cout << "A::foo() iscalled" << endl;}
};
class B: public A
{
public:
virtual void foo() { cout << "B::foo() iscalled" << endl;}
};
//那么,在使用的时候,我们可以:
int main()
{
A* a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
}
仍然针对上面的类层次,但是使用的方法变的复杂了一些:
void bar(A *a)
{
a->foo(); // 被调用的是A::foo()还是B::foo()?
}
因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。
比如,你的老板让所有员工在九点钟开始工作, 他只要在九点钟的时候说:“开始工作”即可,而不需要对销售人员说:“开始销售工作”,对技术人员说:“开始技术工作”, 因为“员工”是一个抽象的事物, 只要是员工就可以开始工作,他知道这一点就行了。至于每个员工,当然会各司其职,做各自的工作。
编译器无法在编译时期判断pEmp->computerPay到底调用哪一个函数,必须在执行期才能判断,这称为后期绑定(late binding)或动态绑定(dynamic binding)。而C函数或C++函数的非虚函数,在编译时期就转换为一个固定地址的调用了,这称为前期绑定(early binding)或静态绑定(static binding)。
多态允许将子类的对象当作父类的对象使用,某父类型的引用指向其子类型的对象,调用的方法是该子类型的方法。这里引用和调用方法的代码在编译前就已经决定了,而引用所指向的对象可以在运行期间动态绑定。再举个比较形象的例子:
比如有一个函数是叫某个人来吃饭,函数要求传递的参数是人的对象,可是来了一个美国人,你看到的可能是用刀和叉子在吃饭,而来了一个中国人你看到的可能是用筷子在吃饭,这就体现出了同样是一个方法,可以却产生了不同的形态,这就是多态!
二、多态的作用:
让处理“基类的对象”的程序代码,能够完全无碍的继续适当处理“派生类的对象”
1. 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。//继承
2. 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。 //多态的真正作用,以前需要用switch实现
简单点说:“一个接口,多种实现”,就是同一种事物表现出的多种形态。多态的本质就是将子类类型的指针赋值给父类类型的指针(在OP中是引用),只要这样的赋值发生了,多态也就产生了,因为实行了“向上映射”。
我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了――代码重用。 那么,多态的作用是什么呢?多态是为了实现另一个目的――接口重用!而且现实往往是,要有效重用代码很难,而真正最具有价值的重用是接口重用,因为“接口是公司最有价值的资源。设计接口比用一堆类来实现这个接口更费时间。而且接口需要耗费更昂贵的人力的时间。”
在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。
多态可以使程序员脱离这种窘境。再回头看看11.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。
三、如何“动态联编”
编译器是如何针对虚函数产生可以在运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。
我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,针对1.1中的例子:
void bar(A * a)
{
a->foo();
}
会被改写为:
void bar(A * a)
{
(a->vptr[1])();
}
因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。
虽然实际情况远非这么简单,但是基本原理大致如此。
再来一个简单的例子:想写一个学生交多少学费的程序,定义了一学生类
//[本文全部代码均用伪码]
class Student
{
public:
Student(){}
~Student(){}
void 交学费(){}
//......
};
//代码实现
class CStudent //学生父类
{
public:
CStudent();
CStudent(const char* nm)
{
strcpy(m_name, nm);
cout << "名字:" << m_name << endl;
}
//~Student(){}
virtual float 交学费()
{
return 0;
}
private:
char m_name[30];
float m_bookmoney;
};
class CXiaoStudent : public CStudent //小学生子类
{
public:
CXiaoStudent(const char* nm) : CStudent(nm)
{
}
//~CAcadStudent(){}
void setBookMoney(float bookMon) { m_bookmoney = bookMon; }
virtual float 交学费()
{
return m_bookmoney;
}
private:
float m_bookmoney;
};
里面有一个 “交学费”的处理函数,因为大学生和小学生一些情况类似,我们从小学生类中派生出大学生类:
class CAcadStudent : public CXiaoStudent //大学生子类
{
public:
CAcadStudent(const char* nm) : CXiaoStudent(nm)
{
}
//~CAcadStudent(){}
void setHotel_expense(float hotelExpense) { m_hotelExpense = hotelExpense; }
virtual float 交学费()
{
float Acad = CXiaoStudent::交学费() + m_hotelExpense;
return Acad;
}
private:
float m_hotelExpense;
};
我们知道,中学生交费和大学生交费情况是不同的,所以虽然我们在大学生中继承了中学生的"交学费"操作,但我们不用把它重载定义大学生自己的交学费操作,这样当我们定义了一个小学生,一个大学生后:
int main()
{
CXiaoStudent xiaoxue("小明");
xiaoxue.setBookMoney(200);
cout << "小学学费: " << xiaoxue.交学费() << endl;
CAcadStudent daxue("大明");
daxue.setBookMoney(300);
daxue.setHotel_expense(1000);
cout << "大学学费: " << daxue.交学费() << endl;
getchar();
return 0;
}
xiaoxue.交学费(); 即调用小学生的,daxue.交学费();是调用大学生的,功能是实现了,但是你要意识到,可能情况不仅这两种,可能N种如:小学生、初中生、高中生、研究生.....它们都可以以CStudent[小学生类]为基类。如果系统要求你在一群这样的学生中,随便抽出一位交纳学费,你怎么做?
:
//A为抽出来的要交学费的同学
{
switch(typeof(A))
{
case 小学生:A.小学生::交学费 ();break;
case 初中生:A.初学生::交学费 ();break;
case 高中生:A.高学生::交学费 ();break;
default:
.............
}
}
首先,我们要在每个类中定义一个 typeof()用来识别它的类型,然后还要在程序中进行区别,这样一来,虽然也行,但是,如果再增加类型则要改动switch,又走了面向过程的老路,而且想通过一个模块进行操作实现起来也有难度。所以C++中提供了多态,即能通过迟后联编的技术实现动态的区分。
在基类的"交学费"前加个Virtual 用来告诉系统,遇到这个处理过程要等到执行时再确定到底调用哪个类的处理过程。这样一来就可以:
void 通用的交学费操作 (Student &A)
{
A.交学费();
}
一下全部搞定,你再加新的类型我也不怕!!![具体的实现原理参考:《Inside The C++ Object Model》]。如果没有 virtual这一声明,那么,系统在执行前就确定了操作,比如
float 交学费() //大学的学费函数
{
float Acad = CXiaoStudent::交学费() + m_hotelExpense;
return Acad;
}
调用的是大学生类中继承于CStudent类中的“交学费操作”。所以虚函数对多态的实现是必要的。