虚函数与虚析构函数原理

----------------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;

//指针p是int型的而shape是Shape类型的取地址时不能直接指

//必须使用强制类型转换将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:


#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"

//通过计算对象的大小来证明虚函数表指针的存在

int main(void)

{

Shape shape;

//当类中出现虚函数或虚析构函数(任一或同时只要有"虚")时

//随着类的实例化,对象的数据成员中产生了虚函数表指针vftable_ptr

//(和普通的指针一样占4个基本内存单元)所以应该打印出4

cout << sizeof(shape) << endl;

int *p = (int *)&shape;

cout << p << endl;//此时shape的前4个内存单元就是虚函数指针所在

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

//继承了父类的虚函数那么也会产生虚函数表(内含指向成员函数入口的指针)

//同时在circle的数据成员中产生指向虚函数表首地址的虚函数表指针

Circle circle(100);

cout << sizeof(circle) << endl;

int *q = (int *)&circle;

//虚函数表指针的地址

cout << q << endl;

//虚函数表指针中存的指向虚函数表的地址

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

q++;

//对象的前四个基本内存单元是虚函数表指针的所在

//后四个基本内存单元是m_iR的所在

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

system("pause");

return 0;

}

//两个类各自产生自己的虚函数表和虚函数表指针

//(1)当只有Shape类中有double calcArea(); 并带virtual时两个虚函数

//表里指向calcArea()的指针相同(此时Circle类中没有定义calcArea())

//(2)当Circle类中也有 double calcArea();父类中有virtual时两个虚函

//数表里指向calcArea()的指针就不同了

//

//

//子类中定义了计算面积的函数那么虚函数表中关于计算面积的函数指针就

//【覆盖】掉了父类中的原有指针的值

//

//如果没有子类中没有定义同名的计算面积的虚函数那么子类的虚函数表里

//就会使用父类的虚函数表里的相应的函数指针(函数入口地址)

//

//如果子类也定义了同名的虚函数在子类的虚函数表中就会把原来的父类的

//虚函数地址给【覆盖】掉覆盖成子类的虚函数的函数地址

//

//

//虚析构函数在父类中通过virtual 修饰析构函数之后通过父类指针指向子类

//对象再通过delete 接父类指针就可以释放掉子类对象

//

//理论前提:执行完子类的析构函数就会执行父类的析构函数

//

//那么如果我们使用父类的指针通过delete的方式去释放子类的对象只要能做到

//通过父类的指针执行到子类的析构函数就可以实现了

//

//虚析构函数没有【覆盖】现象

运行一览:

【made by siwuxie095】

时间: 2024-10-12 03:28:11

虚函数与虚析构函数原理的相关文章

C++虚函数与虚析构函数

在类中,有两个与众不同的成员函数,那就是构造函数和析构函数.当构造函数与析构函数遭遇继承和多态,它们的运行状况又会出现什么变化呢? 多态性是在父类或各子类中执行最合适成员函数.一般来说,只会选择父类或子类中的某一个成员函数来执行.这可给析构函数带来了麻烦!如果有的资源是父类的构造函数申请的,有的资源是子类的构造函数申请的,而虚函数只允许程序执行父类或子类中的某一个析构函数,岂不是注定有一部分资源将无法被释放?为了解决这个问题,虚析构函数变得与众不同. 下面我们就来给析构函数的前面加上保留字vir

为什么构造函数不能为虚函数,而析构函数可以为虚函数

1.  构造函数为什么不能为虚函数? a.  存储空间角度: 虚函数的调用需要虚函数表指针,而该指针存放在对象的内容空间中,需要调用构造函数才可以创建他的值,否则即使开辟了空间,则虚表指针为随机值,不会找到构造函数:若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数——构造函数了. b.  使用上:  从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数: 虚函数主要是实现多态,在运行时才可以明确调用对象,根据传入的对象类型,来调用

C++ 虚函数和虚继承浅析

