继承的概念及定义
继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
其实也很好理解,拿下图举例
假设现在是两个类,但我们可以发现其中大部分类容是可以一样的,我们就可以把这部分提取出来,取名新类,然后让老师和学生分别继承这个类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "knous";
int _age = 18;
};
class Student : public Person
{
protected:
int _stuid;
};
这就是一个继承,现在Student就有了Person的参数,比如一个打印函数,两个被保护起来的对象,一个_name,一个_age,我们可以按照平时调用类里面函数一样调用这个打印函数
-----
继承的定义
定义格式
继承关系与访问限定符
我们知道访问限定符有三种:
public(公开)
protected(保护)
private(私人)
同样,继承方式也有三种:
public(公开)
protected(保护)
private(私人)
继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的protected 成员 | 派生类的protected 成员 | 派生类的protected 成员 | 派生类的private 成员 |
基类的private成 员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可 |
总结:
基类的不可见不是指的没有被继承,本质是继承了,但是不让用,不管是类里面还是外面。
如果是想在类外面不让使用,类里面可以用,可以用protected继承
class默认是private,struct默认是public,跟类访问限定符那里是一样的。
一般都是public继承,protected和private继承都比较少
基类的成员在被继承的时候取权限小为准,如果是公开继承原本公开的,那不变,如果是公开保护继承,那就是保护成员
基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
怎么理解这话呢,看下面的图:
现在要将学生赋值给人,学生里面四个参数,但是人里面只有两个,那编译器在处理的时候会将学生里面多的两个不要,这就叫切片(切割),将多的部分舍弃。
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "knous";
int _age = 18; //
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
int main()
{
Student s;
Person p = s; //将s对象直接赋值给p
Person& rp = s; //将s对象取个别名叫rp
Person* ptrp = &s; //让s对象的地址给p的指针
s = p; //这种会报错,基类不能赋值给派生类
return 0;
}
继承的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
从图里面可以看到,我创建了b,然后调用B里面的fun,B又继承的A,按理来说里面应该有两个fun,但直接调用的是B里面自身的fun,假设我一定要调用A的fun呢?
指定类,像下图:
这样就可以了
派生类的默认成员函数
这里更是重量级,头疼的地方来了
派生类也是类,里面也有六个默认函数,这里就要了解一下这几个成员函数怎么生成的:
构造
初始化对象s的时候,它先调用了Person的初始化构造,然后才是自己,可以这么理解,子类继承的父类被当成了一个自定义类型,会调用自定义类型的构造函数,然后才是自己,如果没有合适的默认构造,就要在初始化列表里面显示调用:
拷贝构造
如果写出了这种代码,就要注意了,这里的问题是之前说的,operator =的运算符重载问题,因为person里面有一个,student也有一个,这里s里面有基类成员,所以必须用基类的拷贝构造,但是直接写的话就会出现死循环,因为studet里面也有一个拷贝构造,就会出现自己调用自己:
直接报错,位了解决问题,我们必须指定用的是基类的拷贝构造:
这样就没问题了。
析构
可以看到,基类先构造,但后析构,这里说一句,哪怕是手动先析构基类,但后面还是会再析构一次,因为编译器一定会保持先子后父的顺序
继承与友元
友元不继承,基类友元不能访问子类私有和保护
继承与静态成员
基类定义的static成员,则继承的整个体系里面都只会有一个这样的成员,无论派生了多少,都只有一个
复杂的菱形继承及菱形虚拟继承
继承又分为两种:
单继承:每次只继承一个
多继承:一个子类有多个父类
菱形继承:
当出现菱形继承,会发现什么、
类4里面有两个类1
因为类2里面有类1,类3里面有类1,但类4又继承了类2和类3,假设类1很小,十几个字节还好,但是如果类1很大,几万、甚至几十万字节呢?
出大问题。
这句是菱形继承的问题:形继承有数据冗余和二义性。
如何解决?
虚拟继承
这里举个例子
这就是很明显的菱形继承,Person被Student和Teacher继承,Assistant又继承了Student和Teacher
虚拟进程是这样的,在继承方式的前面多加上关键词:virtual(虚拟)
这就算虚拟继承了,那虚拟继承是如何解决数据冗余和二义性的呢?
这里就直接说结论吧,过程较为复杂。
这里看图:
假设现在是普通继承,那此时的D里面就是这样;
但如果是虚拟继承,就会是这样:
此时的B、C里面就不再是直接放A的地址,而是放一个指针,这个指针称之为虚基表指针,虚基表指针指向的这块空间称之为虚基表,虚基表里面存放的是偏移量,通过偏移量就可以找到下面的A。
---
感谢您的观看,希望对您学习继承有帮助