在某基类中声明为virtual 并在一个或多个派生类中被从新定义的成员函数,用法格式为:html
virtual 函数返回类型 函数名(参数表) {函数体};ios
虚函数是C++语言实现运行时多态的惟一手段,经过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。程序员
举个例子:编程
class A{ public:virtual void p() { cout << "A" << endl; } }; class B : public A { public:virtual void p() { cout << "B" << endl; } }; int main() { A * a = new A; A * b = new B; a->p(); b->p(); delete a; delete b; return 0; }
程序的输出为:数组
若是将上面的程序改写以下:安全
class A{ public: void p() { cout << "A" << endl; } }; class B : public A { public: void p() { cout << "B" << endl; } };
那么输出结果则为:函数
在构造一个派生类对象时,首先将构造它的父类对象,而后才构造本身的对象。如A *a = new A,调用的默认构造函数构造基类A对象,而后调用函数p(),a->p();输出A。而后,A * b = new B;,构造了派生类对象B,B因为是基类A的派生类对象,因此会先构造基类A对象,而后再构造派生类对象。可是因为程序2中的函数p()是非虚函数,B类对象对函数p()的调用在程序编译阶段就已经肯定了。因此,不论基类指针b最终指向的是基类对象仍是派生类对象,只要后面的对象调用的函数不是虚函数,那么就直接调用基类A的p()。布局
对于程序1咱们能够得出:post
1)经过基类引用或指针调用基类中定义的函数时,并不知道执行的函数对象的确切类型,执行函数的对象多是基类类型,也多是派生类类型。性能
2)调用虚函数,则直到运行时才能肯定调用哪一个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。
虚(virtual)函数的通常实现模型是:每个类(class)有一个虚表(virtual table),内含该class之中有做用的虚(virtual)函数的地址,而后每一个对象有一个vptr(virtual table pointer,虚函数表指针),vptr 指向一个被称为vtbl(virtual table,虚函数表)的函数指针数组,每个包含虚函数的类都关联到 vtbl。当一个对象调用了虚函数,实际的被调用函数经过下面的步骤肯定:找到对象的 vptr 指向的 vtbl,而后在 vtbl 中寻找合适的函数指针。
虚函数的地址取决于对象的内存地址,而不是取决于数据类型(对于非虚拟函数的调用,编译器只根据数据类型翻译函数地址,判断调用的合法性。由于对象的内存地址空间中只包含成员变量,并不存储有关成员函数的信息,因此非虚拟函数的地址翻译过程与其对象的内存地址无关)。若是类定义了虚函数,该类及其派生类就要生成一张虚拟函数表,即vtable。而在类的对象地址空间中存储一个该虚表的入口,占4个字节,这个入口地址是在构造对象时由编译器写入的。因此,因为对象的内存空间包含了虚表入口,编译器可以由这个入口找到恰当的虚函数,这个函数的地址再也不由数据类型决定了。故对于一个父类的对象指针,调用虚拟函数,若是给他赋父类对象的指针,那么他就调用父类中的函数,若是给他赋子类对象的指针,他就调用子类中的函数(取决于对象的内存地址)。
例以下面的例子:
class Point { public: virtual ~Point(); virtual Point& mult(float) = 0; float x() const { return _x; } //非虚函数,不做存储
virtual float y() const { return 0; } virtual float z() const { return 0; } // ...
protected: Point(float x = 0.0); float _x; };
1)在Point的对象pt中,有两个东西,一个是数据成员_x,一个是_vptr_Point。其中_vptr_Point指向着virtual table point,而virtual table(虚表)point中存储的内容以下:
~Point() |
mult() |
y() |
z() |
class Point2d : public Point { public: Point2d( float x = 0.0, float y = 0.0 ) : Point( x ), _y( y ) {} ~Point2d(); //1 //改写base class virtual functions
Point2d& mult( float ); //2
float y() const { return _y; } //3
protected: float _y; };
2)在Point2d的对象pt2d中,有三个东西,首先是继承自基类pt对象的数据成员_x,而后是pt2d对象自己的数据成员_y,最后是_vptr_Point。其中_vptr_Point指向着virtual table point2d。因为Point2d继承自Point,因此在virtual table point2d中存储着:改写了的其中的~Point2d()、Point2d& mult( float )、float y() const,以及未被改写的Point::z()函数。
class Point3d: public Point2d { public: Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) : Point2d( x, y ), _z( z ) {} ~Point3d(); // overridden base class virtual functions
Point3d& mult( float ); float z() const { return _z; } // ... other operations ...
protected: float _z; };
3)在Point3d的对象pt3d中,则有四个东西,一个是_x,一个是_vptr_Point,一个是_y,一个是_z。其中_vptr_Point指向着virtual table point3d。因为point3d继承自point2d,因此在virtual table point3d中存储着:已经改写了的point3d的~Point3d(),point3d::mult()的函数地址,和z()函数的地址,以及未被改写的point2d的y()函数地址。
上述1)、2)、3)的状况以下图:
图:virtual table(虚表)的布局:单一继承状况
注意:只有发生继承的时候且父类子类都有virtual的时候才会出现虚函数指针,请不要忘了虚函数出现的目的是为了实现多态。
class Base{ public: virtual void f(); virtual void g(); virtual void h(); }; class Derive :public Base{ public: virtual void f1(); virtual void g1(); virtual void h1(); };
下面,再让咱们来看看继承时的虚函数表是什么样的。假设有以下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,
对于实例:Derive d; 的虚函数表以下:
咱们能够看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
下面,咱们来看一下,若是子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,咱们有下面这样的一个继承关系。
class Base{ public: virtual void f(); virtual void g(); virtual void h(); }; class Derive :public Base{ public: virtual void f(); virtual void g1(); virtual void h1(); };
为了让你们看到被继承事后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f() 。
那么,对于派生类的实例,其虚函数表会是下面的一个样子:
咱们从表中能够看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,咱们就能够看到对于下面这样的程序,
Base *b = new Derive(); b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,因而在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
下面,再让咱们来看看多重继承中的状况,假设有下面这样一个类的继承关系(注意:子类并无覆盖父类的函数):
对于子类实例中的虚函数表,是下面这个样子:
咱们能够看到:
1) 每一个父类都有本身的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样作就是为了解决不一样的父类类型的指针指向同一个子类实例,而可以调用到实际的函数。
下面咱们再来看看,若是发生虚函数覆盖的状况。
下图中,咱们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
咱们能够看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。
这样,咱们就能够任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d; Base1 *b1 = &d; Base2 *b2 = &d; Base3 *b3 = &d; b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
这个是比较很差理解的,对于虚继承,若派生类有本身的虚函数,则它自己须要有一个虚指针,指向本身的虚表。另外,派生类虚继承父类时,首先要经过加入一个虚指针来指向父类,所以有可能会有两个虚指针。
关于虚继承的例子能够参考 类“4、(虚)继承的内存占用大小”中的示例二。
首先,平时所声明的类只是一种类型定义,它自己是没有大小可言的。 所以,若是用sizeof运算符对一个类型名操做,那获得的是具备该类型实体的大小。
计算一个类对象的大小遵照的原则:
1)空类、单一继承的空类、多重继承的空类所占空间大小为1字节。(参考文章为何C++中空类和空结构体大小为1?)
2)一个类中,虚函数、成员函数(静态与非静态)和静态数据成员都不占类对象的存储空间。
3)当类中声明了虚函数(不论是1个仍是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable。
4)虚承继的状况:因为涉及到虚函数表和虚基表,会同时增长一个(多重虚继承下对应多个)vfPtr指针指向虚函数表vfTable和一个vbPtr指针指向虚基表vbTable,这二者所占的空间大小为:8(或8乘以多继承时父类的个数)。
5)在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐。
6)类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括全部的成员函数)的总和+ vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增长的字节。
示例一:含有普通继承
class A { }; class B { char ch; virtual void func0() { } }; class C { char ch1; char ch2; virtual void func() { } virtual void func1() { } }; class D: public A, public C { int d; virtual void func() { } virtual void func1() { } }; class E: public B, public C { int e; virtual void func0() { } virtual void func1() { } }; int main(void) { cout<<"A="<<sizeof(A)<<endl; //result=1
cout<<"B="<<sizeof(B)<<endl; //result=8
cout<<"C="<<sizeof(C)<<endl; //result=8
cout<<"D="<<sizeof(D)<<endl; //result=12
cout<<"E="<<sizeof(E)<<endl; //result=20
return 0; }
执行结果:
前面三个A、B、C类的内存占用空间大小就不须要解释了,注意一下内存对齐就能够理解了。
求sizeof(D)的时候,须要明白,首先VPTR指向的虚函数表中保存的是类D中的两个虚函数的地址,而后存放基类C中的两个数据成员ch一、ch2,注意内存对齐,而后存放数据成员d,这样4+4+4=12。
求sizeof(E)的时候,首先是类B的虚函数地址,而后类B中的数据成员,再而后是类C的虚函数地址,而后类C中的数据成员,最后是类E中的数据成员e,一样注意内存对齐,这样4+4+4+4+4=20。
示例二:含有虚继承
class A { public: virtual void aa() { } virtual void aa2() { } private: char ch[3]; }; class B: virtual public A { public: virtual void bb() { } virtual void bb2() { } }; int main(void) { cout<<"A's size is "<<sizeof(A)<<endl; cout<<"B's size is "<<sizeof(B)<<endl; return 0; }
执行结果:
对于虚继承,类B由于有本身的虚函数,因此它自己有一个虚指针,指向本身的虚表。另外,类B虚继承类A时,首先要经过加入一个虚指针来指向父类A,而后还要包含父类A的全部内容。所以是4+4+8=16。
在安全性方面,虚函数存在着一些漏洞。
咱们知道,子类没有重载父类的虚函数是一件毫无心义的事情。由于多态也是要基于函数重载的。
虽然在上面的图中咱们能够看到Base1的虚表中有Derive的虚函数,但咱们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive(); b1->g1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,即基类指针不能调用子类本身定义的成员函数。因此,这样的程序根本没法编译经过。
但在运行时,咱们能够经过指针的方式访问虚函数表来达到违反C++语义的行为。
若是父类的虚函数是private或是protected的,但这些非public的虚函数一样会存在于虚函数表中。因此,咱们一样可使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易作到的。
举个例子:
class Base { private: virtual void f() { cout << "Base::f" << endl; } }; class Derive : public Base{ }; typedef void(*Fun)(void); void main() { Derive d; Fun pFun = (Fun)*((int*)*(int*)(&d) + 0); pFun(); getchar(); }
执行结果:
class B { public: virtual void fun() { std::cout << "base fun called"; }; }; class D : public B { private: virtual void fun() { std::cout << "driver fun called"<<endl; }; }; int main(int argc, char* argv[]) { B* p = new D(); p->fun(); return 0; }
执行结果:
这是由于:在编译虚拟函数调用的时候,例如p->fun(); 只是按其静态类型来处理的, 在这里p的类型就是B,不会考虑其实际指向的类型(动态类型)。也就是说,碰到p->fun();编译器就看成调用B的fun来进行相应的检查和处理。由于在B里fun是public的,因此这里在“访问控制检查”这一关就彻底能够经过了。而后就会转换成
(*p->vptr[1])(p)这样的方式处理, p实际指向的动态类型是D。 因此p做为参数传给fun后(类的非静态成员函数都会编译加一个指针参数,指向调用该函数的对象,咱们日常用的this就是该指针的值), 实际运行时p->vptr[1]则获取到的是D::fun()的地址,也就调用了该函数, 这也就是动态运行的机理。
为了进一步的实验,能够将B里的fun改成private的,D里的改成public的,则编译就会出错。
class B { public: virtual void fun(int i = 1) { std::cout << "base fun called, " << i << endl; }; }; class D : public B { private: virtual void fun(int i = 2) { std::cout << "driver fun called, " << i << endl; }; }; int main(int argc, char* argv[]) { B* p = new D(); p->fun(); return 0; }
执行结果:
这是由于:“virtual 函数系动态绑定, 而缺省参数倒是静态绑定”,也就是说在编译的时候已经按照p的静态类型处理其默认参数了,转换成了(*p->vptr[1])(p, 1)这样的方式。
可使用函数指针来实现多态
#include<iostream>
using namespace std; typedef void (*fVoid)(); class A { public: static void test() { printf("hello A\n"); } fVoid print; A() { print = A::test; } }; class B : public A { public: static void test() { printf("hello B\n"); } B() { print = B::test; } }; int main(void) { A aa; aa.print(); B b; A* a = &b; a->print(); return 0; }
这样作的好处主要是绕过了vtable。咱们都知道虚函数表有时候会带来一些性能损失。
补充一下虚函数的缺点:虚函数最主要的缺点是执行效率较低,看一看虚拟函数引起的多态性的实现过程,就能体会到其中的缘由,另外就是因为要携带额外的信息(VPTR),因此致使类多占的内存空间也会比较大,对象也是同样的。
《C++ Primer Plus》