虚继承

虚继承是为了在多继承的时候避免引发歧义, 
比如类A有个就是a,B继承了A,C也继承了A,当D多继承B,C时,就会有歧义产生了,所以要使用虚拟继承避免重复拷贝。 
虚函数继承是解决多态性的,当用基类指针指向派生类对象的时候,基类指针调用虚函数的时候会自动调用派生类的虚函数,这就是多态性,也叫动态编联
虚函数继承就是覆盖。即基类中的虚函数被派生类中的同名函数所覆盖。 

 

 1 class parent  
 2 { 
 3 public: 
 4     virtual void foo(){cout < <"foo from parent";}; 
 5     void foo1(){cout < <"foo1 from parent";}; 
 6 }; 
 7 
 8 class son:public parent  
 9 { 
10     void foo(){cout < <"foo from son";}; 
11     void foo1(){cout < <"foo1 from son";}; 
12 }; 
13 
14 int main() 
15 { 
16     parent *p=new son(); 
17     p->foo(); 
18     p->foo1(); 
19     return 0;  
20 } 
其输出结果是: foo from son,foo1 from parent

注意fool1函数还是调用的基类的。


1、真正意义上的虚函数调用,是运行时绑定的;

 

2、什么是真正意义上的虚函数调用?通过指针或者引用执行虚函数;

 

3、通过对象执行虚函数会不会是动态绑定的?不会。

 

4、一个类是否有虚函数,就看它是否包含一个指向虚函数表的指针;

 

5、如果类本身含有virtual 声明的函数,或者继承了virtual 函数,那么它肯定会包含一个指向虚函数表的指针;

 

6、从纯抽象类或者非抽象类继承了virutal,意义上是一样的,效率上是一样的,并不因为你是纯抽象类的继承而效率变高;

 

7、虚函数调用比普通函数调用慢多少?假设这个函数仅执行 return i  > j,大概慢 15%左右,如果是个真正有意义上的函数,效率影响可以不计;
 
9、虚函数会慢,但是那是对内联函数而言的,虚函数会忽略 inline前缀,请注意这一点;
 
10、继承层次不影响虚函数效率,如果你这个类是原始类的第10层继承,那么虚函数调用效率和第1层继承的类没有差别,当然如果你要在该函数中调用上一层的虚函数那就另当别论了;
 
11、每个类应该只有一个virtual table,而不是每个对象有一个(对象只含有指向虚表的指针),那些说虚函数增大空间开销的可以自宫了;

 

12、如果一个类含有虚函数,在构造时,使用memset(this, 0, sizeof(*this))是找死的行为;

 

13、虚函数是运行时多态,模板是编译时多态,一个动,一个是静。
 
14、子类覆盖父类的虚函数的时候,实际上是在构造函数中修改了虚表中的函数指针;因此使得 FatherClass* p = new ChildClass();的情况下,p->VirtualFunc()始终执行的是子类的虚函数;

 

1.为什么要引入虚拟继承

虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如 下:

class A

class B1:public virtual A;

class B2:public virtual A;

class D:public B1,public B2;

虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

 

2.引入虚继承和直接继承会有什么区别呢

由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。

2.1时间:在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。

2.2空间:由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之 多继承节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证 这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针。

 

首先还是先给出虚继承和虚基类的定义。
虚继承:在继承定义中包含了virtual关键字的继承关系;
虚基类:在虚继承体系中的通过virtual继承而来的基类,需要注意的是:
struct CSubClass : public virtual CBase {}; 其中CBase称之为CSubClass
的虚基类,而不是说CBase就是个虚基类,因为CBase还可以不不是虚继承体系中的基类。
 

一个例子

以下面的一个例子为例:

 1 class CA
 2 {
 3     int k; //如果基类没有数据成员,则在这里多重继承编译不会出现二义性
 4 public:
 5     void f() {cout << "CA::f" << endl;}
 6 };
 7 class CB : public CA
 8 {
 9 };
