C++中的虚继承与虚基类

技术交流QQ群:1027579432,欢迎你的加入!

1.Cpp中的虚继承与虚基类

  • 在多继承时,很容易产生命名冲突的问题,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:


    菱形继承
  • 类A派生出类B和类C,类D继承自类B和类C,这个时候类A中的成员变量和成员函数继承到类D中变成了两份,一份来自A-->B-->D这条路径,另一份来自A-->C-->D这条路径。在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类A有一个成员变量a,那么在类D中直接访问a 就会产生歧义,编译器不知道它究竟来自A -->B-->D这条路径,还是来自A-->C-->D这条路径。下面是菱形继承的具体实现:
        #include "iostream"
    
        using namespace std;
    
        // 间接基类
        class A
        {
        protected:
            int m_a;
        };
        // 直接基类B
        class B : public A
        {
        protected:
            int m_b;
        };
    
        // 直接基类C
        class C : public A
        {
        protected:
            int m_c;
        };
    
        // 派生类D
        class D : public B, public C
        {
        public:
            // void seta(int a) { m_a = a; }   命名冲突,为了解决命名冲突,可以使用void B::seta(int a){m_a = a;}
            void setb(int b) { m_b = b; }
            void setc(int c) { m_c = c; }
    
        private:
            int m_d;
        };
    
        int main()
        {
            D d;
            return 0;
        }
    

2.虚继承

  • 为了解决多继承时的命名冲突和冗余数据问题,C++提出了虚继承,使得在派生类中只保留一份间接基类的成员。在继承方式前面加上virtual关键字就是虚继承,请看下面的例子:
        #include "iostream"
    
        using namespace std;
    
        // 间接基类
        class A
        {
        protected:
            int m_a;
        };
        // 直接基类B
        class B : virtual public A   // 加上关键字virtual!
        {
        protected:
            int m_b;
        };
    
        // 直接基类C
        class C : virtual public A
        {
        protected:
            int m_c;
        };
    
        // 派生类D
        class D : public B, public C
        {
        public:
            void seta(int a) { m_a = a; }   // 正确!
            void setb(int b) { m_b = b; }
            void setc(int c) { m_c = c; }
    
        private:
            int m_d;
        };
    
        int main()
        {
            D d;
            return 0;
        }
    
  • 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的A就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。重新梳理一下本例的继承关系,如下图所示:


    虚基类.jpg
  • 观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义D类时才出现了对虚派生的需求,但是如果B类和C类不是从A类虚派生得到的,那么D类还是会保留A类的两份成员。换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身

3.虚基类成员的可见性

  • 因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。
  • 以图2中的菱形继承为例,假设在A中定义了一个名为x的成员变量,当我们在D中直接访问x时,会有三种可能性:
    • 如果B和C中都没有x的定义,那么x将被解析为B的成员,此时不存在二义性。
    • 如果B或C其中的一个类定义了x,也不会有二义性,派生类的x比虚基类的x优先级更高。
    • 如果B和C中都定义了x,那么直接访问x将产生二义性问题。

4.虚继承时的构造函数

  • 在虚继承中,虚基类是由最终的派生类初始化的。换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的
        // 虚基类AA
        class AA{
        protected:
            int m_a;
        public:
            AA(int a);
        };
        // 类外定义虚基类AA的构造函数
        AA::AA(int a):m_a(a){}
        // 直接派生类BB
        class BB: virtual public AA{
        protected:
            int m_b;
        public:
            BB(int a, int b);
            void show();
        };
        BB::BB(int a, int b): AA(a), m_b(b){}
        void BB::show(){
        cout << "m_a = " << m_a << ",m_b = " << m_b << endl;
        }
    
        // 直接派生类CC
        class CC: virtual public AA{
        public:
            CC(int a, int c);
            void show();
        protected:
            int m_c;
        };
        CC::CC(int a, int c):AA(a), m_c(c){}
        void CC::show(){
        cout << "m_a = " << m_a << ",m_c = " << m_c << endl;
        }
    
        // 间接派生类DD
        class DD: public BB, public CC{
        protected:
            int m_d;
        public:
            DD(int a, int b, int c, int d);
            void show();
        };
        DD::DD(int a, int b, int c, int d):AA(a), BB(90, b), CC(100, c), m_d(d){}
        void DD::show(){
        cout <<"m_a = " << m_a << ",m_b = " << m_b << ",m_c = " << m_c << ",m_d = " << m_d << endl;
        }
    
        int main()
        {
            BB bb(10, 20);
            bb.show();
            CC cc(30, 40);
            cc.show();
            DD dd(50, 60, 70, 80);
            dd.show();
            return 0;
        }
    
  • 在最终派生类DD的构造函数中,除了调用BB和CC的构造函数,还调用了AA的构造函数,这说明DD不但要负责初始化直接基类BB和CC,还要负责初始化间接基类AA。而在以往的普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类,用户尝试调用间接基类的构造函数将导致错误。
  • 现在采用了虚继承,虚基类AA在最终派生类DD中只保留了一份成员变量m_a,如果由BB和CC初始化m_a,那么BB和CC在调用AA的构造函数时很有可能给出不同的实参,这个时候编译器就会犯迷糊,不知道使用哪个实参初始化m_a。为了避免出现这种矛盾的情况,C++干脆规定必须由最终的派生类DD来初始化虚基类AA,直接派生类BB和CC对AA的构造函数的调用是无效的。在代码中,调用BB的构造函数时试图将m_a初始化为90,调用CC的构造函数时试图将m_a初始化为100,但是输出结果有力地证明了这些都是无效的,m_a最终被初始化为50,这正是在DD中直接调用AA的构造函数的结果。
  • 另外,需要关注的是构造函数的执行顺序。虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355

推荐阅读更多精彩内容