虚函数

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。

1、普通的继承关系
#include <iostream>

class Base		//定义基类
{
public:
Base(int a) :ma(a) {}
void Show()
{
std::cout << "Base: ma = " << ma << std::endl;
}
protected:
int ma;
}; class Deriver : public Base //派生类
{
public:
Deriver(int b) :mb(b), Base(b) {}
void Show()
{
std::cout << "Deriver: mb = " << mb << std::endl;
}
protected:
int mb;
}; int main()
{
Base* pb = new Deriver(10);
std::cout << sizeof(Base) << std::endl; // 4
std::cout << sizeof(Deriver) << std::endl; // 8
std::cout << typeid(Base).name() << std::endl; // class Base
std::cout << typeid(Deriver).name() << std::endl; // class Deriver
pb->Show(); // Base : ma = 10
return 0;
}

运行结果与我们预想的一样。其中 pb变量为指针类型,指针类型被认为是内置类型,且只与定义点有关,所以 Base * 类型的指针解引用之后是 Base 类型。

查看内存布局

打开命令行的开发者命令提示窗口



命令如下:(记得切换到该项目文件夹下)

cl file.cpp /d1reportSingleClassLayoutXXX
其中 file.cpp 是cpp文件名,XXX是文件中要查看的类名

分别输入命令查看 基类Base、派生类Derive 的内存布局

/*  Base内存布局  */
class Base size(4):
+---
0 | ma
+--- /* Derive内存布局 */
class Derive size(8):
+---
0 | +--- (base class Base)
0 | | ma
| +---
4 | mb
+---

成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。

我们可以看到,在 Derive 的内存布局中,有继承自 Base 类的数据成员。



可以看到派生类继承了基类的成员变量,在内存排布上,先是排布了基类的成员变量,接着排布派生类的成员变量,同样,成员函数不占字节

2、使用 virtual 关键字

给基类的 Show() 函数加上 virtual 关键字

class Base		//定义基类
{
public:
Base(int a) :ma(a) {}
virtual void Show()
{
std::cout << "Base: ma = " << ma << std::endl;
}
protected:
int ma;
};

重新编译并运行,可以看到运行结果发生了改变。

查看内存布局

在基类中加入 virtual 关键字,基类内存布局中增加了一个{vfptr}指针(指向Base的虚表),Base 所占字节数也从 4 字节变成了 8 字节。同时,Base 还增加一个虚表(vftable),在该虚表中写入了Base中所有虚函数的地址。

在派生类中也有一个虚表指针和一个虚表,需要说明的是,派生类中的虚表继承自基类,派生类通过将自己的虚函数写入继承的虚表, 覆盖 掉原来的虚表(基类与派生类各自拥有一个虚表)。因此,在 Derive 的内存布局中只有一个虚表指针。

/*  Base 内存布局  */
class Base size(8):
+---
0 | {vfptr}
4 | ma
+--- Base::$vftable@:
| &Base_meta
| 0
0 | &Base::Show /* Derive 内存布局 */
class Derive size(12):
+---
0 | +--- (base class Base)
0 | | {vfptr}
4 | | ma
| +---
8 | mb
+--- Derive::$vftable@:
| &Derive_meta
| 0
0 | &Derive::Show

内存布局如图所示

3、虚函数表机制

基类指针指向派生类对象实质上是指向派生类对象中基类的起始部分,在虚函数表结构中有三部分组成,分别是:

  • RTTI(Run-Time Type Identification)信息:通过保存在其中的类型信息在运行时能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。
  • 偏移: 成员变量与类对象的偏移地址(在 vs 中,vfptr 相对于成员变量的优先级更大,位于内存分布的首位,所以偏移量为0)。
  • 虚函数入口地址:在调用函数时通过 call 指令跳转到函数,在虚表中保存虚函数的入口地址可以在运行阶段通过查虚表的方式实现动多态。



    vfptr是在构造函数的栈帧进行初始化的时候:在构造函数初始化列表之后并调用构造函数第一行代码之前,函数栈帧开辟后进行赋值虚表地址赋值给vfptr的,This指针的赋值也是在构造函数的栈帧进行。

4、基类指针指向派生类对象

在实例2 中有这样一段代码

	Base* pb = new Derive(10);

虚函数调用:基类指针 pb 指向 派生类对象,而在派生类对象的内存布局中有一个虚表指针,其中虚表指针指向的 Derive 的虚表结构。因此,在 pb->Show() 调用时,实际上是 pb -> vfptr -> Derive::Show(),最终在屏幕上输出了 “Derive: mb = 10” 。



