C++虚拟继承与虚基类

1.多重继承带来的问题

C++虚拟继承一般发生在多重继承的情况下。C++允许一个类有多个父类,这样就形成多重继承。多重继承使得派生类与基类的关系变得更为复杂,其中一个容易出现问题是某个基类沿着不同的路径被派生类继承(即形成所谓“菱形继承”),从而导致一个派生类对象中存在同一个基类对象的多个拷贝。

多重继承带来同一个基类对象在派生类对象中存在多个拷贝的问题,考察如下代码。

#include <iostream>
#include <string>
using namespace std;

//人员类
class Person{
protected:
    string IDPerson;  //身份证号
    string Name;      //姓名
public:
    Person(string s1, string s2){
        IDPerson=s1;
        Name=s2;
    }
};

//学生类
class Student:public Person{
    int No;
public:
    Student(string s1,string s2,int n):Person(s1,s2),No(n){}
};

//员工类
class Employee:public Person{
    int No;
public:
    Employee(string s1,string s2,int n):Person(s1,s2),No(n){}
};

//在职研究生类
class EGStudent:public Employee,public Student{
    int No;
public:
    EGStudent(string s1,string s2,int n): Employee(s1,s2,n),Student(s1,s2,n),No(n){}
    void show(){
        cout<<Employee::IDPerson<<","<<Employee::Name<<","<<No<<endl;
    }
};

int main(){
    EGStudent one("332422199204047275","张三",1111);
    one.show();
    cout<<"sizeof(string)="<<sizeof(string)<<endl;
    cout<<"sizeof(Person)="<<sizeof(Person)<<endl;
    cout<<"sizeof(Student)="<<sizeof(Student)<<endl;
    cout<<"sizeof(Employee)="<<sizeof(Employee)<<endl;
    cout<<"sizeof(EGStudent)="<<sizeof(EGStudent)<<endl;
    getchar();
}

程序运行结果: 332422199204047275,张三,1111 sizeof(string)=28 sizeof(Person)=56 sizeof(Student)=60 sizeof(Employee)=60 sizeof(EGStudent)=124

在这个程序中,EGStudent类有两个父类:Employee和Student,而Employee和Student都是Person类的派生类。通过观察这几个类的大小,可以发现如下等式。

sizeof(Student)= sizeof(Person)+sizeof(int) sizeof(Employee)= sizeof(Person)+sizeof(int) sizeof(EGStudent)= sizeof(Student)+ sizeof(Employee)+ sizeof(int)

也就是说,在一个EGStudent类对象中包含了两个Person类对象,一个来自Employee类对象,一个来自Student类对象。在EGStudent类的成员函数show()中,直接访问IdPerson或Name都会引发编译错误,因为编译器不知道它们指的是哪个Person对象中的成员。所以,在上面的程序中,在show()中显示的是Employee中的成员(IDPerson和Name)。实际上,在EGStudent类对象中还有来自Student类的成员(IDPerson和Name)。

2.如何在派生类中只保留一份基类的拷贝

从逻辑上说,一个在职研究生只可能有一个名字和一个身份证号码,所以在一个EGStudent类对象中有IDPerson和Name字段的两个拷贝是不合理的,只需要一个拷贝就可以了。

虚拟继承就是解决这个问题的,通过把继承关系定义为虚拟继承,在构造EGStudent类对象的时候,EGStudent类的祖先类Person的对象只会被构造一次,这样就可以避免存在多个IDPerson和Name的拷贝问题了。将以上代码修改如下。

#include <iostream>
#include <string>
using namespace std;

//人员类
class Person{
protected:
    string IDPerson;  //身份证号
    string Name;      //姓名
public:
    Person(string s1, string s2){
        IDPerson=s1;
        Name=s2;
    }

    //添加一个默认构造
    Person(){}
};

//学生类
class Student:public virtual Person{
    int No;
public:
    Student(string s1,string s2,int n):Person(s1,s2),No(n){}
};

//员工类
class Employee:public virtual Person{
    int No;
public:
    Employee(string s1,string s2,int n):Person(s1,s2),No(n){}
};

