文档章节

C++虚函数以及虚函数表详解

黑白双键
 黑白双键
发布于 2019/11/18 23:41
字数 3725
阅读 18
收藏 0

在了解虚函数之前先了解下对象模型:

对象模型: 在C++中,有两种数据成员(class data members):static 和nonstatic,以及三种类成员函数(class member functions):static、nonstatic和virtual:

 

说明:采用的是非继承下的C++对象模型:

nonstatic 数据成员被置于每一个类对象中,而static数据成员被置于类对象之外。static与nonstatic函数也都放在类对象之外,而对于virtual 函数,则通过虚函数表+虚指针来支持,具体如下:

  • 每个类生成一个表格,称为虚表(virtual table,简称vtbl)。虚表中存放着一堆指针,这些指针指向该类每一个虚函数。虚表中的函数地址将按声明时的顺序排列,不过当子类有多个重载函数时例外。
  • 每个类对象都拥有一个虚表指针(vptr),由编译器为其生成。虚表指针的设定与重置皆由类的复制控制(也即是构造函数、析构函数、赋值操作符)来完成。vptr的位置为编译器决定,传统上它被放在所有显示声明的成员之后,不过现在许多编译器把vptr放在一个类对象的最前端。
    另外,虚函数表的前面设置了一个指向type_info的指针,用以支持RTTI(Run Time Type Identification,运行时类型识别)。RTTI是为多态而生成的信息,包括对象继承关系,对象本身的描述等,只有具有虚函数的对象在会生成。
  • 圆圈代表类中可以供每个对象共享的部分,矩形表示每个对象自己含有的部分。

C++向上转型(Upcasting)

  • 类其实也是一种数据类型,也可以数据类型,不过只有在基类和派生类之间发生才有意义。 并且只能将派生类赋值给基类,包括对象赋值、指针赋值、引用赋值,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting),其中向上转型安全,由编译器自动完成,向下有风险,由程序员手动干预
  • 赋值的本质是将现有的数据写入已经分配好的内存中,对象内存中只包括成员变量,所以对象之间的赋值只包括成员变量的赋值,成员函数不存在赋值问题,不会影响成员函数和this指针。所以将派生生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,不能使用派生类的成员函数。
  • 将派生类对象赋值给基类对象时,会舍弃派生类新增的成员。这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。

  • 向上转型后通过基类的对象、指针、引用只能访问从基类继承过去的成员(包括成员变量和成员函数),不能访问派生类新增的成员
  • 将派生类指针赋值给基类指针(对象指针之间的赋值)。赋值时,基类对象的指针必须指向子对象中的偏移起始位置

    多继承关系

    将指向D对象的指针赋值给C对象指针时,编译器会进行调整:

    pc = (C*)( (int)pd + sizeof(B) );

    从而将指针偏移至C类子对象的起始位置。如下图。

    指针赋值偏移

问题提出:通过基类指针只能访问派生类的成员变量,不能访问派生类的成员函数,那该如何可以访问那???

  1. 多态简介

于是乎就增加了虚函数,唯一用处就是 构成多态可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,只能访问成员变量。

有虚函数才能构成多态。

虚函数详解:

  • 只需在虚函数的声明处加上virutal关键字,定义时可以不加。
  • 可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽I(覆盖)关系的同名函数都将自动成为虚函数。当基类中声明了虚函数是,如果派生类没有定义新的函数来屏蔽此函数,那么将使用基类的虚函数
  • 只有派生类的虚函数遮蔽了基类的虚函数(函数原型相同,参数类型和个数都一样)才能构成多态(即通过基类指针访问派生类函数)
  • 构造函数不能是虚函数,基类的构造函数只仅仅在派生类构造函数别调用,不是继承。
  • 析构函数可以声明为虚函数。原因有二,一:使用基类指针指向派生类时,非虚函数被访问时,编译器会根据指针的类型来确定要调用的函数,即是指针指向哪个类就调用哪个类的函数,这样派生类的折构函数永远不会调用,局部变量无法释放;二:将基类的折构函数设置为虚函数后,派生类的析构函数也会自动成为虚函数,这是有析构函数的调用顺序决定的,从派生类到基类的顺修调用。

纯虚数:

  • 纯虚数没有函数体,只有函数声明,在结尾加上=0;virtual 返回值类型 函数名 (函数参数) = 0;
  • 因为没有函数体,无法被调用,无法分配内存,也就无法实例化,无法创建对象,即是抽象类
  • 抽象类通常作为基类,让派生类实现纯虚数。派生类必须实现纯虚数才能实例化

 

