C++(五):多态性和虚函数

多态性

多态概念

1.多态:即向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)
2.C++中的表型形式之一:具有不同功能的函数可以用统一个函数,这样就可以用一个函数名调用不同内容的函数。

静态多态性

1.通过函数重载运算符重载实现的,叫静态多态性
2.要求程序编译时就知道调动函数的全部信息
3.在程序编译时,就决定调用的是哪个函数,静态多态性
4.优缺点:调用函数快,效率高,缺乏灵活性

动态多态性

1.通过虚函数实现的多态,叫动态多态性
2.是运行时才决定调用的函数的多态
3.根据具体的执行情况来动态地确定
4.实现这种动态的多态性时,必须使用基类类型的指针变量,并使该指针指向不同的派生类对象,并通过调用指针所指向的虚函数才能实现动态的多态性。
在这里插入图片描述

虚函数

虚函数

原因:
我们想要用同一个语句p->fun(),既可以调用父类的函数fun(),也可以调用子类的同名函数fun(),只需在基类指针p赋值为不同对象,就可以调用不同函数

  1. 所谓虚函数就是在基类中声明的函数但不定义,在子类中才去定义它

  2. 虚函数作用是:当基类成员和派生类成员有函数相同名,包括参数也一样,但是函数体内容不同时,想要调用派生类的同名成员函数时的解决方法;即在基类指针指向派生类对象的情况下,函数若是虚函数可使用派生类成员函数,没有虚函数你只能用基类的函数除非用显示调用等方法才可以调用派生类的成员函数(结合例1,例2了解)

  3. 原理:基类中声明的虚函数,在声明派生类时被重载,这时派生类的同名函数就取代了其基类中的虚函数

  4. 声明为指向基类对象的指针,当他指向派生类对象时,只能利用它来直接访问派生类中从基类继承的成员,不能直接访问派生类中为他成员。若想访问其公有派生类的其他成员,可以将基类指针显示类型转换为派生类指针类型来实现,或显示调用

显示调用
Father *p;
Sun c;
p=&c;
p->Father::show();//父类函数
p->sun::show();//子类函数
  1. 基类指针可以指向派生类对象 ,但反过来派生类指针是不能指向基类指针的
  2. 多态性:一个基类指针可以指向不同派生类对象,实现同个函数名调用,却输出不同内容
  3. 函数重载是同一层次(同个类)的横向重载,而虚函数处理的是不同派生层次上的同名函数问题,是纵向重载(不同类);但前者函数类型可以不一样只要函数名和参数一致就行,而后者需要完全一致(包括返回类型等要一致)

例1:没有使用虚函数情况下

#include<iostream>
using namespace std;
class base
{
	public:
		void who()   如果这里定义为虚函数则结果可能不一样
		{
			cout<<"这是 base"<<endl;
		}
};

class level1:public base
{
	public:
		void who()
		{
			cout<<"这是 level1"<<endl; 
		}
};
int main()
{
	base obj0,*p; 定义基类对象 和 基类指针 
	level1 obj1;
	p=&obj0; 让基类指针指向基类对象
	p->who();
	p=&obj1; 基类指针指向 派生类对象
	p->who();  但它这样并不是输出派生类的who 函数,还是基类
	obj1.who(); 除非这样显示调用
	((level1*)p)->who();或将基类指针 p 强制转化会派生类指针 
};

结果


这是 base
这是 base
这是 level1
这是 level1

例2:使用虚函数情况下

#include<iostream>
using namespace std;
class base
{
	public:
	virtual	void f1()  虚函数定义
		{
			cout<<"这是 base1"<<endl;
		}		
	virtual void f2()
		{
			cout<<"这是 base2"<<endl;
		}
	void f3()   不是虚函数
	{
	  cout<<"这是base3"}
};

