虚办法的调用是怎么实现的(单持续VS多持续)
添加时间:2013-7-25 点击量:
我们知道经由过程一个指向之类的父类指针可以调用子类的虚办法,因为子类的办覆盖父类同样的办法,经由过程这个指针可以找到对象实例的地址,经由过程实例的地址可以找到指向对应办法表的指针,而经由过程这个办法的名字就可以断定这个办法在办法表中的地位,直接调用就行,在多持续的时辰,一个类可能有多个办法表,也就有多个指向这些办法表的指针,一个类有多个父类,怎么经由过程此中一个父类的指针调用之类的虚办法?
其实前面几句话并没有真正说清楚,在单持续中,父类是怎么调用子类的虚办法的,还有多持续又是怎么实现这点的,想知道这些,请卖力往下看。
我们先看单持续是怎么实现的。先上两个简单的类:
#include <iostream>
using namespace std;
class A
{
public:
A():a(0){}
virtual ~A(){}
virtual void GetA()
{
cout<<A::GetA<<endl;
}
void SetA(int _a)
{
a=_a;
}
int a;
};
class B:public A
{
public:
B():A(),b(0){}
virtual ~B(){}
virtual void GetA()
{
cout<<B::GetA<<endl;
}
virtual void GetB()
{
cout<<B::GetB<<endl;
}
private:
int b;
};
typedef int (Fun)(void);
void TestA()
{
Fun pFun;
A a;
cout<<类A的虚办法(第0个是A的析构函数):<<endl;
int pVtab0 = (int)&a;
for (int i=1; (Fun)pVtab0[0][i]!=NULL; i++){
pFun = (Fun)pVtab0[0][i];
cout << [<<i<<] ;
pFun();
}
cout<<endl;
B b ;
A b1=&b;
cout<<类B的虚办法(第0个是B的析构函数)经由过程类B的实例:<<endl;
int pVtab1 = (int)&b;
for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){
pFun = (Fun)pVtab1[0][i];
cout << [<<i<<] ;
pFun();
}
cout<<endl;
cout<<类B的虚办法(第0个是B的析构函数)经由过程类A的指针:<<endl;
int pVtab2 = (int)&b1;
for (int i=1; (Fun)pVtab2[0][i]!=NULL; i++){
pFun = (Fun)pVtab2[0][i];
cout << [<<i<<] ;
pFun();
}
cout<<endl;
cout<< b的地址:<<&b<<endl;
cout<<b1指向的地址:<<b1<<endl<<endl;
}
运行成果如下:
经由过程运行成果我们知道:经由过程父类指向子类的指针调用的是子类的虚办法。在单一持续中,固然父类有父类的虚办法表,子类有子类的虚办法表,然则子类并没有指向父类虚办法的指针,在子类的实例中,子类和父类是公用一个虚办法表,当然只有一个指向办法表的指针,为什么可以公用一个虚办法表呢,虚办法表的第一个办法是析构函数,子类的办覆盖父类的同样的办法,子类新增的虚办法放在虚办法表的后面,也就是说子类的虚办法表完全覆盖父类的虚办法表,即子类的每个虚办法与父类对应的虚办法,在各类的办法表中的索引是一样的。
然则在多持续中就不是如许了,第一个被持续的类应用起来跟单持续是完全一样的,然则后面被持续的类就不是如许了,且细心往下看。
还是先上3个简单的类
#include <iostream>
using namespace std;
class A
{
public:
A():a(0){}
virtual ~A(){}
virtual void GetA()
{
cout<<A::GetA<<endl;
}
int a;
};
class B
{
public:
B():b(0){}
virtual ~B(){}
virtual void SB()
{
cout<<B::SB<<endl;
}
virtual void GetB()
{
cout<<B::GetB<<endl;
}
private:
int b;
};
class C:public A,public B
{
public:
C():c(0){}
virtual ~C(){}
virtual void GetB()//覆盖类B的同名办法
{
cout<<C::GetB<<endl;
}
virtual void GetC()
{
cout<<C::GetC<<endl;
}
virtual void JustC()
{
cout<<C::JustC<<endl;
}
private:
int c;
};
typedef int (Fun)(void);
void testC()
{
C c=new C();
A a=c;
B b=c;
Fun pFun;
cout<<sizeof(C)=<<sizeof(C)<<endl<<endl;
cout<<c的地址:<<c<<endl;
cout<<a的地址:<<a<<endl;
cout<<b的地址:<<b<<endl<<endl<<endl;
cout<<类C的虚办法(第0个是C的析构函数)(经由过程C类型的指针):<<endl;
int pVtab1 = (int)&c;
for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){
pFun = (Fun)pVtab1[0][i];
cout << [<<i<<] <<&pFun<< ;
pFun();
}
cout<<endl<<endl;
cout<<类C的虚办法(第0个是C的析构函数)(经由过程B类型的指针):<<endl;
pVtab1 = (int)&b;
for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){
pFun = (Fun)pVtab1[0][i];
cout << [<<i<<] <<&pFun<< ;
pFun();
}
}
运行成果如下:
从成果措辞:
Sizeof(C)=20,我们并不料外,在单持续的时辰,父类和子类是公用一个指向虚办法表的指针,在多持续中,同样第一个父类和子类公用这个指针,而从第二个父类开端就有本身零丁的指针,其实就是父类的实例在子类的内存中对峙完全的布局,也就是说在多重持续中,之类的实例就是每一个父类的实例拼接而成的,当然可能因为持续的错杂性,会加一些帮助的指针。
指针a与指针c指向同一个地址,即c的首地址,而b所指的地址与a所指的地址相差8字节正好就是类A实例的大小,也就是说在C的内存布局中,先存放了A的实例,在存放B的实例,sizeof(B)=8(字段int b和指向B虚办法表的指针),在家上C本身的字段int c正好是20字节。
让我有点不测的是:办法B::SB,C::GetB并没有呈如今类C的办法表中,并且C::GetB是C覆写B中的GetB办法,怎么没有呈如今C的办法表中呢?在《深切摸索C++对象模型》一书中讲到,这两个办法同时应当呈如今C的办法表中,同样也会覆盖B的虚办法表。可能是不通的编译器有不合的实现,我用的是VS2010,那本书上讲的是编译器cfront
OK,我们不消管不合的编译器实现上的差别,这点小差别无伤大雅,虚办法的调用机制还是一样的。
先来解析几个小例子,看看虚办法的实现机制。
C c=new C();
A a=c;
a->GetA();
c->GetA();
c->GetC();
上方已经说了,a与c指向的是同一个地址,且公用同一个虚办法表,而办法GetA,GetC的地址就在这个办法表中,那么调用起来就简单多了,大致就是下面这个样子:
a->GetA() -> (a->vptr1[1])(a); // GetA在办法表中的索引是1
c->GetA() -> (c->vptr1[1])(c); // GetA在办法表中的索引是1
c->GetC() -> (a->vptr1[2])(c); // GetC在办法表中的索引是2
vptr1默示指向类C第一个办法表的指针,这个指针实际的名字会错杂一些,暂且将指向类C的第一个办法表的指针定名为vptr2,下面会用到这个指针。
再来解析几行代码:
B b=c;
c->GetB();
b->GetB();
指针b和指针c指向的不是同一个地址,那么B b=c;到底是做了啥呢?大致是会转换成下面这个样子:
B b=c+sizeof(A);
c所指的地址加上A的大小,正好是b所指的地址。
c->GetB();同样须要转换,因为办法GetB底子不在c所指的那个办法表中,可能转换成这个样子(实际转换成啥样子我真不知道):
this=c+sizeof(A);
(this->vptr2[2])(c);
若是像编译器cfront所说的那样,办法GetB在vptr1所指的办法表中,那么就不消产生调剂this指针了,若是在vptr1所指的办法表中,就让办法表变大了,且跟此外办法表是反复的。
b->GetB();就不须要做过多的转换了,因为b正好指向vptr2,可能转换成下面这个样子:
b->GetB() -> (b->vptr2[2])(b); // GetB在办法表中的索引是2
总之指针所指的办法表若是没有要调用的办法,就要做调剂,虚办法须要经由过程办法表调用,相对于非虚办法,机能就慢那么一点点,这也是别人常说的C++机能不如C的此中一点。
虚多持续就更麻烦了,不熟悉可能就会被坑。《深切摸索C++对象模型》这本书是如许建议的:不要在一个virtual base class中声明nonstatic data members,若是如许做,你会距错杂的深渊越来越近,终不成拔。
virtual base class还是当做接口来用吧。
彼此相爱,却不要让爱成了束缚:不如让它成为涌动的大海,两岸乃是你们的灵魂。互斟满杯,却不要同饮一杯。相赠面包,却不要共食一个。一起歌舞欢喜,却依然各自独立,相互交心,却不是让对方收藏。因为唯有生命之手,方能收容你们的心。站在一起却不要过于靠近。—— 纪伯伦《先知》
我们知道经由过程一个指向之类的父类指针可以调用子类的虚办法,因为子类的办覆盖父类同样的办法,经由过程这个指针可以找到对象实例的地址,经由过程实例的地址可以找到指向对应办法表的指针,而经由过程这个办法的名字就可以断定这个办法在办法表中的地位,直接调用就行,在多持续的时辰,一个类可能有多个办法表,也就有多个指向这些办法表的指针,一个类有多个父类,怎么经由过程此中一个父类的指针调用之类的虚办法?
其实前面几句话并没有真正说清楚,在单持续中,父类是怎么调用子类的虚办法的,还有多持续又是怎么实现这点的,想知道这些,请卖力往下看。
我们先看单持续是怎么实现的。先上两个简单的类:
#include <iostream>
using namespace std;
class A
{
public:
A():a(0){}
virtual ~A(){}
virtual void GetA()
{
cout<<A::GetA<<endl;
}
void SetA(int _a)
{
a=_a;
}
int a;
};
class B:public A
{
public:
B():A(),b(0){}
virtual ~B(){}
virtual void GetA()
{
cout<<B::GetA<<endl;
}
virtual void GetB()
{
cout<<B::GetB<<endl;
}
private:
int b;
};
typedef int (Fun)(void);
void TestA()
{
Fun pFun;
A a;
cout<<类A的虚办法(第0个是A的析构函数):<<endl;
int pVtab0 = (int)&a;
for (int i=1; (Fun)pVtab0[0][i]!=NULL; i++){
pFun = (Fun)pVtab0[0][i];
cout << [<<i<<] ;
pFun();
}
cout<<endl;
B b ;
A b1=&b;
cout<<类B的虚办法(第0个是B的析构函数)经由过程类B的实例:<<endl;
int pVtab1 = (int)&b;
for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){
pFun = (Fun)pVtab1[0][i];
cout << [<<i<<] ;
pFun();
}
cout<<endl;
cout<<类B的虚办法(第0个是B的析构函数)经由过程类A的指针:<<endl;
int pVtab2 = (int)&b1;
for (int i=1; (Fun)pVtab2[0][i]!=NULL; i++){
pFun = (Fun)pVtab2[0][i];
cout << [<<i<<] ;
pFun();
}
cout<<endl;
cout<< b的地址:<<&b<<endl;
cout<<b1指向的地址:<<b1<<endl<<endl;
}
运行成果如下:
经由过程运行成果我们知道:经由过程父类指向子类的指针调用的是子类的虚办法。在单一持续中,固然父类有父类的虚办法表,子类有子类的虚办法表,然则子类并没有指向父类虚办法的指针,在子类的实例中,子类和父类是公用一个虚办法表,当然只有一个指向办法表的指针,为什么可以公用一个虚办法表呢,虚办法表的第一个办法是析构函数,子类的办覆盖父类的同样的办法,子类新增的虚办法放在虚办法表的后面,也就是说子类的虚办法表完全覆盖父类的虚办法表,即子类的每个虚办法与父类对应的虚办法,在各类的办法表中的索引是一样的。
然则在多持续中就不是如许了,第一个被持续的类应用起来跟单持续是完全一样的,然则后面被持续的类就不是如许了,且细心往下看。
还是先上3个简单的类
#include <iostream>
using namespace std;
class A
{
public:
A():a(0){}
virtual ~A(){}
virtual void GetA()
{
cout<<A::GetA<<endl;
}
int a;
};
class B
{
public:
B():b(0){}
virtual ~B(){}
virtual void SB()
{
cout<<B::SB<<endl;
}
virtual void GetB()
{
cout<<B::GetB<<endl;
}
private:
int b;
};
class C:public A,public B
{
public:
C():c(0){}
virtual ~C(){}
virtual void GetB()//覆盖类B的同名办法
{
cout<<C::GetB<<endl;
}
virtual void GetC()
{
cout<<C::GetC<<endl;
}
virtual void JustC()
{
cout<<C::JustC<<endl;
}
private:
int c;
};
typedef int (Fun)(void);
void testC()
{
C c=new C();
A a=c;
B b=c;
Fun pFun;
cout<<sizeof(C)=<<sizeof(C)<<endl<<endl;
cout<<c的地址:<<c<<endl;
cout<<a的地址:<<a<<endl;
cout<<b的地址:<<b<<endl<<endl<<endl;
cout<<类C的虚办法(第0个是C的析构函数)(经由过程C类型的指针):<<endl;
int pVtab1 = (int)&c;
for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){
pFun = (Fun)pVtab1[0][i];
cout << [<<i<<] <<&pFun<< ;
pFun();
}
cout<<endl<<endl;
cout<<类C的虚办法(第0个是C的析构函数)(经由过程B类型的指针):<<endl;
pVtab1 = (int)&b;
for (int i=1; (Fun)pVtab1[0][i]!=NULL; i++){
pFun = (Fun)pVtab1[0][i];
cout << [<<i<<] <<&pFun<< ;
pFun();
}
}
运行成果如下:
从成果措辞:
Sizeof(C)=20,我们并不料外,在单持续的时辰,父类和子类是公用一个指向虚办法表的指针,在多持续中,同样第一个父类和子类公用这个指针,而从第二个父类开端就有本身零丁的指针,其实就是父类的实例在子类的内存中对峙完全的布局,也就是说在多重持续中,之类的实例就是每一个父类的实例拼接而成的,当然可能因为持续的错杂性,会加一些帮助的指针。
指针a与指针c指向同一个地址,即c的首地址,而b所指的地址与a所指的地址相差8字节正好就是类A实例的大小,也就是说在C的内存布局中,先存放了A的实例,在存放B的实例,sizeof(B)=8(字段int b和指向B虚办法表的指针),在家上C本身的字段int c正好是20字节。
让我有点不测的是:办法B::SB,C::GetB并没有呈如今类C的办法表中,并且C::GetB是C覆写B中的GetB办法,怎么没有呈如今C的办法表中呢?在《深切摸索C++对象模型》一书中讲到,这两个办法同时应当呈如今C的办法表中,同样也会覆盖B的虚办法表。可能是不通的编译器有不合的实现,我用的是VS2010,那本书上讲的是编译器cfront
OK,我们不消管不合的编译器实现上的差别,这点小差别无伤大雅,虚办法的调用机制还是一样的。
先来解析几个小例子,看看虚办法的实现机制。
C c=new C();
A a=c;
a->GetA();
c->GetA();
c->GetC();
上方已经说了,a与c指向的是同一个地址,且公用同一个虚办法表,而办法GetA,GetC的地址就在这个办法表中,那么调用起来就简单多了,大致就是下面这个样子:
a->GetA() -> (a->vptr1[1])(a); // GetA在办法表中的索引是1
c->GetA() -> (c->vptr1[1])(c); // GetA在办法表中的索引是1
c->GetC() -> (a->vptr1[2])(c); // GetC在办法表中的索引是2
vptr1默示指向类C第一个办法表的指针,这个指针实际的名字会错杂一些,暂且将指向类C的第一个办法表的指针定名为vptr2,下面会用到这个指针。
再来解析几行代码:
B b=c;
c->GetB();
b->GetB();
指针b和指针c指向的不是同一个地址,那么B b=c;到底是做了啥呢?大致是会转换成下面这个样子:
B b=c+sizeof(A);
c所指的地址加上A的大小,正好是b所指的地址。
c->GetB();同样须要转换,因为办法GetB底子不在c所指的那个办法表中,可能转换成这个样子(实际转换成啥样子我真不知道):
this=c+sizeof(A);
(this->vptr2[2])(c);
若是像编译器cfront所说的那样,办法GetB在vptr1所指的办法表中,那么就不消产生调剂this指针了,若是在vptr1所指的办法表中,就让办法表变大了,且跟此外办法表是反复的。
b->GetB();就不须要做过多的转换了,因为b正好指向vptr2,可能转换成下面这个样子:
b->GetB() -> (b->vptr2[2])(b); // GetB在办法表中的索引是2
总之指针所指的办法表若是没有要调用的办法,就要做调剂,虚办法须要经由过程办法表调用,相对于非虚办法,机能就慢那么一点点,这也是别人常说的C++机能不如C的此中一点。
虚多持续就更麻烦了,不熟悉可能就会被坑。《深切摸索C++对象模型》这本书是如许建议的:不要在一个virtual base class中声明nonstatic data members,若是如许做,你会距错杂的深渊越来越近,终不成拔。
virtual base class还是当做接口来用吧。
彼此相爱,却不要让爱成了束缚:不如让它成为涌动的大海,两岸乃是你们的灵魂。互斟满杯,却不要同饮一杯。相赠面包,却不要共食一个。一起歌舞欢喜,却依然各自独立,相互交心,却不是让对方收藏。因为唯有生命之手,方能收容你们的心。站在一起却不要过于靠近。—— 纪伯伦《先知》