C++ 虚函数详解
C++ 虚函数详解
这篇文章主要是转载的http://blog.csdn.net/haoel/article/details/1948051这篇文章,其中又加入了自己的理解和难点以及疑问的解决过程,对难懂的地方进行了一些必要的解释注释,当然对错误也进行了纠正。
前言
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家 一个清晰的剖析。当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。不利于学习和阅读,所以这是我想写下这篇文章的原因。也希望大家多给我提意见。言归正传,让我们一起进入虚函数的世界。
虚函数表
对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。 没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:
class Base
{
public:
virtual void f(){cout<<"Base::f"<<endl;}
virtual void g(){cout<<"Base::g"<<endl;}
virtual void h(){cout<<"Base::h"<<endl;}
};
按照上面的说法,我们可以通过Base的实例来得到虚函数表。 下面是实际例程:
typedef void (*Fun)(void); Base b;
Fun pFun=NULL;
cout<<"b对象的地址:"<<(int*)&b<<endl;
cout<<"虚函数表地址的地址:"<<*(int*)(&b)<<endl;
cout<<"虚函数表--第一个函数地址:"<<(int*)*(int*)(&b)<<endl;
//调用第一个虚函数
pFun=(Fun)*((int*)*(int*)(&b));
pFun();
这里简单说一下第一行代码的意思,如果懂得人就不用看了。
typedef void (*Fun)(void);
这段代码是定义了一个指向参数为空,返回值为空的函数的指针类型。
类似于 typedef int length;
实际运行经果如下(Win7 64位操作系统):
Dev C++(g++编译器环境下):
b对象的地址:0x28ff30
虚函数表地址的地址:
虚函数表--第一个函数地址:0x4426c0
Base::f
Visual C++6.0:
b对象的地址:0018FF44
虚函数表地址的地址:
虚函数表--第一个函数地址:0046F08C
Base::f
如果前面的理论正确,那么第二行和第三行的结果应该是相等的,这里涉及到地址和指针在内存里的表示方,我们可以验证一下是否相等。
int i=;
printf("i=%d %08x\n",i,i);
int j=;
printf("j=%d %08x\n",j,j);
输出结果:
i= 004426c0
j= 0046f08c
可以看到是相等的,这说明C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置这个猜测是正确的。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+); // Base::f()
(Fun)*((int*)*(int*)(&b)+); // Base::g()
(Fun)*((int*)*(int*)(&b)+); // Base::h()
简要解释一下(Fun)*((int*)*(int*)(&b))的意思:
&b //取b的地址
(int*)&b //把b的地址转化为整型地址
*(int*)&b //b的地所指向的地方的内容(也就是虚拟表的首地址)
(int*)*(int*)&b //把虚拟表的首地址转化为整型地址
*(int*)*(int*)&b //首地址指向的内容(也就是f()的首地址)
(Fun)*(int*)*(int*)&b //将f()首地址转化为Fun型指针
画个图会更明白一些。如下所示:

注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下(作者是这么说的,没亲自验证过),这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,对于实例:Derive d; 的虚函数表如下:

我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证,代码如下(Base定义在前面)
class Derive:public Base
{
public:
virtual void f1(){cout<<"Derive::f"<<endl;}
virtual void g1(){cout<<"Derive::g"<<endl;}
virtual void h1(){cout<<"Derive::h"<<endl;}
}; typedef void(*Fun)(void); Derive d;
Fun pFun=NULL; //调用虚函数
for (int i=;i<;i++)
{
pFun=(Fun)*((int*)*(int*)(&d)+i);
pFun();
}
输出结果:
Base::f
Base::g
Base::h
Derive::f
Derive::g
Derive::h
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:

我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
可以验证一下,代码如下:
class Derive:public Base
{
public:
virtual void f(){cout<<"Derive::f"<<endl;}
virtual void g1(){cout<<"Derive::g"<<endl;}
virtual void h1(){cout<<"Derive::h"<<endl;}
}; typedef void(*Fun)(void); Derive d;
Fun pFun=NULL; //调用虚函数
for (int i=;i<;i++)
{
pFun=(Fun)*((int*)*(int*)(&d)+i);
pFun();
}
结果如下:
Derive::f
Base::g
Base::h
Derive::g
Derive::h
这样,我们就可以看到对于下面这样的程序,
Base *b=new Derive();
b->f();
输出结果为:
Derive::f
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。

