当我们有多个类需要重复复用一个类的数据时可以以继承的方式复用这一个类的的内容,此时这一个被复用的类被称为父类/基类,复用的多个类则被称为子类/派生类。继承体现了类设计层次的复用。
1.1继承关系和访问限定符
继承的格式下面代码中father是父类也称为基类,son是子类也称为派生类。
class father
{
public:
int age = 10;
};
// 派生类 继承方式 基类
class son :public father
{
public:
int age = 20;
};
3种继承方式和三种访问限定符,这使得我们使用继承时会有9种情况感觉非常复杂,其实在大多数情况下我们使用的都是public继承,由于C++的向前兼容性我们也没办法删除这些语法内容,其他两种继承方式了解即可。
1.2在不同继承方式下基类成员访问访问方式的变化
解释一下上图表的内容
以public继承为例,派生类中只有基类中的private成员变为派生类中的不可见成员,而其他成员还是不变。不可见的具体意思下面会提到。
总结:
- 基类private成员无论怎么继承都是不可见的,这里的不可见是指虽然派生类中有private成员,但是你无论是在类外还类内都无法访问该成员。
- protected成员只可以在派生类内访问,前面我们刚开始学习类时,private成员和protected成员没有什么区别,可以看出保护成员限定符(protected )是为了继承而出现的。
- 基类私有成员在子类都是不可见。基类其他成员在子类的访问方式是取权限较小的一方(即基类访问限定符与继承方式限定符比较),public > protected > private。
- 使用关键词class时的默认继承方式为private,使用struct是默认的继承方式为public,不过最好还是显示写出来。
- 大多数情况下都是使用public继承,很少使用另外两个继承。
2. 继承中的作用域
- 继承体系中基类和派生类都有独立的作用域。
- 当子类和父类有同名的成员时,我们直接访问该成员时,子类成员会屏蔽父类成员这种情况叫做隐藏,也叫重定义,我们可以通过访问限定符访问父类中的成员。(下面给出示例)
#include <iostream> using namespace std; class father { public: int age = 10; }; class son :public father { public: int age = 20; }; int main() { son s1; //正常访问为20 cout << s1.age << endl; //指定作用域访问为10 cout << s1.father::age << endl; return 0; }
- 需要注意的是成员函数只要同名就构成隐藏。
- 最好还是不要定义重名的成员
3.基类和派生类对象赋值转换
- 派生类对象可以赋值给基类对象/基类指针/基类引用
//这里虽然是不同类型,但和我们之前学习中的隐式类型转换不同 son p; father p1 = p; father* p2= &p; father& p3 = p; //隐式类型转换,产生了一个临时变量,这里临时变量具有常性所以需要使用const int a = 10; const double& b = a;
- 基类对象不能给派生类对象赋值(即使进行强制类型转换也不能)
- 基类的指针和引用可以通过强制类型转换赋值给派生类的指针
4.继承结构中子类的6个默认成员函数
类和对象中我们认识到了类中有6个默认构造函数如下图
子类编译默认生成的构造函数
- 自己的成员,对内置类型不处理,对自定义类型调用自定义类型的构造
- 继承的父类成员,必须调用父类的构造函数初始化
- 当父类中没有默认构造函数时,我们需要显示调用父类的构造函数
子类编译默认生成的析构
- 自己的成员,对内置类型不处理,对自定义类型调用自定义类型的析构
- 继承的父类成员,必须调用父类的析构处理
- 需要注意的是子类的析构和父类的析构函数构成隐藏关系。这是由于后面多态的需要编译器将析构函数名统一处理为destructtor()
- 我们不需要显示调用父类的析构函数,这是由于如果我们自己显示调用析构函数,无法保证先创建的对象后析构的顺序(栈后进先出的性质),所以编译器会默认先调用子类的析构函数然后调用父类的析构函数。
子类编译默认生成的拷贝构造(子类默认生成的operator=)
- 自己的成员,内置类型值拷贝,自定义类型调用自定义类型的拷贝构造(赋值)
- 继承的父类成员,必须调用父类的拷贝构造(赋值)
- 当我们需要显示调用父类的赋值函数时需要注意指定作用域,因为父类和子类的赋值函数构造隐藏关系。隐藏关系在继承中的作用域有提到。
5.继承与友元的关系
友元关系不能继承
如果你想要访问该有元函数就在子类中添加该函数为子类的友元
6.继承与静态成员
基类中定义的static静态成员,在整个继承体系中只有一个该成员,即无论有多少个子类都只有一个static成员,一般的成员存储在栈内,static成员存储在静态区。
7.复杂的菱形继承及菱形虚拟继承
- 单继承:一个子类只有一个直接父类
- 多继承:一个子类有两个或以上直接父类
菱形继承:多继承的一种特殊情况
可以看出在菱形继承有数据冗余和二义性的问题。在Assistan的对象中有两份Person成员。
虚继承可以解决这两个问题,即Student和Teacher虚继承Person类
class Student : virtual public Person
虚继承的子类会增加一个指针,这个指针存储了偏移量,子类通过偏移量去找到父类中的成员。
8.声明一个不能被继承的类
在C++98中我们将父类的构造函数私有,这样子类对象实例化,无法调用构造函数,在C++11中觉得这样的方式不够规范所以添加了一个关键词final。使用格式如下:
class A final
{
};
9.继承与组合
- public继承是一种is-a关系。也就是每个派生类都是一个基类对象。
- 组合是一种has-a关系。B组合了A,每个B对象中都有A。下面是组合是使用格式
class father { public: int age; }; class son :virtual public father { father _f; public: int age; };
- 当两个对象之间既可以继承,也可以组合时,优先使用组合。(组合的耦合度低,代码维护性好)
- 组合和继承的区别,继承关系父类如果修改了protected成员,那么就可能会影响子类,而组合不会。
10.总结
- C++的语法复杂,继承就是一个体现。有多继承,所以就有菱形,那就有了菱形虚拟继承,底层的实现就很复杂。
- 多继承可以认为是C++的缺陷之一。
文章评论