C++函数继承
引言
C++三大特征分别为封装,继承和多态,它们构成了面向对象编程的基石,它们协同工作以提升代码的模块化,可复用性和灵活性
封装:提高代码的维护性(当程序出现问题时可以准确定位)
继承:提高代码的复用性(在不做任何修改或者操作源码就能实现代码的复用)
多态:提高代码的扩展性(后期文章会介绍)
组合
C++中实现代码复用操作主要有两种,分别是组合和继承,二者各有优缺点
#include <iostream>using namespace std;class A{public:void func(){cout << "Hello world" << endl;}int m_num;};class B{public://创建A类成员A a;void func(){//此时B包含Aa.func();}};int main(){B b;b.func();return 0;}
执行结果:
上述代码就是一个典型的组合操作的实现,B包含A对象作为成员变量,这样B就可以调用A中的方法,此时修改A的内部逻辑并不会影响B(只要接口不变),操作时也更加直接,可以组合多个类,操作方法都是通过成员对象来访问,这样就在不复制代码内容的情况下使用了其他类中的功能。但这种方法每次组合一个类就要多创建一个成员,此时会增大内存占用。
继承
继承同样在代码复用方面起着重要作用
#include <iostream>using namespace std;class A{public:void print(){cout << "Hello world" << endl;}int m_num;};class B : public A{public://继承之后无需声明A类成员便可直接调用A类内的共有函数void func(){print();}};int main(){A a;B b;b.func();//继承了A类之后便可以直接使用与A相同的成员b.m_num = 10;b.print();cout << b.m_num << endl;cout << a.m_num << endl;cout << sizeof(b) << endl;return 0;}
输出结果
在这里与组合相比,继承就可以减少内存开销,继承之后B称为A的子类或派生类,A称为B的父类。派生类的实例化对象大小:父类的对象大小 + 派生类的新成员;与此同时我们还应该注意继承的空间分配问题,继承后派生类和父类并不公用内存,派生类会在继承后在自己的内存空间内开辟新的内存来存放与父类不同的成员,大致原理如下
根据程序输出结果也不难看出修改了B类成员后A类成员并没有一同发生改变
覆盖
#include <iostream>using namespace std;class A{public:void print(){cout << "Hello world" << endl;}int m_num = 12;};class B : public A{public://与父类重名的成员会在派生类中重新创建int m_num;int count;// 继承之后无需声明A类成员便可直接调用A类内的共有函数void func(){print();}//与父类重名的函数会发生覆盖void print(){cout << "Hi world" << endl;}};int main(){A a;B b;b.func();b.m_num = 10;b.print();cout << sizeof(b) << endl;cout << b.m_num << endl;cout << a.m_num << endl;return 0;}
输出结果
根据输出结果不难推理出派生类中的重名函数以及重名成员会覆盖子类中的对应函数和成员
三种继承方式
公有继承
基类的公有成员和属性成为派生类的共有;基类的被保护的属性和方法成为派生类的被保护;基类私有成员不能被继承
保护继承
基类的公有成员和属性成为派生类的私有;基类的被保护的属性和方法成为派生类的私有;基类私有成员不能被继承
私有继承
基类的公有成员和属性成为派生类的被保护的;基类的被保护的属性和方法成为派生类的被保护的;基类私有成员不能被继承
三种继承方式操作方法基本相同,只是权限控制不同,由于我们使用继承的出发点是实现代码复用,所以三种继承方式中使用最多的就是公有继承
继承后函数的执行顺序
#include <iostream>using namespace std;class A{public:A(){cout << "A" << endl;}explicit A(int num) : m_num(num){cout << "A int" << endl;}~A(){cout << "~A" << endl;}int m_num;};class B : public A{public://使用初始化列表,调用A类的带参构造函数,如果不使用初始化列表则调用无参构造函数B() : A(5){cout << "B" << endl;}~B(){cout << "~B" << endl;}int m_count;};int main(){B b;cout << b.m_num << endl;return 0;}
输出结果
由输出结果不难看出,继承之后构造函数的执行顺序为先调用基类的构造函数然后再派生类的构造函数,析构函数则相反。
同时派生类继承的属性需要基类的构造函数进行初始化;如果没有无参构造函数,那么派生类里所有的构造函数都要显示调用基类的构造函数
子对象的构造顺序
当一个类中声明而多个子对象时它们的构造函数调用顺序又是怎样的呢
#include <iostream>using namespace std;class Test{public:Test(int index) : m_index(index){cout << "Test int" << endl;}~Test(){cout << "~Test" << endl;}int m_index;};class Test1{public:Test1(int index) : m_index(index){cout << "Test1 int" << endl;}~Test1(){cout << "~Test1" << endl;}int m_index;};class B{public://通过初始化列表来初始化对象//子对象在初始化列表中的顺序B() : t1(0),t(0){cout << "B" << endl;}~B(){cout << "~B" << endl;}int m_count;//子对象的声明顺序Test t;Test1 t1;};int main(){B b;return 0;}
输出结果
根据上述代码可以推出子对象构造函数的调用顺序与子对象的声明顺序相关。先声明的对象先构造
多基类的构造顺序
#include <iostream>using namespace std;class A{public:A(int index) : m_index(index){cout << "A int" << endl;}~A(){cout << "~A" << endl;}int m_index;};class B{public:B(int index) : m_index(index){cout << "B int" << endl;}~B(){cout << "~B" << endl;}int m_index;};//继承顺序class C : public A, public B{public://通过初始化列表来初始化对象//列表顺序C() : B(0),A(0){cout << "C" << endl;}~C(){cout << "~B" << endl;}int m_count;};int main(){C c;return 0;}
输出结果
根据输出结果不难看出当继承了多个基类时基类的构造顺序由继承顺序决定
二义性问题
多继承会产生二义性问题,具体形式如下、
直接指定路径
当我们想要在D类内访问a时编译器就会面临两个选择,要么通过类B访问,要么通过类C访问,此时就会产生多继承问题
代码实现如下
#include <iostream>using namespace std;//基类Aclass A{public:A(int a) : m_a(a){cout << "A" << endl;}int m_a;};//基类B继承Aclass B : public A{public:B(int b) : m_b(b),A(1){cout << "B" << endl;}int m_b;};//基类C继承Aclass C : public A{public:C(int c) : m_c(c),A(2){cout << "C" << endl;}int m_c;};//派生类D同时继承B和Cclass D : public B,public C{public:D(int d) : m_d(d), B(2), C(3){cout << "D" << endl;}int m_d;};int main(){D d(4);//引发报错,二义性问题导致编译器我无法确定访问路径cout << d.m_a << endl;return 0;}
输出结果:
此时解决问题的方法由两种,一种是直接指明路径将输出语句内容更换为cout << d.C::m_a << endl;
,此时程序就能正常输出
这种直接指定路径的方法就可以解决二义性的问题,但是通过输出结果我们发现A被构造了两次,因此相比于这种方法我们会倾向于选择使用虚继承来解决问题
虚继承
#include <iostream>using namespace std;class A{public:A(int a) : m_a(a){cout << "A" << endl;}int m_a;};//此时B和C采用虚继承class B : virtual public A{public:B(int b) : m_b(b),A(1){cout << "B" << endl;}int m_b;};class C : virtual public A{public:C(int c) : m_c(c),A(2){cout << "C" << endl;}int m_c;};class D : public B,public C{public:D(int d) : m_d(d), B(2), C(3), A(1){cout << "D" << endl;}int m_d;};int main(){D d(4);cout << d.C::m_a << endl;return 0;}
输出结果
此时我们通过观察发现程序中A类只被构造了一次,在D类中实现的构造,同时需要注意的是当使用虚继承时B和C都要采用虚继承,如果只有一个采用了虚继承那么还是会引发报错,因为此时A被构造了两次,又产生了二义性问题
组合和继承的选择
当两者之间是has-a(例如:学生有书包,汽车有发动机)关系时通常使用组合方式,当两者关系为is-a关系(例如:老师是人,学生也是人)时使用继承方式