




已阅读5页,还剩11页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
虚函数必须是基类的非静态成员函数,其访问权限可以是protected或public,在基类的类定义中定义虚函数的一般形式: virtual 函数返回值类型 虚函数名(形参表) 函数体 派生类可以重置(“覆盖”)基类的非虚函数吗?Effective C+有条款说明这个问题的条款37: 决不要重新定义继承而来的非虚函数有两种方法来看待这个问题:理论的方法和实践的方法。让我们先从实践的方法开始。毕竟,理论家一般都很耐心。假设类D公有继承于类B,并且类B中定义了一个公有成员函数mf。mf的参数和返回类型不重要,所以假设都为void。换句话说,我这么写:class B public: void mf(); .;class D: public B . ;甚至对B,D或mf一无所知,也可以定义一个类型D的对象x,D x; / x是类型D的一个对象那么,如果发现这么做:B *pB = &x; / 得到x的指针pB-mf(); / 通过指针调用mf和下面这么做的执行行为不一样:D *pD = &x; / 得到x的指针pD-mf(); / 通过指针调用mf你一定就会感到很惊奇。因为两种情况下调用的都是对象x的成员函数mf,因为两种情况下都是相同的函数和相同的对象,所以行为会相同,对吗?对,会相同。但,也许不会相同。特别是,如果mf是非虚函数而D又定义了自己的mf版本,行为就不会相同:class D: public B public: void mf(); / 隐藏了B:mf; 参见条款50 .;pB-mf(); / 调用B:mfpD-mf(); / 调用D:mf行为的两面性产生的原因在于,象B:mf和D:mf这样的非虚函数是静态绑定的(参见条款38)。这意味着,因为pB被声明为指向B的指针类型,通过pB调用非虚函数时将总是调用那些定义在类B中的函数 - 即使pB指向的是从B派生的类的对象,如上例所示。相反,虚函数是动态绑定的(再次参见条款38),因而不会产生这类问题。如果mf是虚函数,通过pB或pD调用mf时都将导致调用D:mf,因为pB和pD实际上指向的都是类型D的对象。所以,结论是,如果写类D时重新定义了从类B继承而来的非虚函数mf,D的对象就可能表现出精神分裂症般的异常行为。也就是说,D的对象在mf被调用时,行为有可能象B,也有可能象D,决定因素和对象本身没有一点关系,而是取决于指向它的指针所声明的类型。引用也会和指针一样表现出这样的异常行为。实践方面的论据就说这么多。我知道你现在想知道的是,不能重新定义继承而来的非虚函数的理论依据是什么。我很高兴解答。条款35解释了公有继承的含义是 是一个,条款36说明了为什么 在一个类中声明一个非虚函数实际上为这个类建立了一种特殊性上的不变性。如果将这些分析套用到类B、类D和非虚成员函数B:mf,那么, 适用于B对象的一切也适用于D对象,因为每个D的对象 是一个 B的对象。 B的子类必须同时继承mf的接口和实现,因为mf在B中是非虚函数。那么,如果D重新定义了mf,设计中就会产生矛盾。如果D真的需要实现和B不同的mf,而且每个B的对象 - 无论怎么特殊 - 也真的要使用B实现的mf,那么,每个D将不 是一个 B。这种情况下,D不能从B公有继承。相反,如果D真的必须从B公有继承,而且D真的需要和B不同的mf的实现,那么,mf就没有为B反映出特殊性上的不变性。这种情况下,mf应该是虚函数。最后,如果每个D真的 是一个 B,并且如果mf真的为B建立了特殊性上的不变性,那么,D实际上就不需要重新定义mf,也就决不能这样做。不管采用上面的哪一种论据都可以得出这样的结论:任何条件下都要禁止重新定义继承而来的非虚函数。派生类和基类的关系:1: 派生类对象可以使用基类的方法,条件是方法不是私有的。2: 基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类对象;但基类指针或引用只能用于调用基类的方法;3: 不可以将基类对象和地址赋给派生类引用和指针;4: 将派生类引用或者指针转换成为基类引用或指针被称为向上强制转换,不需要进行显示类型转换。将基类指针或引用转换为派生类指针或引用成为向下强制转换。5: 如果基类函数中没有使用虚函数,使用类型转换才有静态编译;如果基类中有虚函数,则采用动态联编。编译器处理虚函数的方法:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数的地址数组的指针。数组被成为虚函数表,虚函数表中存储了为类对象进行声明的虚函数的地址。/*网上资料*/1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。2. 存在虚函数的类都有一个一维的虚函数表叫做虚表。类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。3. 多态性是一个接口多种实现,是面向对象的核心。分为类的多态性和函数的多态性。4. 多态用虚函数来实现,结合动态绑定。5. 纯虚函数是虚函数再加上= 0。6. 抽象类是指包括至少一个纯虚函数的类。纯虚函数:virtual void breathe()=0;即抽象类!必须在子类实现这个函数!即先有名称,没内容,在派生类实现内容!虚标指针的初始化:那么虚表指针在什么时候,或者说在什么地方初始化呢?答案是在构造函数中进行虚表的创建和虚表指针的初始化。还记得构造函数的调用顺序吗,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。总结(基类有虚函数):1. 每一个类都有虚表。2. 虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。3. 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。 /虚基类 1, 一个类可以在一个类族中既被用作虚基类,也被用作非虚基类。 2, 在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的子对象。 3, 虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。 4, 最派生类是指在继承结构中建立对象时所指定的类。 5, 派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。 6, 从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。 7, 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。虚基类:如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。现在,将类A声明为虚基类,方法如下: class A /声明基类A ; classB:virtual publicA /声明类B是类A的公用派生类,A是B的虚基类 ; classC:virtual public A /声明类C是类A的公用派生类,A是C的虚基类 ; 注意:虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。 虚函数 1, 虚函数是非静态的、非内联的成员函数,而不能是友元函数,但虚函数可以在另一个类中被声明为友元函数。 2, 虚函数声明只能出现在类定义的函数原型声明中,而不能在成员函数的函数体实现的时候声明。 3, 一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。 4, 若类中一个成员函数被说明为虚函数,则该成员函数在派生类中可能有不同的实现。当使用该成员函数操作指针或引用所标识的对象时,对该成员函数调用可采用动态联编。 5, 定义了虚函数后,程序中声明的指向基类的指针就可以指向其派生类。在执行过程中,该函数可以不断改变它所指向的对象,调用不同版本的成员函数,而且这些动作都是在运行时动态实现的。虚函数充分体现了面向对象程序设计的动态多态性。纯虚函数版本的成员函数,而且这些动作都是在运行时动态实现的。虚函数充分体现了面向对象程序设计的动态多态性。 纯虚函数 1, 当在基类中不能为虚函数给出一个有意义的实现时,可以将其声明为纯虚函数,其实现留待派生类完成。 2, 纯虚函数的作用是为派生类提供一个一致的接口。 3, 纯虚函数不能实化化,但可以声明指针。为什么构造函数不能是虚函数2010-05-17 23:52:21|分类: c语言 |标签: |字号大中小订阅 首先,让我们假设他是虚的.当我们在构造函数中时并调用虚函数.大家都知道,对于普通的成员函数虚函数的调用是在运行时决定的(即晚捆绑.因为在编译时无法知道这个对象是属于这个成员函数的那个类,还是属于由他派生出来的类).然而,在构造函数中调用虚函数时,他所调用的仅仅是本地版本.也就是说,虚函数在构造函数中并不工作! 第一,在概念上,构造函数的工作是把对象变成存在物。在任何构造函数中,对象可能只是部分被形成我们只能知道基类已被初始化了,但不知道哪个类是从这个基类继承来的。然而,虚函数是“向前”和“向外”进行调用。它能调用在派生类中的函数。如果我们在构造函数中也这样做,那么我们所调用的函数可能操作还没有被初始化的成员,这将导致灾难的发生。 第二,。当一个构造函数被调用时,它做的首要的事情之一是初始化它的VPTR。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码-既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE。但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。 但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)。另外,许多编译器认识到,如果在构造函数中进行虚函数调用,应该使用早捆绑,因为它们知道晚捆绑将只对本地函数产生调用。无论哪种情况,在构造函数中调用虚函数都没有结果。 所以,构造函数不能是虚的,然而,对于析构函数来说他常常是,而且最好是虚的!这个此处暂时不议. 定义虚函数的限制: (1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。 (2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。 (3)当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。 (4)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种同名函数。 虚函数联系到多态,多态联系到继承。所以本文中都是在继承层次上做文章。没了继承,什么都没得谈虚函数是如何做到的(如果你没有看过Inside The C+ Object Model这本书,但又急切想知道,那你就应该从这里开始) 虚函数是如何做到因对象的不同而调用其相应的函数的呢?现在我们就来剖析虚函数。我们先定义两个类 class A /虚函数示例代码 public: virtual void fun()cout1endl; virtual void fun2()cout2endl; ; class B:public A public: void fun()cout3endl; void fun2()cout4fun(); 毫无疑问,调用了A:fun(),但是A:fun()是如何被调用的呢?它像普通函数那样直接跳转到函数的代码处吗?No,其实是这样的,首先是取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl这里,由于调用的函数A:fun()是第一个虚函数,所以取出vtbl第一个slot里的值,这个值就是A:fun()的地址了,最后调用这个函数。现在我们可以看出来了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务。 而对于class A和class B来说,他们的vptr指针存放在何处呢?其实这个指针就放在他们各自的实例对象里。由于class A和class B都没有数据成员,所以他们的实例对象里就只有一个vptr指针。通过上面的分析,现在我们来实作一段代码,来描述这个带有虚函数的类的简单模型。 #include using namespace std; /将上面“虚函数示例代码”添加在这里 int main() void (*fun)(A*); A *p=new B; long lVptrAddr; memcpy(&lVptrAddr,p,4); memcpy(&fun,reinterpret_cast(lVptrAddr),4); fun(p); delete p; system(pause); 用VC或Dev-C+编译运行一下,看看结果是不是输出3,如果不是,那么太阳明天肯定是从西边出来。现在一步一步开始分析 void (*fun)(A*); 这段定义了一个函数指针名字叫做fun,而且有一个A*类型的参数,这个函数指针待会儿用来保存从vtbl里取出的函数地址 A* p=new B; new B是向内存(内存分5个区:全局名字空间,自由存储区,寄存器,代码空间,栈)自由存储区申请一个内存单元的地址然后隐式地保存在一个指针中.然后把这个地址赋值给A类型的指针P. . long lVptrAddr; 这个long类型的变量待会儿用来保存vptr的值 memcpy(&lVptrAddr,p,4); 前面说了,他们的实例对象里只有vptr指针,所以我们就放心大胆地把p所指的4bytes内存里的东西复制到lVptrAddr中,所以复制出来的4bytes内容就是vptr的值,即vtbl的地址 现在有了vtbl的地址了,那么我们现在就取出vtbl第一个slot里的内容 memcpy(&fun,reinterpret_cast(lVptrAddr),4); 取出vtbl第一个slot里的内容,并存放在函数指针fun里。需要注意的是lVptrAddr里面是vtbl的地址,但lVptrAddr不是指针,所以我们要把它先转变成指针类型 fun(p); 这里就调用了刚才取出的函数地址里的函数,也就是调用了B:fun()这个函数,也许你发现了为什么会有参数p,其实类成员函数调用时,会有个this指针,这个p就是那个this指针,只是在一般的调用中编译器自动帮你处理了而已,而在这里则需要自己处理。 delete p; 释放由p指向的自由空间; system(pause); 屏幕暂停; 如果调用B:fun2()怎么办?那就取出vtbl的第二个slot里的值就行了 memcpy(&fun,reinterpret_cast(lVptrAddr+4),4); 为什么是加4呢?因为一个指针的长度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast(lVptrAddr)+1,4); 这更符合数组的用法,因为lVptrAddr被转成了long*型别,所以+1就是往后移sizeof(long)的长度 三, 以一段代码开始#include using namespace std; class A /虚函数示例代码2 public: virtual void fun() coutA:funendl; virtual void fun2()coutA:fun2endl; ; class B:public A public: void fun() coutB:funendl; void fun2() coutB:fun2*fun)(); fun = &A:fun2; (p-*fun)(); delete p; system(pause); 你能估算出输出结果吗?如果你估算出的结果是A:fun和A:fun2,呵呵,恭喜恭喜,你中圈套了。其实真正的结果是B:fun和B:fun2,如果你想不通就接着往下看。给个提示,&A:fun和&A:fun2是真正获得了虚函数的地址吗? 首先我们回到第二部分,通过段实作代码,得到一个“通用”的获得虚函数地址的方法 #include using namespace std; /将上面“虚函数示例代码2”添加在这里 void CallVirtualFun(void* pThis,int index=0) void (*funptr)(void*); long lVptrAddr; memcpy(&lVptrAddr,pThis,4); memcpy(&funptr,reinterpret_cast(lVptrAddr)+index,4); funptr(pThis); /调用 int main() A* p=new B; CallVirtualFun(p); /调用虚函数p-fun() CallVirtualFun(p,1);/调用虚函数p-fun2() system(pause); CallVirtualFun方法现在我们拥有一个“通用”的CallVirtualFun方法。 这个通用方法和第三部分开始处的代码有何联系呢?联系很大。由于A:fun()和A:fun2()是虚函数,所以&A:fun和&A:fun2获得的不是函数的地址,而是一段间接获得虚函数地址的一段代码的地址,我们形象地把这段代码看作那段CallVirtualFun。编译器在编译时,会提供类似于CallVirtualFun这样的代码,当你调用虚函数时,其实就是先调用的那段类似CallVirtualFun的代码,通过这段代码,获得虚函数地址后,最后调用虚函数,这样就真正保证了多态性。同时大家都说虚函数的效率低,其原因就是,在调用虚函数之前,还调用了获得虚函数地址的代码。关于如何在派生类的虚函数中调用被覆盖掉的同名基类的虚函数,即Java中的super在C+中的使用方法 Posted on 2009-03-25 23:00 魔kyo 阅读(895) 评论(1) 编辑收藏 引用 很久没写BLOG了,这是在设计实现中遇到的一个实际问题,就是类似下面的Java代码在C+中如何实现的问题1importjava.io.*;2importjava.util.*;34classB56publicvoidPrint()78System.out.println(B);9101112classDextendsB1314publicvoidPrint()1516super.Print();17System.out.println(D);18192021classMain2223publicstaticvoidmain(Stringargs)2425Bobj=newD();26obj.Print();272829有时候我们需要将所有子类方法中都会执行的代码放到基类方法中,或者可能这些代码是操作基类成员的本来就不应该放在子类,在Java中可以通过super来调用被覆盖掉的父类中的方法。我居然以为C+无法实现这一点,实在是太无知了。Google之后才知道原来可以这样用1#include2usingnamespacestd;34classB56public:7virtualvoidPrint()89puts(B);1011;1213classD:publicB1415public:16virtualvoidPrint()1718B:Print();19puts(D);2021;2223intmain()2425B*p=newD();26p-Print();2728让我认为C+无法实现这点是有理由的,我们把virtual去掉看以下的代码1#include2usingnamespacestd;34classB56public:7voidPrint()89puts(B);1011;1213classD:publicB1415public:16voidPrint()1718(B*)(this)-Print();19puts(D);2021;2223intmain()2425D*p=newD();26p-Print();2728这段代码是可以正确实行的,因为没有virtual,D类中的Print并没有覆盖B类中的Print,只是同名而已(这是很不好的做法,容易产生误解),所以这时不存在运行时绑定,我只好D* p=new D(); 我认为子类对象在内存中是这样的但是virtual的方法会被覆盖掉,也就是说对于D类的对象,已经没有B类的Print方法了。所以如果加上virtual我们就无法正确执行上面的代码了。可是B:Print(); 又是如何实现的呢?Stockbroker GrapevineTime Limit: 1000MSMemory Limit: 10000KTotal Submissions: 17482Accepted: 9439DescriptionStockbrokers are known to overreact to rumours. You have been contracted to develop a method of spreading disinformation amongst the stockbrokers to give your employer the tactical edge in the stock market. For maximum effect, you have to spread the rumours in the fastest possible way. Unfortunately for you, stockbrokers only trust information coming from their Trusted sources This means you have to take into account the structure of their contacts when starting a rumour. It takes a certain amount of time for a specific stockbroker to pass the rumour on to each of his colleagues. Your task will be to write a program that tells you which stockbroker to choose as your starting point for the rumour, as well as the time it will take for the rumour to spread throughout the stockbroker community. This duration is measured as the time needed for the last person to receive the information.InputYour program will input data for different sets of stockbrokers. Each set starts with a line with the number of stockbrokers. Following this is a line for each stockbroker which contains the number of people who they have contact with, who these people are, and the time taken for them to pass the message to each person. The format of each stockbroker line is as follows: The line starts with the number of contacts (n), followed by n pairs of integers, one pair for each contact. Each pair lists first a number referring to the contact (e.g. a 1 means person number one in the set), followed by the time in minutes taken to pass a message to that person.
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 大学面试题问题及答案
- 月子护理场所管理制度
- 2025年 呼和浩特市机械工程职业技术学校招聘考试笔试试卷附答案
- 2025年 德州交通职业中等专业学校招聘考试笔试试卷附答案
- 新发布的安全培训课件
- 《数控车床加工技术(第2版)》中职全套教学课件
- 志愿者赋能培训
- 收费站恶劣天气应急处置培训
- 书法培训计划方案
- 肢体活动度训练体系构建
- 2025年新高考2卷(新课标Ⅱ卷)英语试卷
- 2024年湖北省初中学业水平考试地理试卷含答案
- 2024年认证行业法律法规及认证基础知识 CCAA年度确认 试题与答案
- 地方病防治技能理论考核试题
- 国家开放大学《民法学(1)》案例练习参考答案
- 老年患者他汀的应用课件
- 2022更新国家开放大学电大本科《计算方法(本)》2023-2024期末试题及答案(试卷代号:1084)
- GB∕T 40278-2021 纸和纸板 加速老化(光照条件下)
- 悬挑式脚手架验收表范本
- 可控震源日常维护及安全操作规程
- T∕ACSC 01-2022 辅助生殖医学中心建设标准(高清最新版)
评论
0/150
提交评论