【C++笔记】C++继承
- 一、继承的概念
- 二、继承的语法和权限
- 三、父类和子类成员之间的关系
- 3.1、子类赋值给父类(切片)
- 3.2、同名成员
- 四、子类中的默认成员函数
- 4.1、构造函数
- 4.2、拷贝构造
- 4.3、析构函数
- 五、C++继承大坑之“菱形继承”
- 5.1、什么是“菱形继承”
- 5.2、解决方法
一、继承的概念
继承:
继承是面向对象编程中的一个重要概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。继承是一种代码重用的方式,它可以减少代码的重复,提高代码的可维护性和可扩展性。
继承的基本原理是子类可以使用父类的属性和方法,而不需要重新编写代码。子类可以继承父类的公共属性和方法,也可以重写父类的方法或添加新的属性和方法。这种机制使得子类可以扩展父类的功能,同时保留父类的特性。
用一个简单的例子演示一下:
这里B继承了A,B不仅可以访问自己的成员变量,也可以访问自己父类的成员变量和成员函数。
二、继承的语法和权限
继承的语法格式如下:
class 子类名字:继承方式 父类名字{};
子类继承父类的继承方法有三种:
public
protected
private
(protected修饰的成员在内里边可以使用,在类外边不可以使用)
不同的修饰方法会使得子类中的父类成员权限不同,而且父类中以不同权限修饰符修饰的成员在继承到子类后的权限也会不同。
比如最简单的以public的方式继承:
public继承方式可以访问到父类中public的成员。
而如果是父类中的private成员就不行了:
还有其他的组合如下表:
有人可能会觉得这张表好复杂,觉得要背下来的话一定很困难。
其实我们并不需要去背这张表,我们仔细观察这张表之后就会发现其中的规律。
因为最后一个行都是不可见,我们可以把最后一行特殊化处理,然后剩下的我们仔细观察后就会发现,表中的任何一个结果都可以总结为:
min(父类中的修饰符,子类的继承方式)
三、父类和子类成员之间的关系
3.1、子类赋值给父类(切片)
因为子类继承了父类的成员,所以子类也可以看作是一种特殊的父类。那么将子类对象赋值给父类对象会怎么样呢?
我们发现父类自己的成员还是可以正常访问的,可若要是想访问子类的成员就不行了:
这样想跟我们平时理解的赋值不一样啊。
其实它在底层执行了一个“切片”操作:
因为父类中没有_id,所以父类并不会接收子类的_id,父类之后接受自己有的成员的值。
所以子类自己独有的成员也就被“切”掉了。
其他的赋值方式例如引用:
引用我们可以理解为,父类的引用只引用了子类中属于父类的那一部分。
指针:
指针其实是并不存在“切片”操作的,只需要执行子类的首地址即可,因为这是Person类型的指针,而指针能访问到的范围其实是由指针类型已经决定了的。
所以父类的指针只会访问到子类中属于父类的成员。
3.2、同名成员
同名成员变量:
有时候父类和之类中会存在一些同名成员变量,比如name:
这时候编译器会议子类的成员优先。
如果真要想访问到父类的就得要加上域作用限定符,限定为访问父类的:
同名成员函数:
如果父类和子类之中存在同名函数又会怎么样呢?
这其实就构成了“隐藏”,并不是函数重载,函数重载一定要是在同一作用域,而两个类之间并不是同一个作用域,即使他们之间是继承关系。
隐藏的条件是只需要函数名相同,对参数列表和返回值都没有要求。
这时候编译器还是会优先选择子类的函数,这也称之为父类的函数被子类的函数隐藏了。
这时候如果想要调用父类的,也需要加上域作用限定符:
四、子类中的默认成员函数
4.1、构造函数
我们来看看,继承关系中的构造函数会怎么做:
如果创建一个子类对象,编译器会先调父类的构造函数再调用子类的构造函数。
这其实是编译器自动调用的,即使我们没有显示的写出子类的构造函数,编译器也会自动去调用父类的构造函数:
而如果我们想要在子类中显示的构造父类对象,就必须将父类对象当成一个整体(当成一个对象)去构造,即调用父类的构造函数:
记住一定要在初始化列表处调用,不然就会存在父类构造被调用两次的问题:
这是因为编译器默认就会在初始化列表中调用父类构造函数。
而我们不能想当然的像下面这样初始化父类:
这是规定!
4.2、拷贝构造
拷贝构造也是要先调用父类的再调用子类的:
再构造函数中我们可以直接将子类对象传递给父类的构造函数,因为父类会通过“切片”操作拿到子类中父类的部分。
4.3、析构函数
析构函数和构造函数正好相反,析构要求的是先调用子类的析构再调用父类的析构。
而如果我们在子类的构造函数中显示的调用父类的构造函数就会发生调两次析构的危险:
而析构函数调用两次是很危险的,这很有可能就会导致同样的资源被释放两次的错误。
其实这是因为父类的析构函数是编译器自动调用且是在子类析构结束后才调用的,所以也就决定了它不能像构造函数一样写在初始化列表里。
也就不能自己调用,只能由编译器调用。
五、C++继承大坑之“菱形继承”
因为C++时出现的比较早的面向对象的语言,也就没有多少其他的面向对象语言可以参考,所以许多面向对象的“坑”也就避免不了了。
其中一个坑就是今天要讲的“菱形继承”。
5.1、什么是“菱形继承”
C++是支持多继承的,也就是一个类可能会存在多个直接父类,例如下面这个例子:
他们之间的继承关系如下图所示:
而如果这些关系再复杂一点儿,就会变成“菱形继承”了:
在这样的继承状态中,处于中间的A、B两个类是没有什么问题的,问题就出在最下面的C这个类:
从代码中我们并不能看出问题出在哪里,我们得到监视窗口中才能看出:
从内存中我们可以看到c1中竟然存了两个_o(一个是A继承的,一个是B继承的),这也就是为什么报错提示访问不明确的原因。
5.2、解决方法
方法一:加类域修饰
既然两个_o一个是A继承的,一个是B继承的,那我们直接指定是哪个类的不就行了:
方法二:virtual虚继承
还有一种方法就是在菱形继承的“腰部”加上virtual继承:
简单来说就是将菱形继承中具有公共父类和公共子类的那几个类继承其公共父类的方式全都改成虚继承。
虽然说菱形继承的问题已经被解决了,但是我们最好还是不要弄出菱形继承的好,多继承是没什么问题的,但是菱形继承就是个大坑。