C++虚函数与多态性

》多态性的概念

        一个面向对象的系统常常要求一组具有相同基本语义的方法能在同一接口下为不同的对象服务,这就是多态性(polymorphism)。与传统的面向过程的高级语言相比,C++不但提供了固有的多态性,还提供了实现自定义多态性的手段。多态性使得不同的但又具有某种共同属性的对象不但能在一定程度上共享代码,而且还能共享接口。这就大大提高了系统的一致性、灵活性和可维护性。

        在C++中:多态性可分为两类:编译时的多态性和运行时的多态性。

              #编译时的多态性是通过函数重载和模板体现的。利用函数重载机制,在调用同名的函数时,编译系统可根据实参的具体情况确定调用的是同名函数中的哪一个。利用函数模板,编译系统可根据模板实参以及模板函数实参的具体情况确定所要调用的是哪个函数,并生成相应的函数实例;利用模板类,编译系统可根据模板实参的具体情况确定所要定义的是哪个类的对象,并生成相应的类实例。由于有关操作所针对的具体目标(函数或类)的确定都是在编译时完成的,与运行时的动态环境无关,因此得名“编译时的多态性”,其实现机制称为静态绑定(static binding,也译作静态联编)。

              #运行时的多态性可以通过一个例子来说明。很多应用软件都通过菜单命令提供了复制-粘贴功能,而且被复制-粘贴的数据可以是各种各样的,如文字,图形等,但无论针对何种类型的数据,所点击的菜单命令都是相同的,这就是运行时的多态性的体现。显而易见,被复制-粘贴的数据的具体类型只能根据点击菜单命令当时的动态状况加以确定,“运行时的多态性”因此而得名,其实现机制则称为动态绑定(dynamic binding,也译作动态联编)。在C++中,运行时的多态性是通过虚函数体现的。


》虚函数

        在非静态成员函数声明的前面加上virtual修饰符,即把该函数声明为虚函数。

        在派生类中可以重新定义从基类继承下来的虚函数,从而提供该函数的适用于派生类的专门版本。但也可能并不需要重新定义,在这种情况下,继承下来的虚函数仍然保持其在基类中的定义,即派生类和基类使用同一函数版本。除少数特殊情况外,在派生类中重定义虚函数时,函数名、形参表和返回值类型必须保持不变。

        虚函数在派生类中被重定义后,重定义的函数仍然是虚函数,可以在其派生类中再次被重定义。注意,对于虚函数的重定义函数,无论是否使用virtual修饰符都是虚函数。当然,最好不要省略virtual修饰符,以免削弱程序的可读性。

        对于虚函数的两种调用方式:

               #非多态调用:是指不借助于指针或引用的直接调用。总是通过成员访问运算符“ . ”进行的。与通常的成员函数调用类似,非多态调用是建立在静态绑定机制基础之上的,不具备多态特征。

               #多态调用:是指借助于指向基类的指针或引用的调用,在C++中,一个基类指针(或引用)可以指向它的派生类对象,而且通过这样的指针(或引用)调用虚函数时,调用的是该指针(或引用)实际所指向的对象所在类的那个重定义版本。

        基类中的实函数也可以在派生类中重定义,但重定义的函数仍然是实函数。调用实函数时,通过基类指针(或引用)所调用的也只能是基类的函数版本,无法调用到派生类中的重定义函数。也就是说,尽管调用的语法形式可能相同,但对实函数的任何形式的调用都是非多态的。

        注:无论是虚函数还是实函数,在派生类中被重定义后,原来的函数版本即被隐藏;在通过成员访问符“ . ”直接调用该函数时,所调用的是重定义版本。但原来的版本依然存在,仍然可以通过函数名前加上作用域运算符(即类名::)来调用它们。

        示   例:

