虚函数与虚析构函数原理

Posted siwuxie095

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚函数与虚析构函数原理相关的知识,希望对你有一定的参考价值。

----------------siwuxie095

   

   

   

   

   

   

   

   

   

   

关于虚函数和虚析构函数的实现原理,因为涉及到 函数指针,

所以先介绍什么是函数指针

   

   

   

函数指针

   

   

如果通过一个指针指向对象,就称其为 对象指针,指针除了可以

指向对象之外,也可以指向函数,就称其为 函数指针

   

   

   

   

函数的本质,其实就是一段二进制的代码,它写在内存中,

可以通过指针来指向这段代码的开头,计算机就会从开头

一直往下执行,直到函数的结尾,并通过指令返回回来

   

   

   

如果有这么 5 个函数指针,它们所存储的就是 5 个函数的函数地址,当

使用时,如:使用 Fun3_Ptr,就可以通过 Fun3_Ptr 拿到 Fun3() 的函数

入口,当用指针指向函数入口,并命令计算机开始执行时,计算机就会使

Fun3() 中的二进制代码不断的得到执行,直到执行完毕为止,其它的

函数也是如此

   

   

   

可能会有人说 函数指针 很神奇,其实,函数的指针与普通的指针,本质上

是一样的,它也是由 4 个基本的内存单元组成,存储着一个内存地址,也

就是 函数的首地址

   

   

   

   

   

虚函数的实现原理

   

   

看如下实例:

   

定义一个形状类:Shape,其中有一个虚函数和一个数据成员

   

   

   

   

再定义一个圆类:Circle,它公有继承了 Shape 类,其中并没有给

Circle 定义一个计算面积的虚函数,即 Circle 所使用的是 Shape 的

虚函数 calcArea() 来计算面积

   

   

   

   

当实例化一个 Shape 对象时,这个对象中除了数据成员 m_iEdge 之外,

它还会有另一个数据成员:虚函数表指针

   

   

   

虚函数表指针,也是一个指针,占有 4 个基本的内存单元

   

虚函数表指针,顾名思义,它指向一个虚函数表,该虚函数表会与 Shape

类的定义同时出现

   

在计算机中,虚函数表也是占有一定空间的,假设虚函数表的起始位置是

0xCCFF,那么这个 虚函数表指针 的值就是 0xCCFF,父类的虚函数表只

有一个,通过父类实例化出来的所有对象,它们的 虚函数表指针 的值都是

0xCCFF,以确保每一个对象的 虚函数表指针 都指向自己的虚函数表

   

在父类 Shape 的虚函数表中,肯定定义了这样一个函数指针,该函数指针

就是计算面积 calcArea() 这个函数的入口地址,如果 calcArea() 的入口地

址是 0x3355,则虚函数表中 calcArea_ptr 的值就是 0x3355,调用时,就

可以先找到 虚函数表指针,再通过 虚函数表指针 找到虚函数表,再通过位

置的偏移找到相应的虚函数的入口地址(即 函数指针),从而最终找到当前

定义的虚函数 calcArea()

   

   

   

当实例化一个 Circle 对象时,因为 Circle 中并没有定义虚函数,但却从父类

中继承了虚函数 calcArea(),所以,在实例化 Circle 对象时也会产生一个虚

函数表

   

   

   

注意:这个虚函数表是 Circle 的虚函数表,和 Shape 的虚函数表不同,

它的起始位置是 0x6688,但是在 Circle 的虚函数表中,计算面积的函

数指针却是一样的,都是 0x3355

   

这就能够保证:在 Circle 中去访问父类计算面积的函数 calcArea(),

也能通过 虚函数表指针 找到自己的虚函数表,在自己的虚函数表中

通过偏移找到的计算面积的函数指针 calcArea_ptr,也是指向父类的

计算面积的函数入口

   

   

   

如果在 Circle 中定义了计算面积的函数,又会是怎样的呢?

   

   

   

   

对于 Shape 类来说,它的情况不变:有自己的虚函数表,并且在

实例化一个 Shape 的对象之后,通过 虚函数表指针 指向自己的

虚函数表,然后虚函数表中有一个指向计算面积的函数指针

   

   

   

   

对于 Circle 类来说,则有些变化:它的虚函数表和之前的虚函数表

是一样的,但因为 Circle 此时已经定义了自己的计算面积的函数,

所以它的虚函数表中关于计算面积的函数指针,已经覆盖掉了父类

中原有的函数指针的值

   

   

   

   

Circle 类 0x6688 中计算面积的函数指针的值是 0x4B2C

Shape 类 0xCCFF 中计算面积的函数指针的值是 0x3355

   

二者是不一样的,如果用 Shape 的指针去指向 Circle 的对象,就会

通过 Circle 对象中的 虚函数表指针 找到 Circle 的虚函数表,通过

就能在 Circle 的虚函数表中找到 Circle 的虚函数的函数入口地址,

从而执行子类中的虚函数

   

   

   

 

   

   

函数的覆盖和隐藏

   

   

函数的覆盖和隐藏,在 C++ 中用的非常多,笔试和面试时遇到的机会

也非常大

   

在没有学习多态时,如果定义了父类和子类,父类和子类出现的同名

函数,这就称之为 函数的隐藏,即 父子关系-成员同名-隐藏

   

在学习多态之后,如果没有在子类中定义同名的虚函数,在子类的虚

函数表中就会写上父类的相应的虚函数的函数入口地址,如果在子类

中也定义了同名的虚函数,那么在子类的虚函数表中就会把原来父类

的虚函数的函数入口地址覆盖一下,覆盖成子类的虚函数的函数入口

