zoukankan      html  css  js  c++  java
  • C++之继承(二)

    C++之继承(二)

    一、多继承

    多继承是指一个子类继承多个父类。多继承对父类的个数没有限制,继承方式可以是公共继承、保护继承和私有继承,
    不写继承方式,默认是private继承。

    #include <iostream>
    
    using namespace std;
    
    class B1{
    public:
        B1(){cout<<"B1
    ";}
    };
    class B2{
    public:
        B2(){cout<<"B2
    ";}
    };
    class C:public B2,public B1{    //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
    };
    int main()
    {
        C c;
        return 0;
    }
    
    • 符号二义性问题
      使用多重继承, 一个不小心就可能因为符号二义性问题而导致编译通不过。最简单的例子,在上面的基类B1和B2中若存在相同的符号,那么在派生类C中或使用C的对象时,若使用这个符号时,就会使编译器搞不清写代码的人是想调用B1中的那个符号还是B2中的那个符号。当然我们可以通过显示指出要调用的是那个类中的符号来解决这个问题,而有时也可以通过在派生类C中重新定义这个符号以覆盖基类中的符号版本,从而使编译器能够正常工作。至于到底使用哪种解决办法,就得具体情况具体分析了。

    //符号二义性问题的举例:编译报错!!!!!!
    #include <iostream>
    
    using namespace std;
    
    class B1{
    public:
        B1(){cout<<"B1
    ";b=1;}
    protected:
        int b;
    };
    class B2{
    public:
        B2(){cout<<"B2
    ";b=2;}
    protected:
        int b;
    };
    class C:public B2,public B1{ //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
    public:
        void Print(){cout<<b<<endl;}
    };
    int main()
    {
        C c;
        c.Print();
        return 0;
    }
    
    
    // 修改方法!
    class C:public B2,public B1{    //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
    public:
        C(){cout<<"C
    ";b=3;}
        void Print(){
            cout<<"B1::b = "<<B1::b<<endl;
            cout<<"B2::b = "<<B2::b<<endl;  //第一种解决办法
            cout<<"C::b = "<<b<<endl;   //第二种解决办法
        }
    protected:
        int b;
    };
    

    二、重复继承

    某个基类被重复继承,间接基类在子类中会有多个副本

    2.1、版本一

    //多个副本
    #include <iostream>
    using namespace std;
    class A{
    public:
        A(int a):m_a(a){};
    protected:
        int m_a;
    };
    class B1:public A{
    public:
        B1(int a):A(a){cout<<"B1
    ";}
    protected:
    };
    class B2:public A{
    public:
        B2(int a):A(a){cout<<"B2
    ";}
    protected:
    };
    class C:public B2,public B1{    //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
    public:
        C(int a1,int a2):B2(a2),B1(a1){cout<<"C
    ";}
        void Print(){
            cout<<"B1::m_a = "<<B1::m_a<<endl;
            cout<<"B2::m_a = "<<B2::m_a<<endl;
            // cout<<"m_a = "<<m_a<<endl;   直接这样调用,编译时会报与上面类似的二义性错误。
        }
    protected:
    };
    int main()
    {
        C c(1,2);
        c.Print();
        return 0;
    }
    

    //1个副本
    #include <iostream>
    
    using namespace std;
    class A{
    public:
        A(){cout<<"无参构造A"<<endl;};
        A(int a):m_a(a){cout<<"有参构造A"<<endl;};
    protected:
        int m_a;
    };
    class B1:virtual public A{  //使用virtural关键字实现虚继承
    public:
        B1(){cout<<"B1
    ";};    //不是必须的
    protected:
    };
    class B2:virtual public A{  //使用virtural关键字实现虚继承
    public:
        B2(){cout<<"B2
    ";};    //也不是必须的
    protected:
    };
    class C:public B2,public B1{    //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
    public:
        C(int a):A(a){cout<<"C
    ";}
        void Print(){
            cout<<"B1::m_a = "<<B1::m_a<<endl;
            cout<<"B2::m_a = "<<B2::m_a<<endl;
            cout<<"m_a = "<<m_a<<endl;
        }
    protected:
    };
    int main()
    {
        C c(3);
        c.Print();
        return 0;
    }
    

    • 虚基类

    1、虚基类是在多重继承中,被虚继承的祖父类,比如上面的类A,抽象基类是在类的定义中,含有纯虚成员函数(只有虚函数声明,没有函数体)。


    2、抽象基类是不能被实例化的,而虚基类理论上一般是可以实例化。


    • 虚基类的初始化

    1、其实从上面例子中的一系列构造函数中,不难看出,这一系列构造函数确实比较奇怪。首先,虚基类A需要定义带一个参数的构造函数来初始化成员变量m_a,这在很多时候是里所当然的;


    2、然后在B1和B2中,根据一般单继承的用法来说,这两个类中都得定义一个带一个参数的构造函数,并在初始化列表中调用A的单参构造函数,然而这里并没有这么做,这是因为我们在B1和B2中不需要做额外的初始化操作;所以,很显然,m_a的初始化工作只能且必须交给类C来完成了,所以在类C中定义了一个单参构造函数,且在其初始化列表中直接(跨过B1和B2)调用类A的构造函数了。(在单继承中,在派生类构造函数的初始化列表中只需调用直接基类的相应构造函数,而不需要跨越式地调用祖宗类的构造函数。)


    3、事实上,在上面的例子中,我们还为类B1、B2和类A分别定义了无参构造函数。其实B1和B2是不需要显示的定义这个无参构造函数,因为编译器会为我们生成一个默认的无参构造函数。而类A必须显式的定义一个无参构造函数,


    4、客观原因是,因为我们已经定义了一个单参构造函数,所以编译器不会再为我们生成默认的无参构造函数了。


    5、主观原因是,虽然在类C中没有显式地来初始化B1和B2,但毕竟类C是派生自类B1和B2,所以在构造C的对象时,必然也要初始化其中B1和B2那部分,这里当然调用的是B1和B2的无参构造函数了,而B1和B2是派生自类A的,类B1和B2中只有无参构造函数(不考虑默认的拷贝构造函数),所以初始化B1或者B2的对象时,就必须调用类A的无参构造函数(当然m_a就得不到初始值了)。所以,综上,在类A中必须显式的定义一个无参构造函数,否则编译器就不干了(至少GCC是这样)。可事实上又是,我们再构造类C的对象时,调用完类B1和B2的无参构造函数后,并没有看到调用类A的无参构造函数。这也好理解,根据运行结果可以看到,由于在类C的初始化列表,最先调用的是A的单参构造函数,所以很早就对A那部分进行了初始化,那么在初始化完B1和B2后,显然没必要对A那部分再次进行初始化,否则成什么样子,结果可以预料吗?


    6、重复继承,是多么奇葩!多么复杂!多么容易出错的一个初始化过程!


    假若B1和B2也有自己的初始化工作要做,切都做了对虚基类A的初始化工作,会怎样呢?看代码

    #include <iostream>
    
    using namespace std;
    class A{
    public:
        A(){cout<<"无参构造A"<<endl;};
        A(int a):m_a(a){cout<<"有参构造A"<<endl;};
    protected:
        int m_a;
    };
    class B1:virtual public A{  //使用virtural关键字实现虚继承
    public:
        B1(){cout<<"B1
    ";};
        B1(int a):A(a){cout<<"有参构造B1
    ";}
    protected:
    };
    class B2:virtual public A{  //使用virtural关键字实现虚继承
    public:
        B2(){cout<<"B2
    ";};
        B2(int a):A(a){cout<<"有参构造B2
    ";}
    protected:
    };
    class C:public B2,public B1{    //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
    public:
        C(int a):A(a){cout<<"C
    ";}
        C(int a,int ba2,int ba1):A(a),B2(ba2),B1(ba1){cout<<"三参构造C
    ";}
        void Print(){
            cout<<"B1::m_a = "<<B1::m_a<<endl;
            cout<<"B2::m_a = "<<B2::m_a<<endl;
            cout<<"m_a = "<<m_a<<endl;
        }
    protected:
    };
    int main()
    {
        C c1(4,5,6);
        c1.Print();
        return 0;
    }
    

    1、其实从构造函数的调用过程来看,出现这个结果的原因与上面的分析是一样的,而上面定义的C的三参构造函数,以及实例对象c1时,传递的三个常量中,5和6都是没意义的,只有在初始化列表中用来初始化A的值会最终赋给m_a。这样的运行结果,于这样的初始化方法,多么不协调啊!


    2、virtual base(虚基类)的初始化责任是由继承体系中的最底层(most derived)class负责,这暗示(1)classes若派生自virtual bases 而需要初始化,必须认知其virtual bases——不论那些bases距离多远,(2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。(引自《Effective C++ 中文版》,侯捷译)


    3、另一个问题就是虚基类的初始化过程是很费时间的,所以通常是不在虚基类中定义成员变量的,只声明接口函数,这就与java中接口的用法很类似。


    4、综上种种,C++是支持多重继承的,但一定要慎用,因为很容易出现各种各样的问题。


    2.2、版本二

    重复继承是指一个派生类多次继承同一个基类,C++中允许出现重复继承

    解决继承的重复问题有两种方法:
    1、使用作用域分辨符来唯一标识并分别访问他们;
    2、将直接基类的共同基类设置为虚基类,这样从不同的路径继承过来的该类成员在内存中只拥有一个复制,这样就解决了同名成员的唯一标识问题

    //作用域分辨符来唯一标识
    #include<iostream>
    using namespace std;
    class A
    {
    public:
    	int x;
    	A(int a){x=a;}
    };
    
    class B:public A
    {
    public:
    	int y;
    	B(int a, int b):A(b){y=a;}
    
    };
    class C:public A
    {
    public:
    
    	int z;
    
    	C(int a, int b):A(b){z=a;}
    
    };
    class D:public B, public C
    {
    
    public:
    	int m;
    	D(int a, int b, int c, int d, int e):B(a,b),C(c,d){m=e;}
    	void disp()
    	{
    		cout<<"x="<<B::x<<", y="<<y<<endl;
    		cout<<"x="<<C::x<<", z="<<z<<endl;
    		cout<<"m="<<m<<endl;
    	}
    };
    
    int main()
    {
    	D d1(1,2,3,4,5);
    	d1.disp();
    	return 0;
    }
    
    //将直接基类的共同基类设置为虚基类
    #include<iostream>
    using namespace std;
    class A
    
    {
    
    public:
    
    	int x;
    
    	A(int a=0){x=a;}
    
    };
    
    class B:virtual public A//由公共基类A虚拟派生出类B
    {
    
    public:
    
    	int y;
    
    	B(int a, int b):A(b){y=a;}
    
    };
    
    class C:virtual public A//由公共基类A虚拟派生出类C
    
    {
    public:
    	int z;
    
    	C(int a, int b):A(b){z=a;}
    
    };
    class D:public B, public C//由基类B,C派生出类D
    
    {
    public:
    
    	int m;
    	D(int a, int b, int c, int d, int e):B(a, b),C(c, d){m=e;}
    	void disp(){
    		cout<<"x="<<x<<", y="<<y<<endl;
    		cout<<"x="<<x<<", z="<<z<<endl;
    		cout<<"m="<<m<<endl;
    	}
    }; 
    
    int main()
    
    {
    	D d1(1,2,3,4,5);
    	d1.disp();
    	d1.x=4;
    	d1.disp();
    	return 0;
    
    }
    

    三、多重继承

    多重继承特点总结如下


    (1)多重继承与多继承不同,当B类从A类派生,C类从B类派生,此时称为多重继承


    (1)当实例化子类时,会首先依次调用所有基类的构造函数,最后调用该子类的构造函数;销毁该子类时,则相反,先调用该子类的析构函数,再依次调用所有基类的析构函数。


    (2)无论继承的层级有多少层,只要它们保持着直接或间接的继承关系,那么子类都可以与其直接父类或间接父类构成 is a的关系,并且能够通过父类的指针对直接子类或间接子类进行相应的操作,子类对象可以给直接父类或间接父类的对象或引用赋值或初始化。

  • 相关阅读:
    HDU 2236 无题Ⅱ
    Golden Tiger Claw(二分图)
    HDU 5969 最大的位或 (思维,贪心)
    HDU 3686 Traffic Real Time Query System (图论)
    SCOI 2016 萌萌哒
    Spring Boot支持控制台Banner定制
    构建第一个Spring Boot程序
    Spring Boot重要模块
    Java fastjson JSON和String互相转换
    BCompare 4 Windows激活方法【试用期30天重置】
  • 原文地址:https://www.cnblogs.com/retry/p/9334042.html
Copyright © 2011-2022 走看看