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 译 ...
随机推荐
- DOM—节点
节点的相关属性 1.nodeType:节点类型.返回的是一个数字,这个数字代表节点类型,只读. 节点类型: 1 — 元素类型:元素节点(标签) 节点类型: 2 — 元素类型:属性节点 节点类型: 3 ...
- SSH整合不错的博客
https://blog.csdn.net/struggling_rong/article/details/63153833?locationNum=9&fps=1 好好看看看哦
- 判断变量是否为 NaN
首先要明确 NaN 的一个特性, NaN不能用相等操作符(== 和 ===) 来判断, NaN === NaN 会返回 false: 下面是测试代码: console.log(isNaN('1')) ...
- Swoole学习(七)Swoole之异步TCP服务器的创建
环境:Centos6.4,PHP环境:PHP7 <?php //创建TCP服务器 /** * $host 是swoole需要监听的ip,如果要监听本地,不对外服务,那么就是127.0.0.1;如 ...
- 照着官网来安装openstack pike之keystone安装
openstack基础环境安装完成后,现在开启安装keystone服务(在控制节点上执行下面所有操作) 1.为keystone创建数据库 mysql -u root -p MariaDB [(none ...
- #ifndef用法
用于避免重复包含头文件 #ifndef _STDIO_H_ #define _STDIO_H_ ...... #endif
- Redis-数据操作
数据操作 redis是key-value的数据,所以每个数据都是一个键值对 键的类型是字符串 值的类型分为五种: 字符串string 哈希hash 列表list 集合set 有序集合zset 数据操作 ...
- LeetCode——Arithmetic Slices
Question A sequence of number is called arithmetic if it consists of at least three elements and if ...
- Windows下如何配置apache虚拟主机
其实apache配置虚拟主机说简单也简单,但是就是就有几个坑,要是稍不注意就掉坑里了. --小树前言 坑三连 没遇到这三个坑,就配置得很顺畅了 用自己指定的域名进入不了任何页面. 只能进apache的 ...
- Spring Cloud 坑点
1 配置中心 1.config 默认Git加载 通过spring.cloud.config.server.git.uri指定配置信息存储的git地址,比如:https://github.com/spr ...