C++ 虚拟继承

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++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

为什么需要虚继承?

由于C++支持多重继承,那么在这种情况下会出现重复的基类这种情况,也就是说可能出现将一个类两次作为基类的可能性。比如像下面的情况

 1 #include<iostream>
 2 using std::cout;
 3 using std::endl;
 4 class Base
 5 {
 6 protected:
 7 int value;
 8 public:
 9 Base()
10 {
11 cout<<"in Base"<<endl;
12 }
13 };
14 class DerivedA:protected Base
15 {
16 public:
17 DerivedA()
18 {
19 cout<<"in DerivedA"<<endl;
20 }
21 };
22 class DerivedB: protected Base
23 {
24 public:
25 DerivedB()
26 {
27 cout<<"in DerivedB"<<endl;
28 }
29 };
30 class MyClass:DerivedA,DerivedB
31 {
32 public:
33 MyClass()
34 {
35 cout<<"in MyClass"<<value<<endl;
36 }
37 };

编译时的错误如下

这 中情况下会造成在MyClass中访问value时出现路径不明确的编译错误,要访问数据,就需要显示地加以限定。变成DerivedA::value或 者DerivedB::value,以消除歧义性。并且,通常情况下,像Base这样的公共基类不应该表示为两个分离的对象,而要解决这种问题就可以用虚 基类加以处理。如果使用虚继承,编译便正常了,类的结构示意图便如下。

虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示,正是如上图所示。

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

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

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

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

3.笔试,面试中常考的C++虚拟继承的知识点

第一种情况:         第二种情况:          第三种情况            第四种情况: class a           class a              class a              class a {              {                {                 {     virtual void func();      virtual void func();       virtual void func();        virtual void func(); };              };                  char x;              char x; class b:public virtual a   class b :public a           };                }; {              {                class b:public virtual a      class b:public a     virtual void foo();        virtual void foo();     {                 { };              };                  virtual void foo();        virtual void foo();                                };                };

如果对这四种情况分别求sizeof(a),  sizeof(b)。结果是什么样的呢?下面是输出结果:(在vc6.0中运行) 第一种:4,12 第二种:4,4 第三种:8,16 第四种:8,8

详细分析可参考:http://blog.csdn.net/wangqiulin123456/article/details/8059536

想想这是为什么呢?

因为每个存在虚函数的类都要有一个4字节的指针指向自己的虚函数表,所以每种情况的类a所占的字节数应该是没有什么问题 的,那么类b的字节数怎么算呢?看“第一种”和“第三种”情况采用的是虚继承,那么这时候就要有这样的一个指针vptr_b_a,这个指针叫虚类指针,也 是四个字节;还要包括类a的字节数,所以类b的字节数就求出来了。而“第二种”和“第四种”情况则不包括vptr_b_a这个指针,这回应该木有问题了 吧。

1 class a
2 {
3     virtual void func();
4 };
5 
6 class b:public a
7 {
8     void foo();
9 };

此时:sizeof(a) = 4 , sizeof(b) = 4

1 class a
2 {
3     virtual void func();
4 };
5 
6 class b:public  a
7 {
8  virtual void foo();
9 };

奇怪的是,此时:sizeof(a) = 4 , sizeof(b) = 4。 尽管class b中在voif foo()前加了virtual,但结果却相同。

1 class a
2 {
3     virtual void func();
4 };
5 
6 class b:public virtual a
7 {
8     void foo();
9 };

此时:sizeof(a) = 4 , sizeof(b) = 8

1 class a
2 {
3    void func();
4 };
5 
6 class b:public a
7 {
8     virtual void foo();
9 };

此时:sizeof(a) = 1 , sizeof(b) = 4

1 class a
2 {
3    void func();
4 };
5 
6 class b:public a
7 {
8     void foo();
9 };

此时:sizeof(a) = 1 , sizeof(b) = 1

如下例:

 1 class A
 2 {
 3 };
 4 class A2
 5 {
 6 };
 7 class B : public A
 8 {
 9 };
10 class C : public virtual B
11 {
12 };
13 class D : public A , public A2
14 {
15 };

以上答案分别是1 , 1 , 4 , 1. 这说明:空类所占空间为1单一继承的空类空间也为1多重继承的空类空间还是1.但是虚继承涉及到虚表(虚指针),所以sizeof(C)的大小为4

我相信经过上面的分析和对比,以后看到这类问题不会再疑惑,会有一种“水落石出”的感觉。

关于字节的求取更详细的总结见:http://www.cnblogs.com/heyonggang/p/3253036.html

http://www.cnblogs.com/heyonggang/archive/2012/12/11/2812304.html

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

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

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

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

4.4 三种情况怎么执行:

4.4.1 重载:看参数。

4.4.2 隐藏:用什么就调用什么。

4.4.3 覆盖:调用派生类。

5.C++子类继承父类后子类的大小

 1 #include <iostream>
 2 using namespace std;
 3 class  A 
 4 {
 5 private:
 6  int a;
 7 };
 8 
 9 class B:public  A
10 {
11 private:
12  int b;
13 };
14 
15 int main()
16 {
17  cout<<sizeof(A)<<endl;
18  cout<<sizeof(B)<<endl;
19  return 0;
20 }

