多态与虚函数表

来源:互联网 发布:风云白露进阶数据 编辑:程序博客网 时间:2024/05/16 09:34

什么是多态,多态有什么用途。

  1. 定义:“一个接口,多种方法”,程序在运行时才决定调用的函数。
  2. 实现:C++多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和overload的区别,overload是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。

3.目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。

4.用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。

补充一下关于重载、重写、隐藏(总是不记得)的区别:

复制代码
Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。(1)相同的范围(在同一个类中);(2)函数名字相同;(3)参数不同;(4)virtual 关键字可有可无。Override(覆盖):是指派生类函数覆盖基类函数,特征是:(1)不同的范围(分别位于派生类与基类);(2)函数名字相同;(3)参数相同;(4)基类函数必须有virtual 关键字。
注:重写基类虚函数的时候,会自动转换这个函数为virtual函数,不管有没有加virtual,因此重写的时候不加virtual也是可以的,不过为了易读性,还是加上比较好。Overwrite(重写):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
复制代码

补充一下虚函数表:

多态是由虚函数实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。

如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。如下图:

这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。

注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。

  • 原始基类的虚函数表

  下图是原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数)

  • 单继承时的虚函数(无重写基类虚函数

假设现在派生类继承基类,并且重新定义了3个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表

  Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放

  • 单继承时的虚函数(重写基类虚函数

现在派生类重写基类的x函数,可以看到这个派生类构建自己的虚函数表的时候,修改了base::x()这一项,指向了自己的虚函数。

  • 多重继承时的虚函数(Derived ::public Base1,public Base2)

这个派生类多重继承了两个基类base1,base2,因此它有两个虚函数表。

  

  它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。

  多重继承时指针的调整:

Derive b;Base1* ptr1 = &b;   // 指向 b 的初始地址Base2* ptr2 = &b;   // 指向 b 的第二个子对象

因为 Base1 是第一个基类,所以 ptr1 指向的是 Derive 对象的起始地址,不需要调整指针(偏移)。

因为 Base2 是第二个基类,所以必须对指针进行调整,即加上一个 offset,让 ptr2 指向 Base2 子对象。

当然,上述过程是由编译器完成的。

Base1* b1 = (Base1*)ptr2;b1->y();                   // 输出 Base2::y()Base2* b2 = (Base2*)ptr1;b2->y();                   // 输出 Base1::y()

其实,通过某个类型的指针访问某个成员时,编译器只是根据类型的定义查找这个成员所在偏移量,用这个偏移量获取成员。由于 ptr2 本来就指向 Base2 子对象的起始地址,所以b1->y()调用到的是Base2::y(),而 ptr1 本来就指向 Base1 子对象的起始地址(即 Derive对象的起始地址),所以b2->y()调用到的是Base1::y()

  • 虚继承时的虚函数表

  虚继承的引入把对象的模型变得十分复杂,除了每个基类(MyClassA和MyClassB)和公共基类(MyClass)的虚函数表指针需要记录外,每个虚拟继承了MyClass的父类还需要记录一个虚基类表vbtable的指针vbptr。MyClassC的对象模型如图4所示。

  

   虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。比如MyClassA的虚基类表第二项记录值为24,正是MyClass::vfptr相对于MyClassA::vbptr的偏移量,同理MyClassB的虚基类表第二项记录值12也正是MyClass::vfptr相对于MyClassA::vbptr的偏移量。(虚函数与虚继承深入探讨)

对象模型探讨:

 

1.没有继承情况,vptr存放在对象的开始位置,以下是Base1的内存布局

m_iData :100


 2.单继承的情况下,对象只有一个vptr,它存放在对象的开始位置,派生类子对象在父类子对象的最后面,以下是D1的内存布局

B1:: m_iData : 100

B1::vptr : 4294800

B2::vptr : 4294776

D::m_iData :300


4. 虚拟继承情况下,虚父类子对象会放在派生类子对象之后,派生类子对象的第一个位置存放着一个vptr,虚拟子类子对象也会保存一个vptr,以下是VD1的内存布局

 

 Unknown : 4294888

B1::vptr :4294864

VD1::vptr :        4294944

VD1::m_iData :  200

VD2::Unknown : 4294952

VD::m_iData : 500

B1::m_iData :  100

5. 棱形继承的情况下,非虚基类子对象在派生类子对象前面,并按照声明顺序排列,虚基类子对象在派生类子对象后面

VD1::Unknown : 4294968

VD2::vptr :    4   294932

VD2::m_iData : 300

B1::vptr :       4294920

B1::m_iData :  100

 

补充一下纯虚函数:

  • 定义: 在很多情况下,基类本身生成对象是不合情理的。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)纯虚函数不能再在基类中实现,编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。
  • 特点:

1,当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;(避免类被实例化且在编译时候被发现,可以采用此方法)

2,这个方法必须在派生类(derived class)中被实现;

  • 目的:使派生类仅仅只是继承函数的接口。
补充一下纯虚函数:
  • 定义:称带有纯虚函数的类为抽象类。
  • 作用:为一个继承体系提供一个公共的根,为派生类提供操作接口的通用语义。
  • 特点:1.抽象类只能作为基类来使用,而继承了抽象类的派生类如果没有实现纯虚函数,而只是继承纯虚函数,那么该类仍旧是一个抽象类,如果实现了纯虚函数,就不再是抽象类。
      2.抽象类不可以定义对象。
补充一下多重继承和虚继承:
多重继承:
定义:派生类继承多个基类,派生类为每个基类(显式或隐式地)指定了访问级别——publicprotected 或 private
    class Panda : public Bear, public Endangered {    }

构造:

    1. 派生类的对象包含每个基类的基类子对象。
    2. 派生类构造函数初始化所有基类(多重继承中若没有显式调用某个基类的构造函数,则编译器会调用该基类默认构造函数),派生类只能初始化自己的基类,并不需要考虑基类的基类怎么初始化。
    3. 多重继承时,基类构造函数按照基类构造函数在类派生列表中的出现次序调用。
析构:总是按构造函数运行的逆序调用析构函数。(基类的析构函数最好写成virtual,否则再子类对象销毁的时候,无法销毁子类对象部分资源。)假定所有根基类都将它们的析构函数适当定义为虚函数,那么,无论通过哪种指针类型删除对象,虚析构函数的处理都是一致的。
 
拷贝构造/赋值:如果要为派生类编写拷贝构造函数,则需要为调用基类相应拷贝构造函数并为其传递参数,否则只会拷贝派生类部分。
深拷贝与浅拷贝:浅拷贝:默认的复制构造函数只是完成了对象之间的位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。
    这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。深拷贝:自定义复制构造函数需要注意,对象之间发生复制,资源重新分配,即A有5个空间,B也应该有5个空间,而不是指向A的5个空间。

虚继承与虚基类:

定义:在多重继承下,一个基类可以在派生层次中出现多次。(派生类对象中可能出现多个基类对象)在 C++ 中,通过使用虚继承解决这类问题。虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类

用法:istream 和 ostream 类对它们的基类进行虚继承。通过使基类成为虚基类,istream 和 ostream 指定,如果其他类(如 iostream 同时继承它们两个,则派生类中只出现它们的公共基类ios的一个副本。通过在派生列表中包含关键字 virtual 设置虚基类:
    class istream : public virtual ios { ... };    class ostream : virtual public ios { ... };    // iostream inherits only one copy of its ios base class    class iostream: public istream, public ostream { ... };
0 1
原创粉丝点击