继承

继承概念

所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类。

继承特点

  1. 子类拥有父类的所有属性和方法(除了构造函数和析构函数)。
  2. 子类可以拥有父类没有的属性和方法。
  3. 子类是一种特殊的父类,可以用子类来代替父类。
  4. 子类对象可以当做父类对象使用。

继承格式

class 派生类类名: 继承方式 基类名
{
成员变量和成员函数的声明
}
继承方式: public protected private

不同继承方式对子类访问的影响

总结

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public >protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
  6. 友元关系不能继承,也就是说基类友元不能访问子类的私有和保护变量。

基类和派生类对象赋值转换

  1. 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
  2. 基类对象不能赋值给派生类对象。(可以理解为父类的结构小于子类,子类能切割和父类相同大小的结构给父类赋值,而父类不能满足子类的大小所以无法给子类赋值)
  3. 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。
class Father
{protected :int _age; // 年龄
};
class Son : public Father
{public :int _Nu ; // 学号
};
void Test ()
{Son s1 ;// 1.子类对象可以赋值给父类对象/指针/引用Father f1 = s1;Father *f2 = &s1;Father& f3 = s1;//2.基类对象不能赋值给派生类对象s1 = f1;// 3.基类的指针可以通过强制类型转换赋值给派生类的指针Father *f2 = &s1;Son* s2 = (Son*)f2; //把父类指针强转为子类指针s2->_Nu = 10;

继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 来进行显示访问)
  3. 成员函数的隐藏,子类只需要和父类函数名相同就能构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
class Father
{public:int _num = 100;
};
class Son : public Father
{public:void p(){cout << _name << "  " << _num << endl;  //这样只会输出子类的 _num,因为变量名相同子类会屏蔽父类cout << _name << "  " << Father::_num << endl; // 想要输处父类的 _num 需要再变量前加 基类::基类}string _name = "Hi";int _num = 10;
};
int main()
{Son s;s.p();
}

派生类的默认成员函数

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。(如果我们想构建一个不能被继承的类就可以把父类的构造函数私有化)
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。

继承与静态成员

基类定义的 static 静态成员,则整个继承体系里面有且只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

class Person
{public:Person() { ++_count; }
public:static int _count; // 统计个数。
};
int Person::_count = 0;
class Student : public Person
{protected:int _stuNum;
};
class Graduate : public Student
{protected:int _GrdNum;
};
int main()
{Student s1;Student s2;Student s3;Graduate s4;cout << " 人数 :" << Person::_count << endl; // 4Student::_count = 0;cout << " 人数 :" << Person::_count << endl;  // 0return 0;
}

菱形继承

  1. 单继承:一个子类只有一个直接父类时称这个继承关系为单继承

    2.多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

    3.菱形继承:菱形继承是多继承的一种特殊情况

    4.菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。

解决方式:

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在 Student 和 Teacher 的继承 Person 时使用虚拟继承(继承方式前面加 virtual),即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

一、虚继承原理