对于子类实例中的虚函数表,是下面这个样子:

代码验证一下上面这幅图的正确性:
class Base1
{
public:
virtual void f(){cout<<"Base1::f"<<endl;}
virtual void g(){cout<<"Base1::g"<<endl;}
virtual void h(){cout<<"Base1::h"<<endl;}
}; class Base2
{
public:
virtual void f(){cout<<"Base2::f"<<endl;}
virtual void g(){cout<<"Base2::g"<<endl;}
virtual void h(){cout<<"Base2::h"<<endl;}
}; class Base3
{
public:
virtual void f(){cout<<"Base3::f"<<endl;}
virtual void g(){cout<<"Base3::g"<<endl;}
virtual void h(){cout<<"Base3::h"<<endl;}
};
class Derive:public Base1,public Base2,public Base3
{
public:
virtual void f1(){cout<<"Derive::f"<<endl;}
virtual void g1(){cout<<"Derive::g"<<endl;}
virtual void h1(){cout<<"Derive::h"<<endl;}
}; typedef void(*Fun)(void); Derive d;
Fun pFun=NULL;
int* dPoint=(int*)&d;
cout<<"Base1虚函数表首地址:"<<dPoint<<endl;
int* pBase=dPoint;//Base虚函数表首地址
for (int j=;j<;j++)
{
pBase=dPoint+j;//分别得到Base1,Base2,Base3;
int table_size;//虚拟表的大小
if(j==)
table_size=;//Base1大小是6
else
table_size=;//Base2,Base3大小是3
for (int i=;i<table_size;i++)
{
pFun=(Fun)*((int*)*pBase+i);
pFun();
}
printf("\n");
}
输出结果:
Base1虚函数表首地址:0018FF3C
Base1::f
Base1::g
Base1::h
Derive::f
Derive::g
Derive::h Base2::f
Base2::g
Base2::h Base3::f
Base3::g
Base3::h
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。

