https://blog.youkuaiyun.com/lixungogogo/article/details/51138493【多态篇,包含系列专题(重载覆盖等),大牛】
继承
1.基础概念
a.类展现了C++的封装特性,即将具体的实现过程隐藏,只向外暴露公有接口,即数据抽象,通过数据抽象,我们可以将类的接口与实现分离,(即设计类)。
b.与C相比,类可以通过相互间的继承和派生形成层次结构,派生类继承了基类的数据结构和方法 [编译时]。
c.使用继承,可以定义相似的类型并对其相似关系建模。
e.继承体现了OOP中事物的普遍性和特殊性。
f.OOP强调软件的可重用性(software reuseablility).C++提供类的继承机制,解决了软件重用的问题。
而动态绑定即体现了多态的思想。
g.派生类对象包含基类对象,使用公有继承,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
2.覆盖(override)、隐藏(hide)、重载(overload)
a.成员函数被重载的特征
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
//virtual关键字在这里看不懂也可以,在下一篇文章中我会详细解答
b.覆盖是指派生类函数覆盖基类函数,特征是
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,这种机制就叫做覆盖。
派生类对象调用的是派生类的覆盖函数
指向派生类的基类指针调用的也是派生类的覆盖函数
基类的对象调用基类的函数
c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数(本质是因为作用域的原因。无论局部隐藏、基类与派生类之间的隐藏都是因为作用域的缘故。)
https://bbs.youkuaiyun.com/topics/390745300(比较好的隐藏讨论)
实现:(运用Name Lookup & Best Match规则)
C++ Primer 隐藏规则的底层原因其实是C++的名字解析过程。在继承机制下,派生类的类域被嵌套在基类的类域中。派生类的名字解析过程如下: (编译器会在该类中查找函数,如果找不到,就沿着继承链向上找
这个查找只是名字查找,不涉及到参数的检查,只要找到了函数的名字,就不向上找了(即使它的参数是错误的))
1、首先在派生类类域中查找该名字。
2、如果第一步中没有成功查找到该名字,即在派生类的类域中无法对该名字进行解析,则编译器在外围基类类域对查找该名字的定义。
避免被隐藏:
1.加作用域 obj.A::f(1);
2.using
规则:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
//例子
在谭浩强的C++程序设计这本书第十一章,351页最下面有这么一段话:
可在派生类中声明一个与基类成员同名的成员函数,则派生类中的新函数会覆盖基类的同名成员,但应注意:如果是成员函数,不仅应是函数名相同,而且函数的参数表(参数的个数和类型)也应相同,如果不相同,就会成为函数重载而不是覆盖了、用这样的方法可以用新成员取代基类的成员。
但是经过实验,这段话就是错误的,派生类中定义与基类成员函数同名不同参数表的函数是不能构成函数重载的
还有在《Effective C++》第三版第33条“Avoid hiding inherited names”,也提到过隐藏
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
派生类
1.特征:
1.派生类对象存储了基类的数据成员(派生类继承了基类的实现)
2.派生类对象可以使用基类的方法(派生类继承了基类的接口)
那么我们还需要给派生类添加什么呢?
1.派生类需要自己的构造函数
2.派生类可以根据需要添加额外的数据成员和成员函数。
2.构造函数
https://blog.youkuaiyun.com/qq_33757398/article/details/81331918(构造函数和初始化列表的区别)
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。
即派生类构造函数必须使用基类构造函数。
构造函数不同于其他类的方法,因为它创建新的对象,而其他类的方法仅仅是被现有的对象调用,这是构造函数不能被继承的一个原因。继承意味着派生类对象可以使用基类的方法,然而构造函数在完成其工作之前,对象并不存在。在创建派生类对象时,程序首先创建基类对象,即基类对象应当在程序进入派生类构造函数之前被创建。
创建顺序:
单继承:这就说明了,在创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数,而析构的顺序相反。因为在创建时是在栈内进行的,栈有着先进后出的属性,所以先创建的后析构,后创建的先析构。
多继承: 这就是在上一篇我曾讲过的,多继承中继承列表与派生类对象模型的关系。多继承时派生类的对象模型是与继承列表的顺序相关的。
也可以理解为,因为派生类的对象模型中,基类成员在模型的最上面,所以要先调用基类的构造函数,再调用派生类的构造函数。
在上述代码中,基类是由默认的构造函数的,我们在Base的构造函数中给了它缺省值,那么,如果基类没有默认的构造函数,可以吗?答:编译不通过,给出的错误为no appropriate default constructor available
基类中没有可用的构造函数,那么在创建派生类对象时,如果没有默认的构造函数,我们如何在创建派生类对象之前,需要先创建基类对象
还记得C++中的成员初始化列表吗?
在C++中,成员初始化列表句法可以完成这个工作。
总结以下上面的内容:
1.基类对象首先被创建
2.派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
3.派生类构造函数应初始化派生类新增的数据成员(这个在上面代码中没有体现)**
创建派生类对象时,程序首先调用基类的构造函数,然后在调用派生类的构造函数,(与派生类对象模型有关),基类构造函数负责初始化派生类继承的数据成员,派生类的构造函数主要用于初始化新增的数据成员。
派生类的构造函数总是低于一个基类构造函数。
可以使用初始化列表句法指明要使用的基类构造函数,否则将使用默认的基类构造函数。
派生类对象析构时,程序首先调用派生类析构函数,再调用基类析构函数。
3.拷贝构造函数(也称复制构造函数)
定义:拷贝构造函数接受其所属类的对象为参数。
在下述情况下,将使用拷贝构造函数
1.将新的对象初始化为一个同类对象
2.按值将对象传递给函数
3.函数按值返回对象
4.编译器生成临时对象
如果程序没有显式定义拷贝构造函数,编译器将自动生成一个。
当然,如果想在派生类中构造基类对象,那么不仅仅可以用构造函数,也可以用拷贝构造函数
注意:
如果没有给基类定义拷贝构造函数,编译器会自动给基类生成了一个拷贝构造函数(浅拷贝)
1.基类中定义的没有指针成员,所以浅拷贝可以满足要求
2.在基类成员中有指针变量,必须要进行显式定义拷贝构造函数,即进行深拷贝。不然会造成同一块内存空间被析构两次的问题。
4.赋值操作符
默认的赋值操作符用于处理同类对象之间的赋值,赋值不是初始化,如果语句创建新的对象,则使用初始化,如果语句修改已有对象的值,则为赋值。
注意:赋值运算和拷贝构造是不同的,赋值是赋值给一个已有对象,拷贝构造是构造一个全新的对象
赋值运算符是不能被继承的,原因很简单。派生类继承的方法的特征与基类完全相同,但赋值操作符的特征随类而异,因为它包含一个类型为其所属类的形参。
如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值操作符。这个操作符的默认版本将采用成员赋值,即将原对象的相应成员赋给目标对象的每个成员。
如果对象属于派生类,编译器将使用基类赋值操作符来处理派生对象中基类部分的赋值,如果显示的为基类提供了赋值操作符,将使用该操作符。
例1:将派生类对象赋给基类对象
实际上,赋值语句将被转换成左边的对象调用的一个方法
a.operator=(d);
//左边的为基类对象
- 简而言之,可以将派生对象赋给基类对象,但这只涉及到基类的成员。
例2:基类对象赋给派生类对象
上述赋值语句将被转换为:
d.operator=(a);
//Derive::operator=(const Derive&)
- 左边的对象为派生类对象,不过派生类引用不能自动引用基类对象,所以上述代码不能运行。或者运行出错。
-
除非有下面的函数
Derive(const Base&) {}
总结:
- 是否可以将基类对象赋给派生类对象,答案是也许。如果派生类包含了转换构造函数,即对基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生对象。
- 派生类对象可以赋给基类对象。
4.继承与转换——赋值兼容规则(public继承)
公有继承条件下
派生类和基类之间的特殊关系为:
1.派生类对象可以使用基类的方法,条件是基类的方法不是私有的
2.基类指针可以在不进行显示类型转换的情况下指向派生类对象
3.基类引用可以再不进行显示类型转换的情况下引用派生类对象,但是基类指针或引用只能用于调用基类的方法,不能用基类指针或引用调用派生类的成员及方法
void FunTest(const Base&d)
{
}
void FunTest1(const Derive&d)
{
}
int main()
{
Derive d;
Base b(0);
b = d;//可以
d = b;//不行,访问的时候会越界
//上面两行代码在上一条中已经解释过了
FunTest(b);
FunTest(d);
FunTest1(b); //不可以
FunTest1(d);
Base* pBase = &d;
Derive*pD = &b;//错了
//如果非要这么做只能通过强制类型转换
Derive*pD = (Derive*)&b;//如果访问越界,会崩溃
}
通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是个例外。但是这个例外是单向的,即仅仅不可以将基类对象和地址赋给派生类引用和指针。
如果允许基类引用隐式的引用派生类对象,则可以使用基类引用为派生类对象调用基类的方法,因为派生类继承了基类的方法,所以这样不会出现问题。
但是如果可以将基类对象赋给派生类引用,那么派生类引用能够为基类对象调用派生类方法,这样做会出现问题,例如:用基类对象调用派生类中新增的方法,是没有意义的,因为基类对象中根本没有派生类的新增方法。
1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。
2) 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。
3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的(delete 指向子类的父类指针时,不会析构子类)。
4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。(与第5点结合)
5) 纯虚函数通常没有定义体,但也完全可以拥有。
6) 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
7) 非纯的虚函数必须有定义体,不然是一个错误。
8) 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。
9)基类private纯虚函数:在C++中virtual表示多态,public、protected和private表示访问权限(),它们是正交的两个概念(java不同)
实际使用场景:
当你实现一个算法的时候,通常不希望将实现信息暴露给外部和给外部使用,包括将会被继承的类。但是,算法有要有扩充性,可以针对不同的信息实现不同的结果。怎么办?私有的虚函数:这个函数只有我可以使用,但是却能够迟后决定调用的具体函数。从而在封装上和扩展性能上都有比较好的性能
a.纯虚函数可以设计成私有的,不过这样不允许在本类之外的非友元函数中直接调用它,子类中只有覆盖这种纯虚函数的义务,却没有调用它的权利
b.虚函数的重载性和它声明的权限无关(c++ primarily)
c.虚函数是通过虚表来实现的。虚表可以理解为一个函数指针数组,编译器会根据函数名称和原型找到对应函数在虚表中的index. 派生类可能override了基类的实现,这样只是虚表中的函数指针变了,索引并没变。又因为是通过基类的指针调用的,访问权限也是按照基类的。很简单的东西,要解释清楚却很费事 :)
d.这个问题与多态实现机制无关,试图从虚表角度去解析的行为不过是自我形成的错觉。真正也是唯一的原因是:可访问性是静态解析的,不是动态解析的。即是说,无论最终覆盖者的可访问性如何,可访问性均取决于函数调用后缀表达式的可访问性。派生类中虚函数的访问权限是在编译阶段由基类确定的,在运行阶段不再检查访问权限,所有,虚函数的访问权限与派生类就没关系了
e.d.结论
=====以下待验证
纯虚函数的在派生类中的实现不受限于它声明时的权限。
e. 提出问题:
1. 基类中公有虚函数是否可以被派生类protected或者private 覆盖?
答:可以,派生类可以访问基类公有虚函数,故可以实现覆盖。
2. 基类中私有纯虚函数是否可以被派生类public 覆盖?
答:不可以,派生类不能访问基类私有虚函数。
————————————————
版权声明:本文为优快云博主「tjian2014」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/tjian2014/article/details/17301249