当前位置: 首页 > news >正文

C++之继承(二)

目录

  • C++之继承(二)
    • 一、多继承
    • 二、重复继承
    • 三、多重继承

C++之继承(二)

一、多继承

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

#include <iostream>

using namespace std;

class B1{
public:
    B1(){cout<<"B1\n";}
};
class B2{
public:
    B2(){cout<<"B2\n";}
};
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\n";b=1;}
protected:
    int b;
};
class B2{
public:
    B2(){cout<<"B2\n";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\n";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\n";}
protected:
};
class B2:public A{
public:
    B2(int a):A(a){cout<<"B2\n";}
protected:
};
class C:public B2,public B1{    //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
public:
    C(int a1,int a2):B2(a2),B1(a1){cout<<"C\n";}
    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;
}

1437274-20180719094012378-700071192.jpg

//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\n";};    //不是必须的
protected:
};
class B2:virtual public A{  //使用virtural关键字实现虚继承
public:
    B2(){cout<<"B2\n";};    //也不是必须的
protected:
};
class C:public B2,public B1{    //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
public:
    C(int a):A(a){cout<<"C\n";}
    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;
}

1437274-20180719094028348-1028675571.jpg

  • 虚基类

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\n";};
    B1(int a):A(a){cout<<"有参构造B1\n";}
protected:
};
class B2:virtual public A{  //使用virtural关键字实现虚继承
public:
    B2(){cout<<"B2\n";};
    B2(int a):A(a){cout<<"有参构造B2\n";}
protected:
};
class C:public B2,public B1{    //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
public:
    C(int a):A(a){cout<<"C\n";}
    C(int a,int ba2,int ba1):A(a),B2(ba2),B1(ba1){cout<<"三参构造C\n";}
    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;
}

1437274-20180719094044159-1209338732.jpg

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;

}

三、多重继承

1437274-20180719093940698-266329118.jpg

多重继承特点总结如下


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


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


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

转载于:https://www.cnblogs.com/retry/p/9334042.html

相关文章:

  • docker mariadb集群_Docker搭建Django+Mariadb环境
  • VueX源码分析(1)
  • python合并文件夹_Python实现合并同一个文件夹下所有PDF文件的方法示例
  • iOS 应用性能调优的25个建议和技巧
  • arm ubuntu 编译boost_BFL库的安装(适用ubuntu)
  • 宏与内联函数
  • api接口怎么对接_微服务手册:API接口9个生命节点,构建全生命周期管理
  • rsyslogd 重启_syslog/rsyslog的使用
  • 经典例题
  • paste shell 分隔符_26. Bash Shell - 文本处理:cut、paste、join
  • SpringBoot整合Mybatis-Plus
  • html typora 图片_七牛云 + Mpic图床神器 + Typora“现代”博客图片托管操作总结
  • 跟牛牛老师学习python自动化的第七天
  • 华为笔记本机械盘卡死_可运行3Dmax的笔记本电脑推荐,2020了运行3Dmax还用操心?...
  • 上下栏固定, 中间滚动的HTML模板
  • [译]如何构建服务器端web组件,为何要构建?
  • 「面试题」如何实现一个圣杯布局?
  • AzureCon上微软宣布了哪些容器相关的重磅消息
  • ECS应用管理最佳实践
  • GraphQL学习过程应该是这样的
  • interface和setter,getter
  • jdbc就是这么简单
  • MySQL QA
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • PHP 使用 Swoole - TaskWorker 实现异步操作 Mysql
  • React as a UI Runtime(五、列表)
  • win10下安装mysql5.7
  • 编写符合Python风格的对象
  • 当SetTimeout遇到了字符串
  • 分布式任务队列Celery
  • 讲清楚之javascript作用域
  • 聊一聊前端的监控
  • 手机app有了短信验证码还有没必要有图片验证码?
  • 小程序button引导用户授权
  • 摩拜创始人胡玮炜也彻底离开了,共享单车行业还有未来吗? ...
  • 如何在招聘中考核.NET架构师
  • ​猴子吃桃问题:每天都吃了前一天剩下的一半多一个。
  • # 透过事物看本质的能力怎么培养?
  • #我与Java虚拟机的故事#连载14:挑战高薪面试必看
  • (23)Linux的软硬连接
  • (C语言)strcpy与strcpy详解,与模拟实现
  • (Git) gitignore基础使用
  • (Mac上)使用Python进行matplotlib 画图时,中文显示不出来
  • (转)【Hibernate总结系列】使用举例
  • (转)ORM
  • (转)全文检索技术学习(三)——Lucene支持中文分词
  • .360、.halo勒索病毒的最新威胁:如何恢复您的数据?
  • .NET 2.0中新增的一些TryGet,TryParse等方法
  • .Net MVC + EF搭建学生管理系统
  • .NET/ASP.NETMVC 大型站点架构设计—迁移Model元数据设置项(自定义元数据提供程序)...
  • .NET/C# 推荐一个我设计的缓存类型(适合缓存反射等耗性能的操作,附用法)
  • @JoinTable会自动删除关联表的数据
  • @TableLogic注解说明,以及对增删改查的影响
  • [1]-基于图搜索的路径规划基础
  • [2018/11/18] Java数据结构(2) 简单排序 冒泡排序 选择排序 插入排序