继承、多继承

继承是面向对象编程的重要特性,允许在保持原有类特性的基础上扩展创建新的派生类。文章详细介绍了继承的概念、继承方式(public、protected、private)及其对基类成员访问的影响,以及基类和派生类之间的构造函数、拷贝构造函数、赋值重载和析构函数的使用规则。还讨论了多继承、菱形继承及虚拟继承的问题,以及继承与组合的区别。
摘要由CSDN通过智能技术生成

继承

image-20230308213700819

继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的重要手段,它允许程序员**在保持原有类特性的基础上进行扩展,这样产生新的类,称派生类。**继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

继承的定义

image-20230114165416378

实例代码,写了一个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成员

image-20230114170047528

image-20230114170504760

那么具体的继承方式是怎么样的呢?

继承方式和访问限定符

image-20230114171102999

image-20230114171159252

继承基类成员访问方式的变化
类成员/继承方式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,不过最好显示的写出继承方式。

image-20230114182619262

基类和派生类对象赋值转换

1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去

image-20230114190333313

image-20230114190742010

image-20230114190909622

继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

可以看到这里类B的fun函数是带参的,那么调用带参的自然就是类B的,那如果直接调用不带参的呢?会不会直接是类A的呢?

image-20230114192915528

可以看到报错了,类B和类A的fun函数函数名相同就构成隐藏/重定义,所以需要显示访问!

image-20230114192948402

image-20230114193016057

派生类的默认成员函数

构造函数

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。【等同于在初始化列表把传的参数传给基类拿去初始化】

如果没有写初始化列表,那么构造出来的对象关于基类那部分的值由基类的缺省参数决定,反之由主函数传的参数决定

我写了一个Person类和Student类,Person类是Student的父类;里面都有构造、拷贝构造、赋值重载、析构函数。

然后一个简单的调用子类。

image-20230114211841436

现在我把子类构造函数的初始化列表注释掉,可以看到子类构造函数调用了父类的构造函数。

image-20230114211645334

如果基类没有默认的构造函数(基类自身没有写构造函数,编译器才会产生默认构造函数),派生类构造函数的初始化列表阶段显示调用。

image-20230114214136942

拷贝构造

派生类继承基类的那部分成员必须要必须调用基类的拷贝构造完成基类的拷贝初始化,其余的部分调用派生类的拷贝构造即可。

如果没有写初始化列表,那么拷贝构造出来的对象关于基类那部分的值由基类的缺省参数决定,反之由主函数传的参数决定

image-20230114215129022

赋值重载

派生类继承基类那部分成员必须必须要调用基类的operator=完成基类的赋值,否则派生类会自己赋值给自己。其他部分调用派生类自身的赋值重载。

image-20230114215600171

析构函数

1.派生类析构函数和基类析构函数构成隐藏(由于多态关系需求,所有析构函数都会被特殊处理成destructor函数名)

2.析构顺序为派生类先析构,基类后析构。派生类析构不需要调用基类析构函数,派生类析构完会自动调用基类析构

若在派生类显示调用基类析构函数呢?

image-20230307162409777

可以看到基类析构函数被调用了两次,若基类中有使用到空间资源,那么那部分空间会被析构两次,第二次是越界访问则会报错!

正常的是不调用基类的析构函数,可以看到依次顺序是基类构造-派生类构造-派生类析构-基类析构

image-20230307162611706

继承和友元

友元不能继承,即基类不能访问派生类的私有成员和保护成员

父类的友元可以访问父类的成员,也可以访问子类的公有成员

image-20230307212557130

但是父类的友元不能访问子类的私有和保护成员

image-20230307212026750

如果硬要访问子类的私有或保护成员需要在子类里也写上友元声明

image-20230307212916553

继承和静态成员

基类定义了static静态成员,整个类的体系(包括基类和派生类)里都只存放这样的一个成员。

image-20230307220212661

多继承,菱形继承和菱形虚拟继承

单继承:一个子类只有一个直接父类

image-20230307221818114

多继承:一个子类有两个或两个以上的直接父类

image-20230308183742403

多继承的一种情况:菱形继承

菱形继承存在的问题:在空间上会有数据冗余;在访问方式上会存在二义性

image-20230308184245995

很形象的说明:Person类中有一份string name,student类和teacher类都继承了Person类,那么各自都有一份string name。Mr.Li继承了student类和teacher类,Mr.Li类中就含有两份string name 拉

image-20230308185347732

image-20230308185916022

这里有一个A类,里面有一份_a,B类(里面有一份 _b)和C类(里面有一份 _c)都继承了A类,D类(里面有一份 _d )即继承了B类也继承了C类。

进行对象创建和对四个值赋值后,通过监视窗口可以看到,_a数据各有一份且是独立的。

image-20230308201136174

那么解决办法有其一:显示指定访问哪一个父类的成员可以解决二义性,而数据冗余无法解决。解决办法其二就是菱形虚拟继承

image-20230308190638876

虚拟菱形继承

菱形继承的成员变量只有一份且是公共的,即且以最后赋值的为准。菱形继承的子类是通过指针(虚基表指针)进入虚基表找到里面存放的偏移量,从而找到公共的成员变量

以下图为例,这里有一个A类,里面有一份_a,B类(里面有一份 _b)和C类(里面有一份 _c)都虚拟继承了A类,D类(里面有一份 _d )即继承了B类也继承了C类。

B类中有一个地址(0x00107bdc)其次是B类的成员变量,指针找到该地址即虚基表,在虚基表找到存放的偏移量(20个字节),然后在B类的这个虚基表的地址(0x004FF7C4)通过偏移量找到公共的成员变量的地址(0x004FF7C4+20=0x004FF7D8) _a(A类的成员变量)

C类也同理

image-20230308210209710

菱形继承的空间是线性排列的。当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的关系
然而大多数工程都要求高内聚,低耦合。但使用继承还是组合要根据具体环境选择。

对继承的介绍就到这里

评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值