以下是lw.nseac.com论文网与您分享的一篇关于基于C++与虚
2013-05-22 01:29
导读:计算机应用论文论文,以下是lw.nseac.com论文网与您分享的一篇关于基于C++与虚样式参考,免费教你怎么写,格式要求,科教论文网提供的这篇文章不错:
以下是lw.nseac.com网与您分享的一篇关于基于C++与虚函数的多
以下是lw.nseac.com网与您分享的一篇关于基于C++与虚函数的多态性编程实现机制探讨的计算机应用,欢迎浏览!
关键词: 动态联编;虚函数;多态性
摘要: 关于虚函数能否解决类的派生过程中出现的切片问题,
结合具体实例,给出了在C++中利用虚函数实现程序多态性的设计方法,并进一步探讨了多态性编程的实现机制。
中图分类号: TP311 文献标识码:A 文章编号:1004-633X(2011)12-0061-02
多态性是面象对象程序设计的重要特征,封装性、继承性和多态性一起并称为OOP(Object Oriented Programming)的三大特征,多态性是面象对象编程技术的核心组件之一。多态性对于C++的学习者来说是学习过程中最困难的概念,也是理解OOP的转折点[1]。本文基于具体案例,给出了在C++中利用虚函数实现面向对象编程的多态性的设计方法,并进一步探讨了多态性编程的实现机制。
1 多态性与虚函数
多态性是指为一个函数名关联多种含义的能力,即同一种调用方式可以映像到不同的函数。这种把函数的调用与适当的函数体对应的活动又称为绑定(binding)。根据绑定所进行阶段的不同,可分为早期绑定(early binding)、晚期绑定(late binding),早期绑定发生在程序的编译阶段,称为静态联编(static binding),晚期绑定发生在程序的运行阶段,称为动态联编(dynamic binding)。C++支持基于静态联编和动态联编的两种不同的多态性:编译时的多态性和运行时的多态性。
1.1 编译期多态
编译期多态在OOP中的具体体现是函数重载(overloading),函数重载指的是允许多个不同函数使用同一个函数名,但要求这些同名函数具有不同的参数表(当然,函数体的代码也不同):参数表中的参数个数不同、参数表中对应的参数类型不同或参数表中不同类型参数的次序不同。
系统对函数重载这种多态性的分辨与处理是在编译阶段完成的。因为重载函数的形参表是不同的,因些,在编译过程中,系统就可以根据函数调用时的实参类型来匹配这些同名的函数,进而确定函数调用语句实际上对应的函数体代码(位置),故称这种多态性是编译期多态。
1.2 运行期多态
通过动态联编技术,为一个函数名关联多种含义的能力,称为运行期多态性。
为增强多态性,C++不仅支持函数的重载,还允许在不同的类中出现其原型完全相同的函数,即所谓的超载。函数超载(overriding) 是指在基类与其派生类的范围内,允许多个不同函数使用完全相同的函数名、函数参数表和函数返回类型。函数的超载在更高的层次上充分体现了程序的多态性。由于函数超载允许不同的函数具有完全相同的函数原型,因此在编译阶段无法判别此次调用应执行哪段函数代码。只有到了运行过程执行到此处时,[本文来自论文之家:www.papershome.com,转载请保留此标记]才有可能临时判别执行哪一段函数代码,即动态联编。函数的超载需要动态联编技术的支持,而动态联编是通过虚函数来实现的。
1.3虚函数(virtual function)
在定义某一基类(或其派生类)时,若将其中的某一个非静态成员函数的属性说明为virtual,则称该函数为虚函数。
虚函数的使用与函数超载密切相关[2]。第一,基类中某函数被说明为虚函数;第二,其派生类中又用到与该函数同一接口,但函数体不同的超载函数。二者齐备,当编译到对此函数的调用时,相当于告诉编译器:“不用绑定与此函数名对应的具体的函数代码段,等到运行时再从具体对象实例中确定它的具体实现。”[3]这种技术即晚期绑定技术,这种技术使得运行期对一个虚函数的调用随着对象实例的变化可以呈现多种结果,即程序呈现运行期的多态性。虚函数是这种运行期多态性得以实现的必要条件,函数超载是多态性的意义所在。
2 利用虚函数实现程序的多态性
有了虚函数的支持就一定能实现动态联编,在程序运行中呈现多态性吗?在程序设计中要采用什么方法才能利用动态联编技术,体现程序的多态性呢?下面分三种情况讨论。
(1)虚函数并不是程序多态的充分条件。下面程序1中类的成员函数虽声明为虚函数,但仍发生了切片问题[3]。
程序1
#include<iostream>
#include<string>
using namespace std;
class pet
{public:
string name;
virtual void print();
};
class dog:public pet
{public:
string breed;
virtual void print();
};
void main()
{ pet pet_object; dog dog_object;
dog_object.name="珍妮";
dog_object.breed="Great Dane";
pet_object =dog_object;
pet_object.print();//切片问题,
}
void pet::print()
{cout<<"宠物名字:"<<name<<endl;}
void dog::print()
{cout<<"宠物名字:"<<name<<endl;
cout<<"宠物品种:"<<breed<<endl;
}
虽然程序中将一个派生类宠物狗对象dog_object赋给了一个基类宠物对象pet_object,但调用虚函数print()时,并没有随着对象实例的变化而呈现多态现象,从而去绑定派生类对象的虚函数。程序的运行结果是:“宠物名字:珍妮”,没有输出派生类宠物狗对象dog_object所有的属性值,而是切去了派生类新增部分的属性,出现了切片问题。
(2)在程序设计中,将类及其派生类重载的成员函数声明为虚函数,再利用派生类对象初始化基类对象的引用,能实现程序的多态性,即:
BaseClass& Base_object= derived_object
如程序2所示,银行允许开设多种不同的帐户,下面程序2中为基本银行帐户定义了一个基类BankAccount,又定义了两个派生类MarketAccount和 CdAccount,分别代表具有不同取款规则的帐户,在每个类中都重载了取款成员函数withdraw(),以适用于所有不同取款规则的帐户。转帐操作应在所用对象之间进行,在完成转帐操作的成员函数convert()实现代码中,取款成员函数withdraw()应随着实例对象(取款帐户)other的变化而分别绑定不同类的重载的函数段,则对于重载的成员函数withdraw()必须采用动态联编的方式进行绑定,为此在下面程序中做了这样的设计:①声明withdraw()为虚函数; ②将转帐函数convert()的形参设计成基类对象的引用BankAccount&,当转帐函数以不同的帐户对象(特别是派生类对象)被调用时,实际上是用派生类对象初始化了基类对象的引用。程序运行测试结果达到了预期的目的,当然,程序中略去了所有其它与本文讨论无关的成员函数及实现的细节,将存款函数声明为虚函数也不是必须的,这是为了下节说明的方便。
程序2
#include<iostream>
#include<cstring>
using namespace std;
class BankAccount
{ char name[80]; double balance;
public:
BankAccount(char [],double);
virtual void deposit(double amount);
virtual bool withdraw(double);
void convert(BankAccount&,int );
};
class MarketAccount
:public BankAccount
{ int withdraw_numbers;
public:
MarketAccount(char [],double);
virtual bool withdraw(double )
};
class CdAccount:public BankAccount
{ double interest_rate;
public:
CdAccount(char [],double,double);
virtual bool withdraw(double );
};
bool BankAccount
::withdraw(double amount)
{ balance-=amount; return 1; }
bool CdAccount
::withdraw(double amount)
{cout<<"提前取款,罚利息的25%\n";
double temp=amount+get_balance()
*interest_rate/100;
balance-=temp; return 1;
}
bool MarketAccount
::withdraw(double amount)
{ cout<<"取款交手续费1.5\n;
balance-=amount+1.5; return 1;
}
//将other帐户的钱转至当前帐户
void BankAccount::convert
(BankAccount& other,int amount)
{ other.withdraw(amount)
deposit(amount);
}
// ……(略去其余函数的实现部分)
void main()
{ CdAccount zhang ("张华",1000,3);
MarketAccount wang ("王芳",300);
wang.convert(zhang ,500);
}
(3)程序设计中,将类及其派生类重载的成员函数声明为虚函数,利用指向基类和派生类指针的赋值兼容性,实现程序的多态性,即:
BaseClass * Base_object _point=&derived_object
真对上述程序2,只须将转帐函数的参数类型改为指向基类对象的指针,并作如下调整:
void BankAccount::convert
(BankAccount* other,int amount)
{ other->withdraw(amount))
deposit(amount);
}
对函数调用时的实参对象要采用对象地址的格式。
在以树形为主要结构的类的派生关系中,虚函数是架构程序模块的主要方式,虚函数在基类中对某种操作提供一个框架,在派生类中为框架提供不同的实现,虚函数的语义决定了只有当用父类指针访问它们时,才能实现运行时的多态性[4],从而解决切片问题。
3 虚函数的实现机制
C++中的虚函数是实现面向对象程序设计最重要的语言机制,是动态联编技术实现程序多态性的基石。但是程序执行的时候,代码已经脱离编译器、链接器的干预,那么是谁完成执行时的动态联编?背后的技术是什么?
实际上编译器在程序静态编译期已经做好了必要的准备:它为每一个包含了虚函数的类产生一个虚拟函数表vtab,为该类的所有对象共享;为类的每一个实例对象添加一个指针分量vptr,该指针指向所属类的虚函数表vtab[5]。
虚函数表vtab中的每一项是一个虚函数地址,类中的每一个虚函数都在表中确切的占有一项。程序2中派生类对象CdAccount::zhang的内存部局和类的虚函数表如图1所示。
在图1中注意到,编译器会把指向虚函数表的指针存放于对象实例最前面的位置,这保证运行时我们可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中的函数指针,并调用相应的虚函数。在程序2的主函数中增加如下代码:
typedef void(*Fun)(void);
Fun pfun=NULL;
//由对象zhang的地址得到对象头部的指针分量
cout<<(int*)(&zhang)<<endl;
//取得虚函数表vtab的地址
cout<<(int*)*(int*)(&zhang)<<endl;
pfun=(Fun)*((int*)*(int*)(&zhang)+1);
通过上述代码,可以取得派生类CdAccount的虚函数表vtab的地址和指向函数withdraw()入口地址的函数类型的指针(pfun),进而调用相应的虚函数。程序实际运行结果与图2中显示结果一致。图2是在VC的IDE环境中的Debug状态下展开类的实例得到的显示结果。在图2中我们可以观察到程序2实际运行过程中类的实例对象的内存部局和类的虚函数表的地址。
4 结束语
C++中的虚函数是实现程序多态性的关键技术,虚函数表的建立使运行时的多态性成为可能,虚函数的使用提高了程序的效率,但代价是过多的空间开销。虚函数的语义决定了只有当用父类型指针访问子类时,才能实现运行时的多态性。但是,当我们用父类型指针访问子类没有重载的自己的虚函数时,却被编译器视为非法,对于程序员者来说,探求是无止境的。