地址,这就称之为 函数的覆盖,即 父子关系-虚函数同名-覆盖

   

   

   

   

   

   

   

虚析构函数的实现原理

   

   

虚析构函数的特点是:在父类中通过 virtual 修饰析构函数后,通过

父类指针再去指向子类对象,然后通过 delete 接父类指针,就可以

释放掉子类对象了

   

   

   

   

   

有了这个前提,如果使用父类的指针通过 delete 的方式去释放子类的

对象,那么只要能够实现通过父类的指针执行到子类的析构函数即可

   

   

看如下实例:给 Shape 和 Circle 都加上虚析构函数

   

 

   

   

如果 Circle 中不写虚析构函数,计算机会默认给你定义一个虚析构函数,

前提是你在父类中得有 virtual 来修饰父类的析构函数

   

   

   

   

在使用时:

   

如果在 main() 函数中通过父类指针指向子类对象,然后通过 delete

接父类指针释放子类对象

   

   

   

   

此时,虚函数表的工作:

   

如果在父类中定义了虚析构函数,那么在父类的虚函数表中就会

有一个父类析构函数的函数指针,指向父类的析构函数

   

 

   

   

   

   

而在子类的虚函数表中也会产生一个子类析构函数的函数指针,

指向子类的析构函数(注意:虚析构函数没有覆盖)

   

   

   

   

当 Shape 的指针指向 Circle 的对象,通过 delete 接 Shape 的

指针时,就可以通过 Circle 对象的 虚函数表指针 找到 Circle 的

虚函数表,通过 Circle 的虚函数表找到 Circle 的析构函数

   

从而使得子类的析构函数得以执行,子类的析构函数执行完毕后,

系统会自动执行父类的析构函数

   

   

   

   

   

程序 1

   

Shape.h:

   

#ifndef SHAPE_H

#define SHAPE_H

   

#include <iostream>

using namespace std;

   

   

class Shape{

public:

Shape();

~Shape();

double calcArea();

};

   

#endif

   

   

   

Shape.cpp:

   

#include "Shape.h"

   

Shape::Shape()

{

//cout << "Shape()" << endl;

}

   

Shape::~Shape()

{

//cout << "~Shape()" << endl;

}

   

double Shape::calcArea()

{

//cout << "Shape::calcArea()" << endl;

return 0;

}

   

   

   

Circle.h:

   

#ifndef CIRCLE_H

#define CIRCLE_H

   

#include "Shape.h"

   

class Circle :public Shape

{

public:

Circle(int r);

~Circle();

protected:

int m_iR;

};

   

#endif

   

   

   

Circle.cpp:

   

#include "Circle.h"

   

Circle::Circle(int r)

{

m_iR = r;

//cout << "Circle()" << endl;

}

   

Circle::~Circle()

{

//cout << "~Circle()" << endl;

}

   

   

   

main.cpp:

   

#include <stdlib.h>

#include "Circle.h"

#include <iostream>

using namespace std;

   

   

int main(void)

{

 

Shape shape;

   

//对象的大小是其数据成员大小的总和这里对象shape没有任何的数据成员

//理论上其所占的内存单元是0

//

//但实际上却打印出了1 这是因为shape在实例化的时候要标明自己的存在

//C++对一个数据成员都没有的对象1个内存单元去标记它只是标记它的存在

cout << sizeof(shape) << endl;

   

//指针pint型的shapeShape类型的取地址时不能直接指

//必须使用强制类型转换Shape类型的地址转换成int型的

int *p = (int *)&shape;

   

//打印出对象shape的地址

cout << p << endl;

//cout << (unsigned int)(*p) << endl;

Circle circle(100);

   

//这里的circle有一个int型的数据成员m_iR 理论上应该打印出4

//实际上也是4 而不是5 没有1

//因为它已经有了数据成员能够标记出自己的存在

//不需要额外的内存单元来标记自己的存在

cout << sizeof(circle) << endl;

   

 

int *q = (int *)&circle;

   

//打印出对象circle的地址

cout << q << endl;

   

//circle的地址第一个位置应该放的就是其数据成员m_iR

//m_iR处在对象地址的第一个位置指针q就是指向了m_iR

//这里的 unsigned int 要不要均可

//会打印出实例化circle时传入的100

cout << (unsigned int)(*q) << endl;

   

/*q++;

cout << (unsigned int)(*q) << endl;*/

   

   

   

system("pause");

return 0;

}

   

//概念:

//1)对象的大小:在类实例化出的对象中,它的数据成员所占据的内存大小

//(注意:是数据成员,而不包括成员函数)

//

//2)对象的地址:通过一个类实例化了一个对象,该对象在内存中会占有

//一定的内存单元,该内存单元的第一个内存单元的地址,即对象的地址

//

//3)数据成员的地址:当用一个类去实例化一个对象之后,这个对象当中可能

//有一个或多个数据成员,每一个数据成员所占据的地址,就是这个对象的数据成

//员地址。对象的每一个数据成员,因为数据类型可能不同,所以占据的内存大小

//也有不同,地址也是不同的

//

//4)虚函数表指针:在具有虚函数的情况下,实例化一个对象时,该对象的

//第一块内存中所存储的是一个指针,即虚函数表指针,因为它也是一个指针,

//所以占据的内存大小也应该是 4

   

   

运行一览:

   

   

   

   

   

   

   

   

   

程序 2:

   

Shape.h:

   

#ifndef SHAPE_H

#define SHAPE_H

   

#include <iostream>

using namespace std;

   

   

class Shape

{

public:

Shape();

~Shape();

//double calcArea();

//virtual ~Shape();

virtual double calcArea();

 

};

#endif

   

   

   

Shape.cpp: