文章目录
- 一、继承的定义
- 1.1 概念
- 1.2 父类访问变化
- 1.3 继承类模版
- 二、赋值兼容转化
- 三、继承中的作用域
- 四、派生类的默认成员函数
- 4.1 构造函数
- 4.2 拷贝构造
- 4.3 赋值重载
- 4.4 析构
- 4.5 总结
- 文章结语
一、继承的定义
1.1 概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用
定义格式:Person是基类,也称作父类。Student是派生类,也称作子类
通过具体实例来理解:
#include<iostream>#include<string>usingnamespacestd;classParent{public:voididentity(){cout<<"identity()"<<_name<<endl;}protected:string _name="张三";};classStudent:publicParent{public:voidstudy(){cout<<"study()"<<endl;}protected:string id;};classTeacher:publicParent{public:voidteach(){cout<<"teach()"<<endl;}protected:string _title;};intmain(){Student s1;Teacher t1;s1.identity();return0;}1.2 父类访问变化
由于不同的继承方式,继承基类成员访问方式的变化,原来的成员变量会变成另外一种(private除外)
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
|---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式==Min(成员在基类的访问限定符,继承方式),public>protected>private.
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用
protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
1.3 继承类模版
基类是类模板时,需要指定一下类域,因为模版是按需实例化,当成员函数未实例化时,编译器找不到基类成员函数
#defineContainvectornamespaceA{template<classT>classstack:publicContain<T>{public:voidpush(constT&x){Contain<T>::push_back(x);}voidpop(){Contain<T>::pop_back();}constT&top(){returnContain<T>::back();}size_tsize(){returnContain<T>::size();}boolempty(){returnContain<T>::empty();}};}intmain(){A::stack<int>st;}二、赋值兼容转化
public继承的派生类对象 可以赋值给 基类的指针/基类的引用。有个形象的说法叫切片或者切割
注意:
基类对象不能赋值给派生类对象(子类有的,父类没有,强转也是不可以的)。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
Student s;Parent&temp=s;//不是隐式类型转化,如果是的话,会产生临时变量,不支持引用Parent*temp1=&s;Parent temp2=s;三、继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域
- 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派生类成员函数中,可以使用基类::基类成员显示访问)
classParent{protected:int_num=10;};classStudent:publicParent{public:voidFuc(){cout<<_num<<endl;//优先访问当前作用域的cout<<Parent::_num<<endl;}protected:int_num=100;};intmain(){Student st;st.Fuc();}- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。不是重载(不在同一作用域)
classParent{public:voidFuc1(){cout<<"haha"<<endl;}};classStudent:publicParent{public:voidFuc1(inti){cout<<"hehe"<<endl;}};intmain(){Student st;st.Parent::Fuc1();}- 注意在实际中在继承体系里面最好不要定义同名的成员。
四、派生类的默认成员函数
关于默认成员函数,不熟悉的小伙伴可以通过这篇文章来了解下
【C++初阶】类和对象(二):默认成员函数详解与日期类完整实现
4.1 构造函数
默认生成的构造函数的行为
- 内置类型->不确定
- 自定义类型->调用默认构造
- 继承父类成员看做一个整体对象,要求调用父类的默认构造
派生类的构造函数必须调基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
具体实例:
classParent{public:Parent(string name):_name(name){}protected:string _name;};classStudent:publicParent{public:Student(string id,string name,intscore):_id(id)//,_name(name),Parent(name),_score(score){}protected:string _id;int_score=100;};intmain(){Students("123","lishi",60);return0;}4.2 拷贝构造
默认生成的拷贝构造函数的行为
- 内置类型->值
- 自定义类型->调用默认构造拷贝
- 继承父类成员看做一个整体对象,要求调用父类的默认拷贝构造
与赋值重载、析构类似,当有资源释放时,才需要写拷贝构造
派生类的拷构造函数必须调基类的拷贝构造完成基类的拷贝初始化(如果不调用父类的拷贝构造,会调用默认构造,不满足需求)。
classParent{public:Parent(string name):_name(name){}Parent(constParent&s){cout<<"Parent(Parent& s)"<<endl;}protected:string _name;};classStudent:publicParent{public:Student(string id,string name,intscore):_id(id)//,_name(name),Parent(name),_score(score){}Student(constStudent&s):_id(s._id),_score(s._score),Parent(s){}protected:string _id;int_score=100;//int* _ptr =new[20];};intmain(){Students("123","lishi",60);Students1(s);return0;}从声明的角度,初始化列表会先调父类的拷贝构造(按内存的顺序),父类在前面
4.3 赋值重载
严格说student赋值重载默认生成的就够用了如果有需要深拷贝的资源,才需要自己实现
派类的operator=必须要调基类的operator=完成基类的复制。需要注意的是派类的operator=隐藏了基类的operator=,所以显示调基类的operator=,需要指定基类作域
classParent{public:Parent(string name):_name(name){}Parent(constParent&s){cout<<"Parent(Parent& s)"<<endl;}Parent&operator=(constParent&s){cout<<"Person operator=(const Person& p)"<<endl;if(this!=&s)_name=s._name;return*this;}protected:string _name;};classStudent:publicParent{public:Student(string id,string name,intscore):_id(id)//,_name(name),Parent(name),_score(score){}Student(constStudent&s):_id(s._id),_score(s._score),Parent(s){}Student&operator=(constStudent&s){if(this!=&s){_id=s._id;_score=s._score;Parent::operator=(s);return*this;}}protected:string _id;int_score=100;//int* _ptr =new[20];};intmain(){Students("123","lishi",60);Students1(s);Students2(s);s1=s2;return0;}4.4 析构
如果没有资源申请,不需要在子类显示写析构函数,父类会自动调析构函数
生类的析构函数会在被调完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
派生类对象初始化先调用基类构造再调派生类构造。
派生类对象析构清理先调用派生类析构再调基类的析构。
补充:子类的析构和父类的析构构成隐藏关系,编译器会对析构函数名进行特殊处理,都处理成destructor()
4.5 总结
构造阶段 (创建对象时),先调用基类构造函数,再调用派生类构造函数。
析构阶段 (销毁对象时),先调用派生类析构函数,再调用基类析构函数
文章结语
感谢你读到这里~我是「键盘敲碎了雾霭」,愿这篇文字帮你敲开了技术里的小迷雾 💻
如果内容对你有一点点帮助,不妨给个暖心三连吧👇
👍点赞| ❤️收藏| ⭐关注
(听说三连的小伙伴,代码一次编译过,bug绕着走~)
你的支持,就是我继续敲碎技术雾霭的最大动力 🚀
🐶 小彩蛋:
/^ ^\ / 0 0 \ V\ Y /V / - \ / | V__) ||摸一摸毛茸茸的小狗,赶走所有疲惫和bug~我们下篇见 ✨