class level1:public base
{
	public:
		void f1()
		{
			cout<< "this level1"<<endl; 
		} 仍为虚函数 
		void f2(int a)
		{
			cout<<a<<endl;
		} 失去虚特性 ,变为重载函数 
		int f2(); 这样会报错
	void f4()
	{ 
	cout<<"level3"<<endl;
	}
};
int main()
{
	base *p; 基类指针
	level1 obj1;  派生类对象
	p=&obj1; 
	p->f1();重新定义了虚函数所以可以输出派生类的新内容 
	p->f2();丢失了虚函数,所以只能用基类的虚函数代码
	p->f3();输出的是基类成员函数,(相比下有虚函数和没有的区别) ;为什么是基类呢? 因为 p 指针是基类指针,
          	他会在基类找到跟派生类f3()一样的成员函数
	 
}; 

结果:
this level1
这是 base2 
这是base3
  1. 虚函数是在基类和派生类中说明相同而实现不同的成员函数,在派生类中重新定义基类中的虚函数时,可以不加virtual因为虚特性可以传递,但函数声明的原型必须与基类中完全相同,否则失去虚特性
  2. 基类中的虚函数具有下传给派生类的性质 ,一个函数如果被定义成虚函数,则不管经历多少次派生,仍将保持其虚特性,以实现“一个接口,多个形态”
  3. 派生类函数必须与基类虚函数声明一样的返回值 函数名和参数个数类型;如果是参数不同则变为重载函数;如果是返回值不同则 是个报错;
  4. 派生类中没有重新定义虚函数,则派生类对象将使用其基类中的虚函数代码
  5. 虚函数必须是类的一个成员函数,不能是友元函数,也不能是静态的成员函数
  6. 虚函数与一般的成员函数相比较,调用时的执行速度要慢一些。为了实现多态性,在每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现的。因此,除了要编写一些通用的程序,并一定要使用虚函数才能完成其功能要求外,通常不必使用虚函数。

例3:复杂调用(往上走的)

#include<iostream>
using namespace std;
class A
{
 public:
      A()
	  {
	  	cout<<"创建A"<<endl;
	  	f();
	   } 
	 virtual f() // 虚函数 
	  {
	  	cout<<"A::f()"<<endl;
	  }
	 void g()
	  {
	  	cout<<"A::g()"<<endl;
	  }
	  void h()
	  {
	  	cout<<"A::h()"<<endl;
	  	f(),g();
	  }
} ;

class B:public A
{
	public:
		void f()
		{
			cout<<"B::f()"<<endl;
		}
		void g()
		{
			cout<<"B::g()"<<endl;
		}
};

int main()
{
	A a,*p;
	B b; // 调用 b.B::B() , 调用 b.A::A() ,调用 b.A::f() ;虽然A B 都有 f函数 ,
	     //但构造函数调用虚函数只能调用本身或者基类的函数(本类优先基类)
	cout<<"__________"<<endl; 
	p=&b;
	p->f();// 调用 b.B::f() // f为虚函数 ,所以调用本类的函数就行跟(跟上面的知识点不一样)
	p->g();// 调用 b.A::g() 不是虚函数所以是调用基类
	cout<<"__________"<<endl; 
	p->h();// 调用 b.A::h(); 调用 b.B::f(); 调用 b.A::g(); 虽然A B 都有 f 和 g 函数 但是基类指针指向派生类对象时虚函数优先 
}

在这里插入图片描述

虚函数使用情况

1.是否作为基类,且成员函数在子类中是否有被更改的可能性。
2.考虑对成员函数的调用是对象的方式还是基类指针的方式。
3.该类只作为基类且成员函数是空的,即抽象类
4.虚析构函数

静态关联和动态关联

  1. 静态发生在编译阶段,动态发生在运行阶段
  2. 编译阶段只做代码的语法性检查
  3. 有静态关联:
    在编译阶段就知道要调用哪个函数或者使用哪个对象的成员函数;有函数重载和对象调用这两种
函数重载
int student(int a,int b);
int student(int c);
student(12,11);
student(13);
对象调用
Studdent :public people{};
Student a;
Studnet b;
a.getname();
b.getname();
  1. 动态关联:
    编译时候并不知道,但运行时候才知道用谁,
    实现动态联编可以使用基类指针调用虚函数