解决让虚函数(virtual)是为了c++实现多态而采用的方法,并使用了动态绑定的技术-虚函数表。每一个包含虚函数的类都会有一个虚表,若基类含有虚函数,则派生类也要有自己的虚表。 通过使用这些虚函数表,即使使用的是基类的指针来调用函数,也可以达到正确调用运行中实际对象的虚函数。

 例如: 类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

则类A的虚表(虚表只包含虚函数)如图:

虚表其实是一个指针数组,每个元素是指向虚函数的指针,普通函数为非虚函数,不需要通过虚表,虚表内的条目即虚函数指针的赋值发生在编译器的编辑阶段,在代码编译时,就已经构造出来了。

2.虚表指针

虚表属于类所有,不属于某个具体的对象。一个类只需要有一个虚表即可。同一个类的对象使用同一个虚表。为了指定对象的虚表,对象内部包含一个指向虚表的指针,来指向自己所使用的虚表。 为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表 当类的对象在创建是便有了这个指针,且指针的值会自动被设置为指向类的虚表。

 

说明:一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

4.动态绑定

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

class B : public A {
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};

class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

 对象模型如图:

说明:A是基类,B继承了A,C又继承了B。三个类中都有虚函数,故编译器为每个类都会创建一个虚表。每个类中的每个对象都有一个虚表指针,指向自己所属类的虚表。

类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()和A::vfunc2()。
类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。
类C继承于类B,故类C可以调用类B的函数,但由于类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。
法则:对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向器继承的最近一个类的虚函数

 

5.父类通过虚表指针访问子类的虚函数

 

int main() 
{
    B bObject;
    A *p = & bObject;
}

说明:bObject是类B的一个对象,故bObject包含一个虚表指针。声明一个类A的指针指向对象bObject。

以上代码执行步骤:

  1. 根据虚表指针p->__vptr来访问对象bObject对应的虚表。虽然指针p是基类A*类型,但是*__vptr也是基类的一部分,所以可以通过p->__vptr可以访问到对象对应的虚表。
  2. 在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。对于 p->vfunc1()的调用,B vtbl的第一项即是vfunc1对应的条目。
  3. 根据虚表中找到的函数指针,调用函数。从图3可以看到,B vtbl的第一项指向B::vfunc1(),所以 p->vfunc1()实质会调用B::vfunc1()函数。

 

  《-------》    指针p是基类A*类型,但是*__vptr也是基类的一部分的理解。

 

6.静态绑定和动态绑定,运行时多态,编译时多态

把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。 动态绑定的三个条件:

  • 通过指针来调用函数
  • 指针upcast向上转型(继承类向基类的转换称为upcast)
  • 调用的是虚函数

符合三个条件,编译器就会把该函数调用编译为动态绑定,调用时走虚表的机制。

 

7.RTTI机制(Run-Time Type Identification)- 运行时类型识别

  1. 如果类包含了虚函数,那么该类的对象内存中还会额外增加类型信息,也即 type_info对象。例如以下代码:
//基类
class Base{
public:
	virtual void func();
protected:
	int m_a;
	int m_b;
};
void Base::func(){ cout<<"Base"<<endl; }

//派生类
class Derived: public Base{
public:
	void func();
private:
	int m_c;
};
void Derived::func(){ cout<<"Derived"<<endl; }

int main() {
	Base *p;
	int n;

	cin>>n;
	if(n <= 100){
		p = new Base();
	}else{
		p = new Derived();
	}
	cout<<typeid(*p).name()<<endl;
	return 0;
}

其中Base和Derived的对象内存模型如图所示:

说明:typeid运算符:用来获取一个表达式的类型信息。信息主要包括:

  • 对于基本类型(int、float 等C++ 内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型
  • 对于类类型的数据(也就是对象),类型信息是指对象所属的类所包含的成员所在的继承关系等。

在编译时,不会为所有的类型创建type_info对象, 只会为使用了typeid运算符的类型创建。不过有一种特殊情况,就是带虚函数的类(包括继承来的),不管有没有使用typeid运算符,编译器都会为带虚函数的类创建type_info对象。

 

 

编译器会在虚函数表vftable的开头插入一个指向type_info对象的指针。程序运行时通过对象指针p找到虚函数表指针vfptr,再通过vfptr找到type_info对象的指针,进而获得类型信息。(**(p->vfptr -1))可以获得type_info对象

2. 编译器在编译阶段无法确定p指向哪个对象,也就无法获取*p的类型信息,但是编译器可以在编译阶段做好各种准备,这样程序在运行后可以借助这些准备好的数据来获取类型信息。这些准备包括:

  • 创建type_info对象,并在vftable的开头插入一个指针,指向type_info对象。
  • 将获取类型信息的操作转换成类似**(p->vfptr - 1)这样的语句。 这种在程序运行后确定对象的类型信息的机制称为运行时类型识别(Run-Time Type Identification,RTTI)。在C++ 中,只有类中包含了虚函数时才会启用 RTTI 机制,其他所有情况都可以在编译阶段确定类型信息。

7.静态绑定和·动态绑定

  • 符号绑定:C/C++中变量来存储数据,函数来定义代码,它们最终都要放到内存中才能CPU使用。CPU通过地址来取得内存中代码和数据,程序在执行时会告诉CPU相关的地址。其实变量名和函数名只是地址的一种助记符。当源文件被编译和链接成可执行的程序后,它们都会被替代成地址,这一过程称作符号绑定,这也是编译和链接过程中的一个重要任务。
  • 函数绑定:函数调用实际上就是执行函数体中的代码,函数体是内存中的代码段,函数名是代码段的首地址。函数执行时找到函数名对应的地址,然后将函数调用处用该地址替换。
  • 静态绑定:编译和链接期间就能找到函数名对应的地址,完成函数的绑定,程序运行时直接使用该地址即可
  • 动态绑定:有些程序必须要等到程序运行后根据具体的环境才能决定。比如RTTI机制。
  •  

© 著作权归作者所有

黑白双键
粉丝 0
博文 48
码字总数 64825
作品 0
成都
私信 提问
加载中

评论(0)

从编译器的辅助信息看c++对象内存布局

编程 cpp 预知识 本文的内容使用的是32位的编译器编译出的结果,可以打印出类的内存布局信息 DevCPP IDE 这个IDE是我比较喜欢的windows下的cpp的IDE之一,它有一个工具->编译选项,可以选择编...

在河之简
2019/11/02
0
0
C++对象模型:单继承,多继承,虚继承

什么是对象模型 有两个概念可以解释C++对象模型: 语言中直接支持面向对象程序设计的部分。 对于各种支持的底层实现机制。 类中成员分类 数据成员分为静态和非静态,成员函数有静态非静态以及...

天王盖地虎626
2019/02/28
34
0
C++学习笔记 -- 虚析构函数与纯虚析构函数

开始学C++了,所以又重拾以前学习过的相关概念… 析构函数是当一个对象的生命周期结束时,会自动执行析构函数。 析构函数的定义: #ifndef AH #define AH class A { public: A(void); A(int...

meteoric
2013/05/08
0
0
跟我一起学习C++虚函数--第一篇

我们知道,虚函数作为C++实现多态的方式,具有强大的RTTI(RunTime Type Identification)功能。虚函数使用起来比较简单,但是也很容易出错。本系列将带着你一步一步了解虚函数的内部实现机制...

pathenon
2012/07/14
192
1
C++灵魂所在之---多态的前世与今生

开头先送大家一句话吧: 众所周知,在20世纪80年代早期,C++在贝尔实验室诞生了,这是一门面向对象的语言,但它又不是全新的面向对象的语言,它是在传统的语言(C语言)进行面向对象扩展而来...

loving_forever_
2016/06/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

00-Java 面试准备

面试之前 面试前准备简历需要注意的几个方面: 写简历、改简历,这个一定要干的。简历有两个作用,一个是吸引别人,能让别人邀请你去面试,这是前提;另一个是引导面试的人,让面试的人问你所...

源程序
今天
54
0
OSChina 周二乱弹 —— 大王(@罗马的王)颜值制霸Osc社区

Osc乱弹歌单(2020)请戳(这里) 【今日歌曲】 @巴拉迪维 :Lunik的单曲《Seeing You Soar》 I hope you’re smiling,When seeing me soar. #今日歌曲推荐# 《Seeing You Soar》- Lunik 手...

小小编辑
今天
75
0
wordcount代码

1.写出map类 public class WCMapper extends Mapper<LongWritable,Text,Text,LongWritable>{ @Override protected void map(LongWritable key,Text value,Context context)throws IOExcepti......

七宝1
今天
59
0
Spring Batch 小任务(Tasklet)步骤

Chunk-Oriented Processing不是处理 step 的唯一方法。 考虑下面的一个场景,如果你仅仅需要调用一个存储过程,你可以在 ItemReader 中实现这个调用,然后在存储过程完成调用后返回 null。这...

honeymoose
今天
67
0
Linux日志分析

1. Linux日志文件的类型 2. 系统服务日志 2.1 syslogd的简介 2.2 syslogd的配置和使用 2.3 日志的安全性设置 2.4 远程日志记录服务 3. 日志的轮替 3.1 logrotate简介 3.2 logrotate的配置 3....

JiaMing
昨天
67
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部