`
isiqi
  • 浏览: 16496747 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

C++ 虚函数 多态

阅读更多

1、什么是虚函数和多态


虚函数是在类中被声明为virtual的成员函数,当编译器看到通过指针或引用调用此类函数时,对其执行晚绑定,即通过指针(或引用)指向的类的类型信息来决定该函数是哪个类的。通常此类指针或引用都声明为基类的,它可以指向基类或派生类的对象。
多态指同一个方法根据其所属的不同对象可以有不同的行为(根据自己理解,不知这么说是否严谨)。

举个例子说明虚函数、多态、早绑定和晚绑定:
李氏两兄妹(哥哥和妹妹)参加姓氏运动会(不同姓氏组队参加),哥哥男子项目比赛,妹妹参加女子项目比赛,开幕式有一个参赛队伍代表发言仪式,兄妹俩都想去露露脸,可只能一人去,最终他们决定到时抓阄决定,而组委会也不反对,它才不关心是哥哥还是妹妹来发言,只要派一个姓李的来说两句话就行。运动会如期举行,妹妹抓阄获得代表李家发言的机会,哥哥参加了男子项目比赛,妹妹参加了女子项目比赛。比赛结果就不是我们关心的了。
现在让我们来做个类比(只讨论与运动会相关的话题):
(1)类的设计:
李氏兄妹属于李氏家族,李氏是基类(这里还是抽象的纯基类),李氏又派生出两个子类(李氏男和李氏女),李氏男会所有男子项目的比赛(李氏男的成员函数),李氏女会所有女子项目的比赛(李氏女的成员函数)。姓李的人都会发言(基类虚函数),李氏男和李氏女继承自李氏当然也会发言,只是男女说话声音不一样,内容也会又差异,给人感觉不同(李氏男和李氏女分别重新定义发言这个虚函数)。李氏两兄妹就是李氏男和李氏女两个类的实体。
(2)程序设计:
李氏兄妹填写参赛报名表。
(3)编译:
李氏兄妹的参赛报名表被上交给组委会(编译器),哥哥和妹妹分别参加男子和女子的比赛,组委会一看就明白了(早绑定),只是发言人选不明确,组委会看到报名表上写的是“李家代表”(基类指针),组委会不能确定到底是谁,就做了个备注:如果是男的,就是哥哥李某某;如果是女的,就是妹妹李某某(晚绑定)。组委会做好其它准备工作后,就等运动会开始了(编译完毕)。
(4)程序运行:
运动会开始了(程序开始运行),开幕式上我们听到了李家妹妹的发言,如果是哥哥运气好抓阄胜出,我们将听到哥哥的发言(多态)。然后就是看到兄妹俩参加比赛了。。。

但愿这个比喻说清楚了虚函数、多态、早绑定和晚绑定的概念和它们之间的关系。再说一下,早绑定指编译器在编译期间即知道对象的具体类型并确定此对象调用成员函数的确切地址;而晚绑定是根据指针所指对象的类型信息得到类的虚函数表指针进而确定调用成员函数的确切地址。


2、揭密晚绑定的秘密

编译器到底做了什么实现的虚函数的晚绑定呢?我们来探个究竟。

编译器对每个包含虚函数的类创建一个表(称为V TA B L E)。在V TA B L E中,编译器放置特定类的虚函数地址。在每个带有虚函数的类中,编译器秘密地置一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个V P T R,并在V TA B L E表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置V TA B L E、初始化V P T R、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。利用虚函数,这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。(《C++编程思想》)

在任何类中不存在显示的类型信息,可对象中必须存放类信息,否则类型不可能在运行时建立。那这个类信息是什么呢?我们来看下面几个类:

class no_virtual
{
public:
void fun1() const{}
int fun2() const { return a; }
private:
int a;
}

class one_virtual
{
public:
virtual void fun1() const{}
int fun2() const { return a; }
private:
int a;
}

class two_virtual
{
public:
virtual void fun1() const{}
virtual int fun2() const { return a; }
private:
int a;
}

以上三个类中:
no_virtual没有虚函数,sizeof(no_virtual)=4,类no_virtual的长度就是其成员变量整型a的长度;
one_virtual有一个虚函数,sizeof(one_virtual)=8;
two_virtual有两个虚函数,sizeof(two_virtual)=8; 有一个虚函数和两个虚函数的类的长度没有区别,其实它们的长度就是no_virtual的长度加一个void指针的长度,它反映出,如果有一个或多个虚函数,编译器在这个结构中插入一个指针( V P T R)。在one_virtual 和two_virtual之间没有区别。这是因为V P T R指向一个存放地址的表,只需要一个指针,因为所有虚函数地址都包含在这个表中。

这个VPTR就可以看作类的类型信息。

那我们来看看编译器是怎么建立VPTR指向的这个虚函数表的。先看下面两个类:
class base
{
public:
void bfun(){}
virtual void vfun1(){}
virtual int vfun2(){}
private:
int a;
}

class derived : public base
{
public:
void dfun(){}
virtual void vfun1(){}
virtual int vfun3(){}
private:
int b;
}

两个类VPTR指向的虚函数表(VTABLE)分别如下:
base类
——————
VPTR——> |&base::vfun1 |
——————
|&base::vfun2 |
——————

derived类
———————
VPTR——> |&derived::vfun1 |
———————
|&base::vfun2 |
———————
|&derived::vfun3 |
———————

每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个VTABLE,如上图所示。在这个表中,编译器放置了在这个类中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类的这个虚函数地址。(在derived的VTABLE中,vfun2的入口就是这种情况。)然后编译器在这个类中放置VPTR。当使用简单继承时,对于每个对象只有一个VPTR。VPTR必须被初始化为指向相应的VTABLE,这在构造函数中发生。
一旦VPTR被初始化为指向相应的VTABLE,对象就"知道"它自己是什么类型。但只有当虚函数被调用时这种自我认知才有用。

VPTR常常位于对象的开头,编译器能很容易地取到VPTR的值,从而确定VTABLE的位置。VPTR总指向VTABLE的开始地址,所有基类和它的子类的虚函数地址(子类自己定义的虚函数除外)在VTABLE中存储的位置总是相同的,如上面base类和derived类的VTABLE中vfun1和vfun2的地址总是按相同的顺序存储。编译器知道vfun1位于VPTR处,vfun2位于VPTR+1处,因此在用基类指针调用虚函数时,编译器首先获取指针指向对象的类型信息(VPTR),然后就去调用虚函数。如一个base类指针pBase指向了一个derived对象,那pBase->vfun2()被编译器翻译为 VPTR+1 的调用,因为虚函数vfun2的地址在VTABLE中位于索引为1的位置上。同理,pBase->vfun3()被编译器翻译为 VPTR+2的调用。这就是所谓的晚绑定。

我们来看一下虚函数调用的汇编代码,以加深理解。

void test(base* pBase)
{
pBase->vfun2();
}

int main(int argc, char* argv[])
{
derived td;



test(&td);

return 0;
}

derived td;编译生成的汇编代码如下:
mov DWORD PTR _td$[esp+24], OFFSET FLAT:??_7derived@@6B@ ; derived::`vftable'
由编译器的注释可知,此时PTR _td$[esp+24]中存储的就是derived类的VTABLE地址。

