继承、多态知识概括
继承
所谓继承,也就是在已有类的基础上创建新类的过程,适当的使用继承可以节省代码量,优化整个程序的结构。
派生类的生成过程
派生类的生成过程经历了三个步骤:
●吸收基类成员(全部吸收(构造、析构除外),但不一定可见)
在C++的继承机制中,派生类吸收基类中除构造函数和析构函数之外的全部成员。但是不论种方式继承基类,派生类都不能直接使用基类的私有成员 。
公有继承中,派生类对基类的所有成员进行复制(仅把数据的“型”复制,数据的“值”之间没有关系),在此基础上在添加新的成员。
注意:基类和派生类的同名成员一共保留两份副本,访问时通过类名 :: 成员
方式访问基类的成员
基类定义的静态成员,将被所有派生类共享(基类和派生类共享基类中的静态成员)(共用同一空间,数据同步修改) 派生类中访问静态成员,显式说明类名访问类名 :: 成员
或通过对象访问 对象名 . 成员
●改造基类成员
通过在派生类中定义同名成员(包括成员函数和数据成员)来屏蔽(隐藏)在派生类中不起作用的部分基类成员。
派生类定义了与基类同名的成员,在派生类中访问同名成员时屏蔽(hide)了基类的同名成员
在派生类中使用基类的同名成员,需显式地使用类名限定符:
类名 :: 成员
●添加派生类新成员
定义派生类对象时,只是复制基类的空间但是没有赋值
基类的初始化:
派生类构造函数 ( 变元表 ) : 基类 ( 变元表 ) , 对象成员1( 变元表 )… 对象成员n ( 变元表 )
{
// 派生类新增成员的初始化语句
}
构造函数执行顺序:基类 ->对象成员->派生类
基类成员调用基类构造函数对其进行初始化
(1)当派生类中不含对象成员时
●在创建派生类对象时,构造函数的执行顺序是:基类的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序是:派生类的析构函数→基类的析构函数。
(2)当派生类中含有对象成员时
●在定义派生类对象时,构造函数的执行顺序:基类的构造函数→对象成员的构造函数→派生类的构造函数;
●在撤消派生类对象时,析构函数的执行顺序:派生类的析构函数→对象成员的析构函数→基类的析构函数。
析构函数的执行顺序与构造函数的执行顺序完全相反
多继承
class 派生类名(参数总表) : 访问控制 基类名1 (参数表1), 访问控制 基类名2 (参数表2), … , 访问控制 基类名n (参数表n)
{
数据成员和成员函数声明
// 派生类新增成员的初始化语句
};
多继承方式下构造函数的执行顺序:
处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的基类顺序,与派生类构造函数中所定义的成员初始化列表顺序没有关系。
●先执行所有基类的构造函数
●再执行对象成员的构造函数
●最后执行派生类的构造函数
二义性
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性。也就是如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象。
要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为虚基类,虚继承声明使用关键字virtual
赋值兼容规则
在程序中需要使用基类对象的任何地方,都可以用公有派生类的对象来替代。也就是基础数据类型中形如double->int转型,强制类型转换。
赋值兼容规则中所指的替代包括以下的情况:
a 派生类的对象可以赋给基类对象
b 派生类的对象可以初始化基类的引用
c 派生类的对象的地址可以赋给基类类型的指针
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
如想用派生类指针引用基类对象,派生类指针只有经过强制类型转换之后,才能引用基类对象 。
基类指针和派生类指针与基类对象和派生类对象4种可能匹配:
- 直接用基类指针引用基类对象;
- 直接用派生类指针引用派生类对象;
- 用基类指针引用一个派生类对象;
- 用派生类指针引用一个基类对象。
多态
多态性(Polymorphism)是指一个名字,多种语义;或界面相同,多种实现。
多态性的实现和联编这一概念有关。所谓联编(Binding,绑定)就是把函数名与函数体的程序代码连接(联系)在一起的过程。联编分成两大类:静态联编和动态联编。
静态联编优点:调用速度快,效率高,但缺乏灵活性;动态联编优点:运行效率低,但增强了程序灵活性。
C++为了兼容C语言仍然是编译型的,采用静态联编。为了实现多态性,利用虚函数机制,可部分地采用动态联编。
多态从实现的角度来讲可以划分为两类:编译时的多态和运行时的多态。
编译时的多态是通过静态联编来实现的。静态联编就是在编译阶段完成的联编。编译时多态性主要是通过函数重载和运算符重载实现的,是程序的匹配、连接在编译阶段实现(如函数重载)
运行时的多态是用动态联编实现的。动态联编是运行阶段完成的联编。运行时多态性主要是通过虚函数来实现的,程序联编推迟到运行时进行(如switch、if语句)
静态联编
普通函数重载的两种方式:
1、在一个类说明中重载
2、基类的成员函数在派生类重载
(1)根据参数的特征加以区分
(2)使用“ :: ”加以区分
(3)根据类对象加以区分
动态联编
根据赋值兼容,用基类类型的指针指向派生类,就可以通过这个指针来使用类(基类或派生类)的成员函数。
如果这个函数是普通的成员函数,通过基类类型的指针访问到的只能是基类的同名成员。
而如果将它设置为虚函数,则可以使用基类类型的指针访问到指针正在指向的派生类的同名函数。从而实现运行过程的多态。
实现动态联编方式的前提:
●先要声明虚函数
●类之间满足赋值兼容规则
●通过指针与引用来调用虚函数。
实现多态的两个条件:
1、类的定义时存在继承关系,存在虚机制,派生类重写虚函数
2、运行时,以基类对象的指针或引用,将派生类对象绑定给基类对象或指针,并运行虚函数
有关虚函数:
- 一个虚函数,在派生类层界面相同的重载函数都保持虚特性
- 虚函数必须是类的成员函数
- 不能将友元说明为虚函数,但虚函数可以是另一个类的友元
- 析构函数可以是虚函数,但构造函数不能是虚函数
关于重载和覆盖:
- 重载是函数名称相同,参数个数、类型不同,返回值相同(返回值不能区分函数的重载,如果返回值类型不同会被c++认为错误重载)
- 覆盖是函数名、返回类型、参数个数、参数类型和顺序完全相同,如果函数原型不同,仅函数名相同,丢失虚特性。
纯虚函数和抽象类
- 纯虚函数是一个在基类中说明的虚函数,在基类中没有定义, 要求任何派生类都定义自己的版本,为各派生类提供一个公共界面。
- 纯虚函数说明形式:
virtual 类型 函数名(参数表)= 0 ;
- 一个具有纯虚函数的基类称为抽象类。
- 定义抽象类不能直接生成对象,但是可以生成指针和引用
- 派生抽象类时,一定要重写纯虚函数
实际使用
在我们实际使用过程中,首先根据需求设计出类,分析这些类,看一下这些类里面是否有重复的部分,如果有,那么就将重复的部分抽象出来形成一个基类,在需要使用这些重复功能的子类中继承基类,从而节省了代码量。
以图书管理系统为例,在上一个版本中,实现了管理员端,在管理员端中有需要用到查找的地方,比如管理员对书、记录等的查找,而在现在的版本中实现了客户端,在用户使用的时候也会对书籍、记录等信息进行查询,这两部分代码的查找部分是重复的, 所以将这两部分抽象出一个新的基类,然后管理员、客户端分别继承这个基类,从而实现查找的部分功能,在继承查找功能后,再对某些功能进行修改改造,比如在客户端中,查找读者的借阅记录只能查询本人的,那么需要在派生类中对改函数进行重写覆盖;另外客户端和管理员端本身还有其他的功能,比如管理员的对书和读者的增删查改啊,客户端的借、续、还操作啊,都是需要在派生类中新添加的。
class Find
{
protected:
vector<Book> book;
vector<Record> record;
multimap<string, int>bookname;
multimap<string, int>bookid;
multimap<Time, int>booktime;
multimap<string, int>bookpub;
multimap<string, int>bookwriter;
multimap<string, int>bmap;
multimap<string, int>rmap;
public:
Find();
~Find();
void getBRecord(string id);
void getRRecord(string id);
void findBookByName(string name);
void findBookById(string id);
void findBookByPub(string pub);
void findBookByPDate(Time date);
void fuzzyFindBname(string str);
void multifind1(string str1, string str2);//str1是作者,str2是书名
void multifind2(string str1, string str2);//str1是出版社,str2是作者
void findBookByTime(Time t1, Time t2);
};
Find::Find() {}
Find ::~Find() {}
void Find::multifind1(string str1, string str2)
{
//实现过程略
}
void Find::multifind2(string str1, string str2)
{
//实现过程略
}
void Find::fuzzyFindBname(string str)
{
//实现过程略
}
void Find::findBookByTime(Time t1, Time t2)
{
//实现过程略
}
void Find::findBookByName(string name)
{
//实现过程略
}
void Find::findBookByPub(string name)
{
//实现过程略
}
void Find::findBookByPDate(Time str)
{
//实现过程略
}
void Find::findBookById(string id)
{
//实现过程略
}
void Find::getBRecord(string id)
{
//实现过程略
}
void Find::getRRecord(string id)
{
//实现过程略
}
class Operation :public Find
{
private:
Reader reader;
Time time;
public:
Operation(Reader r, Time t)
{
loadBook();
loadRecord();
reader = r;
time = t;
}
~Operation()
{
saveBook();
saveRecord();
}
void loadBook();
void loadRecord();
void saveBook();
void saveRecord();
void getRRecord();
void borrowBook(string str);
void renewBook(string str);
void returnBook(string str);
//void bookDisplay();
};
void Operation::loadBook()
{
//实现过程略
}
void Operation::saveBook()
{
//实现过程略
}
void Operation::loadRecord()
{
//实现过程略
}
void Operation::saveRecord()
{
//实现过程略
}
void Operation::getRRecord()//重写函数
{
Find::getRRecord(this->reader.getSno());
}
void Operation::borrowBook(string str)
{
//实现过程略
}
void Operation::renewBook(string str)
{
//实现过程略
}
void Operation::returnBook(string str)
{
//实现过程略
}
在使用过程中注意,定义派生类对象时,只是复制基类的空间但是没有赋值,所以在使用派生类对象时,操作的都是派生类本身生成的对象空间,和基类无关。函数也是如此,仅仅是将基类的函数代码复制到派生类中。
当然,作为面向对象中三大特性中的继承和多态,其作用远远不止节省代码这么简单,在设计模式中,其主要思想就是利用继承和多态,实现更优的软件框架,从而具有更好的可扩展、可复用、可维护性,进而将编程的过程视为“工程”,提高代码的规范性,最终提高效率。
下面以C++实现抽象工厂模式为例,简单说明继承和多态的实际应用
#include<iostream>
using namespace std;
//真正的车
class AbstractRealCar
{
public:
virtual void run() = 0;
};
//玩具车
class AbstractToyCar
{
public:
virtual void run() = 0;
};
//BMW
class RealBMW : public AbstractRealCar
{
public:
virtual void run()
{
cout << "real BMW run!" << endl;
}
};
//BenZ
class RealBenZ : public AbstractRealCar
{
public:
virtual void run()
{
cout << "real BenZ run!" << endl;
}
};
//BMW
class ToyBMW : public AbstractToyCar
{
public:
virtual void run()
{
cout << "toy BMW run!" << endl;
}
};
//BenZ
class ToyBenZ : public AbstractToyCar
{
public:
virtual void run()
{
cout << "toy BenZ run!" << endl;
}
};
//抽象工厂
class AbstractFactory
{
public:
virtual AbstractRealCar* CreateRealCar() = 0;
virtual AbstractToyCar* CreateToyCar() = 0;
};
class BMWFactory : public AbstractFactory
{
public:
virtual AbstractRealCar* CreateRealCar()
{
return new RealBMW();
}
virtual AbstractToyCar* CreateToyCar()
{
return new ToyBMW();
}
};
class BenZFactory : public AbstractFactory
{
public:
virtual AbstractRealCar* CreateRealCar()
{
return new RealBenZ();
}
virtual AbstractToyCar* CreateToyCar()
{
return new ToyBenZ();
}
};
void test01()
{
AbstractFactory* factory = NULL;
factory = new BMWFactory;
AbstractRealCar* real_car = factory->CreateRealCar();
real_car->run();
delete real_car;
real_car = NULL;
AbstractToyCar* toy_car = factory->CreateToyCar();
toy_car->run();
delete toy_car;
toy_car = NULL;
delete factory;
factory = NULL;
factory = new BenZFactory;
real_car = factory->CreateRealCar();
real_car->run();
delete real_car;
real_car = NULL;
toy_car = factory->CreateToyCar();
toy_car->run();
delete toy_car;
toy_car = NULL;
delete factory;
factory = NULL;
}
int main()
{
test01();
return 0;
}
其UML类图
所以,在适当的时候利用继承和多态可以更好的发挥出面向对象编程的优点
学习感想
至此,本学期的C++理论课程已接近尾声,虽然短短的一学期学到的可能也只是C++的一部分,但是学到现在也可以站到整个语言的角度来和其他语言做对比,在学C++继承和多态的过程中,由于上个学期学过了java语言,又零零散散地接触了一些其他的语言,C++语言给我的印象是“功能全面但过于臃肿”。C++是基于C语言改编的,所以在很多场景都少不了C语言的影子,而C++本身又是一门面向对象的语言,所以更少不了面向对象的功能,更何况C++考虑的还很全面,照顾到了各种情况,许多java等纯面向对象的语言没有的功能C++都提供了,这样一组合造成了C++的臃肿,所以有一些功能其实虽然有,但是并不好用。比如前面的友元就很鸡肋,破坏了好不容易才封装的一个类,所以只有重载运算符等特殊情况才会用,还有继承这里的多继承,java是没有多继承的,只有对多接口的实现,很好的避免了二义性的问题,而C++提供了完整的多继承方案,十分强大,虽然会产生二义性,但是还特地提供了虚基类来解决二义性问题,那么强大的功能为啥不好呢?因为其在使用过程中非常的麻烦,而且多继承的情况很少,不太实用。综上,我个人现阶段认为C++的特点是“功能强大,使用复杂”。所以,我认为C++作为初学编程的小白的敲门砖是很合适的,因为可以通过C++全面的了解编程的整体思路,熟悉各类功能的具体用法,毕竟虽然语言的语法不同,但是功能基本相通,学好了C++以后再学什么编程语言我相信都是“轻车熟路”。
下面谈一下近期的系统开发收获,最近系统开发这方面最近有些放松,但是学到继承多态这里发现,继承和多态对一个系统来说十分重要。像上面介绍其应用时所说,一方面可以节省代码量,另一方面还可以优化代码的结构,使得我们的代码可扩展、可复用、可维护。所以,在今后的编程中,一定不要忘记多态的合理使用。对于这种的系统开发,应该首先根据需求功能实现出来几个类,然后再观察这些类是否有重复的部分,如果有则将其共有的部分抽象出来,形成一个新类做父类,在需要使用这些重复功能的子类中继承父类,从而节省了代码量。也就是说,我们在开发系统的过程中,需要从下向上的进行,而不能从父类向下写,那样有可能出现考虑不周的问题。在完成类的抽象后,再分析类与类之间的关系,如果类之间功能相近,可以考虑构造抽象类、虚函数,从而实现多态。
最后反思一下最近的学习状态,开学的时间突然公布,这是我意想不到的,前两天我还估计山东省开学一定会在全国两会之后,接着没多久就打脸了,虽然最终到底能不能返校尚不清楚,但是瞬间感到了压力。因为一回到学校,生活可能会有各种不便,另一方面是估计一返校就要迎接不少考试,还有已经拖了很久的工作上的问题,所以一返校估计也是忙的不可开交。这就导致最近学习有点小浮躁,总是关注各种群,各种小道消息,但很多消息之间是相互矛盾的,可信度也没有保证,这两天一想,其实这么关注开学也没有用,开不开学也不是我能决定的,该啥时候开学我不想去也得去,关注这些小道消息耗费精力没有一点用处,还不如现在专心学习,啥时候通知返校了啥时候回去就是,我唯一能做的是尽量提前做一些准备工作,如果返校的话也不至于太忙。另外,现在已经13周了,一部分课程已经结课了,有考试的压力,还有几篇论文压着没写,另外课外的东西好长时间没学了,也想抽个时间练练手,20年建模下通知了,也要暑假前练一下,往年按理说现在应该有不少的建模模拟比赛,今年到底什么样还都不明朗。所以虽然课程少了,但压力没少多少,还是需要保持一个紧张的学习状态。今年的疫情打乱了所有的安排,但是我相信今年虽然比赛少了,但是在这个时间不为了拿奖,安心学习知识,提高能力,以后可能拿到的奖含金量更高。到现在感觉时间是真快啊,时间才是最宝贵的东西,如果不返校的话,我们回去的时候就是大三的老学长了,说的严峻点也就是还有一年的大学生活,所以还是希望我自己不要忘记初心,不要松懈,一方面把课内的知识学好,另一方面不要把自己的理想放松,珍惜宝贵的时间,多学点知识,加油!