可以这么理解,Base pb = new Derived();生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived到Base*的转换并没有改变虚表指针,pb 所指向的对象它在构造的时候就已经指向了子类的Derive::Show(),所以调用的是子类的虚函数,这就是多态了。

另外,在基类指针 pb 解引用时,优先查看 RTTI 信息中保存的类型信息。因此,*pb 的类型被解析成 Derive 类型。

5、虚函数与析构

在 main 函数中,派生类是在 new 形成的,也就是说在堆内存上开辟的,但在结束时我们并没有手动的释放就会造成内存泄漏。修改上述代码如下:

#include <iostream>

class Base		//定义基类
{
public:
Base(int a) :ma(a)
{
std::cout << "Base()" << std::endl;
}
virtual void Show()
{
std::cout << "Base: ma = " << ma << std::endl;
}
///////////////////////////////////////////////////
/* 以下内容为新添加 */
~Base()
{
std::cout << "~Base()" << std::endl;
}
///////////////////////////////////////////////////
protected:
int ma;
}; class Derive : public Base //派生类
{
public:
Derive(int b) :mb(b), Base(b)
{
std::cout << "Derive()" << std::endl;
}
void Show()
{
std::cout << "Derive: mb = " << mb << std::endl;
}
///////////////////////////////////////////////////
/* 以下内容为新添加 */
~Derive()
{
std::cout << "~Derive()" << std::endl;
}
///////////////////////////////////////////////////
protected:
int mb;
}; int main()
{
Base* pb = new Derive(10);
delete pb;
return 0;
}

在原有的 Base类 和 Derive类 中,添加了析构函数,可以看到运行结果中类构造了两次,最后却被析构了一次。Derive 类是继承自 Base 类的,因此,在构造 Derive 对象时会先构造他的父类 Base 类,析构的时候理应也析构两次才对,这里却只析构了一次。



分析:

  • 析构函数跟普通成员没有什么不同,只是编译器在会在特定的时候自动调用析构函数(离开作用域或者执行delete操作);甚至我们可以通过对象手动调用析构。

  • 由于 Base 类中析构函数不是虚函数,就不满足动多态发生的条件,在 delete pb 时就会被当做普通函数调用,而 pb 本质上是 Base* 类型指针,因此在释放基类指针时调用了基类析构(~Base() ),而派生类对象还没有被析构造成了内存泄露。

综上,在 Derive类 与 Base类 中,在 delete pb 时,可以看做是发生了 pb -> ~Base() 的函数调用(对象指针调用析构函数,这里的pb为 Base* 类型),而我们希望执行的是 pb -> Derive() 的函数调用(这里的 pb 是Derive 类型),因此我们可以设法让 pb 在调用时实现动多态,而调用派生类的函数。

基类的析构函数声明为虚函数

将基类 Base 的析构函数申明为虚函数,这样析构函数就会被写入虚函数表,可以实现析构时的动多态。

/*  Base  */
virtual ~Base()
{
std::cout << "~Base()" << std::endl;
}

内存布局

/*  Base  */
Base::$vftable@:
| &Base_meta
| 0
0 | &Base::Show
1 | &Base::{dtor} /* Derive */
Derive::$vftable@:
| &Derive_meta
| 0
0 | &Derive::Show
1 | &Derive::{dtor}

运行测试,和我们预想的结果一致。

其中,destroy()函数就是我们平时说的析构函数。添加了 virtual 关键字后基类中析构函数是虚函数,派生类的析构函数自动成为虚函数。基类就有一张虚函数表,派生类继承基类的时候会把自己的析构函数覆盖到虚函数表中,delete基类指针的时候,调用的就是该派生类析构函数而该派生类析构函数会先释放派生类对象再释放基类对象。这样的话就不会造成派生类的资源没有释放的问题。

因此,在继承中做父类(基类),并且可能会发生动多态时(比如 Base * pb = new Derive() )父类的析构函数要声明为虚函数,如果不用虚函数,子类的析构函数不能得到调用,会造成内存泄漏。但并不是要把所有类的析构函数都写成虚函数。只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

那么是不是所有类的析构函数都可以设置成虚析构呢?

可以,但没必要,也不建议这么做。设置成虚析构并不影响析构函数的调用,但设置虚析构会产生额外的开销,因为系统会产生虚表和虚表指针占用类的存储空间。

