C++多态性的实现过程
2023-06-15李家宏孙庆英
李家宏 孙庆英
摘要:多态性特征是C++中最为重要的一个特征,熟练使用多态是学好C++的关键,而理解多态的实现机制及实现过程则是熟练使用多态的关键。文章在分析多态性基本属性的基础上,结合具体程序实例重点分析了动态多态的实现机制,并结合虚函数和联编原理分析了动态多态的实现过程。
关键词:C++;多态性;虚函数
中图分类号:TP312.1 文献标志码:A
0 引言
面向对象程序设计(Object Oriented Programming)是以对象为程序的基本单元,将数据和操作封装其中,提高了软件的重用性、灵活性和扩展性,C++是面向对象程序设计语言的主流之一。现实世界的诸多事物,包括一些抽象规则、计划或事件都可以描述成对象。对象是由数据(描述事物的属性)和作用于数据的操作(事物的行为)构成的一个独立整体。
封装、继承和多态是面向对象设计的3大特点。封装就是把客观事物抽象得到的数据和行为封装成一个整体,在C++中,实现数据和行为封装的程序单元就叫类。封装就是将代码模块化,实现了类内部对象的隐蔽。继承是由已经存在的类创建新类的机制,体现在类的层次关系中,子类拥有父类中的数据和方法,子类继承父类的同时可以修改和扩充自己的功能。多态是指父类的方法被子類重写、可以各自产生自己的功能行为。封装和继承的目的是代码的重用,多态就是实现接口重用,即“一个接口,多种方法”。相比封装和继承,多态因其复杂性、灵活性更难以掌握和理解。
1 多态的概念
多态(polymorphism)一词最早来源于拉丁语poly(意为多)和morphos(意为形态),意指具有多种形式或形态。它反映了人们在思索解决问题的办法时,对相似的问题的一种求解方法[1]。
多态性一词最早来源于生物学,是指地球上所有生物,从食物链系统、物种水平、群体水平、基因水平等层次上所体现出的形态和状态的多样性[2]。多态性是指同样的消息被不同类型的对象接收时会产生完全不同的行为,即根据操作环境的不同采用不同的处理方式,一组具有相同基本语义的方法能在同一接口下为不同的对象服务[3]。在C++中利用类继承的层次关系来实现多态,通常是把具有通用功能的声明存放在类层次高的地方,而把实现这一个功能的不同方法放在层次较低的类中,C++语言通过子类重定义父类函数来实现多态。
2 多态的分类
多态通常分为两种:通用多态和特定多态,其中,通用多态又细分为参数多态和包含多态[4]。参数多态在C++中就是利用函数模板或类模板,给出的不同参数类型,得到不同的结果,实现一个具有多种形态的结构。包含多态在C++中的基础就是虚函数,即同样的操作可用于一个类型及其子类型。特定多态细分为重载多态和强制多态。重载多态在C++中就是函数重载和运算符重载,即同一个名(操作符、函数名)在不同的上下文中有不同的类型。强制多态,这里强制也称为类型转换,在C++中一般指基本类型转换和自定义类型转换,即在编译的时候发生数据混合运算时,程序通过语义操作,改变操作对象的类型以符合运行时函数和操作符的要求。通用多态和特定多态的区别是:通用多态对工作的类型不加限制,允许不同类型的值执行相同的代码,从语义上为相关联性的类型,特定多态对有限的类型有效。不同类型的值可能要执行不同的代码,从语义上为无关联的类型。
3 多态的实现
3.1 类型兼容与函数重写
C++中的继承遵循了类型兼容性原则,即当子类以Public方式继承父类时,将继承父类的所有属性和方法,因此,可以变相的理解成子类是一种特殊的父类,可以使用子类对象初始化父类,也可以使用父类的指针或引用来调用子类的对象。
在程序设计过程中,很多时候会出现这样一种情况,子类继承父类的A函数,但父类的A函数不能满足子类的需求,此时需要在子类中对A函数进行重写。C++中的函数重写是指:函数名、参数、返回类型均相同。如果程序中类型兼容性原则遇到了函数重写会怎么样,调用父类的A函数还是子类中重写的A函数,类型兼容与函数重写之间的关系可以用以下程序代码阐释:
#include
using namespace std;
class Animal // 父类
{
public:
void Speak()
{
cout << "动物在说话" << endl;
}
};
class Dog :public Animal// 子类
{
public:
void Speak()
{
cout << "小狗在汪汪叫" << endl;
}
};
int main()
{
Dog dog;
dog.Speak();
dog.Animal::Speak();
Animal animal1 = dog;
animal1.Speak();
Animal * animal2 = & dog;
animal2->Speak();
return 0;
}
Animal animal1 = dog;
Animal * animal2 = & dog;
程序的运行结果如图1所示。
上述程序中定义了Animal和Dog两个类,其中,Dog类以Public方式继承了Animal类,并且重写了Speak()方法。根据程序运行结果不难看出:main()函数中定义的Dog类对象dog的调用方法dog.Speak()是通过子类对象的Speak()函数来实现小狗在汪汪叫功能。dog.Animal::Speak()是子类对象通过使用操作符作用域调用父类的Speak()函数来实现:动物在说话。定义的Animal的对象animal1通过调用拷贝构造函数,把dog的数据拷贝到animal1中,animal1仍为父类对象,所以animal1.Speak()执行的结果是动物在说话。最终定义了一个指向Animal类的指针animal2,将派生类对象dog的地址赋给父类指针animal2,利用该变量调用animal2–>speak()方法。得到的结果是:动物在说话。原因是C++编译器进行了类型转换,允许父类和子类之间进行类型转换,即父类指针可以直接指向子类对象。根据赋值兼容,编译器认为父类指针指向的是父类对象,因此,编译结果只可能是调用父类中定义的同名函数。在此时,C++认为变量animal2中保存的就是Animal对象的地址,即编译器不知道指针animal2指向的是一个什么对象,编译器认为最安全的方法就是调用父类对象的函数,因为父类和子类肯定都有相同的Speak()函数。因此,在main()函数中执行animal2–>Speak()时,调用的是Animal对象的Speak()函数。
3.2 动态联编与静态联编
以上程序出现这种情况的原因涉及C++在具體编译过程中函数调用的问题,这种确定调用同名函数的哪个函数的过程就叫做联编(又称绑定)。在C++中联编就是指函数调用与执行代码之间关联的过程,即确定某个标识符对应的存储地址的过程,在C++程序中,程序的每一个函数在内存中会被分配一段存储空间,而被分配的存储空间的起始地址则为函数的入口地址。
按照程序联编所进行的阶段,联编可分为两种:静态联编和动态联编。静态联编就是在程序的编译与连接阶段就已经确定函数调用和执行该调用的函数之间的关联。在生成可执行文件中,函数的调用所关联执行的代码是确定好的,因此,静态联编也称为早绑定(Early Binding)。动态联编是在程序的运行时根据具体情况才能确定函数调用所关联的执行代码,因此,动态联编也称为晚绑定(Late Binding)[5]。
当类型兼容原则与函数重写发生冲突时,程序员希望根据程序设计的子类对象类型来调用子类对象的函数,而不是编译器认为的调用父类的对象函数。也就是说,如果父类指针(引用)指向(引用)父类的对象时,程序就应该调用父类的函数,如果父类指针(引用)指向(引用)子类的对象时,程序就应该调用子类的函数。这一功能可以通过动态联编实现。与静态联编相比,动态联编是在程序运行阶段,根据成员函数基于对象的类型不同,编译的结果就不同,这就是动态多态。动态多态的基础是虚函数。虚函数是用来表现父类和子类成员函数的一种关系。
3.3 虚函数
虚函数的定义方法是用关键字virtual修饰类的成员函数,虚函数的定义格式:virtual〈返回值类型〉〈函数名〉(〈形式参数表〉)<函数体>。
在类的层次结构中,成员函数一旦被声明为虚函数,那么,该类之后所有派生出来的新类中其都是虚函数。父类的虚函数在派生类中可以不重新定义,若在子类中没有重新改写父类的虚函数,则调用父类的虚函数。对兼容性与函数重写程序,进行适当的修改,将父类Animal中的Speak()函数使用关键子Virtual将其定义为虚函数,代码如下所示。
#include
using namespace std;
class Animal // 父类
{
public:
virtual void Speak() //用virtual 关键子定义Speak()为虚函数
{
cout << "动物在说话" << endl;
}
};
class Dog :public Animal// 子类Dog以public方式继承了Animal
{
public:
void Speak()//重写了Speak()函数
{
cout << "小狗在汪汪叫" << endl;
}
};
int main()
{
Dog dog;
dog.Speak();
dog.Animal::Speak();
Animal animal1 = dog;
animal1.Speak();
Animal * animal2 = & dog;
animal2->Speak();
return 0;
}
运行结果如图2所示。
Animal *animal2=&dog,animal2.Speak()时,由于在父类Animal的Speak()函数前加关键字Virtual,使得Speak()函数变成虚函数,编译器在编译的时候,发现animal类中有虚函数,此时,编译器会为每个包含虚函数的类创建一个虚函数表,该表是一个一维数组,在这个数组中存放每个虚函数的地址,这样就实现了动态联编,也就是晚绑定。也就实现了前面说的当调用父类指针(引用)指向(引用)子类对象函数时,调用的是子类对象的函数,实现了动态多态。
通过分析发现,要想实现动态多态要满足以下3个条件:(1)必须存在继承关系,程序中的Dog类以public的方式继承了Animal类。(2)继承关系中必须要有同名的虚函数。在两个类中Speak()函数为同名虚函数,子类重写父类的虚函数。(3)存在父类的指针或引用调用子类该虚函数。
了解多态是如何实现的之前,先要了解虚函数的调用原理,虚函数的调用原理和普通函数不一样,编译器在程序编译的时候,发现类中有关键字virtual的虚函数时,编译器会自动为每个包含虚函数的类创建一个虚函数表用来存放类对象中虚函数的地址,并同时创建一个虚函数表指针指向该虚函数表[6]。每个类使用一个虚函数表,每个类对象用一个指向虚表地址的虚表指针。父类对象包含一个指针指向父类所有虚函数的地址,子类对象也包含一个指向独立地址的指针。如果子类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址,如果子类提供了虚函数的新定义,该虚函数表将保存新函数的地址。示例程序中定义了两个类A和B,类B继承自类A,父类A中定义了两个虚函数,子类B中重写了其中一个虚函数,代码如下所示:
class A
{
public:
virtual void fun1()
{
cout << "fun1是类A虚函数";
}
virtual void fun2()
{
cout << "fun2是虚类A函数";
}
};
class B :public A
{
public:
virtual void fun1()
{
cout << "fun1是类B的虚函数";
}
};
分析上述程序,对于父类A中的两个虚函数fun1()和fun2(),由于子类B重写了类A中的fun1()函数,就导致子类B的虚函数表的第一个指针指向的是类B的fun1()的函数而不是父类A的fun1()函数,具体如表1所示。
3.4 动态多态的实现过程
编译器进行编译程序时发现有virtual声明的函数,就会在这个类中产生一个虚函数表。即使子类中没有用virtual定义虚函数,由于父类中的定义,子类通过继承后仍为虚函数。程序中Animal类和Dog类都包含一个虚函数Speak(),因此,编译器会为这两个类都建立一个虚函数表,将虚函数地址存放到该表中(见图3)。
编译器在为每个类创建虚函数表的同时,还为每个类的对象提供了一个虚函数表指针(vfptr),虚函数表指针指向了对象所属类的虚表。根据程序运行的对象类型去初始化虚函数表指针。虚函数表指针在没有初始化的情况下,程序是无法调用虚函数的。虚函数表的创建和虚函数表指针的初始化是在构造函数中实现的,在构造子类对象时,先调用父类的构造函数,并初始化父类的虚函数指针,指向父类的虚函数表,当子类对象执行构造函数时,子类对象的虚函数表指针也被初始化,指向子类的虚函数表。实现了在调用虚函数时,就能够找到正确的函数,如图4所示。
C++编译器在编译时,发现Animal类的Speak()函数是虚函数,此时C++就会采用动态联编技术。程序编译时并不确定具体调用的函数,而是在运行时,依据对象的类型来確认调用的是哪一个函数,这种能力就叫做C++的多态性。在构造子类Dog对象dog时,按照构造函数调用的顺序,先调用父类Animal的构造函数并初始化父类对象虚函数表指针,该指针指向父类的虚函数表。执行子类Dog构造函数时,子类对象的虚函数表指针被初始化,指向自身的虚函数表。Dog类的dog对象构造完毕后,其内部虚函数表指针被初始化为指向Dog类的虚表。在调用时,根据虚表中的函数地址找到Dog类的Speak()函数完成对虚函数的调用,从而实现动态绑定,实现了动态多态。
4 结语
多态性作为面向对象程序设计语言的3大要素之一,因其灵活性、伸缩性和复杂性而难以掌握。本文着重分析多态的分类、特征及动态多态的实现机制和原理,但本文对于动态多态的分析仅仅局限于单继 承的情况,对于多继承的情况原理基本相同,本文未作过多说明。
参考文献
[1]李明明,管志伟.浅析C++多态的作用及实现原理[J].无线互联科技,2014(7):116.
[2]吴克力.C++面向对象程序设计[M].北京:清华大学出版社,2021.
[3]谢云博.多态性实现机制在C++与JAVA中的比较分析[J].软件导刊,2014(6):45-46.
[4]姚云霞.浅析C++中类的多态性[J].陇东学院学报,2012(1):9-11.
[5]刘晨.基于静态联编与动态联编多态性的研究[J].价值工程,2010(19):248-249.
[6]柯栋梁,李军利.C++虚函数实现多态之案例驱动教学方法探讨[J].安徽工业大学学报(社会科学版),2012(4):114-115.
(编辑 何 琳)
Implementation of C++ polymorphism
Li Jiahong, Sun Qingying*
(Huaiyin Normal University, Huaian 223300, China)
Abstract: Polymorphism is the most important feature in C++. Skillful use of polymorphism is the key to learn C++well, while understanding the implementation mechanism and process of polymorphism is the key to use polymorphism skillfully. Based on the analysis of the basic attributes of polymorphism, this paper focuses on the implementation mechanism of dynamic polymorphism with specific program examples, and analyzes the implementation process of dynamic polymorphism with virtual function and binding principle.
Key words: C++; polymorphism; virtual function