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 译 ...
随机推荐
- Java设计原则—开闭原则(转)
原文出自:http://www.cnblogs.com/muzongyan/archive/2010/08/05/1793454.html 开闭原则(Open Closed Principle)是Ja ...
- oracle 11g怎样配置才能连接远程数据库
打开所有程序->找到oracle-oradb11g-home1->Net Configuration Assistant,如图所示 选择本地网络服务名配置,点击下一步 选择添 ...
- AVAudioSession(4):响应音频中断事件
本文转自:AVAudioSession(4):响应音频中断事件 | www.samirchen.com 本文内容主要来源于 Responding to Interruptions. 当一个音频中断到来 ...
- 前端学习笔记之HTML中的id,name,class区别
name 属性用于在 JavaScript 中对元素进行引用,或者在表单提交之后,对表单数据进行引用. html的name和id可以类比身份证的姓名和身份证编号,编号id具有唯一性,一个id只出现一次 ...
- CSS 图像透明/不透明
CSS 图像透明/不透明 使用CSS很容易创建透明的图像. 注意:CSS Opacity属性是W3C的CSS3建议的一部分. 一.示例一:创建一个透明图像 CSS3中属性的透明度是 opacity. ...
- ubuntu下apt-get的配置文件是哪个
答:在/etc/apt/apt.conf 这个配置文件里可以指定使用代理,如: Acquire::https::proxy "http://myproxy.com:8080/";
- RabbitMQ脑裂
在RabbitMQ3.4.x中会出现脑裂的现象,本文通过实验验证此脑裂现象,愿小伙伴们少走弯路. Preview 网上有两篇帖子(需要FQ) https://groups.google.com/for ...
- Linux禁止普通用户使用crontab命令
cron计划任务默认root用户与非root用户都可以执行,当然如果在安全方面想禁用这部分用户,则可以通过两个文件来解决: cron.allow cron.deny cron.allow:定义允许使用 ...
- rocketmq事务消息
rocketmq事务消息 参考: https://blog.csdn.net/u011686226/article/details/78106215 https://yq.aliyun.com/art ...
- Ubuntu 安装 networkx
参考:ubuntu 下NetworkX的安装和使用 Dependences pip setuptools Commands 1.install networkx sudo pip install ne ...