1 / 33

多态是面向对象理论的三大支柱之一。 多态是不能凭空独立的支柱,需要另外两个支 柱构成三足鼎立之势 , 另外两个支柱是封装和继承。

第 19 章 多态和虚函数. 多态是面向对象理论的三大支柱之一。 多态是不能凭空独立的支柱,需要另外两个支 柱构成三足鼎立之势 , 另外两个支柱是封装和继承。 封装是数据和算法的抽象描述,继承是关于抽 象描述的演化发展,多态则是对于程序流程前瞻性 的刻画。 简单的说多态是基对象的指针调用派生类的虚 函数。. 五、名称索引的优先级 六、虚函数动态绑定调用算例 七、虚拟析构函数 八、纯虚函数和抽象类. 五、名称索引的优先级 在类上下继承层次中使用成员名称有两种方式:一种是

Download Presentation

多态是面向对象理论的三大支柱之一。 多态是不能凭空独立的支柱,需要另外两个支 柱构成三足鼎立之势 , 另外两个支柱是封装和继承。

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 第19章 多态和虚函数 • 多态是面向对象理论的三大支柱之一。 • 多态是不能凭空独立的支柱,需要另外两个支 • 柱构成三足鼎立之势,另外两个支柱是封装和继承。 • 封装是数据和算法的抽象描述,继承是关于抽 • 象描述的演化发展,多态则是对于程序流程前瞻性 • 的刻画。 • 简单的说多态是基对象的指针调用派生类的虚 • 函数。

  2. 五、名称索引的优先级 • 六、虚函数动态绑定调用算例 • 七、虚拟析构函数 • 八、纯虚函数和抽象类

  3. 五、名称索引的优先级 • 在类上下继承层次中使用成员名称有两种方式:一种是 • 有意模棱两可的(implicit)隐约索引。即不带类域分辨符方式 • 的索引如:Draw(x,y),Draw 是成员函数的函数名。 • 另一种是一清二楚(explicit)的带类域分辨符方式的全限 • 定索引如:CBase::Draw(x,y),严格全限定索引在避免函数 • 的递归调用是必须的。 • 隐约索引的目的一方面是减少代码书写量,另一方面替 • 虚拟函数的隐约调用铺平道路。 • 在成员函数中出现的名称编译器根据下面的次序搜寻:

  4. 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,强制编译 • 器搜寻它们引入的数据类型名。

  5. 对于obj.m_n 或pobj-> Draw(x,y)访问格式,如果派生 • 类没有提供相关的成员,则使用从基类继承的可访问成员。 • 如果派生类中存在相应的成员优先采用派生类的覆盖成员。 • 这称为优先采用派生类成员名称的支配原则。 • 对于虚函数,对象指针的动态类型确定继承树层次搜寻 • 的入口点,对于非虚函数对象指针的静态类型确定继承树层 • 次的切入点。 • 如果两者的类型都是ClassX*,则以ClassX作为搜寻的 • 入口点,向上搜寻指定的成员直到最快找到指定的成员为 • 止。

  6. 六、 虚函数动态绑定调用算例 • 基类的虚函数是公共的,派生类的覆盖版本可 • 以是私有的。 • 这样仅允许基对象指针在上层动态调用派生类 • 的虚函数,而不由对象直接外部访问,这保证虚函 • 数族入口的唯一性。

  7. [例]关键字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(); • };

  8. 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); • }

  9. 说明: • 在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,

  10. 类中函数声明中的前置关键字virtual对于成员函数其着类中函数声明中的前置关键字virtual对于成员函数其着 • 动静绑定的切换作用,在成员函数的调用点 • [ void Show(ClassX* pBase) {pBase->F();} ] • pBase->F(); • 编译器扫描声明部分,如果得知F是非虚函数则进行静 • 态联编,即根据pBase的静态类型进行函数的调用匹配, • pBase的静态类型就是形参列表中定义的类型即ClassX*, • 因此[pBase->F();]的隐约调用转换为全限定名的显式调用 • [pBase->ClassX::F();],这就是静态联编。

  11. 不管程序运行过程中虚实结合时pBase得到何值,调用不管程序运行过程中虚实结合时pBase得到何值,调用 • 的总是早诞生的成员函数版本。 • 上面多次调用中pBase得到的对象指针动态值是不同的,但 • 一律执行基类的版本。 • 如果探查到Show是虚函数则进行动态绑定,即根据 • pBase的动态类型进行函数的调用匹配。 • 如果虚实结合时pBase的动态类型为ClassY*,则调用 • 虚函数ClassY::F()。pBase的动态类型为ClassX*,则调用 • 虚函数ClassX::F()。

  12. [例]静态联编调用非虚成员函数,派生类的覆盖版本是公[例]静态联编调用非虚成员函数,派生类的覆盖版本是公 • 共的 • #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 };

  13. 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,

  14. 多态类对象内存的代价是明显的,即相应于非多态类的多态类对象内存的代价是明显的,即相应于非多态类的 • 对象增添4个字节的内存空间,从[4,8]对应地增至[8,12]。 • 获得的好处就是只需简单的写一条隐约调用的语句,编 • 译器替程序员作了许多常规的分支判断选择。

  15. [例]对象引用的隐含类型转换作用 • # 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; • }

  16. 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; • }

  17. //程序运行输出结果: • operator<<(CStream&,1); operator<<( CStream&,2); operator<<( CStream&,3); • CStream::operator<<(double d=4.0000000); • operator<<(CStream&,5); operator<<( CStream&,6); operator<<( CStream&,7); • 引用形式强制类型转换的含义是根据目标类型描述的数 • 据结构和算法操作源对象的内存数据。 • 本题中对象数据由根基类中声明,派生类提供算法,算 • 法在内存中不属于对象,因此对象引用的上下映射在内存的 • 角度看是一致的。

  18. 函数调用operator<<(wout,1) 可以等价地写为 • operator<<((CStream&)wout,1), • 在接口处对应着CStream& r=wout的继承层次上下间对象 • 引用隐含类型转换。 • 简洁的调用形式[wout<<2<<3; wout<<4.0;]也采用向上 • 映射的默许规则。 • 函数调用operator<<((CStream&) cio,5)采用向下映射 • 形式的强制类型转换,含义为基对象借用派生类的算法操作 • 基对象的内存数据。 • 隐含类型转换对于非虚成员函数不起动态绑定的作用。

  19. 对于ostream类库的运算符重载函数operator<<以及 • istream类库的operator>>运算符重载函数而言,这一默许 • 的类型转换机制有利于它们被派生类对象隐含地继承调用, • 提供统一的接口而无须强制类型转换。 • sprintf 函数是格式化转换函数,sprintf函数的目的地 • 是一个字符缓冲区,其变换规则与printf 函数相同。 • 格式为: • int sprintf (char *buffer, const char * format, • variable_list); • sprintf返回实际写入buffer数组的字符个数。

  20. 七、虚拟析构函数 • 构造函数本身决不作为虚函数。 • 构造函数中可以调用虚函数,但这个虚函数不起向下的 • 动态绑定的作用。 • 对于存在虚函数的类,将析构函数声明为虚拟的成员函 • 数是至关重要的,这样可以保证内存的泄漏降至最少。 • 在基类的析构函数前加上关键字virtual声明,则该析构 • 函数就成为虚析构函数,派生类的析构函数也跟着成为虚析 • 构函数,派生类的析构函数前可以明显地前置virtual声明, • 也可以隐含的省去不写。

  21. [例]虚析构函数在动态绑定中的重要性 • #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; • };

  22. void main() • { ClassX* pobjx= new ClassY(); • delete pobjx; • } • 当基类析构函数是虚函数时程序输出结果: • ClassX(); ClassY(); ~ClassY(); ~ClassX(); • 去掉基类析构函数前的关键字virtual输出结果: • ClassX(); ClassY(); ~ClassX();

  23. new ClassY()导致系统先调用new运算符函数,new运 • 算符函数调用的结果如果成功,则继续调用构造函数 • ClassY(int),派生类的构造函数诱发自基类构造函数起从上 • 到下的连续序列调用。 • delete 的调用次序与new运算符相反,delete首先调用 • new运算符诞生的heap对象所隶属的析构函数,然后释放 • new运算符动态获取的内存空间。 • 但此处由于派生类对象指针向上映射的结果,new • ClassY()诞生的对象是派生类的对象,但接管该对象的指针 • 则是基对象指针,这是编译器暗许的类型转换。

  24. 对于此种指针动静类型不一致的情形: • ClassX* pobjx= new ClassY(); delete pobjx; • delete pObjx激发的析构函数的调用就根据析构函数的 • 虚拟与否进行选择: • 1. 析构函数若是虚拟的,则pobjx的动态类型决定调用 • ClassY类的析构函数 • 这实际上也就是保证 delete pobjx 能够暗中正确的还 • 原为delete pobjy,以便和pobjx=new ClassY()遥相呼应。 • 2. 析构函数是非虚拟的,则pobjx的静态类型决定调用 • ClassX类的析构函数

  25. 八、 纯虚函数和抽象类 • 多态是用基对象的指针调用派生类的虚函数,此种调用 • 在基对象指针静态赋值指向距离顶层基类较远的派生类对象 • 时,容易导致上层基类虚函数形成死码的局面。 • 死码是程序运行时处理器不能到达的指令。于是提出一 • 个明显的指标,提醒基类的虚函数应该轻灵短小。

  26. 最空灵的虚函数莫过于一无所有的虚函数,以一个=0最空灵的虚函数莫过于一无所有的虚函数,以一个=0 • 来标志这样的虚函数如下: • class ClassA • { public: • virtual ~ClassA ()=0 {} • virtual int vPureFunct (...)=0; • virtual int PureFunct ()=0 {return 1;} • int m_a; • };

  27. 在基类的声明格式中以=0作标志的虚函数称为纯虚函在基类的声明格式中以=0作标志的虚函数称为纯虚函 • 数。纯虚函数一般不带实现部分,期待派生类提供覆盖版 • 本;纯虚函数在定义的时候也可以将空函数作为函数部分, • 这在纯虚析构函数是必要的。 • 对于多态类析构函数通常是虚拟函数,一般不作为纯虚 • 函数。 • 将至少存在一个纯虚函数的类称为抽象类。抽象类旨在 • 为多态类提供统一的无负担的接口(尽量避免死码的出现), • 而接口的有份量的激活的代码由派生类覆盖版本提供。 • 抽象类的特点是不能用其定义对象,即 • [ClassA *pObja=new ClassA();] • 和 [ClassA obja;] 是非法的定义语句。

  28. 以上这种抽象类无实例的概念渗透到下面几个层面:以上这种抽象类无实例的概念渗透到下面几个层面: • 1. 无抽象类的对象即无 ClassA obja但可以有抽象类的 • 对象指针如ClassA* pObja; • 2. 无抽象类的数值形参即无f(ClassA a)但可以有抽象类 • 的引用形参如f(ClassA& r); • 3. 不返回抽象类的对象即无 ClassA f() 到可以返回抽象 • 类的指针如ClassA* f(); • 抽象类的构造函数不宜调用纯虚函数,无论直接或间 • 接,否则结果是不确定的。由于不存在抽象类的对象,因此 • 抽象类基对象指针由具体派生类对象的地址初始化,抽象类 • 的构造函数适宜处理为保护的访问控制属性,由派生类构造 • 函数调用。

  29. 抽象类常作为多态类的基类,在抽象基类派生的类如果抽象类常作为多态类的基类,在抽象基类派生的类如果 • 出现了全不等于0的虚函数的覆盖版本,称为具体类。 • 抽象类的直接基类可以是具体类,具体类的行为不受其 • 后派生的抽象类的影响。抽象类的位置和其它类一样是非常 • 灵活的。 • 如MFC类中一个至关重要的纯虚函数OnDraw就是首度 • 在视图类CView中亮相的,因此程序员必须在CView之下派 • 生类如CUserView中提供不等于0的OnDraw实现。 • 每一次窗口重绘,应用程序框架通过多态机制调用派生 • 类的OnDraw函数。

  30. [例]函数调用运算符成员函数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};

  31. [例]指向成员函数的指针和虚函数 • 指向成员函数的指针可以捕获虚函数的地址,一旦虚函 • 数地址赋给成员函数指针,这个指针在关联虚函数地址期 • 间,对成员函数指针的间接访问导致虚函数的动态绑定。 • #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");} };

  32. void main() • { D objd; • B*pobjb=&objd; • B::set(); • (pobjb->*pfm)(); //等价于pObjb->f(); • } • //输出结果:D::f(); • 指向成员函数的指针由对象指针pobjb来调度,间接调 • 用(pObjb->*pfm)()在成员函数指针关联虚函数期间诱发多 • 态的机制,根据pobjb的动态类型进行函数调用的匹配。

  33. 请打开“第20章.ppt”

More Related