C++继承
继承
继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。
继承定义格式
protected/private 成员对于基类 一样的,类内可以访问,类外不可访问
protected/private 成员对于派生类 private成员不可用, protected成员类内可以访问
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
这里有个形象的说法叫切片 或者切割。
寓意把派生类中父类那部分切来赋值过去。 基类对象不能赋值给派生类对象。 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类 的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
1 class A 2 { 3 public: 4 int n; 5 }; 6 7 class B :public A 8 { 9 public: 10 //int a; 11 }; 12 13 14 int main() 15 { 16 B s; 17 A p = s; 18 A& rp = s; 19 A* rip = &s; 20 //rp rip p的地址都是相同的 21 22 return 0; 23 }
继承作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(尽量不要设计隐藏)
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
子类与父类在相同权限下,有相同的成员,子类优先调用自己的成员,而父类的同名成员被隐藏,也可以显示调用 前面加上作用域即可
函数也是同理,值不过是在对象调用时 显示调用 s.A::func()例
class A { public: void fun() { cout << "1"; } }; class B :public A { public: void fun(int i) { cout << "2"; } }; int main() { B s; s.fun();//编译报错 s.A::fun()//显示调用 //因为A的fun被隐藏,除非显示调用,不然不可能调用到A 但 //建议:尽量不要重定义同名成员、函数
派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类 中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
子类的构造函数原则:调用父类构造函数初始化继承过来的成员,自己再初始化自己的成员 析构、拷贝构造、赋值重载也类似
class Teach { public: Teach(char name = ' ', int age = 0) :name(name) ,age(age) { } ~Teach() { ; } Teach& operator=(const Teach& s) { name = s.name; age = s.age; } char name; int age; }; //先构造Teach 再构造Student //先析构Student 再析构Teach class Student :public Teach { public: Student(char name = ' ', int age = 0, int n = 0) : Teach(name,age)//调用父类构造函数初始继承过来的成员 ,n(n)//自己的成员初始化 { } ~Student()//父子析构自动为隐藏关系 //析构函数名统一会被处理成 destructor() //子类析构函数完成后自动调用父类的析构函数,所以我们不需要显示调用 { ; }//完成后 自动调用父类的析构函数 Student& operator=(const Student& s) { Teach::operator=(s);//继承过来的成员用父类的 赋值重载即可 因为这里会调用自己的= 会造成死循环 栈溢出,所以需要显示调用父类的 n = s.n; } int n; };
友元关系无法继承 父类的友元不是子类的友元
父类中定义了一个static静态的成员,继承给子类,子类不会再创建一个静态成员,而是会共享父类的静态成员
多继承和菱形继承造成的问题
会导致二义性和数据沉余
二义性
子类继承了多个父类 多个父类又继承同一个父类 那么子类会无法区分相同的成员和函数
解决方法:显示调用 加上作用域调用
数据沉余
子类继承了多个父类 多个父类又继承同一个父类 那么子类会多出父类的父类的成员和函数 导致相同的数据多出
在腰部加上 virtual(虚拟继承) 可以解决数据沉余 class student :virtual public Teach
总结
C++应当尽量避免菱形继承,菱形继承在复杂度和性能上均有问题
class A { ; }; class B :public A { ; };//继承 class C { //; }; class D { C c; };//组合
在选择时 适合继承就用继承,时候组合就用组合,两个都适合 推荐用组合
因为原则为 低耦合,高内聚。而继承是破坏了封装性,增加了耦合性,所以能用组合就用组合。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被 封装。
白箱: A类的保护公有,B都可以直接用
黑箱:C类的共有D可以直接用,但保护不行
黑箱具有低耦合性,优先使用黑箱
以上就是本篇的所有内容,如果对您有帮助,希望能获得您的赞,您的赞就是对我的最大支持!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律