330 likes | 396 Views
第 19 章 多态和虚函数. 多态是面向对象理论的三大支柱之一。 多态是不能凭空独立的支柱,需要另外两个支 柱构成三足鼎立之势 , 另外两个支柱是封装和继承。 封装是数据和算法的抽象描述,继承是关于抽 象描述的演化发展,多态则是对于程序流程前瞻性 的刻画。 简单的说多态是基对象的指针调用派生类的虚 函数。. 五、名称索引的优先级 六、虚函数动态绑定调用算例 七、虚拟析构函数 八、纯虚函数和抽象类. 五、名称索引的优先级 在类上下继承层次中使用成员名称有两种方式:一种是
E N D
第19章 多态和虚函数 • 多态是面向对象理论的三大支柱之一。 • 多态是不能凭空独立的支柱,需要另外两个支 • 柱构成三足鼎立之势,另外两个支柱是封装和继承。 • 封装是数据和算法的抽象描述,继承是关于抽 • 象描述的演化发展,多态则是对于程序流程前瞻性 • 的刻画。 • 简单的说多态是基对象的指针调用派生类的虚 • 函数。
五、名称索引的优先级 • 六、虚函数动态绑定调用算例 • 七、虚拟析构函数 • 八、纯虚函数和抽象类
五、名称索引的优先级 • 在类上下继承层次中使用成员名称有两种方式:一种是 • 有意模棱两可的(implicit)隐约索引。即不带类域分辨符方式 • 的索引如:Draw(x,y),Draw 是成员函数的函数名。 • 另一种是一清二楚(explicit)的带类域分辨符方式的全限 • 定索引如:CBase::Draw(x,y),严格全限定索引在避免函数 • 的递归调用是必须的。 • 隐约索引的目的一方面是减少代码书写量,另一方面替 • 虚拟函数的隐约调用铺平道路。 • 在成员函数中出现的名称编译器根据下面的次序搜寻:
1. 首先编译器在当前的内层块范围搜寻,由内向外一直 • 到最外层的函数范围,包括函数的形参和隐约名称如m_n或 • 者如Draw(x,y)。 • 2. 编译器在当前(最晚)派生的类中搜寻所出现的名称。 • 3. 如果在当前类中未搜寻到指定的隐约名称,则上溯 • 到直接基类中搜寻直到根基类。 • 4. 然后在全局范围内搜寻。 • 5. ClassX::m_f 或this->ClassX::m_f或::g_f的全限定 • 名方式清晰地强制编译器搜寻指定的唯一名称。分辨符::左 • 边的名称是class,struct,union或enum引入的类型名。 • 6.如果名称前有关键字class,struct和union,强制编译 • 器搜寻它们引入的数据类型名。
对于obj.m_n 或pobj-> Draw(x,y)访问格式,如果派生 • 类没有提供相关的成员,则使用从基类继承的可访问成员。 • 如果派生类中存在相应的成员优先采用派生类的覆盖成员。 • 这称为优先采用派生类成员名称的支配原则。 • 对于虚函数,对象指针的动态类型确定继承树层次搜寻 • 的入口点,对于非虚函数对象指针的静态类型确定继承树层 • 次的切入点。 • 如果两者的类型都是ClassX*,则以ClassX作为搜寻的 • 入口点,向上搜寻指定的成员直到最快找到指定的成员为 • 止。
六、 虚函数动态绑定调用算例 • 基类的虚函数是公共的,派生类的覆盖版本可 • 以是私有的。 • 这样仅允许基对象指针在上层动态调用派生类 • 的虚函数,而不由对象直接外部访问,这保证虚函 • 数族入口的唯一性。
[例]关键字virtual的开关作用和隐约调用 • #include <stdio.h> • class ClassX • {public: virtual void F(); • private: long m_nx; • }; • void ClassX::F() • { printf ("ClassX=%d,",sizeof (ClassX)); } • class ClassY:public ClassX • { private: long m_ny; • void F(); • };
void ClassY::F(){ printf("ClassY=%d,",sizeof(ClassY)); } • void Showv (ClassX objx) { objx.F(); } • void Show (ClassX* pBase){ pBase->F(); } • void Show (ClassX& r) { r.ClassX::F(); } • void main () • { ClassX* pobjx=new ClassX(); • ClassY objy; int k=1; • scanf ("%d",&k); • if (k==1) • { Show (pobjx); Show (&objy); } • else { Show (*pobjx); Show(objy); } • Showv (objy); • }
说明: • 在ClassX类中成员函数F()前带关键字virtual且k=1输出 • 结果: • ClassX=8,ClassY=12, ClassX=8, 当k=2输出: • ClassX=8,ClassX=8, ClassX=8, • 在ClassX类中成员函数F()前不带关键字virtual即去掉 • virtual时程序输出结果: • ClassX=4, ClassX=4, ClassX=4,
类中函数声明中的前置关键字virtual对于成员函数其着类中函数声明中的前置关键字virtual对于成员函数其着 • 动静绑定的切换作用,在成员函数的调用点 • [ void Show(ClassX* pBase) {pBase->F();} ] • pBase->F(); • 编译器扫描声明部分,如果得知F是非虚函数则进行静 • 态联编,即根据pBase的静态类型进行函数的调用匹配, • pBase的静态类型就是形参列表中定义的类型即ClassX*, • 因此[pBase->F();]的隐约调用转换为全限定名的显式调用 • [pBase->ClassX::F();],这就是静态联编。
不管程序运行过程中虚实结合时pBase得到何值,调用不管程序运行过程中虚实结合时pBase得到何值,调用 • 的总是早诞生的成员函数版本。 • 上面多次调用中pBase得到的对象指针动态值是不同的,但 • 一律执行基类的版本。 • 如果探查到Show是虚函数则进行动态绑定,即根据 • pBase的动态类型进行函数的调用匹配。 • 如果虚实结合时pBase的动态类型为ClassY*,则调用 • 虚函数ClassY::F()。pBase的动态类型为ClassX*,则调用 • 虚函数ClassX::F()。
[例]静态联编调用非虚成员函数,派生类的覆盖版本是公[例]静态联编调用非虚成员函数,派生类的覆盖版本是公 • 共的 • #include <stdio.h> • class ClassX { long m_nx; public: void F(); }; • class ClassY:public ClassX • { long m_ny; public: void F(); }; • void ClassX::F() • { printf ( "ClassX=%d,",sizeof (ClassX)); } • void ClassY::F() • { printf ("ClassY=%d,",sizeof (ClassY)); } • enum { classx,classy };
void Show (ClassX* pBase,int morph) • { switch (morph) • { case classx: pBase->F();break; • case classy: ((ClassY*)pBase) ->F ();break; • } • } • void main() • { Show (new ClassX(),classx); • Show (new ClassY,classy); • } • //输出:ClassX=4, ClassY=8,
多态类对象内存的代价是明显的,即相应于非多态类的多态类对象内存的代价是明显的,即相应于非多态类的 • 对象增添4个字节的内存空间,从[4,8]对应地增至[8,12]。 • 获得的好处就是只需简单的写一条隐约调用的语句,编 • 译器替程序员作了许多常规的分支判断选择。
[例]对象引用的隐含类型转换作用 • # include<stdio.h> • class CIo {public:char obuffer[48];}; • class CStream : public CIo • {public:CStream& operator<<(double); }; • class CWithassign : public CStream {public: }; • CStream& operator<<(CStream& r,int n) • {static const char* fmt = "operator<<( CStream&, %d);"; • sprintf (r.obuffer,fmt,n); • printf ("%s",r.obuffer); • return r; • }
CStream& CStream::operator<<(double d) • {static const char* fmt = "CStream::operator<< • (double d=%f);\n"; • sprintf (obuffer,fmt,d); • printf ("\n%s",obuffer); • return *this; • } • void main () • { CWithassign wout; operator<<(wout,1); • wout<<2<<3; wout<<4.0; • CIo cio; operator<<((CStream&) cio,5); • (CStream&) cio<<6<<7; • }
//程序运行输出结果: • operator<<(CStream&,1); operator<<( CStream&,2); operator<<( CStream&,3); • CStream::operator<<(double d=4.0000000); • operator<<(CStream&,5); operator<<( CStream&,6); operator<<( CStream&,7); • 引用形式强制类型转换的含义是根据目标类型描述的数 • 据结构和算法操作源对象的内存数据。 • 本题中对象数据由根基类中声明,派生类提供算法,算 • 法在内存中不属于对象,因此对象引用的上下映射在内存的 • 角度看是一致的。
函数调用operator<<(wout,1) 可以等价地写为 • operator<<((CStream&)wout,1), • 在接口处对应着CStream& r=wout的继承层次上下间对象 • 引用隐含类型转换。 • 简洁的调用形式[wout<<2<<3; wout<<4.0;]也采用向上 • 映射的默许规则。 • 函数调用operator<<((CStream&) cio,5)采用向下映射 • 形式的强制类型转换,含义为基对象借用派生类的算法操作 • 基对象的内存数据。 • 隐含类型转换对于非虚成员函数不起动态绑定的作用。
对于ostream类库的运算符重载函数operator<<以及 • istream类库的operator>>运算符重载函数而言,这一默许 • 的类型转换机制有利于它们被派生类对象隐含地继承调用, • 提供统一的接口而无须强制类型转换。 • sprintf 函数是格式化转换函数,sprintf函数的目的地 • 是一个字符缓冲区,其变换规则与printf 函数相同。 • 格式为: • int sprintf (char *buffer, const char * format, • variable_list); • sprintf返回实际写入buffer数组的字符个数。
七、虚拟析构函数 • 构造函数本身决不作为虚函数。 • 构造函数中可以调用虚函数,但这个虚函数不起向下的 • 动态绑定的作用。 • 对于存在虚函数的类,将析构函数声明为虚拟的成员函 • 数是至关重要的,这样可以保证内存的泄漏降至最少。 • 在基类的析构函数前加上关键字virtual声明,则该析构 • 函数就成为虚析构函数,派生类的析构函数也跟着成为虚析 • 构函数,派生类的析构函数前可以明显地前置virtual声明, • 也可以隐含的省去不写。
[例]虚析构函数在动态绑定中的重要性 • #include <stdio.h> • class ClassX • { public: virtual ~ClassX(){printf("~ClassX();"); } • protected: ClassX() { printf("ClassX();"); } }; • class ClassY:public ClassX • { public: ClassY (int n=1) • { m_n=n; m_p=new long [m_n]; printf ("ClassY();"); } • private: ~ClassY() • { delete [ ] m_p; printf ("~ClassY();"); } • long m_n; long* m_p; • };
void main() • { ClassX* pobjx= new ClassY(); • delete pobjx; • } • 当基类析构函数是虚函数时程序输出结果: • ClassX(); ClassY(); ~ClassY(); ~ClassX(); • 去掉基类析构函数前的关键字virtual输出结果: • ClassX(); ClassY(); ~ClassX();
new ClassY()导致系统先调用new运算符函数,new运 • 算符函数调用的结果如果成功,则继续调用构造函数 • ClassY(int),派生类的构造函数诱发自基类构造函数起从上 • 到下的连续序列调用。 • delete 的调用次序与new运算符相反,delete首先调用 • new运算符诞生的heap对象所隶属的析构函数,然后释放 • new运算符动态获取的内存空间。 • 但此处由于派生类对象指针向上映射的结果,new • ClassY()诞生的对象是派生类的对象,但接管该对象的指针 • 则是基对象指针,这是编译器暗许的类型转换。
对于此种指针动静类型不一致的情形: • ClassX* pobjx= new ClassY(); delete pobjx; • delete pObjx激发的析构函数的调用就根据析构函数的 • 虚拟与否进行选择: • 1. 析构函数若是虚拟的,则pobjx的动态类型决定调用 • ClassY类的析构函数 • 这实际上也就是保证 delete pobjx 能够暗中正确的还 • 原为delete pobjy,以便和pobjx=new ClassY()遥相呼应。 • 2. 析构函数是非虚拟的,则pobjx的静态类型决定调用 • ClassX类的析构函数
八、 纯虚函数和抽象类 • 多态是用基对象的指针调用派生类的虚函数,此种调用 • 在基对象指针静态赋值指向距离顶层基类较远的派生类对象 • 时,容易导致上层基类虚函数形成死码的局面。 • 死码是程序运行时处理器不能到达的指令。于是提出一 • 个明显的指标,提醒基类的虚函数应该轻灵短小。
最空灵的虚函数莫过于一无所有的虚函数,以一个=0最空灵的虚函数莫过于一无所有的虚函数,以一个=0 • 来标志这样的虚函数如下: • class ClassA • { public: • virtual ~ClassA ()=0 {} • virtual int vPureFunct (...)=0; • virtual int PureFunct ()=0 {return 1;} • int m_a; • };
在基类的声明格式中以=0作标志的虚函数称为纯虚函在基类的声明格式中以=0作标志的虚函数称为纯虚函 • 数。纯虚函数一般不带实现部分,期待派生类提供覆盖版 • 本;纯虚函数在定义的时候也可以将空函数作为函数部分, • 这在纯虚析构函数是必要的。 • 对于多态类析构函数通常是虚拟函数,一般不作为纯虚 • 函数。 • 将至少存在一个纯虚函数的类称为抽象类。抽象类旨在 • 为多态类提供统一的无负担的接口(尽量避免死码的出现), • 而接口的有份量的激活的代码由派生类覆盖版本提供。 • 抽象类的特点是不能用其定义对象,即 • [ClassA *pObja=new ClassA();] • 和 [ClassA obja;] 是非法的定义语句。
以上这种抽象类无实例的概念渗透到下面几个层面:以上这种抽象类无实例的概念渗透到下面几个层面: • 1. 无抽象类的对象即无 ClassA obja但可以有抽象类的 • 对象指针如ClassA* pObja; • 2. 无抽象类的数值形参即无f(ClassA a)但可以有抽象类 • 的引用形参如f(ClassA& r); • 3. 不返回抽象类的对象即无 ClassA f() 到可以返回抽象 • 类的指针如ClassA* f(); • 抽象类的构造函数不宜调用纯虚函数,无论直接或间 • 接,否则结果是不确定的。由于不存在抽象类的对象,因此 • 抽象类基对象指针由具体派生类对象的地址初始化,抽象类 • 的构造函数适宜处理为保护的访问控制属性,由派生类构造 • 函数调用。
抽象类常作为多态类的基类,在抽象基类派生的类如果抽象类常作为多态类的基类,在抽象基类派生的类如果 • 出现了全不等于0的虚函数的覆盖版本,称为具体类。 • 抽象类的直接基类可以是具体类,具体类的行为不受其 • 后派生的抽象类的影响。抽象类的位置和其它类一样是非常 • 灵活的。 • 如MFC类中一个至关重要的纯虚函数OnDraw就是首度 • 在视图类CView中亮相的,因此程序员必须在CView之下派 • 生类如CUserView中提供不等于0的OnDraw实现。 • 每一次窗口重绘,应用程序框架通过多态机制调用派生 • 类的OnDraw函数。
[例]函数调用运算符成员函数operator()作为虚函数[例]函数调用运算符成员函数operator()作为虚函数 • #include <stdio.h> • struct SB • { virtual int operator()(int i)=0{return a[i]; } • static int a[ ]; }; • struct SC:public SB{ int operator()(int i) {return a[i+1];}}; • struct SD:public SC{ int operator()(int i) {return a[i+2];}}; • int f (SB& r){ return r(1);} • void main() • { SD d; SC c; //输出:3,2,1 • printf ("%d,%d,%d\n",f(d),f(c),d.SB::operator ()(1)); } • int SB::a[ ]={0,1,2,3,4,5,6,7,8,9,10};
[例]指向成员函数的指针和虚函数 • 指向成员函数的指针可以捕获虚函数的地址,一旦虚函 • 数地址赋给成员函数指针,这个指针在关联虚函数地址期 • 间,对成员函数指针的间接访问导致虚函数的动态绑定。 • #include <stdio.h> • class B; • void (B::*pfm)(); • class B • { virtual void f()=0; • public:static void set () {pfm=&B::f; } • }; • class D:public B{ void f(){printf ("D::f();\n");} };
void main() • { D objd; • B*pobjb=&objd; • B::set(); • (pobjb->*pfm)(); //等价于pObjb->f(); • } • //输出结果:D::f(); • 指向成员函数的指针由对象指针pobjb来调度,间接调 • 用(pObjb->*pfm)()在成员函数指针关联虚函数期间诱发多 • 态的机制,根据pobjb的动态类型进行函数调用的匹配。