刚开始我一想子类继承父类不会继承父类的私有变量,如此我认为结果为4,4(错误)。而事实上结果是4,8。也就是说子类把父类的私有变量也继承下来了,但是却无法访问,对于我这种菜鸟来说一下子没法转个弯来,后来看看资料焕然大悟,子类虽然无法直接访问父类的私有变量,但是子类继承的父类的函数却可以访问,不然的话如果只继承函数而不继承变量,哪么父类的函数岂不成了无米之炊了。所以必须把父类的所有变量都继承下来,这样既能保护父类的变量也能使用父类的函数。

6.C++虚拟继承的实际大小

输出下面class的大小:

[cpp] view plaincopyprint?

  1. class X{};  
  2. class Y : public virtual X{};  
  3. class Z : public virtual X{};  
  4. class A : public Y, public Z{};  

      继承关系如下图:

   这是可能大家就会觉得他们的大小都应该是0,因为他们中没有任何一个有明显的数据,只表示了继承关系。但是至少也认为class x应该是0吧,他什么都没有。结果却让你想不到,我在vs2010环境下测试的大小是:(不同编译器可能这个大小是不一样)

[cpp] view plaincopyprint?

  1. cout<<"sizeof X: " <<sizeof X<<endl  
  2.     <<"sizeof Y: " <<sizeof Y<<endl  
  3.     <<"sizeof Z: " <<sizeof Z<<endl  
  4.     <<"sizeof A: " <<sizeof A<<endl;  

      很奇怪吧,为什么是这个结果呢。一个空的class事实上并不是空,它有一个隐藏的1 byte,这个是编译器安插进去的char,这样就可以保证定义的对象在内存中的大小是独一无二的,这个地方你可以自己测试下,比如:

[cpp] view plaincopyprint?

  1. X xa,xb;  
  2. if (&xa == &xb)  
  3.     cout<<"is equal"<<endl;  
  4. else
  5.     cout<<"not equal"<<endl;  

     但是让人搞不懂的是Y、Z的大小。主要大小受三个因素的影响:

  • 语言本身所造成的额外负担,当语言支持虚基类的时候,就导致一个额外的负担,这个一般都是一个虚表指针。里面存储的就是虚基类子对象的地址,就是偏移量。
  • 编 译器对于特殊情况所提供的优化处理,因为class X有1 byte的大小,这样就出现在了class Y和class Z身上。这个主要视编译器而定,比如某些存在这个1byte但是有些编译器就将他忽略了(因为已经用虚指针了所以这个1byte就可以不用作为内存中的一 个定位)。
  • Alignment的限制,就是所谓的对齐操作,比如你现在占用5bytes编译器为了更有效率地在内存中存取就将其对齐为8byte。

      下面说明在vs2010中的模型,因为有了虚指针后所以1byte就不用了,所以class Y和class Z的大小就是4bytes,如下图:

      现在你觉得class A的大小应该是多少呢?一个虚基类子对象只会在派生类中存在一份实体,不管他在继承体系中出现多少次,所以公用一个1byte的classX实体,再加上 class Y和class Z这样就有9bytes,如果有对齐的话就是12bytes但是vs2010中省略了那1byte所以就不存在对齐就直接是8bytes。谜底终于揭开 了!!!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员互动联盟

【专业知识】C/C++指针三

今天我们主要介绍函数指针、函数的指针参数以及返回指针的函数 A) 函数指针   C++规定,一个函数的地址就是这个函数的名字。我们需要指出的就是一个指针需要指定...

29070
来自专栏测试开发架构之路

C++之类和对象的特性

简介:C++并不是一个纯粹的面向对象的语言,而是一种基于过程和面向对象的混合型的语言。 凡是以类对象为基本构成单位的程序称为基于对象的程序,再加上抽象、封装、...

35860
来自专栏chafezhou

小说python中的迭代器(Iterator)

9920
来自专栏闪电gogogo的专栏

Python入门学习(一)

看完了莫烦Python的视频,对于Python有了一点感觉,接下来打算把小甲鱼的视频啃完,附上学习网址:http://blog.fishc.com/catego...

46280
来自专栏MasiMaro 的技术博文

C/C++中整数与浮点数在内存中的表示方式

在C/C++中数字类型主要有整数与浮点数两种类型,在32位机器中整型占4字节,浮点数分为float,double两种类型,其中float占4字节,而double...

13930
来自专栏编程

给初学者:JavaScript 中数组操作注意点

作者:CarterLi https://segmentfault.com/a/1190000012463583 不要用 for_in 遍历数组 这是 JavaS...

19460
来自专栏Vamei实验室

Python标准库13 循环器 (itertools)

在循环对象和函数对象中,我们了解了循环器(iterator)的功能。循环器是对象的容器,包含有多个对象。通过调用循环器的next()方法 (__next__()...

20980
来自专栏HTML5学堂

switch语句以及与if的比较

HTML5学堂:JS的三大语句类型当中,有一种分支/选择性语句——switch。我们常说switch可以适当的和if配合使用,那么switch语句到底怎么书写,...

38870
来自专栏Java帮帮-微信公众号-技术文章全总结

Java面试系列12

一、排序都有哪几种方法?请列举。用JAVA实现一个快速排序。 排序的方法有: 插入排序(直接插入排序、希尔排序), 交换排序(冒泡排序、快速排序), 选择排序...

32960
来自专栏灯塔大数据

技术 | Python从零开始系列连载(十二)

导读 为了解答大家初学Python时遇到各种常见问题,小灯塔特地整理了一系列从零开始的入门到熟练的系列连载,每周五准时推出,欢迎大家学积极学习转载~ 上一期学习...

409150

扫码关注云+社区

领取腾讯云代金券