我们知道,面向对象有三大特性:封装,继承,多态。封装前面已经说过,这篇文章主要说说继承。
文章目录
- 1.继承的概念及定义
- 1.1继承的概念
- 1.2 继承定义
- 1.2.1定义格式
- 1.2.2继承关系和访问限定符
- 2. 基类和派生类对象赋值转换
- 3.继承中的作用域
- 4.派生类的默认成员函数
- 4.1 构造
- 4.2 拷贝构造
- 4.3 赋值
- 4.4 析构
- 5. 如何设计一个不能被继承的类
1.继承的概念及定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继
承是类设计层次的复用。
举个例子:
比如说我们学校有个管理系统,管理老师和学生。老师和学生中都会记录他们的名字,电话,年龄等等,但是他们的学校id是不一样的,一个是学号,一个是工号。那么有些数据和方法每个角色都有的,就设计重复了。此时继承就是类设计层次的复用,那么如何使用,我们来看:
继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
Student和Teacher复用了Person的成员。
调用Print可以看到成员函数的复用。那么继承的语法格式是什么样的,我们来看。
1.2 继承定义
1.2.1定义格式
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。
这个继承方式又该如何来理解呢?
1.2.2继承关系和访问限定符
继承方式和访问限定符都各有3个:
所以它们就会组成9种不同的情况:
那么我们该如何去理解这些呢?
1.基类的私有成员在派生类都是不可见。基类的其它成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
举个例子:
派生类继承下来的限制是这两种中小的那个。这里派生类中的_name和_age是protected。
2.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
举个例子:
从上面两幅图中可以看出私有的和公有的是一样大的,所以私有的数据被继承下来了。
继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
3.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
可以看出派生类中能访问,外面不能访问。以前我们写一个类时,private和protected,两者其实没有什么区别,现在在继承中就能体现它们的区别了。
4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。。
5.在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
2. 基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
这里和不同类型的引用不一样,不同类型的引用会产生一个临时变量。但是这里不一样,它是语法天然支持的,没有类型转换。
子类赋值给父类的情况如下:
代码演示:
从上图可以看出,已经赋值给父类了。这里的赋值是深拷贝,不是浅拷贝。
子类地址给父类指针的情况如下:
是一个指针指向子类的一部分。
子类引用给父类的情况如下:
它是子类一部分的别名。
基类对象不能赋值给派生类对象。
强制类型转换也不行。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run Time Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下)。
3.继承中的作用域
1. 在继承体系中基类和派生类都有独立的作用域。
我们知道:同一个域中不能有相同的名字,不同的域中就可以有相同的名字。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
那么这里子类和父类都有_id,此时打印会打印谁的呢?
那么我们也可以在子类成员函数中,可以使用 基类::基类成员 显示访问。
成员函数也是同样的道理,先调用子类自己的。
如果我们想调用父类的,指定作用域就行。
3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
这两个fun函数是重载还是隐藏关系呢?答案是:隐藏关系。原因是:函数重载要求在同一作用域,而这两个函数在不同的作用域里。
4.注意在实际中在继承体系里面最好不要定义同名的成员。
4.派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
4.1 构造
举个例子:
从这个例子中,我们可以看出什么呢?
子类构造函数原则:
a、调用父类构造函数初始化继承自父类成员
所以继承父类的成员_name还是用父类的构造函数来初始化。
b、自己再初始化自己的成员 – 规则参考普通类
析构、拷贝构造、复制重载也类似
子类的本身的成员还是去调用子类的来初始化。
那我们显示的写,又是什么情况呢?
显示的去写,也没有办法去构造,只能显示的去传参数。
这样写就像使用匿名对象方式。那么这样写,它是先调用的是子类的构造还是先调用父类构造呢?
答案是:先调用父类的。原因是:初始化列表执行的顺序不是我们写的顺序,而是声明的顺序。在子类中,它是认为父类在前,子类在后。
4.2 拷贝构造
拷贝构造也是相似的道理:
父类的成员变量拷贝时,会去调用父类的拷贝构造。子类的成员变量还是调用自己的拷贝构造。这里子类调用的是自己默认的拷贝构造。那么子类的拷贝构造我们该如何去显示写呢?
父类的显示定义是这样去写的。
因为拷贝构造是引用,所以我们可以把子类直接传给父类,这样就形成了切片:
所以它会自动的找子类里面父类成员来拷贝构造。那么什么时候去显示写的,就是当我们子类需要深拷贝的时候就要显示写了。
4.3 赋值
这里赋值也是相同的道理,父类的成员去调用父类的赋值函数。子类显示写如下:
这里就是显示的去调用父类的赋值函数。然后父类的赋值函数也是引用,可以把子类切片。但是这样写会出现栈溢出,原因就是这样写它调用的是子类自己的赋值函数,它把父类的隐藏了。我们要加上类域:
这里还有一个隐藏的切片,就是this。像s1=s3中,s3转到s会被切割,而this指针也会被切割(也就是s1也会切割)。
4.4 析构
我们先把子类的析构显示的来写:
我们发现这里显示调用发生了错误。原因是:父子类的析构函数构成隐藏关系,但可能有的同学就会问了,它们的名字不是不相同吗?原因是:多态的需要,析构函数名统一会被处理成destructor()。
所以我们就这样写:
但这样还有一些情况:
我们只有s1和s2两个对象,子类调用了两次析构,但是父类调用了四次析构。这是为什么呢?
原因是:为了保证析构顺序:先子后父。子类析构函数完成后会自动调用父类析构函数,所以不需要我们显示调用。
为什么一定是先子后父呢?因为父类先构造,然后子类再构造,析构时,子类先析构,父类再析构。然后我们如果要显示的来析构,可能就会导致父类先析构了。就违反了一些规则。
5. 如何设计一个不能被继承的类
我们可以把父类的构造函数设置成私有:
这样B就无法构造对象了。但此时A自己也不能被构造了啊,我们该怎么办呢?我们可以去写一个函数来构造:
但是调用类里面的函数必须要有对象,这就产生了死循环。我们可以这样:
这样父类自己还可以使用,子类如果继承父类,则不能构造使用。