C++复习7.虚表的概念
C++ 类的虚表 20130929
关键技术:封装、继承、组合、虚函数、抽象基类、动态绑定、多态性等等
1.首先整理一下在阿里巴巴面试遇到的函数虚表的问题。
在C++中的Class中的函数式存储在Class数据机构的虚表中。每一个Class对应的所有的函数地址都会在Class的数据结构虚表中,每一个Class的对象在对象开始的地方都会有一个指针(计算机的位数一般是是32位)指向Class的函数虚表,函数虚表中每一个函数地址是按照在Class中声明的顺序。一般也是一个32bit的指针。
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 Base的函数虚表中就是三个函数指针,分别指向这三个对应的函数,但是我们如何获取这些函数指针的值。
我们声明一个对象Base base;
这样在对象base中的首地址开始的一个机器长度的空间是一个指针,指向的是虚表的首地址,其中虚表的首地址第一个函数指针就是指向class中的第一个声明的函数。然后对该函数指针++ 操作便可以指向下一个函数,这也正是可以通过函数指针访问private函数的方式。(防君子不防小人)。
一般继承中是没有函数覆盖的,所以在虚表中首先是父类的函数指针,后面是子类的函数指针。
继承中有虚函数覆盖的情况,则子类中的Class虚表中对应的函数指针指向的是子类的函数地址,其他的不变。
对于多重继承,则在对象的首地址前面有n个父类的虚表指针,分别指向对应父类的虚表,子类的函数地址存放在第一个父类的虚表中。这样是为了解决不同的父类指针指向同一个子类的对象的时候,可以调用实际对应的函数。
当出现覆盖的情况的时候,就会将这些函数虚表中的虚函数换成对应子类的虚函数地址,在调用的时候会根据父类指针的类型,分别调用不同的父类的虚函数,而对于多态,则调用子类中的虚函数,在三个对应的虚表中,所有的改虚函数指针都会被修改指向子类的虚函数地址。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d;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()
存在的一些不友好的地方:任何使用父类的指针调用子类没有覆盖父类成员函数的行为都会被编译器视为非法行为,出现编译出错的问题。但是可以在运行期间通过指针的方式访问虚函数表来达到违反C++语义的行为。
正常的方式访问
typedef
void(*Fun)(void);//
这是一个函数指针,使用Func
来表示指向一个函数,该函数的返回值类型是
void,参数是void。使用方式: Func
pFunc = NULL ; 指向一个函数指针
pFunc=(Fun)*((int*)*(int*)(&base));
class
Base{
public:
virtual
void
fun_a(void){
cout
<< "base::fun_a"
<< endl;
}
virtual
void
fun_b(void){
cout
<< "base::fun_b"
<< endl;
}
virtual
void
fun_c(void){
cout
<< "base::fun_c"
<< endl;
}
private:
int
a;
};
class
Derive:
public
Base{
public:
virtual
void
fun_a(void){
cout
<< "derive::fun_a"
<< endl;
}
virtual
void
fun_b(void){
cout
<< "derive::fun_b"
<< endl;
}
virtual
void
fun_d(void){
cout
<< "derive::fun_d"
<< endl;
}
private:
int
b;
};
int
main(){
Derive derive;
cout
<< "sizeof(derive):"<<
sizeof(derive)
<< endl;
cout
<< "derive 首地址:" << &derive <<endl;
((Fun)*((int*)*(int*)(&derive)
))();
((Fun)*((int*)*(int*)(&derive)
+ 1 ))();
((Fun)*((int*)*(int*)(&derive)
+ 2 ))();
((Fun)*((int*)*(int*)(&derive)
+ 3 ))();
Base base;
Fun pFun = NULL;
cout
<< "对象base的地址:"
<< &base << endl;
cout
<< "第一个虚函数的地址:"
<< (int*)*(int*)(&base)
<< endl;
pFun
= (Fun)*((int*)*(int*)(&base));
pFun();
((Fun)*((int*)*(int*)(&base)
+ 1))();
((Fun)*((int*)*(int*)(&base)
+ 2))();
cout
<<"sizeof(base):"<<
sizeof(base)
<< endl;
return
0;
}
防君子不防小人的实现:
对于父类中的private and protected修饰的虚函数,在继承的时候,同样会存在在函数的虚表中,这样的话我们便可以通过访问虚函数的方式访问这些非public函数。
typedef
void(*Fun)(void);
class
Base{
private:
virtual
void
fun_a(void){
cout
<< "base::fun_a"
<< endl;
}
virtual
void
fun_b(void){
cout
<< "base::fun_b"
<< endl;
}
virtual
void
fun_c(void){
cout
<< "base::fun_c"
<< endl;
}
private:
int
a;
};
class
Derive:
public
Base{
private:
int
b;
};
int
main(){
Derive derive;
Base* base = &derive;
Fun pFun = NULL;
pFun
= (Fun)*(int*)*(int*)&(*base);
pFun();
((Fun)* ((int*)*(int*)&(*base)
+ 1 ) )();
((Fun)* ((int*)*(int*)&(*base)
+ 2 ) )();
return
0;
}
C++复习7.虚表的概念的更多相关文章
- Delegate 委托复习(-) 委托的基本概念
1. 声明一个delegate对象,它应当与你想要传递的方法具有相同的参数和返回值类型. 声明一个代理的例子: public delegate int MyDelegate(stri ...
- [白话解析] 通过实例来梳理概念 :准确率 (Accuracy)、精准率(Precision)、召回率(Recall)和F值(F-Measure)
[白话解析] 通过实例来梳理概念 :准确率 (Accuracy).精准率(Precision).召回率(Recall)和F值(F-Measure) 目录 [白话解析] 通过实例来梳理概念 :准确率 ( ...
- Javascript操作DOM常用API总结
基本概念 在讲解操作DOM的api之前,首先我们来复习一下一些基本概念,这些概念是掌握api的关键,必须理解它们. Node类型 DOM1级定义了一个Node接口,该接口由DOM中所有节点类型实现.这 ...
- NOIP2003pj栈[卡特兰数]
题目背景 栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表. 栈有两种最重要的操作,即pop(从栈顶弹出一个元素)和push(将一个元素进栈). 栈的重要性不言自明,任何 ...
- AC日记——codevs 1086 栈 (卡特兰数)
题目描述 Description 栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表. 栈有两种最重要的操作,即pop(从栈顶弹出一个元素)和push(将一个元素进栈). ...
- Oracle 学习系列之二(会话与事务级临时表和dual表 )
一. 会话临时表 --创建会话临时表create global temporary table tmp_user_session(user_id int, user_name varchar2(20) ...
- [NOIP2003]栈
2003年普及组 题目背景 栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表. 栈有两种最重要的操作,即pop(从栈顶弹出一个元素)和push(将一个元素进栈). 栈的重 ...
- JavaScript 操作 DOM 常用 API 总结
文本整理了javascript操作DOM的一些常用的api,根据其作用整理成为创建,修改,查询等多种类型的api,主要用于复习基础知识,加深对原生js的认识. 基本概念 在讲解操作DOM的api之前, ...
- 怎样从一个DLL中导出一个C++类
原文作者:Alex Blekhman 翻译:朱金灿 原文来源: http://www.codeproject.com/KB/cpp/howto_export_cpp_classes.aspx 译 ...
随机推荐
- 中线,基线,垂直居中vertical-align:middle的一些理解
基线:小写字母xxxxx的下边缘线就是我们的css基线:一般的行内元素都是vertical-align: baseline;默认设置: x-height:就是指小写字母xxxx的高度,下边缘线到上边缘 ...
- [HZNUOJ] 使用Excel + Word 批量制作准考证
一般程序设计考试或者ACM比赛都会使用临时账号登录,这时候就需要批量制作密码条 首先需要用Excel 存储队伍的信息 比如像这样分门别类的放好 然后在word 中制作好密码条样式 选择邮件->开 ...
- [转]VMware-Transport(VMDB) error -44:Message.The VMware Authorization Service is not running解决方案
转自:http://blog.sina.com.cn/s/blog_70c9c4b40101i01v.html 1.VMware Workstation中新建的虚拟机在开机的时候出现这种错误:Tran ...
- 并发队列ConcurrentLinkedQueue与阻塞队列LinkedBlockingQueue的区别
1. 介绍背景 在Java多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列. Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是Block ...
- Android系统机制
Android系统机制 本文主要介绍Android系统整体运行机制 Linux中的一些概念 uboot加载系统内核到内存,系统内核运行起来的后,会创建第一个用户进程叫init进程,该进程是所有用户进程 ...
- MyEclipse 2014优化设置(禁用myeclipse updating indexes)
1.指定本机java环境 Windows-->preferences-->java-->Insetallel JREs 右侧 单击ADD standard VM-->Next ...
- ubuntu 14.04 163镜像
1.备份原来/etc/apt/sources.list 2.以下内容覆盖原来文件内容 deb http://mirrors.163.com/ubuntu/ trusty main restricted ...
- 【VS2015】未能创建 Visual C# 2015编译器
今天在安装完成Visual Studio 2015后,在执行update 3时出错,导致再次打开VS2015时提示错误:“未能创建 Visual C# 2015编译器”和“未能正确加载CSharpPa ...
- jQuery与直接写JS的区别详细解析
jQuery代码具体的写法和原生的Javascript写法在执行常见操作时的区别如下所示.需要的朋友可以过来参考下 要使用jQuery,首先要在HTML代码最前面加上对jQuery库的引用,比 ...
- 基于tomcat集群做session共享
前端代理服务器nginx:192.168.223.136 tomcat服务器:采用的一台多实例192.168.223.146:8081,192.168.223.146:8082(如何构建多实例tomc ...