目录
前言
概念定义
基类与派生类对象转换
作用域
派生类的默认成员函数
与友元&&与静态成员
菱形继承及菱形虚拟继承
多继承
菱形继承
虚拟继承
1.介绍
2.原理
继承总结
后记
封装、继承、多态作为c++的三大特性,在学完封装的有关内容之后,今天来学习学习继承相关内容,涉及到作用域、派生类的默认成员函数、与友元关系、与静态成员关系及菱形继承等,这些内容也是在面试过程中的高频问点,将相关内容一学习,加上左后的总结归纳,继承的相关知识点就不成问题,快往下看看吧。
C++中的继承是指派生类继承基类的特性和功能。派生类可以访问基类的非私有成员变量和成员函数,并且可以在派生类中添加自己的成员变量和成员函数,从而扩展基类的功能,比如对于下面的例子,Person类是一个人的类型,基本属性有姓名、年龄等,但这个人可以具体到是个学生(也可以是老师,老板等),除了人的特性以外,还应该具有其他属性,比如学号等,此时这个学生类不仅有人类型的成员,还有属于一个学生独有的成员。
继承可以分为公有继承、私有继承和保护继承三种形式,它们的访问权限限定了派生类对基类成员的访问情况,具体如下图所示。总的来说,继承是C++中的一个重要概念,它可以提高代码的复用性和可维护性,减少代码冗余。
注意:
①基类的private成员在派生类中无论以什么方式继承都是不可见的,不可见就是类内外都不可访问;
②在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,所以,想可以在类外访问就基类成员写成public,不想可以类外访问,基类成员就写protected;
③在基类(或者单独的一个类)中,protected与private并无区别,都是类内可访问,类外不可访问,区别在继承中的子类中才会体现,比如对于public继承方式,protected成员还是protected,但private成员变成不可见。
eg:
class People
{
protected:
string _name;
int _age;
};
class Student : public People
{
protected:
int _stuid;
};
前提在public继承方式下,派生类的对象 / 指针 / 引用可以赋值给基类的对象 / 指针 / 引用,叫切片或者切割,意思就是把派生类中父类那部分切割下来赋值过去,如下图一。
注意:反之不可以,即基类对象不能赋值给派生类对象,如下图二。
eg:
在继承体系中,基类和派生类都有独立的作用域,所以可以有同名成员;若在子类和父类中有同名成员,子类将屏蔽父类中的同名成员的直接访问,这种情况叫隐藏(也叫重定义),但在子类成员函数中,可以通过使用【基类::基类成员】显式访问,不指明类域的默认是子类的成员。
注意:
①如果是成员函数的隐藏,只需要函数名相同就构成隐藏;
②在实际中,继承体系里面最好不要定义同名的成员;
③在继承体系中,函数名相同不可能是重载关系,因为它们不在同一作用域。
eg:
1)构造函数
构造函数一般不用自动生成的,而是用自己显式实现的,因为可以通过初始化列表初始化成员,所以必须知道,派生类对象初始化先调用基类构造函数初始化基类的那一部分成员,再调派生类构造函数初始化剩下的成员,如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
如下代码,基类的构造函数正常像以往一样实现,对于派生类,一方面可以传入想要的基类和子类的成员的值,通过初始化列表初始化(注意初始化基类成员的写法),另一方面也可以只传入自己的成员的值在初始化列表初始化,而基类的成员就会调用基类的默认构造函数初始化。
代码:
//基类
class People
{
People(string name = "未知", int age = 0)
:_name(name)
, _age(age)
{
}
protected:
string _name;
int _age;
};
//派生类
class Student : public People
{
public:
//Student(int stuid = 0)
Student(string name = "未知", int age = 0, int stuid = 0)
:People(name, age)
,_stuid(stuid)
{
}
}
2)拷贝构造函数&&赋值操作符重载
派生类对象的拷贝构造函数必须调用基类的拷贝构造函数完成基类成员的拷贝,即通过切片的方式将基类部分传给目标对象的基类部分,再用初始化列表初始化派生类部分成员。
而赋值操作符重载的实现也是一样,只是不能通过初始化列表,必须在函数体内赋值,先调用基类的赋值操作符重载函数赋值基类部分的成员,再赋值剩下派生类部分成员。
代码:
//显式实现拷贝构造(不写也可以,有申请的空间时需要自己写,因为需要深拷贝)
Student(const Student& s)
:People(s) //切片
, _stuid(s._stuid)
{
}
//显式实现赋值操作符重载(不写也可以)
Student& operator=(const Student& s)
{
if (this != &s)
{
People::operator=(s); //构成隐藏&&切片
_stuid = s._stuid;
}
return *this;
}
3)析构函数
派生类对象析构清理先调用派生类的析构函数再调基类的析构函数。所以,应该先释放派生类中申请空间的成员(如果有的话),再调用基类的析构函数People::~People()。
但这里为什么~Student()和~People()不同名不构成隐藏,还要加上People::?
因为后面多态的需要,父子类的析构函数名会被编译器统一为destructor(),导致同名,所以子类的析构函数和父类的析构函数构成隐藏。
但为什么又注释掉了People::~People(),即不需要调用父类的析构函数?
因为在每个子类析构函数后面编译器会自动调用父类析构函数,保证“先构造的后析构”的顺序(即先构造的父类,再构造的子类,所以要先析构子类,再析构父类),不然代码实现会容易搞反顺序,所以不需要显示调用父类析构函数。
//显式实现析构函数(不写也可以)
~Student()
{
//People::~People();
}
1)与友元
友元关系不能继承,即作为基类的友元,可以访问基类的私有和保护成员,但并不能访问子类的私有和保护成员。
2)与静态成员
当基类中定义了一个静态成员,则整个继承体系中都只有这一份,即无论派生出多少个子类,都只有这一个静态成员。
eg1):
eg2):
按照以往写的,一个子类只有一个直接父类时称这个继承关系为单继承(如图一),而一个子类有两个或以上直接父类时称这个继承关系为多继承(如图二)。
菱形继承是多继承的一种特殊情况,是一种对象模型中的问题,指的是当一个类同时继承自两个不同的类,而这两个类又各自继承自同一个父类时,就会形成一个类似于菱形的继承结构,如图。
那菱形继承存在什么问题呢?从上图可以看出,D类型的对象中会有两份A的成员,存在数据冗余,同时又存在二义性问题。
而二义性问题很好解决,只需要去指定访问哪个类的成员即可,比如
但数据冗余问题又如何解决呢?所以这个方法并不完善,有无一个方法直接一下解决这两个问题?下面就要引出虚拟继承的概念了。
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在B和C继承A时使用虚拟继承,即在中间两个类实现上加上virtual关键字,如下图。值得注意的是,虚拟继承不要在其他地方去使用,目前只用在解决菱形继承上。
先来看看改为虚拟继承前后的D的内存对象成员模型,如下图。
改为虚拟继承前,B、C拥有各自的A成员,改为虚拟继承后,B、C类中除了存储自己的成员之外,还会有一个虚基表指针指向一个虚基表,这个虚基表中,前四个字节存储的是0,下面四个字节存储的就是A相对于自己的一个偏移量(字节为单位),根据偏移量就能算出A类型的位置,比如B的虚基表中偏移量为20,可以知道0x012FFD08+20=0x012FFD1C为A的位置,进而得到A类成员。值得注意的是,偏移量找到的并不是A的某个成员,而是顺着地址找到整个A类成员。
从多继承可以看出,继承是C++一个比较复杂的知识点,在其之后的语言都舍弃了这个语法,虽复杂,但场景适合的时候也是可以用的,这里说一下继承和组合之间的区别与联系。
public继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象;组合是一种has-a的关系,即每个B对象中都有一个A对象成员,如下图。
在一个场景中,当两个关系都可以使用时,优先使用对象组合,而不是类继承。而且实际中,尽量多去用组合,因为组合的耦合度低,代码维护性好。不过继承用处也是很多的,有些关系就适合继承那就用继承,另外在实现多态中,也必须要继承。
继承中存在不少的面试高频考点,但也存在许多缺陷或者不必要有的东西,比如多继承、菱形继承等,使得c++变得很复杂,但c++毕竟是第一个“吃螃蟹的人”,这也造就了c++的经典之处,这些缺陷在后来的众多语言中都被舍弃。既然我们选择学习c++,那么我们不能区别对待,只要面试中会问到的东西我们就要去了解,而且继承也是后面多态的前提基础,以上就是继承涉及到的相关知识点,记得反复观看,加油,拜拜!