只要基类在定义成员函数时已经声明了virtue关键字,在派生类实现的时候覆盖该函数时,virtue关键字可加可不加,不影响多态的实现。
容易与隐藏混淆:
隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
1) 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
2) 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
比如,在下面的程序中:
#include <iostreamh>
class Base
{
public:
virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
void g(float x){ cout << "Base::g(float) " << x << endl; }
void h(float x){ cout << "Base::h(float) " << x << endl; }
};
class Derived : public Base
{
public:
virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
通过分析可得:
1) 函数Derived::f(float)覆盖了Base::f(float)。
2) 函数Derived::g(int)隐藏了Base::g(float),注意,不是重载。
3) 函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。
看完前面的示例,可能大家还没明白隐藏与覆盖到底有什么区别,因为我们前面都是讲的表面现象,怎样的实现方式,属于要分析覆盖与隐藏在应用中到底有什么不同之处。在下面的程序中bp和dp指向同一地址,按理说运行结果应该是相同的,可事实并非如此。
void main(void)
{
Derived d;
Base pb = &d;
Derived pd = &d;
// Good : behavior depends solely on type of the object
pb->f(314f); //运行结果: Derived::f(float) 314
pd->f(314f); //运行结果: Derived::f(float) 314
// Bad : behavior depends on type of the pointer
pb->g(314f); //运行结果: Base::g(float) 314
pd->g(314f); //运行结果: Derived::g(int) 3
// Bad : behavior depends on type of the pointer
pb->h(314f); //运行结果: Base::h(float) 314
pd->h(314f); //运行结果: Derived::h(float) 314
}
请大家注意,f()函数属于覆盖,而g()与h()属于隐藏。从上面的运行结果,我们可以注意到在覆盖中,用基类指针和派生类指针调用函数f()时,系统都是执行的派生类函数f(),而非基类的f(),这样实际上就是完成的“接口”功能。而在隐藏方式中,用基类指针和派生类指针调用函数f()时,系统会进行区分,基类指针调用时,系统执行基类的f(),而派生类指针调用时,系统“隐藏”了基类的f(),执行派生类的f(),这也就是“隐藏”的由来。
析构函数的工作方式是:最底层的派生类(most derived class)的析构函数最先被调用,然后调用每一个基类的析构函数。
因为在C++中,当一个派生类对象通过使用一个基类指针删除,而这个基类有一个非虚的析构函数,则结果是未定义的。运行时比较有代表性的后果是对象的派生部分不会被销毁。然而,基类部分很可能已被销毁,这就导致了一个古怪的“部分析构”对象,这是一个泄漏资源。排除这个问题非常简单:给基类一个虚析构函数。于是,删除一个派生类对象的时候就有了你所期望的正确行为。将销毁整个对象,包括全部的派生类部分。
但是,一般如果不做基类的类的析构函数一般不声明为虚函数,因为虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式。vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl。当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针。这样子会使类所占用的内存增加。
对于同一种功能,不同的形参使用同一个函数名会更加方便。
例如用多态时:
void deal(int);void deal(double);
如果没有多态的话:
void dealI(int);void dealD(double);
这还只是比较少的的例子,如果不同形参的函数多起来的话,不一定能记的很清楚
最简单的做法是在结构体里存函数指针,然后初始化结构体的时候将函数的地址赋值给它。
复杂一点的,就得按照C++的多态原理,用虚函数表(其实就是能记录函数名对应的函数地址就好)记录每种结构体的成员函数地址,然后每个结构体里多存一个虚函数表的地址。。。然后调用函数的时候查到函数地址强转成函数类型然后调用。。。这个可以自己研究一下c++的虚函数表机制。
多态是一种不同的对象以单独的方式作用于相同消息的能力,这个概念是从自然语言中引进的。例如,动词“关闭”应用到不同的事务上其意思是不同的。关门,关闭银行账号或关闭一个程序的窗口都是不同的行为;其实际的意义取决于该动作所作用的对象。大多数面向对象语言的多态特性都仅以虚拟函数的形式来实现,但C++除了一般的虚拟函数形式之外,还多了两种静态的(即编译时的)多态机制:2、模板:例如,当接受到相同的消息时,整型vector对象和串vector对象对消息反映是不同的,我们以关闭行为为例:vector < int > vi; vector < string > names; string name("VC知识库");vipush_back( 5 ); // 在 vector 尾部添加整型namespush_back (name); // 添加串和添加整型体现差别的潜在的 *** 作静态的多态机制不会导致与虚拟函数相关的运行时开。此外, *** 作符重载和模板两者是通用算法最基本的东西,在STL中体现得尤为突出。 那么接下来我们说说以虚函数形式多态: 通常都有以重载、覆盖、隐藏来三中方式,三种方式的区别大家应该要很深入的了解,这里就不多说了。 许多开发人员往往将这种情况和C++的多态性搞混淆,下面我从两方面为大家解说: 1、 编译的角度 C++编译器在编译的时候,要确定每个对象调用的函数的地址,这称为早期绑定(early binding)。2、 内存模型的角度为了确定对象调用的函数的地址,就要使用迟绑定(late binding)技术。当编译器使用迟绑定时,就会在运行时再去确定对象的类型以及正确的调用函数。而要让编译器采用迟绑定,就要在基类中声明函数时使用virtual关键字(注意,这是必须的,很多开发人员就是因为没有使用虚函数而写出很多错误的例子),这样的函数我们称为虚函数。一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。 那么如何定位虚表呢?编译器另外还为每个类的对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。 正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。那么虚表指针在什么时候,或者说在什么地方初始化呢? 答案是在构造函数中进行虚表的创建和虚表指针的初始化。还记得构造函数的调用顺序吗,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。 要注意:对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。总结(基类有虚函数):1、 每一个类都有虚表。2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
多态性的实现与静态联编、动态联编有关。静态联编支持的多态性称为编译时的多态性,也称静态多态性,它是通过函数重载和运算符重载实现的。动态联编支持的多态性称为运行时的多态性,也称动态多态性,它是通过继承和虚函数实现的。
C++允许在参数类型不同的前提下重载函数。重载的函数与具有多态性的函数(即虚函数)不同处在于:调用正确的被重载函数实体是在编译期间就被决定了的;而对于具有多态性的函数来说,是通过运行期间的动态绑定来调用我们想调用的那个函数实体。多态性是通过重定义(或重写)这种方式达成的。请不要被重载(overloading)和重写(overriding)所迷惑。重载是发生在两个或者是更多的函数具有相同的名字的情况下。区分它们的办法是通过检测它们的参数个数或者类型来实现的。重载与CLOS中的多重分发(multiple
dispatching)不同,对于参数的多重分发是在运行期间多态完成的。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)