一. 继续的概念及界说
1.1 继续的概念
继续(inheritance)机制是⾯向对象程序计划使代码可以复⽤的最紧张的⼿段,它允许我们在保持原有 类特性的底子上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),如许产⽣新的类,称派⽣类。继续 出现了⾯向对象程序计划的条理布局,体现了由简朴到复杂的认知过程。从前我们打仗的函数条理的 复⽤,继续是类计划条理的复⽤。
下⾯我们看到没有继续之前我们计划了两个类Student和Teacher,Student和Teacher都有姓名/地点/ 电话/年事等成员变量,都有identity⾝份认证的成员函数,计划到两个类⾥⾯就是冗余的。当然他们 也有⼀些差别的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣ 的独有成员函数是学习,⽼师的独有成员函数是授课。
直接上一个例子看看。
这两个类是有许多类似的属性的,我们能不能把类似的只放在一个类中镌汰冗余呢?
这时间继续就可以美满办理这个题目了。
此时我们就把老师和同砚类似的属性全部都抽出来了,放在了一个Person类中,这里你可以先看看继续的格式是怎么写的。
1.2 继续界说
下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(由于翻译的缘故起因,以是既叫基类/派⽣类,也叫⽗类/⼦类)。
继续方式和访问限定符都是有三类的。
这个就是继续的方式,访问限定符会缩小,但是不会变大。我们下面来看一下。明白一下。
我们来举个例子就很清晰了。
此时这个门生类是通过public继续的,以是父类中各个访问限定符就不会改变,照旧原来的,稳固。
但是我们换个方式继续看看。
我们通过protected访问限定符来继续,此时创建子类对象访问父类中的方法时,这里的父类的public访问限定符就会酿成protected,父类对象不受影响,我们可以验证一下。
我们看到没,子类对象此时访问不到父类中的identity方法了,但是父类对象可以。
我们再改为private继续试一下。
此时子类对象访问父类方法时,父类中的方法全部变为private范例的了。
此时派生类中照旧按照原来的访问,只是子类对象的访问限定符改变了。
各人可以好悦目看这个例子就可以明白这两种环境了。
1. 基类private成员在派⽣类中⽆论以什么⽅式继续都是不可⻅的。这⾥的不可⻅是指基类的私有成员 照旧被继续到了派⽣类对象中,但是语法上限定派⽣类对象不管在类⾥⾯照旧类外⾯都不能去访问它。
这句话的意思就是我改为私有的,此时除了在自己类中可以访问的到,在其他类中都是访问不到的,派生类中也是访问不到的。
2. 基类private成员在派⽣类中是不能被访问,假如基类成员不想在类外直接被访问,但须要在派⽣类 中能访问,就界说为protected。可以看出掩护成员限定符是因继续才出现的。
3. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员 在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继续⽅式),public > protected >
private。
4. 使⽤关键字class时默认的继续⽅式是private,使⽤struct时默认的继续⽅式是public,不外最好显 ⽰的写出继续⽅式。
5. 在实际运⽤中⼀般使⽤都是public继续,⼏乎很少使⽤protetced/private继续,也不提倡使⽤
protetced/private继续,由于protetced/private继续下来的成员都只能在派⽣类的类⾥⾯使⽤,实
际中扩展维护性不强。
1.3 继续类模板
这个我们就直接上代码来明白吧。
这个就是我们常用的栈,我们在这的实现是通过让它继续vector容器来实现的,此时我们是可以继续vector中的方法的。
上面的代码大概会有些疑惑的地方,好比,为什么要在方法前面加上vector<T>呢?
最重要有两个方面的作用。
1.制止二义性:当模板类stack继续自std::vector<T>时,stack类中大概会有与父类std::vector<T>成员函数同名的成员函数或者其他成员。假如不利用vector<T>::限定,编译器大概无法确定你要调用的是父类的成员函数照旧子类中大概存在的同名成员,加上作用域限定符可以明白指定要调用的是父类std::vector<T>中的成员函数,制止产生二义性。
2.模板的延迟实例化:模板是按需实例化的。在stack类模板中,当实例化stack<int>时,固然也会实例化vector<int>,但vector<int>中的成员函数如push_back、pop_back等大概并没有立即被实例化。假如直接利用push_back等函数而不加vector<T>::限定,编译器在编译stack类的成员函数时大概无法找到这些函数的界说,由于它们大概还没有被实例化。而加上vector<T>::限定,编译器就可以或许明白知道要在父类std::vector<T>的作用域中查找这些函数,纵然它们还没有被实例化,也能准确地进行编译,在须要的时间再进行实例化。
简朴来说就是,你的vector<T>中的T固然被实例化了,但是vector这个类中的方法大概没有被实例化,你直接利用大概编译器不知道利用哪个push_back方法,由于我们都知道模板的底层照旧多个范例的函数吗,只是编译器帮我们写了,我们不须要自己写了,以是我们不加前缀,他不知道调用哪个范例的。
看此时我们不但可以调用自己的方法,还可以调用基类中的方法,结果都是一样的。
你看假如我们直接说明了T的范例,此时就不消加前缀了,此时确定了是int,它的方法就直接会实例化了。
1.4 基类和派⽣类间的转换
public继续的派⽣类对象 可以赋值给 基类的指针 / 基类的引⽤。这⾥有个形象的说法叫切⽚或者切
割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分
基类对象不能赋值给派⽣类对象。
举个例子吧。
我们由浅入深来看一下吧。
我们看一下这个代码,前两行的int引用引用int变量是没有题目标,我们发现double引用引用int范例是不可的,这是由于范例转换会产生暂时变量,引用无法指向暂时变量,必须加const延长暂时对象的声明周期。
如许就没题目了。
我们再来看看继续中的范例转换。
这时间为什么没有报错呢?不是须要范例转换吗?天生暂时变量为什么可以呢?
起首第一个,我们用平常范例是无法实现的,为什么我们的继续可以呢?尚有第二个,我们不是须要加const吗,为什么这里不消加呢?
答案是不会的,你可以把它明白为切割,由于父类中的成员子类都可以访问(这个例子中的),以是当用指针或者引用是,它会只切割自己要的部分,不会产生暂时变量,基类指针或者引用可以或许指向派生类对象。这是由于 public 继续维持了基类接口的公共性,派生类对象可被视为基类对象,以是这种转换是安全的。
此时就把派生类对象当作一个基类对象了。
我们用protected和private继续是不能实现的。
基类指针或者引用不可以直接指向派生类对象。private 继续意味着派生类把基类的接口酿成了自己的私有接口,外部无法将派生类对象当作基类对象来处置惩罚。
你可以明白为外部对象无法访问到基类中的方法了,此时不能将一个派生类当作一个基类了。
1.5 继续中的作⽤域
1.5.1 隐蔽规则:
1. 在继续体系中基类和派⽣类都有独⽴的作⽤域。
2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种环境叫隐蔽。
(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)
举个简朴的例子看一下吧。
可以看A中的func函数,假如直接访问id,默认访问的是自己的,由于隐蔽规则吗,此时B的id就把A的id给覆盖了,想访问A的可以用下面的那种方法,指定作用域即可。
3. 须要留意的是假如是成员函数的隐蔽,只须要函数名类似就构成隐蔽。
这个我们举个例子,印象能更深刻,我们直接看两个题。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)" <<i<<endl;
}
};
int main()
{
B b;
b.fun(10);
b.fun();
return 0;
};
结合上面的代码看两个题吧。
假如没有上面那句话的提示,大概许多多少人都会选A C了,但是结合上面的话我们可以知道,只要名字类似就是隐蔽和参数没关系,他不会构成重载,而是隐蔽,此时也会编译报错,A类的方法被隐蔽了,你调用的func函数都是B类的,你没有传参数,此时就会编译报错了。
4. 留意在实际中在继续体系⾥⾯最好不要界说同名的成员
1.6 派⽣类的默认成员函数
1.6.1 4个常⻅默认成员函数
6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这⼏个成员函数是怎样⽣成的呢?
1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。假如基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
我们举个例子。
我们发现如许是没题目标,它的过程就是通过自己的默认构造去调用Person的默认构造,由于Person是有缺省值的构造,你不传参数也是可以的,可以以为就是默认构造。
假如我们没给默认构造呢?
此时就会报错了,由于派生类的默认构造在调用基类的默认构造的时间,发现找不到基类的默认构造,此时就报错了,此时我们该怎么办呢?
只须要自己实现一下派生类的构作育行了。
须要留意一点:子类中父类成员变量当成团体对象,作为子类自界说范例成员对待
此时假如我们像之前那样初始化,此时就又错了,我们不能如许初始化,而是应该把基类的全部变量当作一个对象来初始化。
如许就没题目了,相当于你须要调用基类的构造先初始化基类,否则就会报错了。
此时发现没题目了。
2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
我们照旧看上图的末了一个的拷贝构造,它会调用父类的拷贝构造,id再调用自己的默认拷贝构造,由于这里我们的成员变量不须要深拷贝,此时就不消写子类的拷贝构造了,除非须要深拷贝。
我们给子类加了一个指针变量,此时就须要深拷贝了,我们来写一下子类的拷贝构造吧。
此时还碰面临一个题目,我们和构造一样须要调用父类的拷贝构造,须要传入一个父类对象,但是我们此时没有父类的对象呀,此时该怎么办呢?
我们通过上面我们讲的转换原则,直接传入一个派生类对象即可,它会自己切割得到自己想要的基类对象。
3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。须要留意的是派⽣类的
operator=隐蔽了基类的operator=,以是显⽰调⽤基类的operator=,须要指定基类作⽤域
这是我们子类写的=运算符我们先来运行一下。
我们发现瓦解了,这是为什么呢?
我们通过调试发现,栈溢出了,这是由于,又是构成隐蔽了,不绝调用的是自己的operator=,自己调用自己无法竣事,就导致栈溢出了。
只须要像如许指定一下类域即可。
4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清算基类成员。由于如许才能包管派⽣类对象先清算派⽣类成员再清算基类成员的次序。
这里我们写了子类的析构去调用父类的析构为什么报错了呢?
由于调用父类的析构须要指定类域。
为什么调用了四次析构呢?理论应该两次啊,实在这里就是派生类的析构会自动调用基类的析构,又表现调用了一下,此时就会一个对象调用两次,我们两个对象,以是调用了四次。
5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
这个你可以通过调试看看,不绝都是先走父类的构造初始化。
6. 派⽣类对象析构清算先调⽤派⽣类析构再调基类的析构。
以是我们不须要写表现调用父类的析构了,如许也可以包管析构的时间先子后父,由于构造的时间是先父后子,先创建的后析构吗。
7. 由于多态中⼀些场景析构函数须要构成重写,重写的条件之⼀是函数名类似(这个我们多态章节会解说)。那么编译器会对析构函数名进⾏特别处置惩罚,处置惩罚成destructor(),以是基类析构函数不加
virtual的环境下,派⽣类析构函数和基类析构函数构成隐蔽关系。
1.7 实现⼀个不能被继续的类
一共有两种做法。
一.c++98实现的
我们可以看一下,就是利用了一个语法毛病,把父类的友元函数弄成私有的,此时调用不到,就会无法实例化对象,但是也有缺陷,此时A也无法利用默认构造了。
二.c++11完成的
利用final关键字,此时就不能被继续,但是不影响A的默认构造。
1.8 继续与友元
这里报了一个错误,各人不要被编译器的报错疑惑,实在不是这里的错误,是上面的缘故起因,由于编译器为了提拔编译速度只会向上找,此时找不到B,此时这里就会有题目,我们只须要在上面给上B的声明即可。
此时我们可以看到,父类的友元函数是父类的,但是子类没有继续过来,不能继续,简朴来说就是你爸爸的朋侪不是你的朋侪。
1.9 继续与静态成员
静态成员是可以继续的。
基类界说了static静态成员,则整个继续体系⾥⾯只有⼀个如许的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。 这个也是很好明白的就是全部实例化出来的对象访问的都是同一个静态成员。 我们可以通过这个来看一下。 我们发现平常变量都是独立的,每个对象都是差别的,从地点我们也可以看出来,但是这个静态成员变量是唯一的,多个对象访问的都是同一个静态成员变量,静态成员是可以直接通过类名来访问的不须要实例化对象就可以访问。 1.10 多继续及其菱形继续题目
1.10.1 继续模子
单继续:⼀个派⽣类只有⼀个直接基类时称这个继续关系为单继续
多继续:⼀个派⽣类有两个或以上直接基类时称这个继续关系为多继续,多继续对象在内存中的模子是,先继续的基类在前⾯,后⾯继续的基类在后⾯,派⽣类成员在放到末了⾯。
菱形继续:菱形继续是多继续的⼀种特别环境。菱形继续的题目,从下⾯的对象成员模子构造,可以 看出菱形继续有数据冗余和⼆义性的题目,在Assistant的对象中Person成员会有两份。⽀持多继续就 ⼀定会有菱形继续,像Java就直接不⽀持多继续,规避掉了这⾥的题目,以是实践中我们也是不建议计划出菱形继续如许的模子的。
这就是菱形继续,二义性就是,我们的Student和Teacher都继续了Person的_name变量,导致,我们实例化Assistance的时间,访问_name此时就会堕落了,不知道访问的是Student的照旧Teacher的,此时就出现了二义性,就须要指定类域的方式办理了。
这就是个菱形继续,此时出现了二义性。
办理方法:
指定类域即可,但是数据冗余我们无法办理,我们可以看到,Teacher继续的_name我们现在用不到,没啥用,我们只用一个就行了,冗余了一个。
怎么办理呢?
1.10.2 虚继续
许多⼈说C++语法复杂,实在多继续就是⼀个体现。有了多继续,就存在菱形继续,有了菱形继续就有菱形假造继续,底层实现就很复杂,性能也会有⼀些丧失,以是最好不要计划出菱形继续。多继续可以以为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继续,如Java。
这个就是把两个对象的冗余合并为了一个。
我们知道是Student和Teacher都继续了Person导致的,只须要在这两个类的继续前面加上virtual即可。
就是如图所示。
我们也可以通过调试看出来,确实此时Teacher和Student的_name都是同一个_name了,这只限于自己的派生类继续时如许,创建Student和Teacher对象的时间照旧都是独立的_name。
此时创建的这两个对象的变量照旧独立的值,只会影响下面的派生类的菱形继续。
我们再举个例子明白一下菱形继续。
这显然也是一个菱形继续了,二义性和数据冗余是由于B,C引起的,以是须要在B,C的后面加上virtual。
这是底层的实现,我们就不写代码了,这个不是特别紧张,实践中用的也不多。
我们下面来看一道题吧。
答案是C,下面我们来解说一下。
内存布局
在多继续的环境下,派生类对象的内存布局是按照基类在派生类界说中出现的次序依次分列的,接着是派生类自身的成员。以 Derive 类为例,它先包罗 Base1 的成员,然后是 Base2 的成员,末了是 Derive 自身的成员。内存布局如下:
指针偏移缘故起因
- Base1* p1 = &d;:由于 Base1 是 Derive 继续的第一个基类,p1 指向的地点与 d 对象的起始地点类似,以是 p1 不会发生偏移。
- Base2* p2 = &d;:Base2 是 Derive 继续的第二个基类,Base2 成员在内存中是紧跟在 Base1 成员之后的。为了让 p2 准确指向 Base2 部分的内存,编译器会对 p2 进行偏移操作,使其指向 Base2 成员的起始地点。偏移量就是 Base1 类对象所占的内存巨细。
- Derive* p3 = &d;:p3 是 Derive 范例的指针,它指向 d 对象的起始地点,以是 p3 不会发生偏移。
,简朴明白就是我们上面说的范例转换的切割原理,b2的内存在b1的后面,以是范例转换切割的时间,只切割了b2的内存,以是地点差别,这就是偏移的缘故起因的,简朴来说就是基类的指针要指向自己部分内存的首地点。
我们再来看个例子。
·这个是范例的菱形继续题目,下面我们来思考一个题目。
思考⼀下这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个?
答案是王五。
我们的person构造只在Person类中调用一次,我们调用Student和Teacher的构造的时间,就不会再调用我们Person的构造函数了。
1.11 继续和组合
public继续是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。 • 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
我们来举个组合的例子吧。
这是一个组合关系,stack中有一个vector容器,是has-a的关系,它俩的关系就是组合了,不是继续,继续是is-a的关系,stack不是vector。
• 继续允许你根据基类的实现来界说派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤ (white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继续⽅式中,基类的内部细节对派⽣类可 ⻅ 。继续⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
• 对象组合是类继续之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好界说的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse), 由于对象的内部细节是不可⻅的。对象只以“⿊箱”的情势出现。 组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
• 优先使⽤组合,⽽不是继续。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不外也不太 那么绝对,类之间的关系就得当继续(is-a)那就⽤继续,别的要实现多态,也必须要继续。类之间的关系既得当⽤继续(is-a)也得当组合(has-a),就⽤组合。
下面再来讲一下耦合度吧,好好明白一下。
耦合度就是关联关系,我们在项目中肯定是耦合度越低越好,如许你改变一个类,影响的东西就少了。
举个例子就是,一家人出去旅游,假如一块去坐同一辆车,大概这个有个变乱,谁人有个变乱,大概就会延长,但是假如是两人一辆车,分多辆车去的话,如许,假如两个人有事来不了,但是这个旅游运动照旧可以进行的,影响不到其他人的旅游。
二.竣事语
感谢各人的查察,希望可以资助到各人,做的不是太好还请包涵,此中有什么不懂的可以留言扣问,我都会逐一回复。 感谢各人的一键三连。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|