C++ | 虚函数初探的更多相关文章

  1. C++ | 虚函数产生条件

    虚函数产生的条件 能否成为虚函数主要有以下两种判断依据,如果以下两种条件均满足,则具有成为虚函数的条件. 1.虚函数机制为动多态提供支持,而虚函数表中存放着虚函数的地址.因此虚函数必须是可以取地址的函 ...

  2. C++ 虚函数和多重继承的内存布局初探

    C++ 对象的内存布局 一切以事实说话: 代码: 1: #include <stdio.h> 2:  3: class A { 4: public: 5: int a; 6: int b; ...

  3. C++虚函数和函数指针一起使用

    C++虚函数和函数指针一起使用,写起来有点麻烦. 下面贴出一份示例代码,可作参考.(需要支持C++11编译) #include <stdio.h> #include <list> ...

  4. 匹夫细说C#:从园友留言到动手实现C#虚函数机制

    前言 上一篇文章匹夫通过CIL代码简析了一下C#函数调用的话题.虽然点击进来的童鞋并不如匹夫预料的那么多,但也还是有一些挺有质量的来自园友的回复.这不,就有一个园友提出了这样一个代码,这段代码如果被编 ...

  5. 【C++】多态性(函数重载与虚函数)

    多态性就是同一符号或名字在不同情况下具有不同解释的现象.多态性有两种表现形式: 编译时多态性:同一对象收到相同的消息却产生不同的函数调用,一般通过函数重载来实现,在编译时就实现了绑定,属于静态绑定. ...

  6. 虚函数的使用 以及虚函数与重载的关系, 空虚函数的作用,纯虚函数->抽象类,基类虚析构函数使释放对象更彻底

    为了访问公有派生类的特定成员,可以通过讲基类指针显示转换为派生类指针. 也可以将基类的非静态成员函数定义为虚函数(在函数前加上virtual) #include<iostream> usi ...

  7. C++ 系列:虚函数

    Copyright © 1900-2016, NORYES, All Rights Reserved. http://www.cnblogs.com/noryes/ 欢迎转载,请保留此版权声明. -- ...

  8. EC笔记,第二部分:9.不在构造、析构函数中调用虚函数

    9.不在构造.析构函数中调用虚函数 1.在构造函数和析构函数中调用虚函数会产生什么结果呢? #; } 上述程序会产生什么样的输出呢? 你一定会以为会输出: cls2 make cls2 delete ...

  9. C++构造函数中不能调用虚函数

    在构造函数中调用虚函数,并不会产生多态的效果,就跟普通函数一样. c++ primer 第四版中497页15.4.5构造函数和析构中的虚函数讲到,如果在构造函数或析构函数中调用虚函数,则运行的是为构造 ...

随机推荐

  1. 计算机网络-IP篇

    目录 IP 基本认识 IP地址 IP 地址的分类 公有 IP 地址与私有 IP 地址 IP 地址与路由控制 IP 分⽚与重组 IPv6 基本认识 IPv4 ⾸部与 IPv6 ⾸部 IP协议 DNS A ...

  2. Python:获取某一月的天数

    import calendarcalendar.monthlen(2021,6)30calendar.monthrange(2021,6)(1, 30) calendar.monthrange( ye ...

  3. WPS:将题注与章节连接起来

    右键图片添加题注,在题注设置中勾选题注编号中的包含章节编号

  4. c语言刷 队列题记录

    622. 设计循环队列 https://blog.csdn.net/Galaxy_n/article/details/115978544 typedef struct { int *arrs; int ...

  5. JZ-028-数组中出现次数超过一半的数字

    数组中出现次数超过一半的数字 题目描述 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字.例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}.由于数字2在数组中出现了5次,超 ...

  6. linux echo用法和实例

    echo命令用于在shell中打印shell变量的值,或者直接输出指定的字符串.linux的echo命令,在shell编程中极为常用, 在终端下打印变量value的时候也是常常用到的,因此有必要了解下 ...

  7. .NET6: 开发基于WPF的摩登三维工业软件 (8) - MVVM

    基于WPF开发界面的一个很大优势是可以方便地基于MVVM设计模式开发应用.本文从应用的角度基于MVVM实现参数化管材的创建界面. 1 MVVM MVVM是Model-View-ViewModel的简写 ...

  8. thinkphp 添加数据

    ....控制器方法返回视图 public function create() { // return view(); } ...............表单页面 <!DOCTYPE html&g ...

  9. iframe于iframe页面之间的函数相互调用

    因为iframe页面于包括父页面在内的其他页面通讯有跨域问题,所以只有在服务器环境下或者火狐浏览器下才能测试. 在iframe页面调用父页面的函数采用parent,例子:在父页面有一个say()函数, ...

  10. 活用Windows Server 2008系统的几种安全功能

    与传统操作系统相比,Win2008系统的安全防范功能更加强大,安全防护能力自然也是高人一等,我们只要在平时善于使用该系统新增的各项安全防范功能,完全可以实现更高级别的安全保护目的.现在,本文就为大家贡 ...