下面是对于子类实例中的虚函数表的图:

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
class Derive:public Base1,public Base2,public Base3
{
public:
virtual void f(){cout<<"Derive::f"<<endl;}
virtual void g1(){cout<<"Derive::g"<<endl;}
virtual void h1(){cout<<"Derive::h"<<endl;}
}; typedef void(*Fun)(void); Derive d;
Fun pFun=NULL;
int* dPoint=(int*)&d;
cout<<"Base1虚函数表首地址:"<<dPoint<<endl;
int* pBase=dPoint;//Base虚函数表首地址
for (int j=;j<;j++)
{
pBase=dPoint+j;//分别得到Base1,Base2,Base3;
int table_size;//虚拟表的大小
if(j==)
table_size=;//Base1大小是6
else
table_size=;//Base2,Base3大小是3
for (int i=;i<table_size;i++)
{
pFun=(Fun)*((int*)*pBase+i);
pFun();
}
printf("\n");
} Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f() b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
输出结果如下图:
Base1虚函数表首地址:0018FF3C
Derive::f
Base1::g
Base1::h
Derive::g
Derive::h Derive::f
Base2::g
Base2::h Derive::f
Base3::g
Base3::h Derive::f
Derive::f
Derive::f
Base1::g
Base2::g
Base3::g
安全性
每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
class Base
{
public:
virtual void f(){cout<<"Base::f"<<endl;}
virtual void g(){cout<<"Base::g"<<endl;}
virtual void h(){cout<<"Base::h"<<endl;}
}; class Derive:public Base
{
public:
virtual void f1(){cout<<"Derive::f"<<endl;}
virtual void g1(){cout<<"Derive::g"<<endl;}
virtual void h1(){cout<<"Derive::h"<<endl;}
}; Base *b=new Derive();
b->f1();
error C2039: 'f1' : is not a member of 'Base'
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。有兴趣可以看看下面的代码:
class Base
{
public:
virtual void f(){cout<<"Base::f"<<endl;}
virtual void g(){cout<<"Base::g"<<endl;}
virtual void h(){cout<<"Base::h"<<endl;}
}; class Derive:public Base
{
public:
virtual void f1(){cout<<"Derive::f"<<endl;}
virtual void g1(){cout<<"Derive::g"<<endl;}
virtual void h1(){cout<<"Derive::h"<<endl;}
};
typedef void (*Fun)(void);
Fun pFun=NULL; Base* b=new Derive();
int* pBase=(int*)b;
pFun=(Fun)*((int*)*pBase+);
pFun();
输出结果为:
Derive::f
写到这也就结束了,当然我也没完全按照作者原文里来,这里的代码都是我亲自实现和验证过的,详细代码我也附在了文字后面,大家可以选择性的看看,也可以copy下来自己验证看看是否争取,总之感觉原作者的那篇文章还是写的相当不错的。
C++ 虚函数详解的更多相关文章
- C++学习23 虚函数详解
虚函数对于多态具有决定性的作用,有虚函数才能构成多态.上节的例子中,你可能还未发现虚函数的用途,不妨来看下面的代码. #include <iostream> using namespace ...
- 【c++】面向对象程序设计之虚函数详解
一.动态绑定什么时候发生 当且仅当通过指针或引用调用虚函数时,才会在运行时解析该调用 二.派生类中的虚函数 当我们在派生类中覆盖了某个虚函数时,可以再一次使用virtual指出该函数的性质,但是这么做 ...
- SHGetFileInfo函数详解
SHGetFileInfo函数: WINSHELLAPI DWORD WINAPI SHGetFileInfo( LPCTSTR pszPath, DWORD dwFileAttributes, SH ...
- 常用socket函数详解
常用socket函数详解 关于socket函数,每个的意义和基本功能都知道,但每次使用都会去百度,参数到底是什么,返回值代表什么意义,就是说用的少,也记得不够精确.每次都查半天,经常烦恼于此.索性都弄 ...
- malloc 与 free函数详解<转载>
malloc和free函数详解 本文介绍malloc和free函数的内容. 在C中,对内存的管理是相当重要.下面开始介绍这两个函数: 一.malloc()和free()的基本概念以及基本用法: 1 ...
- NSSearchPathForDirectoriesInDomains函数详解
NSSearchPathForDirectoriesInDomains函数详解 #import "NSString+FilePath.h" @implementation ...
- JavaScript正则表达式详解(二)JavaScript中正则表达式函数详解
二.JavaScript中正则表达式函数详解(exec, test, match, replace, search, split) 1.使用正则表达式的方法去匹配查找字符串 1.1. exec方法详解 ...
- Linux C popen()函数详解
表头文件 #include<stdio.h> 定义函数 FILE * popen( const char * command,const char * type); 函数说明 popen( ...
- kzalloc 函数详解(转载)
用kzalloc申请内存的时候, 效果等同于先是用 kmalloc() 申请空间 , 然后用 memset() 来初始化 ,所有申请的元素都被初始化为 0. view plain /** * kzal ...
随机推荐
- 3月23日html(五) 格式与布局练习:360浏览器布局
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> < ...
- Python 番外 消息队列设计精要
消息队列已经逐渐成为企业IT系统内部通信的核心手段.它具有低耦合.可靠投递.广播.流量控制.最终一致性等一系列功能,成为异步RPC的主要手段之一.当今市面上有很多主流的消息中间件,如老牌的Active ...
- NAMESPACE
限定作用域,比类高,比文件低. cpp 和 h 里面都要用到.
- C 语言链表操作例程 (待完善)
#include<stdio.h>#include<malloc.h>#include<conio.h>#include<stdlib.h>#inclu ...
- HDFS操作--文件上传/创建/删除/查询文件信息
1.上传本地文件到HDFS //上传本地文件到HDFS public class CopyFile { public static void main(String[] args) { try { C ...
- 工程和界面—Webstorm入门指南 Webstorm中的工程-备
1.新建工程 “Quick Start”界面新建工程: 也可以点击顶部菜单栏“File”-> “New Project”. 弹出如下界面: “Location”指向想要创建的工程目录(如果该目录 ...
- hdu Number Sequence
这道题是寻找规律.别的方法一般都是超时. #include <cstdio> #include <cstring> #include <algorithm> usi ...
- 带KEY的SCP命令,老是要查,这次写在这里吧,
有些东东记不住,急要用时老是想不起,放在这里吧, scp -r -i /xxx/rsa.key -P port user@ip:/source/ /target/
- 树莓派入门教程——使用Qt开发界面程序
前言 Qt是一个1991年由奇趣科技开发的跨平台C++图形用户界面应用程序开发框架.它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器.Qt是面向对象的框架,使用特 ...
- C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped 转
原文:C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped 转 节点通信存在两种模型:共享内存(Shared memory)和消息传递(Messages passing ...