本文针对C++里的虚函数,虚继承表现和原理进行一些简单分析,有希望对大家学习C++有所帮助.下面都是以VC2008编译器对这两种机制内部实现为例. 虚函数 以下是百度百科对于虚函数的解释: 定义:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数[1] 语法:virtual 函数返回类型 函数名(参数表) { 函数体 } 用途:实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数 函数声明和定义和普通的类成员函数一样,只是在返回值之前加入了关键字"vir

C++纯虚函数、虚函数、实函数、抽象类,重载、重写、重定义

首先,面向对象程序设计(object-oriented programming)的核心思想是数据抽象.继承.动态绑定.通过数据抽象,可以使类的接口与实现分离,使用继承,可以更容易地定义与其他类相似但不完全相同的新类,使用动态绑定,可以在一定程度上忽略相似类的区别,而以统一的方式使用它们的对象. 虚函数的作用是实现多态性(Polymorphism),多态性是将接口与实现进行分离,采用共同的方法,但因个体差异而采用不同的策略.纯虚函数则是一种特殊的虚函数.虚函数联系到多态,多态联系到继承. 一.虚函

C++虚函数及虚函数表解析

一.背景知识(一些基本概念) 虚函数(Virtual Function):在基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数.纯虚函数(Pure Virtual Function):基类中没有实现体的虚函数称为纯虚函数(有纯虚函数的基类称为虚基类).C++  “虚函数”的存在是为了实现面向对象中的“多态”,即父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数.通过动态赋值,实现调用不同的子类的成员函数(动态绑定).正是因为这种

C++ 虚函数&amp;纯虚函数&amp;抽象类&amp;接口&amp;虚基类(转)

http://www.cnblogs.com/fly1988happy/archive/2012/09/25/2701237.html 1. 多态 在面向对象语言中,接口的多种不同实现方式即为多态.多态是指,用父类的指针指向子类的实例(对象),然后通过父类的指针调用实际子类的成员函数. 多态性就是允许将子类类型的指针赋值给父类类型的指针,多态是通过虚函数实现的. 多态可以让父类的指针有“多种形态”,这是一种泛型技术.(所谓泛型技术,就是试图使用不变的代码来实现可变的算法). 2. 虚函数 2.1

虚函数/纯虚函数/抽象类/接口/虚基类

1.多态 在面向对象语言中,接口的多种不同实现方式即为多态.多态是指,用父类的指针指向子类的实例(对象),然后通过父类的指针调用实际子类的成员函数. 在Java中,没有指针,就直接用父类实例化子类对象 多态性就是允许将子类类型的指针赋值给父类类型的指针,多态是通过虚函数实现的,多态可以让父类的指针有“多种形态”,这是一种泛型技术. 所谓泛型技术,就是试图使用不变的代码来实现可变的算法 2.虚函数 在基类的类定义中,定义虚函数的一般形式: Virtual 函数返回值类型 虚函数名(形参表){ 函数

C++之虚函数与虚继承详解

准备工作 1.VS2012使用命令行选项查看对象的内存布局 微软的Visual Studio提供给用户显示C++对象在内存中的布局的选项:/d1reportSingleClassLayout.使用方法很简单,直接在[项目P]选项下找到"visual属性"后点击即可.切换到cpp文件所在目录下输入如下的命令即可 c1 [filename].cpp /d1reportSingleClassLayout[className] 其中[filename].cpp就是我们想要查看的class所在的

C++基础知识 基类指针、虚函数、多态性、纯虚函数、虚析构

一.基类指针.派生类指针 父类指针可以new一个子类对象 二.虚函数 有没有一个解决方法,使我们只定义一个对象指针,就可以调用父类,以及各个子类的同名函数? 有解决方案,这个对象指针必须是一个父类类型,我们如果想通过一个父类指针调用父类.子类中的同名函数的话,这个函数是有要求的: 在父类中,eat函数声明之前必须要加virtual声明eat()函数为虚函数. 一旦某个函数被声明为虚函数,那么所有派生类(子类)中eat()函数都是虚函数. 为了避免你在子类中写错虚函数,在C++11中,你可以在函数