#include<iostream>
#include<cstring>
using namespace std;
class base
{
	char base_name[10];
public:
	base(){strcpy(base_name,"BASE");}
	virtual char* my_name(){return base_name;}
	char* class_name(){return base_name;}
};
class derived:public base
{
	char derived_name[10];
public:
	derived(){strcpy(derived_name,"DERIVED");}
	char* my_name(){return derived_name;}
	char* class_name(){return derived_name;}
};
void show_ptr(base *p)
{
	cout<<endl<<p->my_name()<<" "<<p->class_name();
}
void show_ref(base &r)
{
	cout<<endl<<r.my_name()<<" "<<r.class_name();
}
int main()
{
	base bb;
	derived dd;
	show_ptr(&bb);cout<<"<==应显示'BASE  BASE'";
	show_ptr(&dd);cout<<"<==应显示'DERIVED  BASE'";
	cout<<endl;
	show_ref(bb);cout<<"<==应显示'BASE  BASE'";
	show_ref(dd);cout<<"<==应显示'DERIVED  BASE'";
	cout<<endl;
	cout<<endl<<dd.base::my_name()<<" "<<dd.base::class_name();
	cout<<" <==应显示'BASE  BASE'";
	
	cout<<endl<<endl;
	system("pause");
	return 0;
}
        程序运行结果:

        在上面的例子中,基类base有一个私有数据成员base_name,用于存放该类对象的名称“base”。通过虚函数my_name()和实函数class_name()都可以获得这个名称。

        在base的派生类derived中,也有一个私有数据成员derived_name,用于存放该类对象的名称“DERIVED”。在derived中重新定义了my_name()和class_name(),用以返回派生类derived的名称。注意,derived中声明的my_name()函数是对base中虚函数my_name()的重定义,尽管没有用virtual修饰,仍然是一个虚函数;而derived中声明的class_name()仍然是一个实函数。

        注:构造函数不得声明为虚函数,也不要视图在构造函数中对某个虚函数进行多态调用,因为在执行构造函数时,派生类对象的自有部分(非继承部分)尚未形成,因此编译系统将这种情况处理为静态联编,被调用的总是所在类的那个版本。


》虚析构函数

        析构函数也可以通过virtual修饰而声明为虚函数。只要虚基类的析构函数声明为虚函数,由它派生的所有派生类的析构函数也一定是虚函数。

        通常,只要派生类中对析构函数进行了专门的定义,其基类的析构函数就应当声明为虚函数,否则就可能出问题。

        示 例:

#include<iostream>
#include<cstring>
using namespace std;
class AA
{
	int i;
public:
	AA(int n):i(n){};
	virtual ~AA(){cout<<"~AA被调用!"<<endl;}
};
class BB:public AA
{
	char* ptr;
public:
	BB(int n,char* s):AA(n),ptr(strdup(s))
	{
		if(!s)ptr=NULL;
		else{
			ptr=new char[strlen(s)+1];
			strcpy(ptr,s);
		}
	}
	~BB(){delete []ptr;cout<<"~BB被调用!"<<endl;}
};
int main()
{
	AA *ptr=new BB(5,"abcd");
	delete ptr;
	
	cout<<endl<<endl;
	system("pause");
	return 0;
}
      程序运行结果:

       输出结果说明,释放对象时进行了多态调用,调用了派生类析构函数,因此先后调用了BB和AA的析构函数。如果去掉~AA()前的virtual修饰符,即不把AA的析构函数声明为虚函数,则运行的结果是

~AA被调用!

       也就是说,此时进行了非多态调用,仅调用了基类的析构函数,从而使得BB的析构函数没有被执行,因而用以存放字符串“abcd”的空间没有释放!这样的错误是十分严重的。


》纯虚函数与抽象类

        在上面关于虚函数的例子中,基类和派生类都分别给出了一个虚函数的不同版本。但在某些情况下,基类无法确定(或无法完全确定)一个虚函数的具体操作方式或内容,只能靠派生类来提供各个具体的实现版本。基类中这种需要由派生类提供具体实现的虚函数称为纯虚函数。为了将一个虚函数声明为纯虚函数,需要在虚函数原型的语句结束符“;”之前加上“=0”。

        拥有纯虚函数的类称为抽象类,抽象类不能用来定义对象。如果一个抽象类的派生类没有重定义来自其他基类的某个纯虚函数,则该函数在派生类中仍然是纯虚函数,这就使得该派生类也称为抽象类。也就是说,一个派生类可以把重定义纯虚函数的任务进一步转交给它自己的派生类。

        也可以在将一个函数声明为纯虚函数的同时,为该函数提供实现版本。换句话说,一个函数是否为纯虚函数,取决于其原型的尾部是否为“=0;”,与实现版本的有无没有关系。拥有实现版本的纯虚函数仍然有赖于派生类提供重定义版本。纯虚函数的实现版本通常是不完善的版本,但包含了一些共有操作,供各个派生类在重定义函数中调用。派生类在重定义一个纯虚函数时,可以继续将之声明为纯虚函数。

        注:纯虚函数不得声明为内联函数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值