1.多态
2.多态意义
3.静态联编和动态联编
4.虚析构函数
5.重载、重写、重定义
1.什么是多态
1.1浅析多态的意义
如果有几个看上去相似但不完全相同的对象,有时人们要求在向它们发出同一个消息时,它们的反应各不相同,分别执行不同的操作。这种情况就是多态现象。
例如:甲乙丙3个班都是高二年级,它们有基本相同的属性和行为,在同时听到上课铃的时候,它们会分别走向3个不同的教室,而不会走向同一个教室
同样,如果有两支军队,当在战场上听到同种号声,由于事先约定不同,A军队可能实施进攻,而B军队可能准备防御
C++中所谓的多态(polymorphism)是指,由继承而产生的相关的不同的类,其对象对同一消息会作出不同的相应。
多态性是面向对象程序设计的一个重要特性,能增加程序的灵活度。可以减轻系统升级、维护、调试的工作量和复杂度
1.2赋值兼容(多态实现的前提)
(上一节兼容性规则部分)
赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。
赋值兼容是一种默认行为,不需要任何的显示的转化步骤。
赋值兼容规则中所指的替代包括以下的情况:
- 派生类的对象可以赋值给基类对象。 ︎
- 派生类的对象可以初始化基类的引用。
- 派生类对象的地址可以赋给指向基类的指针。
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
class Parent
{
public:
Parent(int a){
this->a = a;
}
virtual void print(){
//virtual是flag后加的
//这里的virtual虚函数和上一节的virtual虚继承是两个完全不同的概念
cout<<"Parent:print(): a = "<<a<<endl;
}
private:
int a;
};
class Child:public Parent
{
public:
Child(int a,int b):Parent(a){
}
//重定义父类函数:发生在父类和子类之间
//当子类重写父类的成员函数,如果父类中这个函数不是虚函数,那么就是函数的重定义
//如果子类重写父类的成员函数,如果父类中的这个函数时虚函数。这是函数的重写
virtual void print(){
//这里的virtual是为了可读性添加的,代表我重写了父类的方法!
//不写也不报错,但还是建议写
cout<<"Child:print(): b = "<<b<<endl;
Parent::print();
}
private:
int b;
};
//2015
class Child2:public Parent
{
public:
virtual void print(){
cout<<"Child2------.."<<endl;
}
private:
int c;
};
//1999 可以利用多态的特性来调用未来写的对象的方法
void myPrintFunc(Parent *p)//让父类指针指向子类对象的时候
{
p->print();//在此时,print函数发生了多态现象
}
int main(void)
{
Child c(10,20);
c.print();//调用了Child::print()
Parent p(100);
p.print();//调用了Parent::print();
cout<<"--------------------------"<<endl;
myPrintFunc(&p);//希望调用父类的print
cout<<"--------------------------"<<endl;
myPrintFunc(&c);//希望调用子类的print(但是失败了),Parent *p = &c;父类指针指向子类对象是可以的,这里是调用了父类的print
//编译器 会 不管你传递进来的父类对象还是子类对象,都会给你调用父类
//编译器做了一个安全的举措,即使你传递的是子类,编译器也认为调用父类的print是安全的
//它能够保证父类的print函数是一定存在的,没有毛线去调用子类的print
//flag--------------------------
//加上virtual关键字后,print成为了虚函数,当父类指针指向子类对象时,编译器不会调用父类方法,而是去调用子类方法
Child2 c2;
myPrintFunc(&c2);
return 0;
}
1.3多态练习
class Hero
{
public:
//当前hero的战斗力是10
virtual int getAck(){
return 10;
}
};
//超级英雄
//多态具有调用未来的意义
class SuperHero:public Hero
{
public:
virtual int getAck(){
return 100;
}
}
//怪兽
class Monster
{
public:
int getAck(){
return 30;
}
};
//战斗的函数
void PlayerFight(Hero *hero,Monster *m)
{
//多态这种现象较 动态联编 是迟绑定 或是 晚绑定
if(hero->getAck() > m->getAck(){//在此hero->getAck()就发生了多态
cout<<"英雄战胜了怪兽"<<endl;
}
else{
cout<<"英雄挂了"<<endl
}
}
int main(void)
{
Hero hero1;
Monster mon1;
SuperHero hero2;
//开始战斗
PlayerFight(&hero1,&m);
PlayerFight(&hero2,&m);//Hero *hero = &hero2;
//指针的三个必要条件
int a = 10;
int *p = NULL;
p = &a;
*p;
//多态的三个必要条件
//1 要有继承
//2 要有子类重写父类的虚函数
//3 父类指针(或者引用)指向子类对象
return 0;
}
2.多态工程的意义
封装:突破了C语言函数的概念。
继承:代码复用,复用原来写好的代码。
多态:
- 多态可以使用未来,80年代写了一个框架,90人写的代码。
- 多态是软件行业追寻的一个目标。
多态是设计模式的基础,多态是框架的基础
3.静态联编和动态联编
1、联编是指一个程序模块、代码之间互相关联的过程。
2、静态联编(staic binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。
3、动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。switch语句和if语句是动态联编的例子。
1、C++与C相同,是静态编译型语言
2、在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
3、由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象
从程序安全的角度,编译器假设父类指针只指向父类对象因此编译的结果为调用父类的成员函数。这种特性就是静态联编。
4、多态的发生是动态联编,实在程序执行的时候判断具体父类指针应该调用
的方法。
4.虚析构函数
- 构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数。
- 析构函数可以是虚的。虚析构函数用于指引delete运算符正确析构动态对象
class A
{
public:
A(){
cout<<"A()...."<<endl;
this->p = new char[64];//给p开辟了空间
memset(this->p,0,64);
strcpy(p,"A construction");
}
virtual void print(){
cout<<this->p<<endl;
}
virtual ~A(){
cout<<"~A() ...."<<endl;
if(p!=NULL){
delete[] p;
p = NULL;
}
}
private:
char *p;
}
class B:public A
{
public:
B(){
cout<<"B()...."<<endl;
this->p = new char[64];
memset(this->p,0,64);
strcpy(this->p,"B construction");
}
virtual void print(){
cout<<this->p<<endl;
}
virtual ~B(){
cout<<"~B() ...."<<endl;
if(p!=NULL){
delete[] p;
p = NULL;
}
}
private:
char *p;
}
class C:public B
{
public:
C(){
cout<<"C()...."<<endl;
this->p = new char[64];
memset(this->p,0,64);
strcpy(this->p,"C construction");
}
virtual void print(){
cout<<this->p<<endl;
}
virtual ~C(){
cout<<"~C() ...."<<endl;
if(p!=NULL){
delete[] p;
p = NULL;
}
}
private:
char *p;
}
void func(A *p)
{
p->print();//在此处发生多态
delete p;//delete一个父类指针,崩了!这里把父类指针释放了
//如何让delete p不是将p看做父类指针来delte而是当子类呢?
//flag-------------------------
//改为虚析构函数
//如果类的析构函数加上virtual delte就会发生多态。delete p会调用C类的析构函数,之后就会自动依次析构了
//如果~() 不加 virtual关键字,不会发生多态,就只是调用父类析构函数
}
void test(){
C c;//A->B->C陆续被构造
}//析构顺序 ~C-> ~B -> ~A
int main(void)
{
// test();
C *cp = new C;
cp->print();//cp的函数
delete cp;
cout<<"---------------------------"<<endl;
//C c;
//func(&c);//这里改成虚析构之后,还是报错,因为这里的c是栈变量,出栈之后会再释放一次!
C *cp1 = new C;//创建为堆的指针就可以防止栈释放了!
func(cp1);
return 0;
}
5.重载、重写、重定义
5.1重载(添加)
- 相同的范围(在同一个类中)
- 函数名字相同
- 参数不同
- virtual关键字可有可无
5.2重写(覆盖)
是指派生类函数覆盖基类函数,特征是:
- 不同的范围,分别位于基类和派生类中
- 函数的名字相同
- 参数相同
- 基类函数必须有virtual关键字
5.3重定义(隐藏)
是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
- 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。