Studdent :public People{};
Peolple *p; 基类指针
Student a;
Studnet b;
P=&a;虽然这里指明了是对象a,但是编译器只做语法检查,所以
p->getname();它是不知道这里调用谁的函数的,只有运行了才知道

访问虚函数:基类指针和对象

1.用基指针访问或用对象名访问虚函数
2.二者差别:
基指针访问虚函数时,指向其实际派生类对象重新定义的函数。实现动态聚束
通过一个对象名访问时,只能静态聚束。即由编译器在编译的时候决定调用哪个函数

构造函数和析构函数中调用虚函数

1.编译系统对构造函数和析构函数中调用虚函数采用静态联编,即它们所调用的虚函数是自己类或基类中定义的函数而不是派生类中重新定义的函数

例1:两次调用的函数是同名函数 vf但是

#include<iostream>
using namespace std;
class base
{
	public:
		base(){};
		virtual void vf()
		{cout<<"基类的函数"<<endl;  
		}
};

class son:public base
{
	public:
	/*	void vf()
		{
			cout<<"son"<<endl;
		 } */
		son()
		{
			vf(); // 调用了是 基类的 vf 函数不是 派生类的 ,如果我在son类增加vf虚函数,调用的本身的vf函数 
		}
		void g()
		{
			vf();
		}
 } ;
 
 class grandson:public son
 {
 	public:
 		void vf()
 		{
 			cout<<"grandson的函数"<<endl; 
		 }
	/*	void g()
		{
			cout<<"666"<<endl;  这里加 g 函数将会覆盖基类继承过来的成员函数 
		}*/
};

int main()
{
	grandson A; 
	A.g();  
}

结果:

基类的函数
grandson的函数

空虚函数

1.空的虚函数:可以使得派生类不访问基类的虚函数,而是访问本身空的虚函数
如果派生类想通过虚函数的机制来访问虚函数(而不是默认访问的),则需要创建一条基类到派生类的虚函数路径,需要把中间那些没有虚函数的类定义为空的虚函数,这样就不会每个类都访问基类的虚函数
2.在派生类中没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时,则调用其基类中的虚函数。

例:
A (有 f 虚函数)
B:public A
C:public B
D:public C(重新定义虚函数)
像B C 是中间类,如果没有且不重新定义空的 f 虚函数,则都会输出A 基类的虚函数,因为C没有重新定义虚函数,所以调用的是A的虚函数,又B也没定义,所以调用A的虚函数

#include<iostream>
using namespace std;
class base
{
	public:
		base()
		{
		 f(); 
		 }
		virtual void f()
		{cout<<"base 的 虚函数"<<endl;  
		}
};

class son:public base
{
	public:
		son()
		{
			f();
		}
		void f(){}

 } ;
 
 class grandson:public son
 {
 	public:
 		void f()
 		{cout<<"grandson 的虚函数"<<endl;
		 }
 
};

int main()
{
grandson A;
A.f();
}

虚函数与重载的注意点

1、当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名参数的类型、顺序、参数的个数必须一一对应,函数的返回的类型也相同。若函数名相同,但参数的个数不同或者参数的类型不同时,则属于函数的重载,而不是虚函数。若函数名不同,显然这是不同的成员函数。
在这里插入图片描述
在这里插入图片描述

虚析构函数

1.可以做虚函数的:只有普通函数和析构函数,而构造函数和成员函数是不行的
2.在实现多态时,当用基类指针操作派生类,在析构时只会析构基类。为了防止只析构基类而不析构派生类的状况发生,一般都将基类的析构函数定义为虚函数

练习

在这里插入图片描述
base0—base1虚----A1—A2
base0—base1虚----B1—B2
答案:

base0 //父类指针子类对象,且无虚函数,只能输出基类内容
base0//父类指针子类对象,且无虚函数,只能输出基类内容
没有结果 (因为是private继承)
没有结果
A1
A2
没有结果
没有结果

抽象类/虚基类

  1. 当定义了一个类,这个类只能用作基类来派生出新的类,而不能用这种类来定义对象时,称这种类为抽象类。当对某些特殊的对象要进行很好地封装时,需要定义抽象类。
  2. 凡是包含纯虚函数的类都是抽象类,将类的构造函数或析构函数的访问权限定义为保护的时,这种类为抽象类。

