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 译 ...
随机推荐
- 受限的用户shell环境
有些特殊情况下需要实现将系统内普通用户限定在指定目录下,并且只能使用系统管理员设定的命令.lshell就是实现这样功能的一个神器. lshell提供了一个针对每个用户可配置的限制性shell,lshe ...
- hdu6208 The Dominator of Strings
地址: 题目: The Dominator of Strings Time Limit: 3000/3000 MS (Java/Others) Memory Limit: 65535/32768 ...
- yarn 与 resource manager ha
YARN最初的思想是把hadoop1中的job tracker的功能拆分出来,把它的资源管理与任务调度功能分成两个单独的进程.yarn体系结构中有两个进程,resource manager和nodem ...
- JS正则表达式从入门到入土(10)—— 字符串对象方法
字符串对象方法 search方法 String.prototype.search(reg) search方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,方法返回第一个匹配结果的 ...
- Python3.x:zip()函数
Python3.x:zip()函数 1,概述: zip函数接受任意多个(包括0个和1个)序列作为参数,返回一个tuple列表: 2,示例: (1).示例1: x = [1, 2, 3] y = [4, ...
- linux及安全第三周总结——20135227黄晓妍
总结部分: Linux内核源代码: Arch 支持不同cpu的源代码:主要关注x86 Init 内核启动的相关代码:主要关注main.c,整个Linux内核启动代码start_kernel函数 K ...
- openwrt下如何只编译linux内核
答:make target/linux/install V=s (此操作也会将最终镜像打包好,V=s会输出日志) 拓展 清空内核: make target/linux/clean V=s 解压内核: ...
- Java ArrayList在foreach中remove的问题分析
目录 iterator itr.hasNext 和 itr.next 实现 倒数第二个元素的特殊 如何避坑 都说ArrayList在用foreach循环的时候,不能add元素,也不能remove元素, ...
- Covariance and Contravariance (C#)
Covariance and Contravariance (C#) https://docs.microsoft.com/en-us/dotnet/articles/csharp/programmi ...
- 【bzoj1369】[Baltic2003]Gem(树形dp+结论)
题目传送门:bzoj1369 这题其实有个结论:节点数为n的树,对其染色使相邻节点颜色不同,且总颜色权值最小,所需的颜色数量是$ O(\log n) $的. 所以我们就可以愉快的dp了:$ f[i][ ...