  • 虚继承用于解决多继承条件下的菱形继承问题(数据冗余、存在二义性)。
    底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(也被称作虚基表,不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
  • 实际上,vbptr 指的是虚基类表指针(virtual base table pointer,也叫虚基表指针),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

二、虚基类的声明和语法形式:

  • class 派生类名:virtual 继承方式 基类名

三、虚基类的使用要点:

  • 一个类可以在一个类族中用作虚基类,也可以用作非虚基类。
  • 在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的对象。
  • 虚基类子对象是由最派生类(最后派生出来的类)的构造函数通过调用虚基类的构造函数进行初始化 (最派生类会先去调用虚基类的构造函数)。
  • 最派生类是指在继承类结构中建立对象时所指定的类。
  • 在派生类的构造函数的成员初始化列表中,必须列出对虚基类构造函数的调用,如果没有列出,则表示使用该虚基类的缺省构造函数。
  • 在虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中,都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。
  • 在一个成员初始化列表中,同时出现对虚基类和非虚基类构造函数的调用时,基类的构造函数先于非虚基类的构造函数执行。
  • 虚基类并不是在声明基类时声明的,而是在声明派生类是,指定继承方式时声明的。因为一个基类可以在生成一个派生类作为虚基类,而在生成另一个派生类时不作为虚基类。

四、虚基表指针的存放位置

 -  虚基表指针是存放在数据段的-  虚基表指针是放在对象的开头的

虚继承的使用例子

// 不使用虚继承
class A
{public:A(string s1){cout << s1 << endl;}};
class B : public A
{public:B(string s1, string s2):A(s1){cout << s2 << endl;}
};
class C :  public A
{public:C(string s1, string s3):A(s1){cout << s3 << endl;}
};
class D : public B, public C
{public:D(string s1, string s2, string s3, string s4) //这里就和继承顺序有关系了: C(s1, s3), B(s1, s2)    //D 是普通继承,所以D中即存在B也存在C,所以D在构造时会根据继承顺序先去调B的构造,而B会在构造自身时先去调用A的构造,C也一样,所以输出顺序为 A、B、A、C、D{cout << s4 << endl;}
};int main()
{D d("A", "B", "C", "D");return 0;
}
**************************************************
//使用虚继承
class A   //此时 A 类也被称作 虚基类
{public:A(string s1):_s1(s1){cout << s1 << endl;}string _s1;
};
class B :virtual public A //B使用了虚继承,B中包含 vbptr(A的虚基表指针)、_s1、_s2
{public:B(string s1, string s2):A(s1), _s2(s2){cout << s2 << endl;}string _s2;
};
class C : virtual public A //C使用了虚继承,C中包含 vbptr(A的虚基表指针)、_s1、_s3
{public:C(string s1, string s3):A(s1), _s3(s3){cout << s3 << endl;}string _s3;
};
class D :  public B,public C  //D是普通继承,所以D中包含了一个B(vptr(A的虚基表指针)、_s2)、C(vptr(A的虚基表指针)、_s3)、_s1 和 _s4,
{public:D(string s1, string s2, string s3, string s4) //在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员并且虚基类子对象是由最派生类(最后派生出来的类)的构造函数通过调用虚基类的构造函数进行初始化
//所以这里会先根据D中的 A(s1)去构造D中的A类对象_s1,然后会再次根据继承的顺序依次去构造B和C,因为D中的_s1独有一份(A输出什么只跟A(s1)中传入的s1相关和B、C中的第一参无关),所以B、C中不会再次去构建_s1,最终输出顺序为 A、B、C、D  :C(s4, s3), A(s1), B(s3, s2), _s4(s4){cout << s4 << endl;}
string _s4;
};int main()
{D d("A", "B", "C", "D");return 0;
}
**************************************************
//不使用虚继承派生类结构体的大小
class A
{protected:int _d;
};
class B : public A
{protected:int _d1;
};
class C : public A
{protected:int _d2;
};
class D : public B, public C
{protected:int _d3;
};
int main()
{D c;    cout << sizeof(c) << endl;   // 20,因为没使用虚继承,B、C中除了自己原有的成员变量之外还各自继承了A中的成员变量,D继承了B、C后,除了自身的成员变量外还继承了B、C的成员变量,所以它里面有 _d1,_d2,_d1,_d3,_d4
}
*************************************************
class A
{protected:int _d;
};
class B :virtual public A  // _d _d1 vbptr
{protected:int _d1;
};
class C : virtual public A // _d _d2  vbptr
{protected:int _d2;
};
class D : public B, public C // _d  (_d1 vbptr) (_d2 vbptr) _d3
{protected:int _d3;
};
int main()
{D d;  // 可以看出 d 中除了 4个 int 还有两个 vbptr(虚基表指针,一个4字节)   所以共 24个字节cout << sizeof(d) << endl; //24
}

继承和组合

  1. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  2. 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  3. 优先使用对象组合,而不是类继承 。(因为继承中一个基类的改变会影响派生类的改变(破坏了类的封装),而组合却不会,组合容错率更好)
  4. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  5. 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
  6. 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系既可以用继承也可以用组合的话,就用组合。

C++普通继承和虚继承详解相关推荐

  1. C++继承详解三 ----菱形继承、虚继承

    转载:http://blog.csdn.net/pg_dog/article/details/70175488 今天呢,我们来讲讲菱形继承与虚继承.这两者的讲解是分不开的,要想深入了解菱形继承,你是绕 ...

  2. 农夫过河算法java,Java农夫过河问题的继承与多态实现详解

    Java农夫过河问题的继承与多态实现详解 发布时间:2020-08-22 06:04:29 来源:脚本之家 阅读:61 作者:小任性嘛 题目描述: 一个农夫带着一匹狼.一只羊.一颗白菜要过河,只有一条 ...

  3. C++对象模型:单继承,多继承,虚继承

    什么是对象模型 有两个概念可以解释C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各种支持的底层实现机制. 类中成员分类 数据成员分为静态和非静态,成员函数有静态非静态以及虚函数 cla ...

  4. C++ 多继承和虚继承的内存布局

    原文链接:https://www.oschina.net/translate/cpp-virtual-inheritance 警告. 本文有点技术难度,需要读者了解C++和一些汇编语言知识. 在本文中 ...

  5. 菱形继承和虚继承、对象模型和虚基表

    1.菱形继承(钻石继承):两个子类继承同一父类,而又有子类同时继承这两个子类.例如B,C两个类同时继承A,但是又有一个D类同时继承B,C类. 2.菱形继承的对象模型 class A { public: ...

  6. C++多继承与虚继承

    目录 多继承与虚继承以及存在的问题 例子 多继承与虚继承以及存在的问题 虚继承 有了多继承,虚继承才会有意义 如果有个菱形结构的继承,爷爷类为A,然后B,C是A的派生类,最后D是B和C的派生类, 如果 ...

  7. C++继承机制(三)——多继承、菱形继承、虚继承原理

    目录: C++继承机制(一)--基本语法.三种继承方式.继承哪些数据 C++继承机制(二)--继承中的构造和析构顺序.继承同名成员的处理方式 C++继承机制(三)--多继承.菱形继承.虚继承原理 本篇 ...

  8. C++之菱形继承与虚继承(含虚函数)

    面向对象的三大特征:封装,多态,继承 前面我们已经讲了继承的一些知识点,在这基础上,我们讲的时候再涉猎一些多态的只是. 下面我们先接着上次讲有虚函数的菱形虚继承 首先什么是虚函数.? 虚函数:在类里面 ...

  9. C++57个入门知识点_50 菱形继承与虚继承(C++中语法允许多重继承造成菱形继承;会造成近亲结婚的问题;可以通过虚继承的方式解决;实际项目中不多用多重继承)

    上篇C++57个入门知识点_49 多重继承与组合(一个类同时具有多个类的属性的方法:多重继承或者组合:多重继承:一个类同时继承多个类:多重继承构造和析构的顺序与普通继承类似:组合:类中包含多个成员对象 ...

最新文章

  1. Redis 高级教程 Redis 基准(3)
  2. 【数据挖掘】神经网络 后向传播算法( 向后传播误差 | 输出层误差公式 | 隐藏层误差公式 | 单元连接权值更新公式 | 单元偏置更新公式 | 反向传播 | 损失函数 | 误差平方和 | 交叉熵 )
  3. 030_html脚本
  4. Asp.net动态加载控件的一些问题
  5. 【转载】 vs2005视频教程 之 抽象类和接口 四 [视频]
  6. 小程序开发之各种弹出框选择框汇总
  7. micropython logging文档
  8. 最长公共子序列问题解析
  9. MySQL详细学习教程(建议收藏)
  10. 图片裁剪,合成(设置透明背景)
  11. qq传离线文件提示服务器超时,QQ传文件时进度条显示不正常的解决办法
  12. win7保护眼睛的颜色设置方法
  13. 从0开始,利用docker搭建一套大数据开发环境(一)
  14. es6 混合commjs_ES6 模块化的时代真的来临了么?Using MJS
  15. Java画图板界面上的添加
  16. 结合电压采样电路介绍RC滤波电路
  17. 基频和倍频的概念_一倍频分析
  18. 外媒曝华为“达芬奇计划” 或对英伟达构成威胁
  19. Go语言系列——01-HelloWorld、02-命名规范、03-变量、04-类型、05-常量、06-函数(Function)、07-包、08-if-else语句、09-循环、10-switch语句
  20. c语言编程数字字母排列组合,用简单的排列组合解决字符排列问题 (C语言代码)...

热门文章

  1. Ubuntu16.04+Cuda8.0+Caffe+ Opencv3.2+Matlab2015b的配置、安装与编译(四)
  2. 几招教你轻松解决手机卡机反应慢问题
  3. JFinal自定义指令Directive
  4. 计算机桌面位置在哪显示器,笔记本电脑怎么截取屏幕 笔记本截图位置在哪
  5. 苹果收购LuxVue,微软将为Windows Phone提供文件管理器
  6. Mac启动任务管理器,在某一个程序崩了时kill掉【亲测可用】
  7. KDD CUP 99 数据集
  8. python财务数据分析代码_人力和财务都可以学的Python数据分析实战
  9. TFIDF | 有权重的计算文本情感得分
  10. js实现两个对象的深度合并