在这里插入图片描述

  1. 抽象类不能生成对象,只能派生子类
  2. 原因:因为纯虚函数没有实现部分,所以不能产生对象。但可以定义指向抽象类的指针,即指向这种基类的指针。当用这种基类指针指向其派生类的对象时,必须在派生类中重载纯虚函数,否则会产生程序的运行错误
class A{
	public :virtual void show()=0;//纯虚函数
}
class B:public A{
	void show(){cout<<"如果这里不重新定义,会报错误"};
}
A *p //抽象类指针
B b; //子类对象
p=&b; //抽象类指针指向子类对象
p->show();
  1. 在以抽象类作为基类的派生类中,必须给出所有纯虚函数的实体,即必须有重载纯虚函数的函数体。否则,这样的派生类也是不能产生对象的
接上一段代码
由上面显然子类B有重新定义纯虚函数,所以可以创建对象b,
否则是不能创建的
  1. 把函数名赋于0,本质上是将指向函数体的指针值赋为初值0。在没有重新定义这种纯虚函数之前,是不能调用这种函数的。与定义空函数不一样,空函数的函数体为空,即调用该函数时,不执行任何动作。
  2. 作用:总的来说,抽象类的唯一用途是为派生类提供基类,纯虚函数的作用是作为派生类中的成员函数的基础,并实现动态多态性。

纯虚函数

  1. 在基类中不对虚函数给出有意义的实现,它只是在派生类中有具体的意义。这时基类中的虚函数只是一个入口,具体的目的地由不同的派生类中的对象决定。这个虚函数称为纯虚函数。
  2. 纯虚函数作用:当一个基类不适合产生该类对象时,可以用纯虚函数;也满足多态的功能; (比如:动物一个类 ,产生一个动物对象没什么意义;所以可以交给派生类自己定义要牛有牛,要虎有虎)
  3. 定义
class    <基类名>
{	virtual <类型><函数名>(<参数表>)=0;
	......
};
  1. 纯虚函数的定义和跟虚函数的区别
class A
{
public:
 virtual void f ()=0; 纯虚函数,没有函数体
 virtual void () 虚函数
 {
 cout<<"456"<<endl;
 }
};

通俗讲就是:
父亲(抽象类)买了块地(纯虚函数),没钱建房,留给子子孙孙继承;
儿子没钱在这块地建房,(所以纯虚函数还在),儿子就还是抽象类
孙子继承,有钱建房(纯虚函数具体化了),所以就变成正常的派生类

综合实例:

#include<iostream>
using namespace std;
class person
{
	public:
		virtual void get()=0; // 定义了纯虚函数 
	//	virtual void h()=0;  我在这里加一句纯虚函数,而派生类没有具体化,则报错,student类就会变成抽象类,除非两个纯虚函数都被具体化了 
	void num()
	{
		cout<<"6666"<<endl;
	 } 
};

class student: public person
{
	public:
		virtual void get()  // 在派生类中将他具体化, 
		{
			cout<<"将他具体化"<<endl; 
		}
 };
 
 //person who(person&); 这里加上去也是错的,抽象类不能作为参数类型,函数返回值或显示转换的类型 
 int main()

{
   //person b; 这里加这句则会报错,抽象类不能生成对象  
  // 
	student a;
	person*prt= &a; // 抽象类可以生成该类指针,指向派生类 
	a.get();
	a.num(); //抽象类的非纯虚函数可以通过派生类对象调用 
}

总结:

  • 含有纯虚函数的类叫抽象类
  • 抽象类的派生类只有将继承的纯虚函数都具体化才变成正常的派生类,否则还是抽象类
  • 抽象类不能产生对象,不能作为参数类型,函数返回值或显示转换的类型
  • 抽象类的非纯虚函数可以通过派生类对象调用
  • 抽象类可以生成该类指针,指向派生类
  • 在成员函数内可以调用纯虚函数,但析构和构造函数内用则会报错

虚析构函数

https://blog.csdn.net/starlee/article/details/619827

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值