【C++第十五章】继承
定义🧐
继承是C++面向对象编程中的一个核心概念,它允许创建一个新类(称为派生类或子类)从已有类(称为基类或父类)中继承属性和方法。
继承的主要用途包括:
- 代码重用:通过继承可以重用基类的实现。
- 扩展与修改:派生类可以在继承的基础上添加新的属性和方法或修改基类的行为。
- 实现多态:继承是实现多态(一种动态类型系统)的基础。
子类可以含有父类的成员变量,可以调用父类成员函数,但是相反的父类无法使用子类的成员,并且友元关系无法继承。·
它的定义格式为:
class Person {//父类成员 };//继承格式 class Teacher : public Person {//子类成员 };
继承方式和访问限定符🧐
继承方式和访问限定符都有三种,public、protected以及private,其中public和private在之前就介绍过,如果是protected那么只有子类能访问,外界无法访问。子类继承方式表示继承的类是什么属性,子类继承方式不写默认为私有。
父类private成员在子类中无论以什么方式继承都是不可见的,其含义是私有成员还是被继承到子类中,但是在语法上限制了派生类对象不管在类里面还是类外面都无法访问。
继承方式大多以public为主,并且不提倡使用protected和private继承,因为继承下来的成员都只能在子类的类里面使用,实际中维护性不强。
父子类赋值兼容规则🧐
公有继承被看做相近类型,可以进行隐式类型转换。
隐式类型转换会生成临时变量,但是在公有继承下,父类与子类是is-a(你就是我)的关系,子类对象赋值给父类对象/父类指针/父类引用,我们认为是天然的,中间不产生临时对象,称为父子类赋值兼容规则(也叫切割/切片)。
父子成员同名🧐
当父类和子类都有同一个成员时,会优先使用自己的成员,如果我们要使用父类成员可以加上父类的域。由此得知,继承中同名函数会构成隐藏,不管参数和返回值,所以尽量不要使用同名函数。
子类的默认成员函数🧐
我们以下面代码为例:
class Person { public:Person(const char* name, int age):_name(name),_age(age){cout << "Person构造函数" << endl;}Person(const Person& p):_name(p._name),_age(p._age){cout << "Person拷贝构造" << endl;}Person& operator=(const Person& p){cout << "Person赋值重载" << endl;if (&p != this){_name = p._name;_age = p._age;}return *this;}void Print(){cout << _age << " " << _name << endl;}~Person(){cout << "Person析构函数" << endl;} protected:int _age = 18;string _name = "Mick"; };class Teacher : public Person { public:void Print(){cout << Person::_age << endl;} protected:int _age = 10;int _jobid = 0; };
子类不写默认构造函数,那么会去调用父类的默认构造函数。
如果要显示初始化子类,且要用父类成员时,需要把父类当成完整对象,复用父类成员完成初始化。
拷贝构造需要在子类中取到父类成员,我们对子类切片即可。
同理,赋值重载也是这种方法。
子类析构要特殊一点,因为多态的存在,析构函数名会被统一处理成destructor,所以父子类析构函数会构成隐藏,当我们调用父类析构时需要加上域。并且,构造时是先构造父类再构造子类,析构则是先子后父,原因在于父类先析构了,但子类依然能够访问父类,那么就会存在风险,所以要先释放子类,父类访问不了子类,则不存在该风险,编译器为了确保安全,所以只需要析构子类,父类会自动帮我们析构。
完整代码如下,可以自己调试学习:
#include<iostream> using namespace std;class Person { public:Person(const char* name, int age):_name(name),_age(age){cout << "Person构造函数" << endl;}Person(const Person& p):_name(p._name),_age(p._age){cout << "Person拷贝构造" << endl;}Person& operator=(const Person& p){cout << "Person赋值重载" << endl;if (&p != this){_name = p._name;_age = p._age;}return *this;}void Print(){cout << _age << " " << _name << endl;}~Person(){cout << "Person析构函数" << endl;} protected:int _age = 18;string _name = "Mick"; };class Teacher : public Person { public:Teacher(const char* name,int age,int id):Person(name,age),_age(age),_jobid(id){cout << name << " " << _age << " " << _jobid << endl;}Teacher(const Teacher& t):Person(t) //切片,_age(t._age),_jobid(t._jobid){cout << "Teacher拷贝构造" << endl;}Teacher& operator=(const Teacher& s){if (&s != this){Person::operator=(s); //这里要指定,不然会发生隐藏_jobid = s._jobid;_age = s._age;}return *this;}//由于多态原因,析构函数统一会被处理成destructor//父子类的析构函数构成隐藏//为了保证析构安全,先子后父//父类析构函数不需要显示调用,子类析构函数会自动调用父类~Teacher(){// Person::~Person();cout << "Teacher析构" << endl;}void Print(){cout << _name << " " << _age << " " << _jobid << endl;} private: protected:int _age = 10;int _jobid = 0; };int main() {Teacher t("张三",18,20023030);Teacher t1("李四", 10, 20043030);t = t1;t.Print();return 0; }
关键字final🧐
在C++11之前,我们想要一个类不能被继承,可以将构造函数私有化,而在C++11后,我们可以在父类加上final来禁止继承。
静态成员继承🧐
静态成员继承的是使用权,它存在静态区中,属于整个类。
多继承🧐
多继承格式如下:
class student {//成员 };class Teacher {//成员 };//多继承格式 class Assistant : public student, public Teacher {//子类成员 }
但是在C++中可能会出现菱形继承,assistant会拥有两份person成员。
菱形继承不仅会造成代码冗余,并且会出现二义性,编译器无法识别我们想调用哪个类的成员,必须要加上类域才能使用。
解决方法是在冗余的继承类加上virtual(虚继承)
虚拟类会多开一个空间存储偏移量表,当我们使用时,会有一个指针去指向偏移量表,计算偏移量然后找到A的地址,我们以下面这段代码为例:
class A { public:int _a; };class B : public A { public:int _b; };class C : public A { public:int _c; };class D : public B, public C { public:int _d; };int main() {D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0; }
我们在内存窗口中看一下对象d的存储情况,发现了数据冗余,A出现了两次。
当我们加上虚继承后发现最开始存储1和2的地方变成了地址,并且将A放到了最下面,这个A同时属于B和C,而B和C存储了两个指针,指向两张表,这个两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的是偏移量,可以通过偏移量找到下面的A。
组合与继承🧐
如下代码,B类和D类大小一样,但一个是继承一个是组合,区别在于继承权限更大,组合只能使用公有的成员,且在类外不能直接调用成员函数,从可维护性来看,组合更好,因为组合依赖关系不强,耦合度低,有助于保持每个类被封装,但从便利角度来看,继承更好用。
#include<iostream> using namespace std;class A { public:void func(){} protected:int _a; }; class B : public A { public:void f(){func(); //直接调用_a = 1; //可以访问_} protected:int _b; }; class C { public:void func(){} protected:int _c; }; class D { public:void f(){_cc.func(); //间接调用_c = 1; //不可以访问} protected:int _d;C _cc; }; int main() {B bb;D dd;bb.func(); //可以调用dd.func(); //无法调用return 0; }
小试牛刀🧐
题目一:
答案:Derive继承了base1和base2,由于是先继承base1,所以p3和p1恰好从同一地址开始,而base2是在base1之后继承的,所以p2地址不同,则选C。
题目二:
答案:D的构造函数会先走初始化列表,而继承顺序决定声明顺序,并且这里是虚继承(如果没有虚继承A会调用两次,A自己本身不需要走初始化列表),A只调用一次,所以选A
总结🧐
- 尽量不要设计多继承,且一定不要设计出菱形继承,这样会在复杂度和性能上存在问题。
- 多继承可以说是C++设计缺陷之一,java中就没有多继承。
- public继承是is-a的关系,组合是一种has-a的关系。
- 优先使用对象组合,而不是类继承。
- 继承允许你根据父类的实现来定义子类的实现,这种通过生成子类的复用通常被称为白箱复用。“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见,继承一定程度破坏了父类的封装,父类的改变,对子类有很大的影响。父子类依赖关系很强,耦合度高。
- 对象组合是类继承的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称为黑箱复用,因为内部细节不可见。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低,能够保证每个类的封装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
结尾👍
以上便是继承的全部内容,如果有疑问或者建议都可以私信笔者交流,大家互相学习,互相进步!🌹