10 class CC : public CA
11 {
12 };
13 class CD : public CB, public CC
14 {
15 };
16 void main()
17 {
18     CD d;
19     d.f();
20 }
当编译上述代码时,我们会收到如下的错误提示:
error C2385: 'CD::f' is ambiguous
即编译器无法确定你在d.f()中要调用的函数f到底是哪一个。这里可能会让人觉得有些奇怪,命名只定义了一个CA::f,既然大家都派生自CA,那自然就是调用的CA::f,为什么还无法确定呢?
这是因为编译器在进行编译的时候,需要确定子类的函数定义,如CA::f是确定的,那么在编译CB、CC时还需要在编译器的语法树中生成CB::f,CC::f等标识,那么,在编译CD的时候,由于CB、CC都有一个函数f,此时,编译器将试图生成这两个CD::f标识,显然这时就要报错了。(当我们不使用CD::f的时候,以上标识都不会生成,所以,如果去掉d.f()一句,程序将顺利通过编译)
要解决这个问题,有两个方法:
1、重载函数f():此时由于我们明确定义了CD::f,编译器检查到CD::f()调用时就无需再像上面一样去逐级生成CD::f标识了;
此时CD的元素结构如下:
|CB(CA)|
|CC(CA)|
故此时的sizeof(CD) = 8;(CB、CC各有一个元素k)
2、使用虚拟继承:虚拟继承又称作共享继承,这种共享其实也是编译期间实现的,当使用虚拟继承时,上面的程序将变成下面的形式:
 1 class CA
 2 {
 3     int k;
 4 public:
 5     void f() {cout << "CA::f" << endl;}
 6 };
 7 class CB : virtual public CA //也有一种写法是class CB : public virtual CA
 8 { //实际上这两种方法都可以
 9 };
10 class CC : virtual public CA
11 {
12 };
13 class CD : public CB, public CC
14 {
15 };
16 void main()
17 {
18     CD d;
19     d.f();
20 }
此时,当编译器确定d.f()调用的具体含义时,将生成如下的CD结构:
|CB|
|CC|
|CA|
同时,在CB、CC中都分别包含了一个指向CA的虚基类指针列表vbptr(virtual base table pointer),其中记录的是从CB、CC的元素到CA的元素之间的偏移量。此时,不会生成各子类的函数f标识,除非子类重载了该函数,从而达到“共 享”的目的(这里的具体内存布局,可以参看钻石型继承内存布局,在白杨的那篇文章中也有)。
也正因此,此时的sizeof(CD) = 12(两个vbptr + sizoef(int));
另注:
如果CB,CC中各定义一个int型变量,则sizeof(CD)就变成20(两个vbptr + 3个sizoef(int)
如果CA中添加一个virtual void f1(){},sizeof(CD) = 16(两个vbptr + sizoef(int)+vptr);
再添加virtual void f2(){},sizeof(CD) = 16不变。原因如下所示:带有虚函数的类,其内存布局上包含一个指向虚函数列表的指针(vptr),这跟有几个虚函数无关。
 

虚继承:子类has虚基类,所以带来的问题是虚基类必须先于子类构造,虚基类要有独立性,如拥有虚函数表指针。
一般继承:子类is基类,属于关系,虚函数表自然可以共享。

首先,说说GCC的编译器.

它实现比较简单,不管是否虚继承,GCC都是将虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针。

 1 class A {
 2     int a;
 3     virtual ~A(){}
 4 };
 5 class B:virtual public A{
 6     virtual void myfunB(){}
 7 };
 8 class C:virtual public A{
 9     virtual void myfunC(){}
10 };
11 class D:public B,public C{
12     virtual void myfunD(){}
13 };

以上代码中sizeof(A)=8,sizeof(B)=12,sizeof(C)=12,sizeof(D)=16.

解释:Aint+虚表指针BC中由于是虚继承因此大小为A+指向虚基类的指针B,C虽然加入了自己的虚函数,但是虚表指针是和基类共享的,因此不会有自己的虚表指针D由于B,C都是虚继承,因此D只包含一个A的副本,于是D大小就等于A+B中的指向虚基类的指针+C中的指向虚基类的指针

如果B,C不是虚继承,而是普通继承的话,那么A,B,C的大小都是8(没有指向虚基类的指针了)D由于不是虚继承,因此包含两个A副本,大小为16.注意此时虽然D的大小和虚继承一样,但是内存布局却不同。

然后,来看看VC的编译器

vc对虚表指针的处理比GCC复杂,它根据是否为虚继承来判断是否在继承关系中共享虚表指针,而对指向虚基类的指针和GCC一样是不共享,当然也不可能共享。

代码同上。

运行结果将会是sizeof(A)=8,sizeof(B)=16,sizeof(C)=16,sizeof(D)=24.

解释:A中依然是int+虚表指针。BC由于是虚继承因此虚表指针不共享,由于B,C加入了自己的虚函数,所以B,C分别自己维护一个虚表指针,它指向自己的虚函数。(注意:只有子类有新的虚函数时,编译器才会在子类中添加虚表指针)因此B,C大小为A+自己的虚表指针+指向虚基类的指针D由于B,C都是虚继承,因此D只包含一个A的副本,同时D是从B,C普通继承的,而不是虚继承的,因此没有自己的虚表指针。于是D大小就等于A+B的虚表指针+C的虚表指针+B中的指向虚基类的指针+C中的指向虚基类的指针

同样,如果去掉虚继承,结果将和GCC结果一样,A,B,C都是8D16,原因就是VC的编译器对于非虚继承,父类和子类是共享虚表指针的

利用visual studio 命令提示(2008),到xx.cpp 文件目录下 运行cl /d1 reportSingleClassLayoutB xx.cpp

第一个vfptr 指向B的虚表,第二个vbptr指向A,第三个指向A的虚表,因为是虚拟继承,所以子类中有一个指向父类的虚基类指针,
防止菱形继承中数据重复,这样在菱形继承中,不会出现祖先数据重复,而只指向祖先数据的指针。

虚函数继承

带 有虚函数的普通继承,前面一个是父类,后面一个是子类,多态实现只需要更新虚函数表即可.,虚函数指针没有变,变得只是覆盖的虚函数表中的函数,你可以把 虚函数指针想象成普通的成员变量,编译的时候自动插入类中,用的是同一个虚函数指针,只不过子类对虚函数表中对应的虚函数进行了更新。

 

3.执行顺序

 1 class Base   
 2 { 
 3 public:  
 4     Base(){cout<<"Base called : " << gFlag++ << endl;} 
 5     void print(){cout<<"Base print" <<endl;}  
 6 };
 7 
 8 class Mid1 : public Base  
 9 {
10 public:  
11     Mid1(){cout<<"Mid1 called" << endl;}  
12 };
13 
14 class Mid2 : public Base  
15 {
16 public:  
17     Mid2(){cout<<"Mid2 called" << endl;}  
18 };
19 
20 class Child:public Mid1, public Mid2  
21 {
22 public:  
23     Child(){cout<<"Child called" << endl;}  
24 };
25 
26 int main(int argc, char* argv[]) 
27 {
28     Child d;
29 
30     //不能这样使用,会产生二意性  
31     //d.print();  
32 
33      //只能这样使用  
34     d.Mid1::print();
35     d.Mid2::print();
36 
37     system("pause");  
38     return 0;  
39 }

 

//output

 Base called : 0

Mid1 called
 Base called : 1 

Mid2 called
 Child called
 Base print

   

◇虚拟继承

在派生类继承基类时,加上一个virtual关键词则为虚拟继承

 1  1 class Base   
 2  2 { 
 3  3 public:  
 4  4     Base(){cout<<"Base called : " << gFlag++ << endl;} 
 5  5     void print(){cout<<"Base print" <<endl;}  
 6  6 };
 7  7 
 8  8 class Mid1 : virtual public Base  
 9  9 {
10 10 public:  
11 11     Mid1(){cout<<"Mid1 called" << endl;}  
12 12 };
13 13 
14 14 class Mid2 : virtual public Base  
15 15 {
16 16 public:  
17 17     Mid2(){cout<<"Mid2 called" << endl;}  
18 18 };
19 19 
20 20 class Child:public Mid1, public Mid2  
21 21 {
22 22 public:  
23 23     Child(){cout<<"Child called" << endl;}  
24 24 };
25 25 
26 26 int main(int argc, char* argv[]) 
27 27 {
28 28     Child d;
29 29 
30 30      
31 31     d.print();  
32 32 
33 33   
34 34     d.Mid1::print();
35 35     d.Mid2::print();
36 36 
37 37     system("pause");  
38 38     return 0;  
39 39 }

 

 

//output

 1: Base called : 0

Mid1 called
 3: Mid2 called
 5: Base print
 7: Base print
 

通过输出的比较

2.声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。 
虚拟继承与虚函数有一定相似的地方,但他们之间是绝对没有任何联系的。

 

4.c++重载、覆盖、隐藏的区别和执行方式

既然说到了继承的问题,那么不妨讨论一下经常提到的重载,覆盖和隐藏
4.1成员函数被重载的特征
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
4.2“覆盖”是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
4.3“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,特征是:

(1)如果派生类的函数与基类的函数同名,但是参数不同,此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,但是参数相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

小结:说白了就是如果派生类和基类的函数名和参数都相同,属于覆盖,这是可以理解的吧,完全一样当然要覆盖了;如果只是函数名相同,参数并不相同,则属于隐藏。

posted @ 2015-06-30 19:01  balingybj  阅读(353)  评论(0编辑  收藏  举报