继承
文章目录
- 继承
- 继承的概念
- 继承的定义
- 继承方式和访问限定符
- 继承基类成员访问方式的变化
- 基类和派生类对象赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 构造函数
- 拷贝构造
- 赋值重载
- 析构函数
- 继承和友元
- 继承和静态成员
- 多继承,菱形继承和菱形虚拟继承
- 单继承:一个子类只有一个直接父类
- 多继承:一个子类有两个或两个以上的直接父类
- 多继承的一种情况:菱形继承
- 虚拟菱形继承
- 继承与组合
继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的重要手段,它允许程序员**在保持原有类特性的基础上进行扩展,这样产生新的类,称派生类。**继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
继承的定义
实例代码,写了一个Person的类,然后一个Teacher和一个Student的类通过public继承它
class Person
{
public:
void Print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
private:
string _name = "pjl";
int _age = 18;
};
class Student:public Person
{
protected:
int studnum;//学号
};
class Teacher:public Person
{
protected:
int _teachnum;//教师学号
};
int main()
{
Student st;
Teacher te;
st.Print();
te.Print();
return 0;
}
通过调试可以看到子类Student、Teacher可以继承到父类Person的public成员,并且在对象里面有父类的private成员
那么具体的继承方式是怎么样的呢?
继承方式和访问限定符
继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
咋理解呢?
1.基类的public成员,派生类public继承那么就是public成员,protected继承就是protected成员,private继承就是private成员,以此类推。即取基类的成员访问限定符和派生类继承方式取小的那一个。
2.但是一个特殊的地方:基类的private成员,在派生类是不可见的,即基类的private成员还是被继承到了派生类对象中,但语法上限制派生类对象无论在类中还是类外都不能去访问它! 如果基类想要自己的private成员不在类外被访问,但可以在派生类中访问就定义成员为protected。这也可以看出保护成员限定符是因继承才出现的。
3.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
基类和派生类对象赋值转换
1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
可以看到这里类B的fun函数是带参的,那么调用带参的自然就是类B的,那如果直接调用不带参的呢?会不会直接是类A的呢?
可以看到报错了,类B和类A的fun函数函数名相同就构成隐藏/重定义,所以需要显示访问!
派生类的默认成员函数
构造函数
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。【等同于在初始化列表把传的参数传给基类拿去初始化】
如果没有写初始化列表,那么构造出来的对象关于基类那部分的值由基类的缺省参数决定,反之由主函数传的参数决定
我写了一个Person类和Student类,Person类是Student的父类;里面都有构造、拷贝构造、赋值重载、析构函数。
然后一个简单的调用子类。
现在我把子类构造函数的初始化列表注释掉,可以看到子类构造函数调用了父类的构造函数。
如果基类没有默认的构造函数(基类自身没有写构造函数,编译器才会产生默认构造函数),派生类构造函数的初始化列表阶段显示调用。
拷贝构造
派生类继承基类的那部分成员必须要必须调用基类的拷贝构造完成基类的拷贝初始化,其余的部分调用派生类的拷贝构造即可。
如果没有写初始化列表,那么拷贝构造出来的对象关于基类那部分的值由基类的缺省参数决定,反之由主函数传的参数决定
赋值重载
派生类继承基类那部分成员必须必须要调用基类的operator=完成基类的赋值,否则派生类会自己赋值给自己。其他部分调用派生类自身的赋值重载。
析构函数
1.派生类析构函数和基类析构函数构成隐藏(由于多态关系需求,所有析构函数都会被特殊处理成destructor函数名)
2.析构顺序为派生类先析构,基类后析构。派生类析构不需要调用基类析构函数,派生类析构完会自动调用基类析构
若在派生类显示调用基类析构函数呢?
可以看到基类析构函数被调用了两次,若基类中有使用到空间资源,那么那部分空间会被析构两次,第二次是越界访问则会报错!
正常的是不调用基类的析构函数,可以看到依次顺序是基类构造-派生类构造-派生类析构-基类析构
继承和友元
友元不能继承,即基类不能访问派生类的私有成员和保护成员
父类的友元可以访问父类的成员,也可以访问子类的公有成员
但是父类的友元不能访问子类的私有和保护成员
如果硬要访问子类的私有或保护成员需要在子类里也写上友元声明
继承和静态成员
基类定义了static静态成员,整个类的体系(包括基类和派生类)里都只存放这样的一个成员。
多继承,菱形继承和菱形虚拟继承
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或两个以上的直接父类
多继承的一种情况:菱形继承
菱形继承存在的问题:在空间上会有数据冗余;在访问方式上会存在二义性
很形象的说明:Person类中有一份string name,student类和teacher类都继承了Person类,那么各自都有一份string name。Mr.Li继承了student类和teacher类,Mr.Li类中就含有两份string name 拉
这里有一个A类,里面有一份_a,B类(里面有一份 _b)和C类(里面有一份 _c)都继承了A类,D类(里面有一份 _d )即继承了B类也继承了C类。
进行对象创建和对四个值赋值后,通过监视窗口可以看到,_a数据各有一份且是独立的。
那么解决办法有其一:显示指定访问哪一个父类的成员可以解决二义性,而数据冗余无法解决。解决办法其二就是菱形虚拟继承
虚拟菱形继承
菱形继承的成员变量只有一份且是公共的,即且以最后赋值的为准。菱形继承的子类是通过指针(虚基表指针)进入虚基表找到里面存放的偏移量,从而找到公共的成员变量
以下图为例,这里有一个A类,里面有一份_a,B类(里面有一份 _b)和C类(里面有一份 _c)都虚拟继承了A类,D类(里面有一份 _d )即继承了B类也继承了C类。
B类中有一个地址(0x00107bdc)其次是B类的成员变量,指针找到该地址即虚基表,在虚基表找到存放的偏移量(20个字节),然后在B类的这个虚基表的地址(0x004FF7C4)通过偏移量找到公共的成员变量的地址(0x004FF7C4+20=0x004FF7D8) _a(A类的成员变量)
C类也同理
菱形继承的空间是线性排列的。当A类的空间很大时,虚拟菱形继承通过指针寻找公共成员变量的方式就节省了空间。而A类的空间很小时,虚拟继承就要给虚基表开辟一部分空间,这部分空间就比原来的菱形继承开辟的空间大了
继承与组合
class X
{
int _x;
};
class Y :public X//继承
{
int _y;
};
class M
{
int _m;
};
class N//组合
{
M _mm;//包含一个M类的对象
int _n;
};
继承是子类能访问父类的公有和保护成员,耦合度高,即白盒复用。父类只要更改了公有或者保护成员 子类就会受到影响。可以理解为is-a的关系
而组合是指一个类只能访问另一个类的公有成员。耦合度较低。即黑盒复用。被包含的类改变了保护成员另一个类不会因此受到影响。可以理解为has-a的关系
然而大多数工程都要求高内聚,低耦合。但使用继承还是组合要根据具体环境选择。
对继承的介绍就到这里