#include<iostream>
using namespace std;
class A
{
public:
virtual void print()
{
cout<<"A"<<endl;
}
private:
int value;
};
class B:public A
{
public:
virtual void print()
{
cout<<"B"<<endl;
}
private:
int value;
};
class D:public A
{
public:
virtual void print()
{
cout<<"D"<<endl;
}
private:
int value;
};
void main()
{
A * pA;
pA=new B();
pA->print();
pA=new D();
pA->print();
}
上述是一个多态的例子,输出结果为:
B
D
如果上例中A类中print()类非virtual函数则输出A,这与B类中print()函数是否为虚类无关(除非涉及到B类的子类)。亦即,virtual是针对父类而言的。
下面为转载部分:
大概意思理解为(自己的理解):
1. 父类A和子类B都有相同的函数,如果不用虚函数的话,A * pA=new B() ,在调用函数F( )时会调用A类的F函数;
如果用虚函数的话,每个类都有自己的虚函数表,如果子类有继承的话,子类会把相应的虚函数重写,这样在调用时就变成自己的虚函数。
2. 父类里函数为virtual类型时,执行其子类中的函数;父类里函数为非virtual类型时,执行父类中的函数。
3. 析构函数略有不同。如果父类中析构函数为非虚函数,此类的行为是未定义的,可能会出现意料之外的错误。父类中的资源被释放,而子类中的资源则不一定。
如果父类中析构函数为虚函数时,首先会调用子类中的析构函数,然后依次往上层调用。所以所有的资源都可被释放。
关于虚函数
C++是支持动态绑定(也即多态)的,不过它不像Java那样天生的就是多态,不仅要在成员函数的前面加上关键字virtual还要通过指针或者引用才可以。
1.虚函数是多态的基础,而且只有非静态非友元非内联的成员函数才能声明为虚函数。
2.virtual只能用在函数声明的时候,不能用在函数实现的时候。
3.当父类中声明虚函数时,子类自动继承该虚函数,无论子类是否声明,同原型的函数都自动为虚函数。这里的同原型指的是函数名,返回类型,参数列表都完全一样,只有函数的实现不一样。
二、虚函数实现机制
简单的说是“旧瓶装新酒”。
当编译器发现类有虚函数,则在对象建立的时候偷偷地加了一个成员,它是一个void类型的指向指针的指针vptr,该指针指向了一个虚函数表vtable,表的每一项都是函数的地址。图示如下:
当通过指针或者引用调用虚函数时,编译器会根据索引到vtable中取得相应的函数入口地址,即为要调用的具有多态性质的函数。这种机制实际是将找谁变成了到哪里去找,也即编译器只知道到某个指定的索引去取函数地址,但它不知道这个函数地址是基类中的函数还是子类中重写的“具有”多态性质的函数。
比如p->z();//z为虚函数
编译器根据z声明的顺序就知道它的索引(后面详述),比如为2,则通过p调用时直接到vtable[2]中取函数地址,然后调用就可以了。至于vtable[2]中的函数是什么它不管。关于细节后面有更详细的讨论。
继承族中虚函数机制的图示:
三、详述细节
我们先看最简单的多态例子:
class B
{
public:
virtual void print()
{
cout<<"B"<<endl;
}
virtual void move()
{}
private:
int b;
};
class D : public B
{
public:
virtual void print()
{
cout<<"D"<<endl;
}
private:
int d;
};
void main()
{
D dd;
B *pB = ⅆ
pB->print();
system("pause");
}
virtual table布局如下:
说明:
1.只有虚函数才能进入虚函数表,而且根据声明的顺序依次排列,如上图。
2.当 pB->print();执行时,编译器将发生转换,将上句转换如下:
对于有参数的则形如:
(*pB->vtable[1])(pB,Arg_list);
3.如果子类中没有重写父类中的虚函数,则形如class D中的B::move见上图,函数地址不改变。
四、多重继承下的虚函数
形如:
class B1;
class B2;
class D : public B1,public B2;
则在D的布局中有两个vptr有两个vtable,其他与单继承相似。多态调用的时候也发生转换,不同的是有可能发生指针的调整,即B2 *pB2 = new D;这种情况下。新的D对象的地址必须调整,以指向其B2 subobject,编译时期会产生这样的代码:
D *tmp = new D;
B2 *pB2 = tmp ? tmp + sizeof(B1) : 0;
关于this指针调整的大小,也即上面的这段代码必须在编译器的某个地方插入,问题是,在哪个地方???
Bjarne在cfront编译器中采用的增大virtual table,将offset也存储起来;比较有效率的解决方法是利用所谓的thunk技术,也即thunk是一小段assembly码,可能看起来是如下的样子:
pB2_methodname_thunk:
this += sizeof(B1);
pB2->virtual_method(this);
虚函数的实现机制比较复杂,细节问题很多,这里只是冰山一角,欢迎讨论。