前言
本文主要介绍了C++中面向对象三大特性之一的多态的相关概念,包含了单继承、多继承、菱形继承以及虚拟继承,最后比较了继承和组合两种类之间的关系。
一、继承的概念和定义
1.概念
继承机制是面向对象程序设计使代码可以复用的有效手段。它允许程序员在原有类基础上进行扩展,增添新的功能,这样产生的类,叫做派生类/子类,其中的原有类叫做基类/父类。(派生类对应基类,子类对应父类,在使用时尽量对应使用)
之前我们了解的代码复用,比如模板类,模板函数等都属于函数复用,而继承属于类设计层次的复用。
继承后父类的成员(成员变量+成员函数)都变成子类的一部分,子类复用了父类的成员。
2.定义
1.格式
2.继承关系和访问限定符
继承方式和访问限定符所使用的关键字是一样的,但他们的功能不同。
3.继承方式的变化
总结:
- 基类的private类成员在派生类中无论以何种方式继承都是不可见的。(这里的不可见是指基类的private成员还是被派生类继承,但是由于语法的限制导致派生类对象无论是在类内还是类外都无法进行访问)
- protected保护成员限定符是因为继承才出现的。protected成员不能在类外直接访问,但在派生类中可以被访问。
- 基类的成员在子类中的访问方式==Min(成员在基类的访问限定符,继承方式)。
其中,public > protected > private(权限) - 使用关键词class的类默认继承方式是private,使用关键词struct的类的默认继承方式是public。但是一般情况下显示写出继承方式比较好。
- 实际运用中大部分都是public继承,很少有protected/private继承。因为protected继承下来只能在派生类中使用,扩展性不好,不利于代码复用。
可以用下面的代码实际感受一下这三种继承方式。
class Person { public: void Print() { cout << _name << endl; } protected: string _name; // 姓名 private: int _age; // 年龄 }; //class Student : protected Person //class Student : private Person class Student : public Person { protected: int _stunum; // 学号 };
二、基类和派生类对象的赋值转换
- 派生类对象可以赋值给基类的对象/基类的指针/基类的引用(形象来说就是将派生类切片,把派生类基类的那部分切片赋值过去)。
- 基类的对象不能赋值给派生类。
class Person { protected: string _name; // 姓名 string _sex; // 性别 int _age; // 年龄 }; class Student : public Person { public: int _No; // 学号 }; void Test() { Student sobj; // 1.子类对象可以赋值给父类对象/指针/引用 Person pobj = sobj; Person* pp = &sobj; Person& rp = sobj; //2.基类对象不能赋值给派生类对象 sobj = pobj; }
三、继承中的作用域
- 继承体系中基类和派生类都有独立的作用域。
- 当派生类中定义与父类同名的成员时,派生类将屏蔽对基类该成员的直接访问,称这种情况为隐藏。而派生类中的同名成员是对基类成员的重写/重定义。
如果要在派生类中访问父类的成员,可以使用:基类::基类成员
这种方式显示访问。 - 如果是成员函数的隐藏,则只需要函数名相同即可构成隐藏。
- 注意:在实际应用中尽量避免定义同名成员。
例子:
class Person { protected: string _name = "张三"; // 姓名 int _num = 666; // 身份证号 }; class Student : public Person { public: void Print() { cout << " 姓名:" << _name << endl; cout << " 身份证号:" << Person::_num << endl; cout << " 学号:" << _num << endl; } protected: int _num = 999; // 学号 }; void Test() { Student s1; s1.Print(); }; int main() { Test(); return 0; }
class A { public: void fun() { cout << "func()" << endl; } }; class B : public A { public: void fun(int i) { A::fun(); cout << "func(int i)->" << i << endl; } }; void Test() { B b; b.fun(10); };
注意:基类和派生类的fun函数并不构成重载,因为他们在不同的作用域,他们是隐藏关系
四、派生类的默认成员函数
1.构造函数
派生类构造函数必须调用基类的构造函数初始化基类那一部分成员,如果基类没有默认构造函数,派生类就必须在初始化列表处显示的调用基类构造函数。
派生类对象初始化时,会先调用基类构造函数,再调用派生类构造函数。
2.拷贝构造
必须调用基类的拷贝构造完成基类部分的拷贝构造。
3.赋值运算符重载
必须调用基类的赋值运算符重载完成基类部分的赋值运算符重载。
4.析构函数
派生类额析构函数会在调用结束后自动调用基类的析构函数清理基类成员,确保先清理派生类的成员再清理基类的成员的析构顺序。
派生类对象析构先调用派生类析构函数再调用基类析构函数。
编译器会对析构函数的函数名进行特殊处理,即派生类和基类的析构函数名都会被处理为destructor()。因此派生类和基类的析构函数回构成隐藏。
五、友元
友元关系不能继承,即基类的友元不能访问派生类的private和protected成员
六、静态成员
基类的静态成员,在整个继承体系中都只有一个这样的成员,即无论有多少个派生类,都只有一个static实例。
七、菱形继承和菱形虚拟继承
1.单继承
一个派生类只有一个直接基类,这种称为单继承。
2.多继承
一个派生类有多个直接基类,这种情况称为多继承。
3.菱形继承
由于C++支持多继承,因此可能会出现菱形继承(一个派生类继承的多个基类拥有共同的基类)的情况。
如图,类D同时继承了类B和类C,类B和类C继承了相同的基类类A,则类A的成员在类D中会出现两份,会造成数据冗余和二义性的问题。
4.虚拟继承
为了解决菱形继承造成的数据冗余和二义性的问题,我们引入了虚拟继承。(注意虚拟继承只能用在菱形继承中)
虚拟继承解决问题的原理:
简单来说是将D类中的A类成员放到所有成员的最下面,此时这一份A同时属于B和C。
那么B和C如何找到A呢?通过B和C中的两个指针分别指向的两张表,这两个指针叫做虚基表指针,这两张表叫做虚基表。虚基表中存放着A成员偏移量。(虚基表,实际上是一个数组)通过偏移量可以找到下面的A。
八、继承的总结和反思
1.一般不要设计出多继承,否则可能会产生菱形继承,就需要使用虚拟继承,就会导致问题便得更加复杂。
2.继承和组合
- 继承是is a的关系,每一个子类对象都有是一个父类对象
- 组合是has a的关系,假如B组合了A,则每一个B类对象都有一个A类对象。
- 继承体系中,父类的实现细节对子类可见,父类与子类之间有很强的依赖关系,父类的改变会一定程度上影响子类,这种风格称为白箱复用(内部细节课件),它破坏了封装,增加了耦合度。
- 组合是继承以外的另一种复用选择,这种复用称为黑箱复用(内部细节不可见),组合类之间没有很强的依赖关系,耦合度低。使用组合有利于保护类的封装。
- 尽量多使用组合,组合的耦合度低,封装性好,代码维护性好。但是继承也是有必须使用的场景,比如:实现多态就需要继承。
- 如果类与类之间的关系既可以使用组合也可以使用继承,优先使用组合。
总结
以上就是今天要讲的内容,本文介绍了C++中继承的相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!