美文网首页
17-多继承

17-多继承

作者: ducktobey | 来源:发表于2019-11-21 20:13 被阅读0次

    多继承

    C++允许一个类可以有多个父类(不建议使用,会增加程序的复杂度)

    关于C++ 的多继承,定义了以下几个类,Student,Worker,Undergraduate,这里可以看到,Undergraduate是继承了Student和Worker。这就是多继承

    struct Student {
        int m_score;
        void study() {
            cout << "Student::study()" << m_score << endl;
        }
    };
    
    struct Worker {
        int m_salary;
        void work() {
            cout << "Worker::work()" << m_salary << endl;
        }
    };
    
    struct Undergraduate : Student ,Worker {
        int m_grade;
        void play() {
            cout << "Undergraduate::play()" << m_grade << endl;
        }
    };
    

    由于Undergraduate是同时继承了Student和Worker,所以Student和Worker的成员变量和函数,都会被继承下来。所以可以通过下面这种方式定义对象,访问成员变量和函数

    Undergraduate ug;
    ug.m_score = 100;
    ug.m_salary = 2000;
    ug.m_grade = 4;
    ug.study();
    ug.work();
    ug.play();
    

    看到这里,发现多继承与平时的单继承,其实也没多大区别,继承一个类,就是将父类的成员变量和方法直接拿过来。所以创建出一个ug对象,会占用12个字节的内存空间,分别是m_score,m_salary,m_grade,并且顺序为父类的成员变量在内存前面,当前类在内存后面,多个父类,先继承的成员变量靠内存的前面。其内存图如下

    请注意,父类成员变量的顺序,只跟继承顺序有关,与其他因素无关。

    多继承体系下构造函数的调用

    现在,将上面代码都增加对应的构造函数

    struct Student {
        int m_score;
        Student(int score) :m_score(score) {}
        void study() {
            cout << "Student::study()" << m_score << endl;
        }
    };
    
    struct Worker {
        int m_salary;
        Worker(int salary) :m_salary(salary) {}
        void work() {
            cout << "Worker::work()" << m_salary << endl;
        }
    };
    
    struct Undergraduate : Student ,Worker {
        int m_grade;
        Undergraduate(int score , int salary ,int grade) :Student(score) , Worker(salary) ,m_grade(grade) {}
        void play() {
            cout << "Undergraduate::play()" << m_grade << endl;
        }
    };
    

    由于是多继承,因此在初始化当前子类对象是,需要初始化父类的成员变量,由于是多继承,因此需要在调用构造函数时,调用父类的构造函数,初始化父类。

    多继承-虚函数

    如果子类继承的多个父类都有虚函数,那么子类对象就会产生对应的多张虚表。

    class Student{
    public:
        virtual void study() {
            cout << "Student::study()" << endl;
        }
    };
    
    class Worker {
    public:
        virtual void work() {
            cout << "Worker::work()" << endl;
        }
    };
    
    class Undergraduate : Student, Worker {
        void play() {
            cout << "Undergraduate::play()" << endl;
        }
    
        void study() {
            cout << "Undergraduate::study()" << endl;
        }
    
        void work() {
            cout << "Undergraduate::work()" << endl;
        }
    };
    

    上面代码中,父类均有虚函数,因此创建子类对象ug,在子类对象中,就会对应生成多张虚表。

    同名函数

    如果多个父类中都有一个相同的函数。例如下面的代码

    class Student {
    public:
        void eat() {
            cout << "Student::eat()" << endl;
        }
    };
    
    class Worker {
    public:
        void eat() {
            cout << "Worker::eat()" << endl;
        }
    };
    
    class Undergraduate : public Student, public Worker {
    public:
        void eat() {
            cout << "Undergraduate::eat()" << endl;
        }
    };
    

    在这种情况下,如果创建Undergraduate对象后,调用eat函数,如下代码。调用的肯定是子类中的eat。

    int main() {
        Undergraduate ug;
        ug.eat();
        return 0;
    }
    

    但是如果想调用其中父类的eat的话,这需要加上作用域来进行调用。

    int main() {
        Undergraduate ug;
        ug.eat();
        ug.Student::eat();
        ug.Worker::eat();
        return 0;
    }
    

    最终,就可以成功调用到对应父类中的函数了。

    同名成员变量

    在多继承中,会出现一个很有意思的现象,多个父类中的成员变量可能重名。并且子类也可能会与子类中的成员变量重名。如下列情况

    class Student {
    public:
        int m_age;
    };
    
    class Worker {
    public:
        int m_age;
    };
    
    class Undergraduate : public Student, public Worker {
    public:
        int m_age;
    };
    

    这种代码,编译是不会报错的,在C++里面,允许成员变量重名。但是很多编程语言是不允许这样做的,例如Java,Objective-C,所以在上面这种情况下,如果创建出一个Undergraduate对象的话,也是占用12个字节的内存空间,会将Student的m_age继承下来,也会将Worker的m_age继承下来,最终就会有3个m_age,只不过访问的时候很有意思,与刚刚重名函数的访问方式一样。

    int main() {
        Undergraduate ug;
        ug.m_age = 10;
        ug.Student::m_age = 20;
        ug.Worker::m_age = 30;
        getchar();
        return 0;
    }
    

    对应的内存结构图为

    通过这张内存结构图,相信就可以一目了然的知道成员变量的布局了。

    菱形继承

    由于C++多继承的支持,所以在开发中可能会出现菱形继承的情况。所谓的菱形继承,就是继承的体系像菱形一样。

    对应到类中,这会出现这样的继承体系。

    对应到代码当中,如下面这种情况

    struct Person {
        int m_age;
    };
    
    struct Student : Person {
        int m_score;
    };
    
    struct Worker : Person {
        int m_salary;
    };
    
    struct Undergraduate : Student, Worker {
        int m_grade;
    };
    

    请问现在假设通过Undergraduate创建出一个对象,会占用多大的内存空间呢?从表面上看,子类继承自父类,会将父类的成员变量继承下来。所以Undergraduate继承自Student,会将Student的成员变量继承过来,继承自Worker,又会将Worker的成员变量继承过来,然后Student和Worker又继承自Person,又会将Person的成员变量继承过来,按照这种思路,Undergraduate应该有4个成员变量, 这样的话,应该是占用16个字节的存储空间。但是实际是多少呢?如果通过sizeof关键字来获取的话,得到的结果是20。

    为什么是20呢?首先,Student继承自Person,所以会将Person的成员变量继承过来,所以Student现在有2个成员变量,Worker继承自Person,所以又会将Person的成员变量继承过来,所以Worker现在有2个成员变量,所以等价于下面这种情况

    struct Student {
        int m_age;
        int m_score;
    };
    
    struct Worker {
        int m_age;
        int m_salary;
    };
    

    然后Undergraduate继承自Student,所以又等下与这种写法

    struct Undergraduate : Worker {
        int m_age;
        int m_score;
        int m_grade;
    };
    

    最后Undergraduate又继承自Worker,所以最终等价于

    struct Undergraduate {
        int m_age;
        int m_score;
        int m_age;
        int m_salary;
        int m_grade;
    };
    

    Undergraduate类中有5个int成员变量。所以会占用20个字节的存储空间。但是这种情况是又问题的,m_age这个成员变量最终是来自Person,所以m_age这个成员变量,只需要保留一份就可以了。基于这种问题的存在,所以菱形继承有以下问题。

    1. 最底下子类从基类继承的成员变量冗余,重复
    2. 最底下子类无法访问基类的成员,有二义性。

    基于菱形继承访问基类成员的二义性,因此下面这种写法会报错

    Undergraduate ug;
    ug.m_age;
    

    因为在这种情况下访问m_age,无法确定是访问Student父类中的m_age还是Worker父类中的m_age。针对这种问题,到底应该怎么解决呢?可以用虚继承的方式来解决

    虚继承

    说到虚,可能会想到虚函数。现在又将了解一个新的概念,虚继承,虚继承可以解决菱形继承带来的问题。虚继承以后,最终导致的效果就是,上面的菱形继承会共用一个m_age成员变量,不会导致成员变量的冗余和重复。Person这个类,就成为虚基类

    对应到代码则是下面这样

    struct Person {
        int m_age;
    };
    
    struct Student : virtual Person {
        int m_score;
    };
    
    struct Worker :virtual Person {
        int m_salary;
    };
    
    struct Undergraduate : Student, Worker {
        int m_grade;
    };
    

    通过虚继承的方式,就可以解决访问基类中成员变量报错的问题。但是需要注意虚继承的位置,虚继承是为了解决基类中成员变量重复的问题。所以虚继承的位置应该是Person的子类虚继承Person

    如果现在为类中的每一个成员变量都赋一个初始值,观察最终的内存图

    struct Person {
        int m_age = 1;
    };
    
    struct Student : virtual Person {
        int m_score = 2;
    };
    
    struct Worker :virtual Person {
        int m_salary = 3;
    };
    
    struct Undergraduate : Student, Worker {
        int m_grade = 4;
    };
    

    需要注意,一旦虚继承的话, 在对象的内存里面,会多4个字节用来存放虚表的地址,与虚函数的方式一样。并且虚继承的成员变量,会放到内存地址的最后面,因此如果创建一个Student对象的话,对应的内存布局是这样的

    然后虚表里面放着2个东西,第一个是虚表指针与本来起始地址的偏移量(一般是0),另外一个是虚基类第一个成员变量与本类起始地址的偏移量(这里就是m_age与本类起始地址的偏移量),所以Student对象虚表指针内存布局与Person对象关系如下

    同理,Worker创建的对象,也是一样的

    创建出一个Undergraduate对象的话,现在Undergraduate对象是同时继承自Student和Worker,如果按照正常的逻辑来讲,继承自Student,会将Student所有的成员变量放到最前面,然后接下来放Worker的成员变量,最后把自己的放最后面,这种做法是默认的做法。如果父类是虚继承的话,最终的话只会将Student父类中的虚表和Student父类中的成员变量放到内存地址的最前面,Worker也同理。接下来放的是自己的成员变量,最终才是虚基类中的成员变量。所以对应的内存布局是这样的。

    对应的虚表中记录的偏移量为

    但是通过虚继承的方式实现,发现了一个问题,最终Undergraduate对象占用的存储空间反而增大了。前面在没有使用虚继承时,占用的是20个字节的存储空间,现在占用了24个字节的存储空间。从表面看,好像并不划算。需要注意,现在这种情况只是在测试阶段,如果是在实际开发中,虚基类中一般会有多个成员变量,哪个时候就节省内存空间。

    多继承的价值

    假设现在要实现这样一个功能

    兼职中心,招聘保姆,岗位如下:

    1. 保姆:扫地,做饭
    2. 老师:踢足球,打棒球

    应聘的角色如下:

    1. 学生
    2. 上班族

    所以可以通过下面这种方式来定义两个角色,学生既可以当保姆,也可以当老师,上班族可以做老师,就可以使用下面方式来定义

    class JobBaomu {
    public:
        virtual void clean() = 0;
        virtual void cook() = 0;
    };
    
    class JobTeacher {
    public:
        virtual void playFootball() = 0;
        virtual void playBaseball() = 0;
    };
    
    class Student : public JobBaomu, public JobTeacher{
        int m_score;
    public:
        void clean() {}
        void cook() {}
    
        void playFootball() {}
        void playBaseball() {}
    };
    
    class Worker : public JobTeacher {
        int m_salary;
    public:
        void playFootball() {}
        void playBaseball() {}
    };
    

    通过这种方式,可以定义规范标准,只要能实现对应的标准,就可以继承该类,然后实现逻辑。这种定义规范的方式,类似于Java中的接口,Objective-C中的协议。

    demo下载地址

    文章完。

    相关文章

      网友评论

          本文标题:17-多继承

          本文链接:https://www.haomeiwen.com/subject/idiaictx.html