15.C++三大重要特性之继承
目录
一、继承的概念和定义
1. 概念
2. 继承格式
3. 继承方式
二、基类和派生类的转换
三、继承的作用域
1. 派生类和基类的关系
2. 隐藏规则
四、派生类的默认成员函数
1. 构造函数
2. 拷贝构造函数
3. operator=
4. 析构函数
五、继承与友元
六、继承与静态成员
七、多继承及菱形继承问题
1. 多继承
2. 菱形继承
3. 虚继承
八、继承与组合
总结:
一、继承的概念和定义
1. 概念
继承是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称为派生类。
继承的核心是有属于关系,即A的属性B也都有,那么A就可以由B继承过来,如Student和Teacher都属于Person,那么继承过来后Student和Teacher都具有Person的属性
2. 继承格式
被继承的一方为基类(父类),继承过来的一方为派生类。(子类)
继承格式为: 派生类:继承方式 基类
3. 继承方式
上面讲到了继承方式,那么继承方式又分为三种:public,protected,private,这三种方式又和公有、私有和保护三种访问限定符一一对应。
三种继承之后改变如下:
基类成员/继承方式 | public继承 | protected继承 | private继承 |
基类中的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类中的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类中的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
从表格中看比较复杂难记,所以我们可以总结一下规律:
- 基类中定义的private成员在派生类中都不可见(不管是类外还是类内都不能直接使用,但可通过一些成员函数间接使用)
- 如果基类中的成员不想在类外访问,但又想继承给基类,就可以定义为protected成员(所以protected是专门为了继承而出现的,当类自己使用时和private没什么区别)
- 继承之后的访问关系取决于原成员的访问限定符和继承方式权限的较小值,关键字权限大小:public>protected>private。例:当基类中的public成员在protected继承后,由于protected的权限比public小,所以基类public成员权限缩小,变为派生类的protected成员
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 实际应用场景中一般都是使用public继承,这样不会改变基类的访问权限,便于使用
接下来看一段简单的示例代码:
#include<iostream>
using namespace std;class Person {
public:string getName() {return _name;}protected:string _name = "张三";// 姓名string _address;// 地址int _age = 20;// 年龄
};class Student: public Person {
public:int getAge() {return _age;}
protected:int _stuid;// 学号
};int main() {Person p;Student s;cout << p.getName() << endl;cout << s.getName() << endl;cout << s.getAge() << endl;return 0;
}
可以看到Person类定义了名字、地址、年龄这三个成员变量,并使用了protected访问限定符,而Student对Person类进行了public继承。而Student对象s成功使用了基类的getName成员函数,且能访问_age成员变量,说明Student顺利继承了Person类的属性。
二、基类和派生类的转换
1. public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片,寓意把派生类中基类那部分切出来。因为派生类可以看成是一种基类延展,所以把延展出来的部分切掉,留下的部分就是一个基类(注意:这里切片不会形成临时变量,而是指向原位置,只是指针指向的范围发生了变化)。赋值后指针会指向切出来的基类那部分。
2. 反过来基类就不能赋值给派生类,因为派生类有的成员基类没有,所以不能给派生类赋值。
3. 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用(这里强转会出现临时变量)。但是必须是基类的指针是指向派生类对象时才是安全的。
三、继承的作用域
1. 派生类和基类的关系
派生类和基类的作用域是独立的,在对成员函数或成员变量进行操作时会优先在派生类里面找,如果找不到再到基类里面找。
2. 隐藏规则
作用域独立也就意味着可以存在同名成员,当基类和派生类具有同名成员时,派生类成员会屏蔽对基类同名成员的访问,这种情况叫隐藏,也可以叫重定义。(不会构成重载,因为两个同名成员其实是在不同作用域)
如果是成员函数构成隐藏的话只需函数名相同即可,与形参类型无关。
如果想要访问基类的同名成员,就需要指定作用域,如Person::
四、派生类的默认成员函数
类会默认生成6个默认成员函数,那派生类中默认成员函数是生成的规则是什么呢,我们先学习四个常见默认成员函数
1. 构造函数
首先派生类在构造函数初始化列表是不能对基类的成员变量初始化的,基类的成员变量必须由基类的构造函数进行初始化,
可以看到Student类中对_stuid初始化却不能对_age初始化。
所以派生类的构造函数必须调用基类的构造函数。如果基类有默认构造函数,编译器会自动调用。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
基类构造函数比派生类优先级高,保证先初始化基类成员,再初始化派生类成员
2. 拷贝构造函数
派生类的拷贝构造函数必须显示调用基类的拷贝构造函数,以完成对基类成员的拷贝。因为拷贝构造函数是构造函数的重载,如果不显示调用拷贝构造,那就会调用默认构造进行初始化。
Student(const Student& s):Person(s)// 调用基类拷贝构造,_stuid(s._stuid)// 对派生类剩余值拷贝
{}
3. operator=
派生类的operator=必须要调用基类的operator=完成基类成员部分的赋值,但需要注意的是基类与派生类中的operator构成隐藏,所以调用时要指定作用域。
Student& operator=(Student& s) {Person::operator=(s);_stuid = s._stuid;
}
4. 析构函数
派生类的析构函数会在对象生命周期结束后自动调用,当派生类的析构函数调用完成后又会自动调用基类的析构函数。
这样保证了先释放派生类成员,再释放基类成员。这样做是为了防止基类成员被释放后派生类在释放过程中再去访问基类成员。
注意:由于多态中的一些场景需要析构函数构成重写(需要基类和派生类中的析构函数同名),所以编译器对析构函数进行了特殊处理,将析构函数的名字统一处理为destructor(),所以需要显示调用基类的析构函数时要指定作用域。
五、继承与友元
友元关系不能从基类继承到派生类,也就是说基类友元不能访问派生类的私有和保护成员。
如果想要友元也能访问派生类的私有和保护成员,那可以将派生类也变成友元。
六、继承与静态成员
基类中的静态成员由自己和所有的派生类共享,即静态成员只有一个,静态成员改变会影响所有派生类和基类。
class Person {
public:static int _count;string _name;
}class Student: public Person {}int main() {Person p;Student s;// 这⾥的运⾏结果可以看到非静态成员_name的地址是不⼀样的// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份cout << &p._name << endl;cout << &s._name << endl;// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的// 说明派⽣类和基类共⽤同⼀份静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员cout << Person::_count << endl;return 0;
}
七、多继承及菱形继承问题
1. 多继承
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多个基类之间用逗号分隔,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后。
2. 菱形继承
菱形继承:菱形继承是多继承的一种特殊情况。从下图中可以看出,B类中有一份A的成员,C中也有一份A的成员,当D同时继承C和D时,相当于继承了两份A的成员,这就形成了两份重复的数据。所以菱形继承有数据冗余和二义性的问题。支持多继承就 ⼀定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们非常不建议使用菱形继承。
3. 虚继承
C++支持菱形继承,那又是怎么解决菱形继承造成的数据冗余和二义性的问题的呢?
这就用到了虚继承,虚继承是将基类中的成员独立存储,如上图中如果使用虚继承,派生类B会将基类A部分的成员放到一块独立区域存储,派生类C也会将基类A部分的成员放到一块独立区域存储。此时D再继承B和C时,由于B和C存储基类成员部分的两块区域是独立出去的,并不在类内部,所以D不再继承B和C中的基类A部分的成员,而是跨过B和C直接继承基类A的成员。
使用方法:在菱形继承中多继承的位置的基类前面加上virtual关键字,如上图中使用虚继承应该表现为: class B : virtual public A class C : virtual public A
八、继承与组合
对于类的复用除了继承外,还有一种方式,即组合,两者有一定差别。
public继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象
组合是一种has-a的关系,是包含关系,即A对象里包含了B对象
继承是一种白箱复用的模型,即基类的内部对派生类来说是可见的。继承在一定程度上破坏了类的封装性,且派生类和基类的关系密切,耦合度高。
组合是一种黑箱复用的模型,即对象的内部细节是不可见的,我们只能去使用黑箱提供的外部接口。组合类之间的耦合度更低。
所以在实践中尽量多用组合而不是继承,这样耦合度更低,有利于代码维护。但也有些情况必须用到继承,如多态。我们也可以根据实际情况更符合is-a还是has-a来选择使用哪种方式。
组合示例代码:
// Tire(轮胎)和Car(⻋)更符合has-a的关系// 轮胎
class Tire {protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺⼨ };class Car {
protected:string _colour = "⽩⾊"; string _num = "陕ABIT00"; // 组合四个轮胎Tire _t1; Tire _t2; Tire _t3; Tire _t4;
};
这里Car类中使用组合方式复用了四个轮胎
总结:
以上便是本文的全部内容了,如果觉得有帮助的话可以点赞收藏加关注支持一下!