//在职研究生类
class EGStudent:public virtual Employee,public virtual Student{
    int No;
public:
    EGStudent(string s1, string s2, int n): Employee(s1,s2,n),Student(s1,s2,n),No(n){}
    void show(){
        cout<<Employee::IDPerson<<","<<Employee::Name<<","<<No<<endl;
    }
};

输出结果: ,,1111 sizeof(string)=28 sizeof(Person)=56 sizeof(Student)=64 sizeof(Employee)=64 sizeof(EGStudent)=80

考察以上程序,注意如下几点。 (1)当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类,就像Student和Employee中的申明那样。

(2)被虚拟继承的基类,叫做虚基类。虚基类实际指的是继承的方式,而非一个基类,是动词,而非名词。

(3)为了实现虚拟继承,派生类对象的大小会增加4。所以,在上面的程序中, sizeof(EGStudent)=sizeof(Employee)+sizeof(Student)-sizeof(Person)+sizoef(int)+4=80。 这个增加的4个字节,是因为当虚拟继承时,无论是单虚继承还是多虚继承,派生类需要有一个虚基类表来记录虚继承关系,所以此时子类需要多一个虚基类表指针,而且只需要一个即可。

(4)虚拟继承中,虚基类对象是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的,派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。上面的程序因Person的默认构造函数啥也没做,因此IDPerson和Name字段是空字符串。

(5)在上面的程序中,如果将类EGStudent的申明改为 class EGStudent:public Employee,public Student 那么除了sizeof(EGStudent)会变成76以外,其他的什么也不会发生。因为虚拟继承只是表名某个基类的对象在派生类对象中只被构造一次,而在本例中类Student和Employee对象在EGStudent对象中本来就不会被构造多次,所以不将它们申明虚基类也是完全可以的。


参考文献

[1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[8.3(P276-P280)]

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏北京马哥教育

从Zero到Hero,一文掌握Python关键代码

本文整体梳理了 Python 的基本语法与使用方法,并重点介绍了对机器学习十分重要且常见的语法,如基本的条件、循环语句,基本的列表和字典等数据结构,此外还介绍...

33370
来自专栏数据科学学习手札

(数据科学学习手札49)Scala中的模式匹配

  Scala中的模式匹配类似Java中的switch语句,且更加稳健,本文就将针对Scala中模式匹配的一些基本实例进行介绍:

11240
来自专栏练小习的专栏

js运算符优先级笔记

运算符的优先级决定了表达式中运算执行的先后顺序,优先级高的运算符最先被执行。 下面是一个简单的例子: 3 + 4 * 5 // 计算结果为23 乘法运算符...

24380
来自专栏成猿之路

Java面试题-基础篇一

12530
来自专栏布尔

想起温习一下JS中的this apply call arguments

很多时候讲到语言入门,大家会认为就是要了解一下语言的语法、数据类型和常用函数。这一课对于所有的计算机专业的毕业生来说都可以自学,然而在最近的实践中(带了两个实习...

230100
来自专栏lulianqi

支持各种特殊字符的 CSV 解析类 (.net 实现)(C#读写CSV文件)

csv(Comma Separated Values)逗号分隔值,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本...

27720
来自专栏Python

python2/3中 将base64数据写成图片,并将图片数据转为16进制数据的方法、bytes/string的区别

python 3中最重要的新特性可能就是将文本(text)和二进制数据做了更清晰的区分。文本总是用unicode进行编码,以str类型表示;而二进制数据以byt...

53120
来自专栏彭湖湾的编程世界

【JavaScript】 JS面向对象的模式与实践

参考书籍 《JavaScript高级语言程序设计》—— Nicholas C.Zakas 《你不知道的JavaScript》  —— KYLE SIMPSON ...

38760
来自专栏技术专栏

Scala入门与进阶(五)- 模式匹配

10210
来自专栏小白的技术客栈

Python面向对象编程-完整版

面向对象是一种编程范式。范式是指一组方法论。编程范式是一组如何组织代码的方法论。编程范式指的是软件工程中的一种方法学。

43430

扫码关注云+社区

领取腾讯云代金券