test(&td);编译生成的汇编代码如下:
lea eax, DWORD PTR _td$[esp+24]
mov DWORD PTR __$EHRec$[esp+32], 0
push eax
call ?test@@YAXPAVbase@@@Z ; test
调用test函数时完成了如下工作:取对象td的地址,将其压栈,然后调用test。

pBase->vfun2();编译生成的汇编代码如下:
mov ecx, DWORD PTR _pBase$[esp-4]
mov eax, DWORD PTR [ecx]
jmp DWORD PTR [eax+4]
首先从栈中取出pBase指针指向的对象地址赋给ecx,然后取对象开头的指针变量中的地址赋给eax,此时eax的值即为VPTR的值,也就是VTABLE的地址。最后就是调用虚函数了,由于vfun2位于VTABLE的第二个位置,相当于 VPTR+1,每个函数指针是4个字节长,所以最后的调用被编译器翻译为 jmp DWORD PTR [eax+4]。如果是调用pBase->vfun1(),这句就该被编译为jmp DWORD PTR [eax]。


现在应该对多态、虚函数、晚绑定有比较清楚的了解了吧。

分享到:
评论

相关推荐

    C++虚函数多态和纯虚函数多态的经典示例源码

    虚函数是C++中实现多态的基础。当在一个基类中声明一个虚函数时,这个函数可以在派生类中被重写,从而允许通过基类指针或引用调用派生类的版本。这样,即使不知道对象的确切类型,也可以执行适当的成员函数。例如: ...

    C++虚函数多态的工作原理

    当调用`fun1()`时,`B`类的VTABLE被访问,找到的第一个表项(因为`fun1`是第一个虚函数)就是`B::fun1()`的地址,进而执行`B::fun1()`,实现了多态。 对于类`A`和`B`,它们的VPTR存储在各自的实例对象中。如果类...

    c++继承与多态,虚函数实例

    虚函数在实现多态中扮演着关键角色,它可以确保程序在运行时能够调用到正确的方法,即便对象的实际类型与指针或引用声明的类型不同。 首先,让我们深入理解“虚函数”这一概念。在C++中,虚函数通过在基类中使用`...

    C++中的虚函数与多态

    在C++编程语言中,虚函数(Virtual Functions)和多态(Polymorphism)是面向对象编程的重要特性,它们使得程序具有高度的灵活性和可扩展性。本文将深入探讨这两个概念,结合示例代码进行详细解释。 首先,我们来...

    C++ 多态 虚函数 虚函数表 最是详细

    高质量的C++多态讲解,详细讲解虚函数,虚函数表,虚函数继承,虚函数继承下的内存分配等

    C++虚函数和多态学习笔记

    ### C++虚函数和多态学习笔记 #### 一、虚函数与多态的基本概念 在C++中,虚函数是实现多态的一种机制。多态是指同一个接口(方法名)可以有不同的行为表现,即“一种接口,多种方法”。通过虚函数,我们可以实现...

    C++和Java多态的区别

    - **虚函数表 vs 方法表**:C++中的虚函数调用依赖于虚函数表,而Java则是通过方法表来实现多态。 - **运行时类型信息**:C++提供RTTI机制来支持运行时类型识别,Java则通过方法表和JVM的动态绑定机制来实现。 - **...

    C++ 虚函数与多态 教学PPT

    C++通过虚函数表(Vtable,Virtual Table)来实现运行时多态。每个含有虚函数的类都有一个虚函数表,其中包含了该类及其所有基类的虚函数地址。当使用基类指针调用虚函数时,会通过虚函数表找到实际对象所属类的相应...

    C++ 虚函数表详解

    C++虚函数表详解 C++中的虚函数表是实现多态机制的关键组件。虚函数表(Virtual Table,简称V-Table)是一种机制,用于存储类的虚函数的地址,解决继承和覆盖的问题,使得父类的指针可以正确地调用子类的成员函数。...

    C++课程,多态练习题

    在C++中,多态是通过虚函数(Virtual Function)和虚继承(Virtual Inheritance)来实现的。 虚函数 虚函数是一种特殊的成员函数,它可以被子类重写。当我们在基类中定义一个虚函数时,子类可以根据需要重写该函数...

    C++多态虚函数表

    ### C++多态虚函数表详解 #### 一、多态的基本概念与内存分布 在C++中,多态是一种让程序可以根据不同的上下文表现出不同行为的重要机制。它允许我们使用基类类型的指针或者引用指向派生类的对象,从而在运行时...

    虚函数实现多态

    这是C++编写的体现多态的程序,是在C++类里通过继承和派生来实现的,比较简单

    C++虚函数的应用

    C++虚函数是面向对象编程中的一个重要特性,它允许我们实现多态性,即一个基类指针或引用可以调用派生类重写的成员函数。这种能力在编写可扩展和可复用的代码时非常关键。下面我们将深入探讨C++虚函数的应用及其实现...

    详细讲述c++虚函数实现

    多态是C++泛型编程的一种体现,它允许我们使用统一的接口处理不同类型的对象,如模板、RTTI(Run-Time Type Information)和虚函数都是实现多态的方式。 虚函数的实现依赖于虚函数表(Virtual Table,简称V-Table)...

    c++多态技术和虚函数表

    ### C++多态技术与虚函数表解析 #### 引言 在计算机科学尤其是面向对象编程领域中,多态(Polymorphism)是一项核心概念和技术,它允许对象以多种形态出现,即通过单一接口实现多样化的功能。在C++中,多态可以通过...

    虚函数、多态、动态联编

    在C++编程语言中,虚函数、多态和动态联编是面向对象编程的重要特性,它们使得程序设计更加灵活,能够实现抽象和代码复用。虚函数是实现多态的关键机制,它允许我们通过父类指针或引用调用子类重写的方法,从而达到...

    C++实验六 多态性和虚函数的应用 课程 实验报告

    综上所述,该实验通过设计抽象类和派生类,使用虚函数实现多态性,展示了面向对象编程的关键概念,如继承、封装、多态和动态绑定。这些概念对于理解和编写复杂的C++程序至关重要。通过这样的实践,学生能够更好地...

    C++继承与多态例子

    虚函数是C++中实现动态多态的关键。在基类中声明虚函数,可以让子类覆盖这个函数并提供自己的实现。这样,我们就可以通过基类指针或引用调用子类的特定版本,即使在编译时不知道实际的对象类型。例如: ```cpp ...

    利用虚函数让private外部访问成为可能!

    ### C++中的虚函数与多态 在C++中,虚函数是用来实现运行时多态的关键机制之一。当一个基类指针或引用指向派生类对象时,通过该指针或引用调用虚函数,将根据实际对象的类型来决定调用哪个版本的函数。这种特性使得...

Global site tag (gtag.js) - Google Analytics