C++ 多态Polymorphism 介绍+动态绑定、静态绑定
什么是多态?
多态(polymorphism)一词最初来源于希腊语polumorphos,含义是一种物质的多种形态。
在专业术语中,多态是一种运行时绑定机制(run-time binding) ,通过这种机制,实现将函数名绑定到函数具体实现代码的目的。
多态的目的
根据赋值兼容,用基类类型的指针指向派生类,就可以通过这个指针来使用派生类的成员函数。如果这个函数是普通的成员函数,通过基类类型的指针访问到的只能是基类的同名成员。而如果将它设置为虚函数,则可以使用基类类型的指针访问到指针正在指向的派生类的同名函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同的行为,从而实现运行过程的多态。
(“当通过指针或是引用使用派生类的一个对象时,此对象可以被当作是一个基类的对象”(但此时可能需要进行对象类型的判断))
#include <iostream> using namespace std; class A { public : void Print( ) { cout<<"A::Print"<<endl ; } }; class B:public A { public : void Print( ) { cout<<"B::Print"<<endl; } }; int main( ) { A a; B b; A *pA = &b;//基类型的指针指向派生类 pA->Print( ); return 0; }
运行结果:
A::Print
通过指针调用成员函数只与指针类型有关,与此刻指向的对象无关。基类指针无论指向基类还是派生类对象,利用pA->Print()调用的都是基类成员函数Print()。若要调用派生类中的成员函数Print()必须通过对象来调用,或定义派生类指针实现。这种通过用户自己指定调用成员函数,在编译时根据类对象来确定调用该类成员函数的方式,是静态绑定。
若将A类中的Print( )函数声明为virtual,则此时就为动态绑定Dynamic binding
程序执行结果为:
B::Print
class A { public : virtual void Print( ) { cout<<"A::Print"<<endl ; } }; class B:public A { public : void Print( ) { cout<<"B::Print"<<endl; } };
虚函数(Virtual functions)允许程序员在基类中声明一个函数,然后在各个派生类中对其进行重定义(redefined). 编译器与装入器将保证对象及其所调用函数的正确对应。这里的 redefine 叫做 Overriding(覆盖,又称重置)。这是C++语言中唯一的一种动态绑定(Dynamic binding)机制。
要让一个函数调用表现出多态特征,必须满足哪些条件?
必须存在继承关系;
继承关系中必须有同名的虚函数,并且它们是覆盖关系(重载不行)。
存在基类的指针,通过该指针调用虚函数。
动态绑定和静态绑定
绑定是程序自身彼此关联的过程,确定程序中的操作调用与执行该操作的代码间的关系。例如把一个标示符名和一个存储地址联系在一起的过程。
用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。
按照绑定进行的阶段的不同,可以分为静态绑定和动态绑定两种。
1.静态绑定:绑定工作在编译连接阶段完成。
因为绑定过程是在程序开始执行之前进行的,因此也称为早期绑定或前绑定。
在编译、连接过程中,系统就可以根据类型匹配等特征确定程序中操作调用与执行该操作代码的关系,即确定了某一个同名标识到底是要调用哪一段程序代码。
C++中,除虚函数外,其他函数均是静态绑定的。
重载函数是静态绑定
普通函数重载与静态联编:
void print(char) void print(char *) void print(float) void print(int) …… print("Hello, overload!");
成员函数重载与静态联编:
class MyClass { public: MyClass( ); MyClass(int i); MyClass(char c); }; MyClass c2(34);
2.动态绑定:和静态绑定相对应,绑定工作在程序运行阶段完成的。
以下面的代码说明:
class TradesPerson { public: virtual void sayHi() { cout << "Just hi." << endl; } void run() { cout << "Base::run " << endl; } }; class Tinker : public TradesPerson { public: virtual void sayHi() { cout << "Tinker." << endl; } void run() { cout << "Tinker::run " << endl; } }; class Tailor : public TradesPerson { public: void sayHi() { cout << "Tailor." << endl; } void run() { cout << "Tailor::run " << endl; } }; int main( ) { TradesPerson* p; int which ; cout << "1 == TradesPerson, 2 == Tinker, 3 == Tailor "; cin >> which; switch( which ) { case 1: p = new TradesPerson; break; case 2: p = new Tinker; break; case 3: p = new Tailor; break; } p->sayHi();///动态绑定,运行结果不可知 p->run();///无virtual,不是虚函数,Base::run delete p; return 0; }
C++中对虚函数的限制:
虚函数只能是普通(对象)成员函数;
只有类的成员函数才能说明为虚函数,因为虚函数仅适用于有继承关系的类对象,所以普通函数不能说明为虚函数。在类的定义中前面有virtual关键字的成员函数就是虚函数。
静态成员函数不能是虚函数;
因为静态成员函数没有this指针,是不受限制于某个对象
类的构造函数不能是虚函数;
因为构造的时候,对象还是一片未定型的空间,只有构造完成后,对象才是具体类的实例。
当类的虚函数在类外定义时,不能有virtual关键字。
类中的声明有virtual关键字。
析构函数可以是虚函数,且通常定义为虚函数。
虚函数动态绑定的实现原理
动态选择被执行的函数
函数调用时,跳转到函数代码的入口地址处执行。把函数入口地址作为变量,在不同情况下赋予不同的值,通过该变量调用函数,就可动态选择被执行的函数。
回顾:函数指针、指向成员函数的指针
虚表
每个多态类有一个虚表(virtual table)。虚表中有当前类的各个虚函数的入口地址。每个对象有一个指向当前类的虚表的指针(虚指针vptr)。
动态绑定的实现
构造函数中为对象的虚指针赋值,通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址,通过该入口地址调用虚函数。
例如:有下面两个父子类
class Base { public: virtual void f(); virtual void g(); private: int i; }; class Derived: public Base { public: virtual void f();//覆盖Base::f virtual void h(); //新增的虚函数 private: int j; };