C++ | 继承
回顾:上一篇我们结束了 模板,接下来这篇文章让我们进入到新的内容 继承 的学习,体会新的设计思路吧~
放个目录
- 一 介绍继承
- 1.1 上代码
- 1.2 语法格式
- 1.3 继承方式&访问限定符
- 1.3.1 介绍
- 1.3.2 怎么访问继承下来的不可访问成员?
- 1.4 继承类模板
- 1.4.1 写一个stack继承vector
- (1)上代码
- 注意点
- (2)比较之前的写法
- 二 基类和派生类之间的转换
- 2.1 指针/引用赋值
- 2.1.1 (回顾)类型转换
- 2.1.2 复制兼容转换
- 2.2 派生类对象赋值给基类对象
- 2.3 基类对象不能赋值给派生类对象
- 三 继承中的作用域
- 3.1 规则
- 3.2 选择题
- 3.2.1 俩fun构成关系
- 3.2.2 程序运行结果
- 怎么才能不报错?
- 3.3 总结
- 四 派生类的默认成员函数
- 4.1 常见的默认成员函数
- 4.1.1 构造函数
- (1)编译器生成的
- (2)我们自己实现
- ①声明的时候给缺省值
- ②走初始化列表
- 1.语法
- 2.初始化顺序
- 4.1.2 拷贝构造
- (1)怎么自己实现?
- 初始化列表里怎么调用基类的拷贝构造?
- (2)测试
- 4.1.3 赋值重载
- (1)怎么自己实现?
- 怎么调用基类的复制重载?
- (2)测试
- 4.1.4 析构函数
- 显式调用基类析构?
- (2)测试
- 为什么析构函数调用了两次?
- 4.2 实现不被继承的类
- 4.2.1 让构造函数私有化
- 4.2.2 给final关键字
- 五 继承和友元
- 六 继承与静态成员
- 6.1 定义static成员变量
- 6.2 测试
- 6.3 再写个同名变量
- 七 多继承及其菱形继承问题
- 7.1 继承模型
- 7.1.1 写个 Assistant 类
- 7.1.2 测试
- 7.1.3 菱形继承的问题
- 7.2 解决方案:虚继承
- 怎么实现?
- 7.3 来个选择题
- 7.4 菱形继承特殊情况
- 八 继承和组合
- 组合优于继承
一 介绍继承
1.1 上代码
classPerson{public:voididentity(){cout<<"void identity()"<<_name<<endl;}private:int_age=18;string _name="xxx";};classStudent:publicPerson{public:voidstudy(){//...}private:int_score=0;};1.2 语法格式
classsub_name:publicbase_name{};- 这里用public继承。
1.3 继承方式&访问限定符
1.3.1 介绍
- 简单来说,min(访问限定符 , 继承方式)。
- 一般来说最多用public继承。
- 不写继承方式,使用class默认是私有继承;使用struct默认是私有继承。
1.3.2 怎么访问继承下来的不可访问成员?
在父类里访问,函数设置成public。
public:stringname(){return_name;}1.4 继承类模板
1.4.1 写一个stack继承vector
(1)上代码
template<classT>classstack01:publicvector<T>{public:voidpush(constT&x){vector<T>::push_back(x);}voidpop(){vector<T>::pop_back();}constT&top(){returnvector<T>::back();}boolempty(){returnvector<T>::empty();}};注意点
- 模板调用之后才实例化,调用之前编译器不知道基类里有什么成员,会报错。
- 所以调用基类成员函数需要指明类域,或者加this指针。
voidpush(constT&x){//vector<T>::push_back(x);this->push_back(x);}(2)比较之前的写法
适配器的方式就是组合。
template<classT,classContainer=vector<T>>classstack02{public:voidpush(constT&x){c.push_back(x);}voidpop(){c.pop_back();}constT&top(){returnc.back();}boolempty(){returnc.empty();}private:Container c;};二 基类和派生类之间的转换
2.1 指针/引用赋值
public继承的派生类对象 可以赋值给 基类的指针 / 基类的引⽤。形象地说就是切片或切割,指向派生类中切出来的基类那部分。
wyzy::Student s;wyzy::Person*p=&s;wyzy::Person&r=s;调试:
为什么可以直接引用?
2.1.1 (回顾)类型转换
隐式类型转换,会产生临时对象,引用需要加const。
// false: Base& ref1 = Base();constBase&ref2=Base();// true2.1.2 复制兼容转换
这是一种特殊处理,没有产生临时对象,引用的是派生类的切片。
2.2 派生类对象赋值给基类对象
编译器会调用基类的拷贝构造函数(没写就默认生成),用切片构造一个基类对象。
wyzy::Student s;wyzy::Person p=s;调试:
2.3 基类对象不能赋值给派生类对象
wyzy::Person p;wyzy::Student s=p;报错:
三 继承中的作用域
3.1 规则
- 基类和派生类有独立的作用域。
- 基类和派生类有同名成员,派生类会隐藏基类的同名成员。
① 如果实在要访问基类的同名成员,就指定基类类域。
② 基类里访问成员变量,通过public函数继承给派生类。
3.2 选择题
classA{public:voidfun(){cout<<"func()"<<endl;}};classB:publicA{public:voidfun(inti){cout<<"func(int i)"<<i<<endl;}};intmain(){B b;b.fun(10);b.fun();return0;}3.2.1 俩fun构成关系
俩fun构成隐藏关系。
3.2.2 程序运行结果
编译报错,基类函数隐藏后调用不到。
怎么才能不报错?
还是指定基类类域。
b.A::fun();3.3 总结
- 建议不要跟派生类不要跟基类搞同名成员。
- 同名函数就构成隐藏,即使参数不同。
四 派生类的默认成员函数
4.1 常见的默认成员函数
4.1.1 构造函数
(1)编译器生成的
继承自基类那部分(当作一个整体),调用基类的默认构造。
- 内置类型初始化为随机值。
- 自定义类型调用默认构造。
(2)我们自己实现
①声明的时候给缺省值
private:int_age=18;string _name="xxx";②走初始化列表
如果我们初始化基类成员会报错(基类被当作一个整体)。
那我们怎么初始化基类成员嘞?
1.语法
:Base(member),...2.初始化顺序
基类成员排在前面初始化(初始化顺序按声明顺序来)。
4.1.2 拷贝构造
没有什么额外的资源开销,就不需要写这个。
(1)怎么自己实现?
先实现一个基类:
Person(constPerson&p):_age(p._age),_name(p._name){cout<<"Person(const Person& p)"<<endl;}函数体里打印个东西方便观察。
初始化列表里怎么调用基类的拷贝构造?
用到上面的切片。
:Base(...),运用上述语法,实现派生类的拷贝构造:
Student(constStudent&s):Person(s),_score(s._score){cout<<"Student(const Student& s)"<<endl;}(2)测试
wyzy::Student s1;wyzy::Students2(s1);运行输出:
监视窗口:
4.1.3 赋值重载
写不写跟4.2一样的情况。
(1)怎么自己实现?
基类实现:
Person&operator=(constPerson&p){cout<<"Person operator=(const Person& p)"<<endl;if(this!=&p)_name=p._name;return*this;}一样整个打印方便观察。
怎么调用基类的复制重载?
注意需要指明基类类域。
Base::operator=(...);派生类实现:
Student&operator=(constStudent&s){cout<<"Student& operator= (const Student& s)"<<endl;if(this!=&s){Person::operator=(s);_score=s._score;}return*this;}(2)测试
wyzy::Student s1;wyzy::Students2(20,"lll",100);s2=s1;运行输出:
调试:
4.1.4 析构函数
写不写情况也一样。
(1)怎么自己实现?
基类:
~Person(){cout<<"~Person()"<<endl;}显式调用基类析构?
~Student(){~Person();cout<<"~Student()"<<endl;}编译报错,因为一些场景下要析构函数构成多态,需要 派生类 和 基类 析构名称相同。
这里底层把析构函数的名称统一处理成destructor,所以和派生类的析构函数重名了,被隐藏了,需要指定基类类域。
Person::~Person();(2)测试
wyzy::Student s;运行结果:
为什么析构函数调用了两次?
- 默认先析构派生类成员,再析构基类成员。
- 所以我们不需要调用基类的析构函数,基类析构完会自动调用。
~Student(){//~Person();cout<<"~Student()"<<endl;}4.2 实现不被继承的类
4.2.1 让构造函数私有化
把构造放到public前。
Person(intage=18,string name="xxx"):_age(age),_name(name){}public:// ...编译报错:
4.2.2 给final关键字
classPersonfinal{// ...}编译报错:
五 继承和友元
给基类写一个友元:
classPerson{public:friendvoidDisplay(constPerson&p);// ...}voidDisplay(constPerson&p){cout<<p._name<<endl;}编译通过。
- 友元关系不能被继承,基类的友元不能访问派生类的private和protected成员。
classPerson{public:friendvoidDisplay(constPerson&p,constStudent&s);// ...}voidDisplay(constPerson&p,constStudent&s){cout<<p._name<<endl;cout<<s._stuNum<<endl;}编译报错:
六 继承与静态成员
6.1 定义static成员变量
基类定义一个static成员,在整个继承体系中只有这一个成员。
classPerson{// ...private:// ...staticint_count;}intPerson::_count=0;6.2 测试
wyzy::Student s1;wyzy::Student s2;调试:
6.3 再写个同名变量
在派生类写个同名成员:
classStudent:publicPerson{// ...private:// ...int_count=0;}调试:可以发现static _count被隐藏了。
七 多继承及其菱形继承问题
7.1 继承模型
单继承:派生类继承自一个基类。
多继承:派生类继承自多个基类。
菱形继承:多继承可能导致菱形继承。
7.1.1 写个 Assistant 类
依照上图,Assistant代表上图的Dderived。
classAssistant:publicStudent,publicTeacher{public:Assistant(intage=20,string name="aaa",intscore=0,intsalary=0,string majorCourse="x")/*:_name("yyy")*/:Student(age,name,score),Teacher(age,name,salary),_majorCourse(majorCourse){}private:string _majorCourse=0;};7.1.2 测试
wyzy::Assistant a;调试:
7.1.3 菱形继承的问题
菱形继承有 数据冗余 和 ⼆义性(有两份基类切片) 的问题。
在Assistant里写个函数访问_age:
voidgrow(intyears=1){_age+=years;}编译报错:
7.2 解决方案:虚继承
引入菱形虚拟继承,会损失性能,最好不要设计出这种虚继承。
怎么实现?
加virtual关键字。
classStudent:virtualpublicPerson{// ...}classTeacher:virtualpublicPerson{// ...}报错问题就不一样了:
具体实现:构造函数也有多份,析构函数也有多份,比较复杂。
7.3 来个选择题
是多继承中指针偏移的问题:
classBase1{public:int_b1;};classBase2{public:int_b2;};classDerive:publicBase1,publicBase2{public:int_d;};intmain(){Derive d;Base1*p1=&d;Base2*p2=&d;Derive*p3=&d;return0;}需要选出正确的一个:
A:p1 == p2 == p3 B:p1 < p2 < p3
C:p1 == p3 != p2 D:p1 != p2 != p3
这里涉及多继承中的切片。
选C:
7.4 菱形继承特殊情况
怎么特殊嘞?如图:
- virtual加在最先触及多继承那一层。
这里就加在Derived1和Derived2上。
八 继承和组合
- 继承是 is-a ,组合是 has-a。
- 这两种都是一种代码复用。
组合优于继承
- 组合相比于继承,更加体现低耦合思想。
- 当然特定场景下,该用继承还是要用继承。
继承 的学习就到这里,下一篇也是学习 C++面向对象的特性[多态],不久后就会更出来啦~
